在java中socket傳輸資料時,資料類型往往比較難選擇。可能要考慮帶寬、跨語言、版本的相容等問題。比較常見的做法有兩種:一是把對象包裝成JSON字元串傳輸,二是采用java對象的序列化和反序列化。随着Google工具protoBuf的開源,protobuf也是個不錯的選擇。對JSON,Object Serialize,ProtoBuf 做個對比。
定義一個待傳輸的對象UserVo:
public class UserVo{
private String name;
private int age;
private long phone;
private List<UserVo> friends;
……
}
初始化UserVo的執行個體src:
UserVo src = new UserVo();
src.setName("Yaoming");
src.setAge(30);
src.setPhone(13789878978L);
UserVo f1 = new UserVo();
f1.setName("tmac");
f1.setAge(32);
f1.setPhone(138999898989L);
UserVo f2 = new UserVo();
f2.setName("liuwei");
f2.setAge(29);
f2.setPhone(138999899989L);
List<UserVo> friends = new ArrayList<UserVo>();
friends.add(f1);
friends.add(f2);
src.setFriends(friends);
JSON格式
采用Google的gson-2.2.2.jar 進行轉義
Gson gson = new Gson();
String json = gson.toJson(src);
得到的字元串:
{"name":"Yaoming","age":30,"phone":13789878978,"friends":[{"name":"tmac","age":32,"phone":138999898989},{"name":"liuwei","age":29,"phone":138999899989}]}
位元組數為153
Json的優點:明文結構一目了然,可以跨語言,屬性的增加減少對解析端影響較小。缺點:位元組數過多,依賴于不同的第三方類庫。
Object Serialize
UserVo實作Serializalbe接口,提供唯一的版本号:
public class UserVo implements Serializable{
private static final long serialVersionUID = -5726374138698742258L;
private String name;
private int age;
private long phone;
private List<UserVo> friends;
序列化方法:
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream os = new ObjectOutputStream(bos);
os.writeObject(src);
os.flush();
os.close();
byte[] b = bos.toByteArray();
bos.close();
位元組數是238
反序列化:
ObjectInputStream ois = new ObjectInputStream(fis);
vo = (UserVo) ois.readObject();
ois.close();
fis.close();
Object Serializalbe 優點:java原生支援,不需要提供第三方的類庫,使用比較簡單。缺點:無法跨語言,位元組數占用比較大,某些情況下對于對象屬性的變化比較敏感。
對象在進行序列化和反序列化的時候,必須實作Serializable接口,但并不強制聲明唯一的serialVersionUID
是否聲明serialVersionUID對于對象序列化的向上向下的相容性有很大的影響。我們來做個測試:
思路一
把UserVo中的serialVersionUID去掉,序列化儲存。反序列化的時候,增加或減少個字段,看是否成功。
public class UserVo implements Serializable{
private String name;
private int age;
private long phone;
private List<UserVo> friends;
儲存到檔案中:
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream os = new ObjectOutputStream(bos);
os.writeObject(src);
os.flush();
os.close();
byte[] b = bos.toByteArray();
bos.close();
FileOutputStream fos = new FileOutputStream(dataFile);
fos.write(b);
fos.close();
增加或者減少字段後,從檔案中讀出來,反序列化:
FileInputStream fis = new FileInputStream(dataFile);
ObjectInputStream ois = new ObjectInputStream(fis);
vo = (UserVo) ois.readObject();
ois.close();
fis.close();
結果:抛出異常資訊
Exception in thread "main" java.io.InvalidClassException: serialize.obj.UserVo; local class incompatible: stream classdesc serialVersionUID = 3305402508581390189, local class serialVersionUID = 7174371419787432394
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:560)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1582)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1495)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1731)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1328)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:350)
at serialize.obj.ObjectSerialize.read(ObjectSerialize.java:74)
at serialize.obj.ObjectSerialize.main(ObjectSerialize.java:27)
思路二
eclipse指定生成一個serialVersionUID,序列化儲存,修改字段後反序列化
略去代碼
結果:反序列化成功
結論
如果沒有明确指定serialVersionUID,序列化的時候會根據字段和特定的算法生成一個serialVersionUID,當屬性有變化時這個id發生了變化,是以反序列化的時候就會失敗。抛出“本地classd的唯一id和流中class的唯一id不比對”。
jdk文檔關于serialVersionUID的描述:
寫道 如果可序列化類未顯式聲明 serialVersionUID,則序列化運作時将基于該類的各個方面計算該類的預設 serialVersionUID 值,如“Java(TM) 對象序列化規範”中所述。不過, 強烈建議 所有可序列化類都顯式聲明 serialVersionUID 值,原因是計算預設的 serialVersionUID 對類的詳細資訊具有較高的敏感性,根據編譯器實作的不同可能千差萬别,這樣在反序列化過程中可能會導緻意外的 InvalidClassException。是以,為保證 serialVersionUID 值跨不同 java 編譯器實作的一緻性,序列化類必須聲明一個明确的 serialVersionUID 值。還強烈建議使用 private 修飾符顯示聲明 serialVersionUID(如果可能),原因是這種聲明僅應用于直接聲明類 -- serialVersionUID 字段作為繼承成員沒有用處。數組類不能聲明一個明确的 serialVersionUID,是以它們總是具有預設的計算值,但是數組類沒有比對 serialVersionUID 值的要求。
Google ProtoBuf
protocol buffers 是google内部得一種傳輸協定,目前項目已經開源(http://code.google.com/p/protobuf/)。它定義了一種緊湊得可擴充得二進制協定格式,适合網絡傳輸,并且針對多個語言有不同得版本可供選擇。
以protobuf-2.5.0rc1為例,準備工作:
下載下傳源碼,解壓,編譯,安裝
tar zxvf protobuf-2.5.0rc1.tar.gz
./configure
./make
./make install
測試:
MacBook-Air:~ ming$ protoc --version
libprotoc 2.5.0
安裝成功!進入源碼得java目錄,用mvn工具編譯生成所需得jar包,protobuf-java-2.5.0rc1.jar
1、編寫.proto檔案,命名UserVo.proto
package serialize;
option java_package = "serialize";
option java_outer_classname="UserVoProtos";
message UserVo{
optional string name = 1;
optional int32 age = 2;
optional int64 phone = 3;
repeated serialize.UserVo friends = 4;
}
2、在指令行利用protoc 工具生成builder類
protoc -IPATH=.proto檔案所在得目錄 --java_out=java檔案的輸出路徑 .proto的名稱
得到UserVoProtos類
3、編寫序列化代碼
UserVoProtos.UserVo.Builder builder = UserVoProtos.UserVo.newBuilder();
builder.setName("Yaoming");
builder.setAge(30);
builder.setPhone(13789878978L);
UserVoProtos.UserVo.Builder builder1 = UserVoProtos.UserVo.newBuilder();
builder1.setName("tmac");
builder1.setAge(32);
builder1.setPhone(138999898989L);
UserVoProtos.UserVo.Builder builder2 = UserVoProtos.UserVo.newBuilder();
builder2.setName("liuwei");
builder2.setAge(29);
builder2.setPhone(138999899989L);
builder.addFriends(builder1);
builder.addFriends(builder2);
UserVoProtos.UserVo vo = builder.build();
byte[] v = vo.toByteArray();
位元組數53
4、反序列化
UserVoProtos.UserVo uvo = UserVoProtos.UserVo.parseFrom(dstb);
System.out.println(uvo.getFriends(0).getName());
結果:tmac,反序列化成功
google protobuf 優點:位元組數很小,适合網絡傳輸節省io,跨語言 。缺點:需要依賴于工具生成代碼。
工作機制
proto檔案是對資料的一個描述,包括字段名稱,類型,位元組中的位置。protoc工具讀取proto檔案生成對應builder代碼的類庫。protoc xxxxx --java_out=xxxxxx 生成java類庫。builder類根據自己的算法把資料序列化成位元組流,或者把位元組流根據反射的原理反序列化成對象。官方的示例:https://developers.google.com/protocol-buffers/docs/javatutorial。
proto檔案中的字段類型和java中的對應關系:
詳見:https://developers.google.com/protocol-buffers/docs/proto
.proto Type | java Type | c++ Type |
double | double | double |
float | float | float |
int32 | int | int32 |
int64 | long | int64 |
uint32 | int | uint32 |
unint64 | long | uint64 |
sint32 | int | int32 |
sint64 | long | int64 |
fixed32 | int | uint32 |
fixed64 | long | uint64 |
sfixed32 | int | int32 |
sfixed64 | long | int64 |
bool | boolean | bool |
string | String | string |
bytes | byte | string |
字段屬性的描述: 寫道 required: a well-formed message must have exactly one of this field.
optional: a well-formed message can have zero or one of this field (but not more than one).
repeated: this field can be repeated any number of times (including zero) in a well-formed message. The order of the repeated values will be preserved. protobuf 在序列化和反序列化的時候,是依賴于.proto檔案生成的builder類完成,字段的變化如果不表現在.proto檔案中就不會影響反序列化,比較适合字段變化的情況。做個測試: 把UserVo序列化到檔案中:
UserVoProtos.UserVo vo = builder.build();
byte[] v = vo.toByteArray();
FileOutputStream fos = new FileOutputStream(dataFile);
fos.write(vo.toByteArray());
fos.close();
為UserVo增加字段,對應的.proto檔案:
package serialize;
option java_package = "serialize";
option java_outer_classname="UserVoProtos";
message UserVo{
optional string name = 1;
optional int32 age = 2;
optional int64 phone = 3;
repeated serialize.UserVo friends = 4;
optional string address = 5;
}
從檔案中反序列化回來:
FileInputStream fis = new FileInputStream(dataFile);
byte[] dstb = new byte[fis.available()];
for(int i=0;i<dstb.length;i++){
dstb[i] = (byte)fis.read();
}
fis.close();
UserVoProtos.UserVo uvo = UserVoProtos.UserVo.parseFrom(dstb);
System.out.println(uvo.getFriends(0).getName());
成功得到結果。 三種方式對比傳輸同樣的資料,google protobuf隻有53個位元組是最少的。結論:
方式 | 優點 | 缺點 |
JSON | 跨語言、格式清晰一目了然 | 位元組數比較大,需要第三方類庫 |
Object Serialize | java原生方法不依賴外部類庫 | 位元組數比較大,不能跨語言 |
Google protobuf | 跨語言、位元組數比較少 | 編寫.proto配置用protoc工具生成對應的代碼 |
以上測試用例覆寫面比較窄,可能無法正确反應真實情況僅代表個人觀點,歡迎随時指正和讨論。