最近在做一些.net轉(zhuǎn)java的開發(fā)工作察滑,碰到了一些在C#中相對比較容易處理毒坛,但是在java中不是那么容易處理有勾,或者說疹启,處理方案不是那么明顯的問題。Protobuf序列化就是其中一個蔼卡。
問題背景是:線上有若干的C#的WCF服務需要調(diào)用喊崖,由于我們的應用是先切換的,在對方服務不改變的情況下雇逞,要能做到我們切換成java之后荤懂,能夠?qū)崿F(xiàn)訪問的平滑過渡。其中一部分服務的請求有對應的.proto契約文件塘砸,利用protoc工具可以生成對應的java文件节仿,從而利用生成的java的類代碼里的parseFrom方法,實現(xiàn)protobuf序列化掉蔬;但是偏偏碰到了一個請求參數(shù)是String字符串類型的服務廊宪,由于沒有proto文件,所以就不能生成對應的類女轿,更沒有對應的parseFrom方法箭启,出現(xiàn)了難題。
在C#中蛉迹,原來采用的protobuf-net.dll這個庫傅寡,里面可以采用如下方式對字符串類型 (或者其他基本類型)進行序列化:
public static string SerializeObject(T obj) where T : class
{
string result = "";
try{
using (MemoryStream stream = new MemoryStream()){
Serializer.Serialize(stream, obj);
result = System.Convert.ToBase64String(stream.ToArray());
//序列化方式
result = string.Format("{0}{1}", "protobuff", result);
}
catch (Exception e){
throw e;
}
return result;
}
}
而Java則開始沒有這種統(tǒng)一的泛型序列化方式,于是趁這個機會北救,了解了一些protobuf底層序列化的原理荐操。
以String類型的序列化為例,首先要說的是protobuf中用到的varint編碼扭倾。
Varint 是一種緊湊的表示數(shù)字的方法淀零。它用一個或多個字節(jié)來表示一個數(shù)字,值越小的數(shù)字使用越少的字節(jié)數(shù)膛壹。這能減少用來表示數(shù)字的字節(jié)數(shù)。比如對于 int32 類型的數(shù)字唉堪,一般需要 4 個 byte 來表示模聋。但是采用 Varint,對于很小的 int32 類型的數(shù)字唠亚,則可以用 1 個 byte 來表示链方。當然凡事都有好的也有不好的一面,采用 Varint 表示法灶搜,大的數(shù)字則需要 5 個 byte 來表示祟蚀。從統(tǒng)計的角度來說工窍,一般不會所有的消息中的數(shù)字都是大數(shù),因此大多數(shù)情況下前酿,采用 Varint 后患雏,可以用更少的字節(jié)數(shù)來表示數(shù)字信息。
對于每個字節(jié)的最高位來說罢维,為了能夠確認淹仑,一個數(shù)是由幾個字節(jié)來進行編碼的,varint規(guī)定:如果該字節(jié)的最高位是1肺孵,則表示下面一個字節(jié)和該字節(jié)一起表示同一個數(shù)匀借;如果前面兩個字節(jié)最高位為1,第三個字節(jié)最高位為0平窘,則表示要用三個字節(jié)來表示一個數(shù)吓肋。舉個例子,比如對應整數(shù)200,200 = 128+64+4,顯然由于一個字節(jié)最多可以表示最大到128瑰艘,所以200至少要2個字節(jié)來進行編碼蓬坡。因此,用二進制表示為: 00000000 11000100. 由于最高位是用來標識是否采用下一個字節(jié)來表示數(shù)磅叛,沒有數(shù)據(jù)意義屑咳,所以對于該二進制表示,應該每7位來劃分:
0000001,1000100
varint編碼采用小端模式弊琴,所以應該把這兩個字節(jié)顛倒過來:
1000100,0000001
由于采用兩個字節(jié)表示兆龙,所以顛倒之后的最高位應該添加1,低字節(jié)的高位補0敲董,從而200最終的varint編碼為:
11000100,00000001
----------------------------------------------------------華麗的分割線--------------------------------------------------------
當了解了varint編碼之后紫皇,string類型進行序列化就可以展開說了。
先說protobuf定義message腋寨,有兩個重要的東東聪铺。1.order,表示定義字段的順序萄窜,顯然如果只是一個string铃剔,就認為order=1;
-
type規(guī)則結(jié)構類型查刻,表示基本類型在protobuf中的類型键兜,type在protobuf中有如下幾個類型:
圖1 protobuf中關于基本類型的枚舉關系
顯然,type有6種穗泵,用3個bit就可以表示普气,protobuf也是這么干的。用一個字節(jié)來表示佃延,高5位bit表示order次序现诀,低三位表示規(guī)則結(jié)構類型夷磕,顯然對于string類型的序列化,可用一個字節(jié)表示:0000 1010仔沿,表示order=1坐桩,type=2.
回到string序列化本身,通過和C#序列化結(jié)果對比于未,發(fā)現(xiàn)string的protobuf序列化結(jié)果由幾個部分構成:
** head+ length的varint編碼+字符串本身的utf-8編碼**
** **其中head就是上面用一個字節(jié)表示的order+type撕攒,length的varint編碼表示字符串本身utf-8字節(jié)數(shù)組長度的varint編碼(可能由1-n個字節(jié)表示),搞清楚這些后烘浦,那string本身的protobuf序列化問題就迎刃而解了抖坪,實現(xiàn)代碼如下:
private static byte[] protobufSerializeString(String str){
byte [] protoBytes = str.getBytes(StandardCharsets.UTF_8);
int byteLen = protoBytes.length;
List<Byte> encodingLen =varIntEncoding(byteLen);
byte[] result = new byte[protoBytes.length+ encodingLen.size() + 1];
result[0] = 0x0a;
for(int i = 1; i <=encodingLen.size(); i++){
result[i] = encodingLen.get(i - 1);
}
System.arraycopy(protoBytes,0,result,encodingLen.size()+ 1,protoBytes.length);
return result;
}
private static List<Byte> varIntEncoding(int number){
int x = number;
List<Byte> results =new ArrayList<Byte>();
if(x <= 0){
results.add(Byte.parseByte("0"));
return results;
}
while(x != 0){
byte littleData = (byte)(x & (byte)0x7f);
results.add(littleData);
x = x >> 7;
}
for(int i = 0 ;i < results.size()-1;i++){
results.set(i, (byte)(results.get(i)| 0x80));
}
return results;
}
通過過程本身,了解到了底層protobuf的序列化原理闷叉,還是很有收獲的擦俐。
如果以上有說的不對的地方,還望閱讀者指出~~~~