當(dāng)前位置:首頁(yè) > 公眾號(hào)精選 > 小林coding
[導(dǎo)讀]大家好,我是熱心網(wǎng)友?——小林。有位讀者問了,我這么一個(gè)問題:不管是RPC或者HTTP,只要傳輸?shù)膬?nèi)容是「對(duì)象」,要想在接收方還原出一摸一樣的「對(duì)象」,那就需要序列化和反序列化。那什么是序列化和反序列化呢?RPC能幫助我們的應(yīng)用透明地完成遠(yuǎn)程調(diào)用,即調(diào)用其他服務(wù)器的函數(shù)就像調(diào)用本...

大家好,我是熱心網(wǎng)友?—— 小林。有位讀者問了,我這么一個(gè)問題:

不管是 RPC 或者 HTTP,只要傳輸?shù)膬?nèi)容是「對(duì)象」,要想在接收方還原出一摸一樣的「對(duì)象」,那就需要序列化和反序列化。

那什么是序列化和反序列化呢?

RPC 能幫助我們的應(yīng)用透明地完成遠(yuǎn)程調(diào)用,即調(diào)用其他服務(wù)器的函數(shù)就像調(diào)用本地方法一樣。發(fā)起調(diào)用請(qǐng)求的那一方叫做調(diào)用方,被調(diào)用的一方叫做服務(wù)提供方。

調(diào)用方和服務(wù)提供方一般是不同的服務(wù)器,所以就需要通過網(wǎng)絡(luò)來傳輸數(shù)據(jù),并且 RPC 常用于業(yè)務(wù)系統(tǒng)之間的數(shù)據(jù)交互,需要保證其可靠性,所以 RPC 一般默認(rèn)采用 TCP 協(xié)議來傳輸。同時(shí), HTTP 協(xié)議也是建立在 TCP 之上的。

網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)必須是二進(jìn)制數(shù)據(jù),但調(diào)用方請(qǐng)求的出入?yún)?shù)都是對(duì)象,而對(duì)象是肯定沒法直接在網(wǎng)絡(luò)中傳輸?shù)模枰崆鞍选笇?duì)象轉(zhuǎn)成二進(jìn)制數(shù)據(jù)」進(jìn)行網(wǎng)絡(luò)傳輸,這個(gè)轉(zhuǎn)換過程就做序列化。相反,服務(wù)提供方收到網(wǎng)絡(luò)數(shù)據(jù)后,需要將「二進(jìn)制數(shù)據(jù)轉(zhuǎn)成對(duì)象」,這個(gè)轉(zhuǎn)換過程就叫做反序列化。

總結(jié)來說,序列化就是將對(duì)象轉(zhuǎn)換成二進(jìn)制數(shù)據(jù)的過程,以方便傳輸或存儲(chǔ)。而反序列就是將二進(jìn)制轉(zhuǎn)換為對(duì)象的過程。

為什么 RPC 經(jīng)常提到序列化呢?

因?yàn)榫W(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)必須是二進(jìn)制數(shù)據(jù),所以在 RPC 調(diào)用中,對(duì)入?yún)?duì)象與返回值對(duì)象進(jìn)行序列化與反序列化是一個(gè)必須的過程。

HTTP 什么時(shí)候需要序列化呢?

舉個(gè)例子。

當(dāng)客戶端和服務(wù)端交互的數(shù)據(jù)是 JSON,這時(shí)候發(fā)送方需要將 JSON 對(duì)象轉(zhuǎn)換成二進(jìn)制數(shù)據(jù)發(fā)送到網(wǎng)絡(luò),接收方需要將接收到的二進(jìn)制數(shù)據(jù)轉(zhuǎn)換成 JSON 對(duì)象。

說了,這么多概念,接下來跟大家說說有哪些常用的序列化方式?

JDK 原生序列化

Java 語(yǔ)言中 JDK 就自帶有序列化的方式,舉個(gè) JDK 序列化的例子。

注意,這個(gè)例子是將 Student 對(duì)象保存到文件,保存到文件的數(shù)據(jù)是二進(jìn)制數(shù)據(jù),所以并不是說序列化只用在網(wǎng)絡(luò)傳輸場(chǎng)景里,只要是保存數(shù)據(jù)的場(chǎng)景只能是二進(jìn)制數(shù)據(jù)時(shí),就需要用序列化。

import?java.io.*;

