關于java中protobuf中序列化基本類型的原理探索及解決方案

最近在做一些.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;

  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的序列化原理闷叉,還是很有收獲的擦俐。
如果以上有說的不對的地方,還望閱讀者指出~~~~

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末握侧,一起剝皮案震驚了整個濱河市蚯瞧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌品擎,老刑警劉巖埋合,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異萄传,居然都是意外死亡甚颂,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進店門秀菱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來振诬,“玉大人,你說我怎么就攤上這事衍菱「厦矗” “怎么了?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵脊串,是天一觀的道長辫呻。 經(jīng)常有香客問我,道長洪规,這世上最難降的妖魔是什么印屁? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮斩例,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘从橘。我一直安慰自己念赶,他們只是感情好础钠,可當我...
    茶點故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著叉谜,像睡著了一般旗吁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上停局,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天很钓,我揣著相機與錄音,去河邊找鬼董栽。 笑死码倦,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的锭碳。 我是一名探鬼主播袁稽,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼擒抛!你這毒婦竟也來了推汽?” 一聲冷哼從身側(cè)響起终畅,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤惰赋,失蹤者是張志新(化名)和其女友劉穎熊咽,沒想到半個月后噩斟,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體绿聘,經(jīng)...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡庸队,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年扛吞,在試婚紗的時候發(fā)現(xiàn)自己被綠了鹃觉。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片厢钧。...
    茶點故事閱讀 40,444評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡鳞尔,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出早直,到底是詐尸還是另有隱情寥假,我是刑警寧澤,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布霞扬,位于F島的核電站糕韧,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏喻圃。R本人自食惡果不足惜萤彩,卻給世界環(huán)境...
    茶點故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望斧拍。 院中可真熱鬧雀扶,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至浪册,卻和暖如春扫腺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背村象。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工笆环, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人厚者。 一個月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓躁劣,卻偏偏與公主長得像,于是被迫代替她去往敵國和親籍救。 傳聞我的和親對象是個殘疾皇子习绢,可洞房花燭夜當晚...
    茶點故事閱讀 45,455評論 2 359

推薦閱讀更多精彩內(nèi)容