什么是protobuf
它是一個對象序列化/反序列化的工具,什么是對象的序列化/反序列化?就是把一個Java堆中存活的對象轉換成一串二進制編碼,然后該編碼可以用於本地存儲和網絡傳輸。反序列化就是根據一串二進制編碼還原出原來的那個對象,protobuf能夠將一個對象以特定的格式轉換為一個二進制串(序列化),然后將二進制串還原成對象(反序列化)。這里涉及到兩個指標:
對同一個目標對象:
1)序列化和反序列化的時間開銷,
2)序列化之后串的長度
protobuf在這兩個方面都有非常出色的表現(網傳)
在Windows下使用protobuf的步驟如下:
第一步:
下載protoc-2.5.0-win32.zip,得到其中的protoc.exe.然后將該protoc.exe的存放路徑加入Path環境變量,便於訪問。比如,我的protoc.exe存放於D:/protobuf,環境變量中添加如下配置:
D:/protobuf
第二步:
編寫.proto文件,它是序列化一個對象的“模板”,protobuf就是根據它來決定如何序列化和反序列化。
編寫的person-entity.proto配置文件如下:
option java_outer_classname = "PersonEntity";//生成的數據訪問類的類名 message Person { required int32 id = 1;//同上 required string name = 2;//必須字段,在后面的使用中必須為該段設置值 optional string email = 3;//可選字段,在后面的使用中可以自由決定是否為該字段設置值 }
message字段代表了一個對象,所以,可以使用message實現對象的嵌套序列化
required表示是強制字段,在后面的使用中必須為該字段設置值;
optional表示是可選字段,在后面的使用中可選地為該字段設置值;
repeated表示集合類型,可以填充多個數據
后面的1,2,3是字段的編號,字段名是讓用戶使用的,字段編號則是讓系統識別的,從1開始。
protobuf自定義的數據類型和Java的數據類型的對應關系見這篇博客https://blog.csdn.net/chuhui1765/article/details/100670318
可以指定字段類型為其他的Message字段類型:
message SearchResponse{ repeated Result result = 1; } message Result{ required string url = 1; optional string title = 2; repeated string snippets = 3; }
可以在message內部嵌套另一個message:
message SearchResponse{ message Result{ required string url = 1; optional string title = 2; repeated string snippets = 3; } repeated Result result = 1; }
有關protobuf更詳細的用法,可以參考這篇優秀的博文https://worktile.com/tech/share/prototol-buffers
第三步:
用protoc.exe生成PersonEntity.class。打開DOS窗口,輸入如下的編譯命令:
protoc.exe -I=D:/protobuf --java_out=D:/protobuf D:/protobuf/person-entity.proto
編譯命令的格式如下:
protoc.exe -I=.protoc文件的存放路徑 --java_out=.class文件的輸出路徑 .protoc文件的具體存放路徑
其中第三個路徑是必須的,而且要寫明.protoc文件
最后會生成PersonEntity.java文件,可以把它理解為一個工具類,幫助我們執行對象的序列化。
第四步:
新建maven項目,將PersonEntity.java復制到項目中,引入maven依賴
<dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version>2.5.0</version> </dependency>
利用工具類序列化和反序列化對象。
/* author:chxy data:2020/4/1 description: */ import java.io.IOException; public class Test { public static void main(String[] args) throws IOException { //模擬將對象轉成byte[],方便傳輸 PersonEntity.Person.Builder builder = PersonEntity.Person.newBuilder(); builder.setId(1); builder.setName("abc"); builder.setEmail("def"); PersonEntity.Person person = builder.build(); System.out.println("after :" +person.toString()); final byte[] bytes = person.toByteArray(); System.out.println("===========Person Byte=========="); for(byte b : person.toByteArray()){ System.out.print(b); } System.out.println(); //反序列化成Person類 byte[] byteArray =person.toByteArray(); PersonEntity.Person person2 = PersonEntity.Person.parseFrom(byteArray); System.out.println("after :" +person2.toString()); } }
這里還犯過一個錯:另外自定義一個Person類與PersonEntity.Person相對應。實際上在我們的項目中可以直接利用這個PersonEntity.Person來實現功能需求。
下面同樣以這個Person類序列化為例,來實際地比較通過各種不同的序列化方式得到的的字節數組長度:
1.protobuf
序列化長度為:
System.out.println(person.getSerializedSize());
結果為:12
2.Java原生序列化
/* author:chxy data:2020/4/2 description: */ import java.io.*; public class Test2 { public static void main(String[] args) throws IOException { Person p = new Person(1,"abc","def"); ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream("E:/object.txt")); objectOutputStream.writeObject(p); FileInputStream fileInputStream = new FileInputStream("E:/object.txt"); final int length = fileInputStream.read(); System.out.println(length); } }
結果為:172
3.通過對象的toString()
序列化:重寫對象的toString()方法,按照特定的格式,將對象的各個字段封裝進一個String字符串,再將字符串按照特定的編碼方式,轉換成byte數組,實現本地存儲和網絡傳輸。
反序列化:對byte數組,重新轉換成字符串,再將字符串按照約定的格式,還原成對象的屬性,從而構造出原來的對象。
這里重點探討的是:String對象轉換成byte數組之后的長度
public static void main(String[] args) { String s = "abc"; System.out.println(s.getBytes().length); String s2 = "嚶嚶嚶"; System.out.println(s2.getBytes().length);
System.out.println(Charset.defaultCharset());
}
輸出結果為:
3
9
UTF-8
可見,如果是英文字母每個字符占一個字節,如果是漢字,每個字符占3個字節。如果通過這種方式序列化,它的空間效率也是非常高的。
這里有必要搞清楚Java默認的字符編碼集:
/** * Encodes this {@code String} into a sequence of bytes using the * platform's default charset, storing the result into a new byte array. * * <p> The behavior of this method when this string cannot be encoded in * the default charset is unspecified. The {@link * java.nio.charset.CharsetEncoder} class should be used when more control * over the encoding process is required. * * @return The resultant byte array * * @since JDK1.1 */ public byte[] getBytes() { return StringCoding.encode(value, 0, value.length); }
Java采用平台的默認字符編碼集來進行字符與二進制byte序列的轉換。當然也可以在序列化的時候顯示指定字符編碼集。
Unicode 與 UTF-8
為什么會出現unicode,這首先要從ASCII碼說起,它起源最早,用7位來表示英文字母、數字等字符。但是后來又出現了很多其他語言的字符,比如漢字,ASCII碼最多只能表示256個字符,無法支持這些”異域“字符,因此出現了unicode碼,將各種語言的字符用一種統一的編碼方式轉換成01序列。但是新的問題又出現了,unicode編碼效率不高,原來一個字符a用ASCII表示只需要一個字符,現在用unicode可能用3個字符,存儲效率和網絡傳輸效率大打折扣。因此出現了中間轉換編碼集,utf-8就是其中之一。”UTF-8(8-bit Unicode Transformation Format)是一種針對Unicode的可變長度字符編碼,又稱萬國碼“,注意,它的編碼長度是可變的。所以,unicode是utf-8的基礎,utf-8是對unicode的一種解釋。