public?class?Student?implements?Serializable?{
????//學(xué)號(hào)
????private?int?no;
????//姓名
????private?String?name;

????public?int?getNo()?{
????????return?no;
????}

????public?void?setNo(int?no)?{
????????this.no?=?no;
????}

????public?String?getName()?{
????????return?name;
????}

????public?void?setName(String?name)?{
????????this.name?=?name;
????}

????@Override
????public?String?toString()?{
????????return?"Student{"?
????????????????"no="? ?no?
????????????????",?name='"? ?name? ?'\''?
????????????????'}';
????}

????public?static?void?main(String[]?args)?throws?IOException,?ClassNotFoundException?{
???????
????????String?basePath?=?"/root";
????????FileOutputStream?fos?=?new?FileOutputStream(basePath? ?"student.dat");
????????
????????//初始化一個(gè)學(xué)生對(duì)象
????????Student?student?=?new?Student();
????????student.setNo(1);
????????student.setName("xiaolin");
????????
????????//將學(xué)生對(duì)象序列化到文件里
????????ObjectOutputStream?oos?=?new?ObjectOutputStream(fos);
????????oos.writeObject(student);
????????oos.flush();
????????oos.close();

????????//讀取文件中的二進(jìn)制數(shù)據(jù),并將數(shù)據(jù)反序列化為學(xué)生對(duì)象
????????FileInputStream?fis?=?new?FileInputStream(basePath? ?"student.dat");
????????ObjectInputStream?ois?=?new?ObjectInputStream(fis);
????????Student?deStudent?=?(Student)?ois.readObject();
????????ois.close();

????????System.out.println(deStudent);
????}
}
從上面的代碼,我們可以知道:

  • JDK 自帶的序列化具體的實(shí)現(xiàn)是由 ObjectOutputStream 完成的;

  • 而反序列化的具體實(shí)現(xiàn)是由 ObjectInputStream 完成的。

既然序列化是將對(duì)象轉(zhuǎn)換為二進(jìn)制數(shù)據(jù),那序列化的過程的二進(jìn)制數(shù)據(jù)肯定是有某種固定的格式。

比如 JDK 自帶的序列化的過程如下圖:

序列化過程就是在讀取對(duì)象數(shù)據(jù)的時(shí)候,不斷加入一些特殊分隔符,這些特殊分隔符用于在反序列化過程中截?cái)嘤谩?/p>
  • 頭部數(shù)據(jù)用來聲明序列化協(xié)議、序列化版本,用于高低版本向后兼容;

  • 對(duì)象數(shù)據(jù)主要包括類名、簽名、屬性名、屬性類型及屬性值,當(dāng)然還有開頭結(jié)尾等數(shù)據(jù),除了屬性值屬于真正的對(duì)象值,其他都是為了反序列化用的元數(shù)據(jù)

  • 存在對(duì)象引用、繼承的情況下,就是遞歸遍歷“寫對(duì)象”邏輯

所以,從這個(gè)例子我們可以知道,任何一種序列化的方式,其核心思想就要設(shè)計(jì)一套將對(duì)象轉(zhuǎn)換成某種特定格式的二進(jìn)制數(shù)據(jù),接著反序列化的時(shí)候,就能根據(jù)規(guī)則從二進(jìn)制數(shù)據(jù)解析出對(duì)象。

這么看,序列化其實(shí)就是一種協(xié)議,序列化方和反序列化方都要遵循相同的規(guī)則,否則就無(wú)法得到正常的數(shù)據(jù)。

不過,JDK 原生序列化缺陷就是不能跨語(yǔ)言,只能在 Java 生態(tài)里使用。

JSON

JSON 數(shù)據(jù)的格式相信大家都很熟悉了吧,在 Web 應(yīng)用里特別常見,通常后端和前端的耦合就是 JSON 數(shù)據(jù)。

JSON 是典型的 Key-Value 方式,沒有數(shù)據(jù)類型。很多語(yǔ)言都實(shí)現(xiàn)了 JSON 序列化的第三庫(kù),所以 JSON 序列化的方式可以跨語(yǔ)言。

比如,Java 語(yǔ)言中常用的 JSON 第三方類庫(kù)有:

  • Gson: 谷歌開發(fā)的 JSON 庫(kù),功能十分全面。

  • FastJson: 阿里巴巴開發(fā)的 JSON 庫(kù),性能十分優(yōu)秀。

  • Jackson: 社區(qū)十分活躍且更新速度很快。

我這里說個(gè) C 的 JSON 第三方庫(kù):JSON for Modern C 。

