基于 socket 進行對象傳輸
先舉個簡單的例子,基于我們前面幾次課程的只是,寫一個 socket 通信的代碼
User.java
public class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
SocketServerProvider.java
public class SocketServerProvider {
public static void main(String[] args) throws
IOException {
ServerSocket serverSocket = null;
BufferedReader in = null;
try {
serverSocket = new ServerSocket(8080);
Socket socket = serverSocket.accept();
ObjectInputStream objectInputStream =
new ObjectInputStream(socket.getInputStream());
User user=(User)objectInputStream.readObject();
System.out.println(user);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (serverSocket != null) {
serverSocket.close();
}
}
}
}
SocketClientConsumer.java
public class SocketClientConsumer {
public static void main(String[] args) {
Socket socket = null;
ObjectOutputStream out = null;
try {
socket = new Socket("127.0.0.1", 8080);
User user = new User();
out = new
ObjectOutputStream(socket.getOutputStream());
out.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
運行結果
這段代碼運行以后瘸爽,能夠實現(xiàn) Java 對象的正常傳輸嗎? 很顯然铅忿,會報錯
如何解決報錯的問題呢剪决?
對 User 這個對象實現(xiàn)一個 Serializable 接口,再次運行就可以看到對象能夠正常傳輸了
public class User implements Serializable {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
了解序列化的意義
我們發(fā)現(xiàn)對 User 這個類增加一個 Serializable檀训,就可以解決 Java 對象的網絡傳輸問題柑潦。這就是今天想給大家講解的序列化這塊的意義。
Java 平臺允許我們在內存中創(chuàng)建可復用的 Java 對象峻凫,但一般情況下渗鬼,只有當 JVM 處于運行時,這些對象才可能存在荧琼,即譬胎,這些對象的生命周期不會比 JVM 的生命周期更長。但在現(xiàn)實應用中命锄,就可能要求在 JVM 停止運行之后能夠保存(持久化)指定的對象堰乔,并在將來重新讀取被保存的對象。 Java 對象序列化就能夠幫助我們實現(xiàn)該功能脐恩。
簡單來說
序列化是把對象的狀態(tài)信息轉化為可存儲或傳輸的形式過程镐侯,也就是把對象轉化為字節(jié)序列的過程稱為對象的序列化。
反序列化是序列化的逆向過程驶冒,把字節(jié)數組反序列化為對象苟翻,把字節(jié)序列恢復為對象的過程成為對象的反序列化
序列化的高階認識
簡單認識一下 Java 原生序列化
前面的代碼中演示了,如何通過 JDK 提供了 Java 對象的序列化方式實現(xiàn)對象序列化傳輸骗污,主要通過輸出流java.io.ObjectOutputStream和對象輸入流java.io.ObjectInputStream來實現(xiàn)袜瞬。
java.io.ObjectOutputStream:表示對象輸出流 , 它的 writeObject(Object obj)方法可以對參數指定的 obj 對象進行序列化,把得到的字節(jié)序列寫到一個目標輸出流中身堡。
java.io.ObjectInputStream:表示對象輸入流 ,它的 readObject()方法源輸入流中讀取字節(jié)序列,再把它們反序列化成為一個對象拍鲤,并將其返回贴谎。
需要注意的是,被序列化的對象需要實現(xiàn) java.io.Serializable 接口
-
serialVersionUID 的作用
在 IDEA 中通過如下設置可以生成 serializeid
字面意思上是序列化的版本號季稳,凡是實現(xiàn) Serializable 接口的類都有一個表示序列化版本標識
符的靜態(tài)變量
演示步驟
- 先將 user 對象序列化到文件中
- 然后修改 user 對象擅这,增加 serialVersionUID 字段
- 然后通過反序列化來把對象提取出來
- 演示預期結果:提示無法反序列化
結論
Java 的序列化機制是通過判斷類的 serialVersionUID 來驗證版本一致性的。在進行反序列化時景鼠, JVM 會把傳來的字節(jié)流中的 serialVersionUID 與本地相應實體類的 serialVersionUID 進行比較仲翎,如果相同就認為是一致的痹扇,可以進行反序列化,否則就會出現(xiàn)序列化版本不一致的異常溯香,即是 InvalidCastException鲫构。
從結果可以看出,文件流中的 class 和 classpath 中的 class玫坛,也就是修改過后的 class结笨,不兼容了,處于安全機制考慮湿镀,程序拋出了錯誤炕吸,并且拒絕載入。從錯誤結果來看勉痴,如果沒有為指定的 class 配置 serialVersionUID赫模,那么 java 編譯器會自動給這個 class 進行一個摘要算法,類似于指紋算法蒸矛,只要這個文件有任何改動瀑罗,得到的 UID 就會截然不同的,可以保證在這么多類中莉钙,這個編號是唯一的廓脆。所以,由于沒有顯指定 serialVersionUID磁玉,編譯器又為我們生成了一個 UID停忿,當然和前面保存在文件中的那個不會一樣了,于是就出現(xiàn)了 2 個序列化版本號不一致的錯誤蚊伞。因此席赂,只要我們自己指定了 serialVersionUID,就可以在序列化后时迫,去添加一個字段颅停,或者方法,而不會影響到后期的還原掠拳,還原后的對象照樣可以使用癞揉,而且還多了方法或者屬性可以用。
tips: serialVersionUID 有兩種顯示的生成方式:
一是默認的 1L溺欧,比如: private static final long serialVersionUID = 1L;
二是根據類名喊熟、接口名、成員方法及屬性等來生成一個 64 位的哈希字段,當實現(xiàn) java.io.Serializable 接口的類沒有顯式地定義一個 serialVersionUID 變量時候姐刁, Java 序列化機制會根據編譯的 Class 自動生成一個 serialVersionUID 作序列化版本比較用芥牌,這種情況下,如果 Class 文件(類名聂使,方法明等)沒有發(fā)生變化(增加空格壁拉,換行谬俄,增加注釋等等),就算再編譯多次弃理, serialVersionUID 也不會變化的溃论。
Transient 關鍵字
Transient 關鍵字的作用是控制變量的序列化,在變量聲明前加上該關鍵字案铺,可以阻止該變量被序列化到文件中蔬芥,在被反序列化后, transient 變量的值被設為初始值控汉,如 int 型的是0笔诵,對象型的是 null。-
writeObject 和 readObject 原理
writeObject 和 readObject 是兩個私有的方法姑子,他們是什么時候被調用的呢乎婿?從運行結果來看,它確實被調用街佑。而且他們并不存在于 Java.lang.Object谢翎,也沒有在 Serializable 中去聲明。
我們唯一的猜想應該還是和 ObjectInputStream 和 ObjectOutputStream 有關系沐旨,所以基于這個入口去看看在哪個地方有調用
從源碼層面來分析可以看到森逮, readObject 是通過反射來調用的。
其實我們可以在很多地方看到 readObject 和 writeObject 的使用磁携,比如 HashMap褒侧。
Java 序列化的一些簡單總結
- Java 序列化只是針對對象的狀態(tài)進行保存,至于對象中的方法谊迄,序列化不關心
- 當一個父類實現(xiàn)了序列化闷供,那么子類會自動實現(xiàn)序列化,不需要顯示實現(xiàn)序列化接口
- 當一個對象的實例變量引用了其他對象统诺,序列化這個對象的時候會自動把引用的對象也進
行序列化(實現(xiàn)深度克峦嵩唷) - 當某個字段被申明為 transient 后,默認的序列化機制會忽略這個字段
- 被申明為 transient 的字段粮呢,如果需要序列化婿失,可以添加兩個私有方法: writeObject 和
readObject
分布式架構下常見序列化技術
初步了解了 Java 序列化的知識以后,我們又得回到分布式架構中啄寡,了解序列化的發(fā)展過程豪硅。
隨著分布式架構、微服務架構的普及这难。服務與服務之間的通信成了最基本的需求。這個時候葡秒,我們不僅需要考慮通信的性能姻乓,也需要考慮到語言多元化問題嵌溢。所以,對于序列化來說蹋岩,如何去提升序列化性能以及解決跨語言問題赖草,就成了一個重點考慮的問題。
由于 Java 本身提供的序列化機制存在兩個問題
- 序列化的數據比較大剪个,傳輸效率低
- 其他語言無法識別和對接
以至于在后來的很長一段時間秧骑,基于 XML 格式編碼的對象序列化機制成為了主流,一方面解決了多語言兼容問題扣囊,另一方面比二進制的序列化方式更容易理解乎折。以至于基于 XML的 SOAP協(xié)議及對應的 WebService 框架在很長一段時間內成為各個主流開發(fā)語言的必備的技術。
再到后來侵歇,基于 JSON 的簡單文本格式編碼的 HTTP REST 接口又基本上取代了復雜的 Web Service 接口骂澄,成為分布式架構中遠程通信的首要選擇。但是 JSON 序列化存儲占用的空間大惕虑、性能低等問題坟冲,同時移動客戶端應用需要更高效的傳輸數據來提升用戶體驗。在這種情況下與語言無關并且高效的二進制編碼協(xié)議就成為了大家追求的熱點技術之一溃蔫。首先誕生的一個開源的二進制序列化框架-MessagePack健提。它比 google 的 Protocol Buffers 出現(xiàn)得還要早。
簡單了解各種序列化技術
XML 序列化框架介紹
XML 序列化的好處在于可讀性好伟叛,方便閱讀和調試私痹。但是序列化以后的字節(jié)碼文件比較大,而且效率不高痪伦,適用于對性能不高侄榴,而且 QPS 較低的企業(yè)級內部系統(tǒng)之間的數據交換的場景,同時 XML 又具有語言無關性网沾,所以還可以用于異構系統(tǒng)之間的數據交換和協(xié)議癞蚕。比如我們熟知的Webservice,就是采用 XML 格式對數據進行序列化的辉哥。 XML 序列化/反序列化的實現(xiàn)方式有很多桦山,熟知的方式有 XStream 和 Java 自帶的 XML 序列化和反序列化兩種。JSON 序列化框架
JSON(JavaScript Object Notation)是一種輕量級的數據交換格式醋旦,相對于 XML 來說恒水, JSON的字節(jié)流更小,而且可讀性也非常好∷瞧耄現(xiàn)在 JSON 數據格式在企業(yè)運用是最普遍的钉凌。
JSON 序列化常用的開源工具有很多:
- Jackson (https://github.com/FasterXML/jackson)
- 阿里開源的 FastJson (https://github.com/alibaba/fastjon)
- Google 的 GSON (https://github.com/google/gson)
這幾種 json 序列化工具中, Jackson 與 fastjson 要比 GSON 的性能要好捂人,但是 Jackson御雕、GSON 的穩(wěn)定性要比 Fastjson 好矢沿。而 fastjson 的優(yōu)勢在于提供的 api 非常容易使用。
Hessian 序列化框架
Hessian 是一個支持跨語言傳輸的二進制序列化協(xié)議酸纲,相對于 Java 默認的序列化機制來說捣鲸,Hessian 具有更好的性能和易用性,而且支持多種不同的語言闽坡。
實際上 Dubbo 采用的就是 Hessian 序列化來實現(xiàn)栽惶,只不過 Dubbo 對 Hessian 進行了重構,性能更高疾嗅。Avro 序列化
Avro 是一個數據序列化系統(tǒng)外厂,設計用于支持大批量數據交換的應用。它的主要特點有:支持二進制序列化方式宪迟,可以便捷酣衷,快速地處理大量數據;動態(tài)語言友好次泽, Avro 提供的機制使動態(tài)語言可以方便地處理 Avro 數據穿仪。kyro 序列化框架
Kryo 是一種非常成熟的序列化實現(xiàn),已經在 Hive意荤、 Storm)中使用得比較廣泛啊片,不過它不能跨語言. 目前 dubbo 已經在 2.6 版本支持 kyro 的序列化機制。它的性能要優(yōu)于之前的hessian2Protobuf 序列化框架
Protobuf 是 Google 的一種數據交換格式玖像,它獨立于語言紫谷、獨立于平臺。 Google 提供了多種語言來實現(xiàn)捐寥,比如 Java笤昨、 C、 Go握恳、 Python瞒窒,每一種實現(xiàn)都包含了相應語言的編譯器和庫文件,Protobuf 是一個純粹的表示層協(xié)議乡洼,可以和各種傳輸層協(xié)議一起使用崇裁。
Protobuf 使用比較廣泛,主要是空間開銷小和性能比較好束昵,非常適合用于公司內部對性能要求高的 RPC 調用拔稳。 另外由于解析性能比較高,序列化以后數據量相對較少锹雏,所以也可以應用在對象的持久化場景中巴比。
但是要使用 Protobuf 會相對來說麻煩些,因為他有自己的語法,有自己的編譯器轻绞,如果需要用到的話必須要去投入成本在這個技術的學習中腰耙。
protobuf 有個缺點就是要傳輸的每一個類的結構都要生成對應的 proto 文件,如果某個類發(fā)生修改铲球,還得重新生成該類對應的 proto 文件。
Protobuf 序列化的原理
那么接下來著重分析一下 protobuf 的序列化原理晰赞,前面說過它的優(yōu)勢是空間開銷小稼病,性能也相對較好。它里面用到的一些算法還是值得我們去學習的掖鱼。
protobuf 的基本應用
使用 protobuf 開發(fā)的一般步驟是
- 配置開發(fā)環(huán)境然走,安裝 protocol compiler 代碼編譯器
- 編寫.proto 文件,定義序列化對象的數據結構
- 基于編寫的.proto 文件戏挡,使用 protocol compiler 編譯器生成對應的序列化/反序列化工具類
- 基于自動生成的代碼芍瑞,編寫自己的序列化應用
Protobuf 案例演示
下載 protobuf 工具,https://github.com/google/protobuf/releases褐墅, 找到 protoc-3.5.1-win32.zip拆檬。
編寫 proto 文件
syntax="proto2";
package com.gupaoedu.serial;
option java_package =
"com.gupaoedu.serial";
option java_outer_classname="UserProtos";
message User {
required string name=1;
required int32 age=2;
}
數據類型:string / bytes / bool / int32(4 個字節(jié))/ int64 / float / double / enum 枚舉類 / message 自定義類
修飾符:required 表示必填字段;optional 表示可選字段妥凳;repeated 可重復竟贯,表示集合。
1逝钥, 2屑那, 3, 4 需要在當前范圍內是唯一的艘款,表示順序持际。
生成實體類
【.\protoc.exe --java_out=./ ./user.proto】
實現(xiàn)序列化
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>RELEASE</version>
</dependency>
UserProtos.User user=UserProtos.User.newBuilder()
.setName("Mic")
.setAge(18).build();
ByteString bytes=user.toByteString();
System.out.println(bytes);
UserProtos.User nUser=UserProtos.User.parseFrom(bytes);
System.out.println(nUser);
protobuf 序列化原理
我們可以把序列化以后的數據打印出來看看結果
我們可以看到,序列化出來的數字基本看不懂哗咆,但是序列化以后的數據確實很小蜘欲,那我們接下來帶大家去了解一下底層的原理。
正常來說岳枷,要達到最小的序列化結果芒填,一定會用到壓縮的技術,而 protobuf 里面用到了兩種壓縮算法空繁,一種是 varint殿衰,另一種是 zigzag。
varint
先說第一種盛泡,我們先來看 age=300 這個數字是如何被壓縮的
這兩個字節(jié)字節(jié)分別的結果是: -84 闷祥、 2
-84 怎么計算來的呢? 我們知道在二進制中表示負數的方法,高位設置為 1凯砍, 并且是對應數字的二進制取反以后再計算補碼表示(補碼是反碼+1)
所以如果要反過來計算
- 【補碼】 10101100 -1 得到 10101011
- 【反碼】 01010100 得到的結果為 84. 由于高位是 1箱硕,表示負數所以結果為-84
字符如何轉化為編碼
“Mic”這個字符,需要根據 ASCII 對照表轉化為數字悟衩。
M =77剧罩、 i=105、 c=99
所以結果為 77 105 99
大家肯定有個疑問座泳,這里的結果為什么直接就是 ASCII 編碼的值呢惠昔?怎么沒有做壓縮呢?有沒有同學能夠回答出來挑势。
原因是镇防, varint 是對字節(jié)碼做壓縮,但是如果這個數字的二進制只需要一個字節(jié)表示的時候潮饱,其實最終編碼出來的結果是不會變化的来氧。
還有兩個數字, 3 和 16 代表什么呢香拉?那就要了解 protobuf 的存儲格式了
存儲格式
protobuf 采用 T-L-V 作為存儲方式
tag 的計算方式是 field_number(當前字段的編號) << 3 | wire_type
比如 Mic 的字段編號是 1 啦扬,類型 wire_type 的值為 2 所以 : 1 <<3 | 2 =10
age=300 的字段編號是 2,類型 wire_type 的值是 0凫碌, 所以 : 2<<3|0 =16
第一個數字 10考传,代表的是 key,剩下的都是 value证鸥。
負數的存儲
在計算機中僚楞,負數會被表示為很大的整數,因為計算機定義負數符號位為數字的最高位枉层,所以如果采用 varint 編碼表示一個負數泉褐,那么一定需要 5 個比特位。所以在 protobuf 中通過sint32/sint64 類型來表示負數鸟蜡,負數的處理形式是先采用 zigzag 編碼(把符號數轉化為無符號數)膜赃,在采用 varint 編碼。
sint32: (n << 1) ^ (n >> 31)
sint64: (n << 1) ^ (n >> 63)
比如存儲一個(-300)的值
-300
原碼: 0001 0010 1100
取反: 1110 1101 0011
加 1 : 1110 1101 0100
n<<1: 整體左移一位揉忘,右邊補 0 -> 1101 1010 1000
n>>31: 整體右移 31 位跳座,左邊補 1 -> 1111 1111 1111
n<<1 ^ n >>31
1101 1010 1000 ^ 1111 1111 1111 = 0010 0101 0111
十進制: 0010 0101 0111 = 599
varint 算法: 從右往做,選取 7 位泣矛,高位補 1/0(取決于字節(jié)數)
得到兩個字節(jié)
1101 0111 0000 0100
-41 疲眷、 4
總結
Protocol Buffer 的性能好,主要體現(xiàn)在 序列化后的數據體積小 & 序列化速度快您朽,最終使得
傳輸效率高狂丝,其原因如下:
序列化速度快的原因:
a. 編碼 / 解碼 方式簡單(只需要簡單的數學運算 = 位移等等)
b. 采用 Protocol Buffer 自身的框架代碼 和 編譯器 共同完成
序列化后的數據量體積小(即數據壓縮效果好)的原因:
a. 采用了獨特的編碼方式,如 Varint几颜、 Zigzag 編碼方式等等
b. 采用 T - L - V 的數據存儲方式:減少了分隔符的使用 & 數據存儲得緊湊
序列化技術的選型
- 技術層面
- 序列化空間開銷倍试,也就是序列化產生的結果大小,這個影響到傳輸的性能
- 序列化過程中消耗的時長蛋哭,序列化消耗時間過長影響到業(yè)務的響應時間
- 序列化協(xié)議是否支持跨平臺县习,跨語言。因為現(xiàn)在的架構更加靈活谆趾,如果存在異構系統(tǒng)通信需求准颓,那么這個是必須要考慮的
- 可擴展性/兼容性,在實際業(yè)務開發(fā)中棺妓,系統(tǒng)往往需要隨著需求的快速迭代來實現(xiàn)快速更新,這就要求我們采用的序列化協(xié)議基于良好的可擴展性/兼容性炮赦,比如在現(xiàn)有的序列化數據結構中新增一個業(yè)務字段怜跑,不會影響到現(xiàn)有的服務
- 技術的流行程度,越流行的技術意味著使用的公司多吠勘,那么很多坑都已經淌過并且得到了解決性芬,技術解決方案也相對成熟
- 學習難度和易用性
- 選型建議
- 對性能要求不高的場景,可以采用基于 XML 的 SOAP 協(xié)議剧防;
- 對性能和間接性有比較高要求的場景植锉,那么 Hessian、 Protobuf峭拘、 Thrift、 Avro 都可以;
- 基于前后端分離庭呜,或者獨立的對外的 api 服務懈贺,選用 JSON 是比較好的,對于調試拣展、可讀性都很不錯彭沼;
- Avro 設計理念偏于動態(tài)類型語言,那么這類的場景使用 Avro 是可以的备埃。
各個序列化技術的性能比較
這 個 地 址 有 針 對 不 同 序 列 化 技 術 進 行 性 能 比 較 :https://github.com/eishay/jvmserializers/wiki