使用起來很方便,僅需要包含一個(gè)頭文件“json.hpp”,沒有外部依賴,也不需要額外的安裝、編譯、鏈接工作,適合快速上手開發(fā)。

因?yàn)?JSON 是 Key-Value 形式,所以 JSON for Modern C 的操作和標(biāo)準(zhǔn)容器 map 一樣,用關(guān)聯(lián)數(shù)組的“[]”來添加任意數(shù)據(jù)。

這里貼幾個(gè)例子:

//?給?JSON?for?Modern?C ?的?json?類取個(gè)別名
using?json_t?=?nlohmann::json;??
//?JSON對(duì)象
json_t?j;???????????????????????????????????

//?"age":18
j["age"]?=?18;
//?"name":"xiaolin"
j["name"]?=?"xiaolin";????
//?"gear":{"suits":"2099"}
j["gear"]["suits"]?=?"2099";?
//?"jobs":["superhero"]??
j["jobs"]?=?{"superhero"};??????????????????

vector<int>?v?=?{1,2,3};??
//?"numbers":[1,2,3]
j["numbers"]?=?v;??????????????????????????

map<string,?int>?m?=???????????????????????
????{{"one",?1},?{"two",?2}};????
//?"kv":{"one":1,"two":2}
j["kv"]?=?m;???????????????????????????????
添加完 JSON 數(shù)據(jù)后,就可以調(diào)用成員函數(shù) dump() 進(jìn)行初始化,得到 JSON 文本形式,也就是 JSON 字符串:

cout?<endl;
反序列化也很簡(jiǎn)單,只要調(diào)用靜態(tài)成員函數(shù) parse() 就行,直接得到 JSON 對(duì)象:

//?JSON文本,原始字符串
string?jsonStr?=?R"({???????????????
????"name":?"xiaolin",
????"age"?:?18
})"
;

//?從字符串反序列化
json_t?j?=?json_t::parse(jsonStr);????

//?驗(yàn)證序列化是否正確
assert(j["age"]?==?18);????????
assert(j["name"]?==?"xiaolin");
對(duì)于通常的應(yīng)用來說,掌握了基本的序列化和反序列化就夠用了,如果想要了解 JSON for Modern C 其他特性,可以去看它的 Github。

JSON 進(jìn)行序列化存在的問題,因?yàn)?JSON 進(jìn)行序列化的額外空間開銷比較大,對(duì)于大數(shù)據(jù)量服務(wù)這意味著需要巨大的內(nèi)存和磁盤開銷,所以如果在傳輸數(shù)據(jù)量比較小的場(chǎng)景,就可以采用 JSON 序列化的方式。

ProtoBuffer

ProtoBuf 是由 Google 出品的,是一種輕便、高效的結(jié)構(gòu)化數(shù)據(jù)存儲(chǔ)格式,可以用于結(jié)構(gòu)化數(shù)據(jù)序列化,支持 Java、Python、C 、Go 等語(yǔ)言。

Protobuf 使用的時(shí)候必須寫一個(gè) IDL(Interface description language)文件,在里面定義好數(shù)據(jù)結(jié)構(gòu),只有預(yù)先定義了的數(shù)據(jù)結(jié)構(gòu),才能被序列化和反序列化。

下面是一個(gè)簡(jiǎn)單的 IDl 文件格式:

syntax = "proto2"; // 使用第2版
package sample; // 定義名字空間

message Person { // 定義消息
required string name = 1; // required表示必須字段
required int32 id = 2;
optional string email = 3; // optional字段可以沒有
}
寫完 IDL 文件后,然后使用不同語(yǔ)言的 IDL 編譯器,生成序列化工具類。

Protobuf 在 Github 有文檔介紹了不同語(yǔ)言是怎么使用編譯器生成序列化工具類了,我在這里就不介紹了。

這里貼 C 和 Java 語(yǔ)言使用 Protobuf 相關(guān)接口進(jìn)行序列化和反序列化的例子。

C 進(jìn)行序列化和反序列化的例子:

//?類型別名
using?person_t?=?sample::Person;????????

//?聲明一個(gè)Protobuf對(duì)象
person_t?p;?????????????????????????????

//?設(shè)置每個(gè)字段的值?
p.set_id(1);???????????????????????????
p.set_name("xiaolin");
p.set_email("xiaolincoding@163.com");

//?序列化到字符串?
string?enc;
p.SerializeToString(
本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除。
關(guān)閉