大型分佈式C++框架《三:序列化與反序列化》

1、前言 

 


    我的感受序列化簡單來講就是按必定規則組包。反序列化就是按組包時的規則來接包。正常來講。序列化不會很難。不會很複雜。由於過於複雜的序列化協議會致使較長的解析時間,這可能會使得序列化和反序列化階段成爲整個系統的瓶頸。就像壓縮文件、解壓文件,會佔用大量cpu時間。php

    因此正常的序列化會在時間和空間上考慮。我的感受對於電商業務時間應該是相對重要些。畢竟用戶沒有那麼多時間等你解析。
 
           咱們是用thrift來序列化的。一份thrift文件生成2份。一份是c++生成的用來編寫服務接口。一份是php生成的。全部請求都會先落到前端機器。而後前端機器用php調用服務端函數接口。返回處理結果。這實際上是遠程調用rpc。
 

2、分配序列化空間的大小


說序列化以前先說下平臺給序列化分配的buf的空間大小前端

一、每一個協程會分配大概固定包頭(56個字節)+特殊buf(200個字節)的空間來保存包頭。因此首先若是收到的包特殊buf(就是放sessionkey和uid等信息)大於200個字節。會報錯不處理。可是並不會給netio 返回一個錯誤包消息。因此客戶端 會一直等到客戶端設置的超時時間
二、每一個container會分配3M的空間來處理數據。因此去掉包頭和特殊buf.剩下的就是能夠用來序列化的空間 3*1024*1024-固定包頭-特殊buf。   因此最少會有 3*1024*1024-56-200的空間
        這裏其實能夠看到協程的好處。這個3M的空間。對於每一個協程來講是共享的。由於咱們是協程的方式,實際上是一種順序流程,沒有協程會跟你競爭使用這個buf的資源。由於能夠本身手動控制協程的切換。
        若是是多線程的話。可能就要對這個buf加鎖。競爭這一個全局資源來處理數據。這也是多線程編程被詬病的一個地方,須要加鎖。
 
 

3、序列化步驟

 


一、咱們先看下請求。

    oCntlInfo.setOperatorUin(10458);
    oCntlInfo.setOperatorKey("abcde");
    oCntlInfo.setRouteKey(1234);
 
    std::string source = "aaaaa";
    std::string inReserve;
    std::string errmsg;
    std::string outStr;
 
 
    std::string machineKey;
    for(int i =0;i<500*1024;i++)
    {
        machineKey.append("a");
    }
 
 
    AoActionInfo oActionInfo;
    oActionInfo.SetDisShopId(1111);
    oActionInfo.SetDistributorId(2222);
 
    uint32_t dwResult = Stub4App.AddActionSupplier(
                oCntlInfo,
                machineKey,
                source,
                1,
                1,
                oActionInfo,
                inReserve,
                errmsg,
                outStr);
    if(dwResult == 0)
    {
        std::cout << "Invoke OK!" << std::endl;
        std::cout << "Invoke OK!" << std::endl;
 
    }

客戶端直接調用函數接口。到服務端請求結果c++

最後須要序列化的東西以下是類_Cao_action_AddActionSupplier_Req

函數的入參都是咱們須要序列化的內容。注意這裏是rpc調用的一個關鍵點。編程

二、序列化開始

a)    先看下咱們的thritf服務器

若是下圖。發現咱們的函數入參也是打上了tag標誌的。做用跟咱們在結構體中打tag標誌是同樣的。爲了標識一個字段的含義。session

序列化的時候把這些tag序列化進去。 而後反序列化的時候靠這些tag來解析多線程

 

b ) 先把圖貼出來。按着圖來說更清晰些app

 

 

c)   首先咱們會建立一個CByteStream的類來。序列化內容。在CByteStream的構造函數會自動寫入一個字節的序列化包頭。值爲1
CByteStream(char* pStreamBuf = NULL, uint32_t nBufLen = 0,bool bStore=truebool bRealWrite = true);
pStreamBuf  是序列化buf指針
pStreamBuf  是序列化的長度
bStore  true表示是否須要包數據存儲下來。  false表示不須要把數據存下來
bRealWrite 表示是否支持讀寫buf
d)   接着就開始寫類_Cao_action_AddActionSupplier_Req的成員變量。其實就是函數入參。寫的時候是先協tag就是下圖中的fid。  其實就是在thrift中已經寫好的函數入參的tag值。
具體寫的過程咱們先看簡單基本類型。好比strMachineKey
      1)先寫tag。  strMachineKey 的tag爲1.  程序裏規定tag佔兩個字節。因此函數入參能夠是0xffff個。
     2 ) 接着會寫4個固定的字節。用來存儲後面緊跟着數據的值。這裏strMachineKey的長度是512000.
     3 ) 寫內容 。  把strMachineKey的內容寫入緊跟着的buf
 
針對整形和長整形就不說了
大同小異
 
e) 接着咱們關注下 是怎麼寫結構體oActionInfo的。 
 
      1)先寫tag。  oActionInfo 的tag爲5.  程序裏規定tag佔兩個字節。
     2 ) 接着針對結構體這裏 會寫4個固定的字節用來存結構體序列化長度。由於開始不知道因此值爲0。
     3 ) 接着寫字段 DistributorId。  它在oActionInfo結構體中的tag值爲6.類型爲int64. 因此先寫tag=6佔兩個字節,接着分配4個字節存長度。最後分配8個字節存內容
     4)跟着寫DisShopId字段。就不細說了
     5)最後寫了2個字節包尾
     6)最後 回寫結構體的長度 
 
這裏注意下寫結構體時候的寫法。不注意的話會看錯。
1)這裏先拿到開始寫結構體的buf指針。注意這裏是用的int32_t。佔四個字節。跟前面保持一直。這裏用來的存後面總序列虛化結構體提的總長度。
2)因爲剛開始的時候  並不知道後面的結構體會序列化多少個字節。因此這裏先寫4個字節。
同時把這便宜的4個字節的內存值 設置爲0 
bs<<0;  (這裏其實建議寫成  bs<<int32_t(0) 會好一點。開起來一致)
這裏開始沒注意。覺得寫4個字節值爲0的 結構體的頭。其實這裏是放結構體長度的
3)最後第5步。 從新賦值 結構體的長度
1)int32_t* pLen = (int32_t*)bs.getRawBufCur();
2)bs << 0;
3)int32_t iLen=bs.getWrittenLength();
4)Serialize_w(bs);
5)*pLen = bs.getWrittenLength() - iLen;

f)最後對整個_Cao_action_AddActionSupplier_Req寫了兩個字節的包尾函數

g) 我麼能夠看到oActionInfo其實有一堆的字段。可是咱們在請求的時候只寫了兩個字段。全部在序列化的時候也只序列化了兩個字段

 

其實咱們能夠看到咱們的這種序列化,很整齊。很規則。比較緊湊。可是並不節省空間。這個裏面有不少數據能夠壓縮的。可是壓縮帶來一個問題就是解壓的時候很消耗cpu的。跟個人業務場景不服和。也不必。ui

 

4、序列化解析


其實知道了數據是怎麼寫入的  解析起來就很容易了。其實這種序列化就是兩邊約定規則。知道規則之後就能夠解析了
        解析的具體步驟就不詳細說了。這裏說下解析的時候幾個特殊的地方

一、由於tag佔2個字節。因此函數入參不能大於0xffff. 一個結構體的字段個數不能大於0xffff
 
二、假如前端傳入的tag在解析端找不到。解析端會偏移處理下一個tag。因此這是爲何咱們能夠刪除字段的緣由。
好比前端傳入的結構以下
struct A{
 1:int  aa
 2:int  bb
}
可是服務端後臺編譯後刪除了一個字段
struct A{
    1:int  aa
}
a)若是前端只填了字段aa。  那麼解析起來沒有任何問題.由於不會把字段bb的任何信息序列化進去。
b)假如前端填了 aa 和 bb字段。
那麼服務端在解析的時候。拿到tag2。發現找不到對應的數據。
那麼它會偏移4個字節取tag2對應字段內容所佔的字節數。好比這裏是4.
接着它發現是4.就偏移4個字節。不處理字段值內容。直接取下一個tag進行處理
 
這也就是咱們爲何能刪除字段的緣由。
這樣看來咱們的函數入參其實也是能夠刪除的
 
三、咱們服務端新增字段從新編譯。前端沒有對應的tag。根本不會序列化進來。這也是咱們能夠增長字段的緣由。
 
四、解析的時候若是發現tag爲0.則會是認爲解析結束。因此咱們的tag是不能爲0的
 
五、這樣咱們也就能爲服務端函數增長入參的。 同一個函數好比前端的入參是4個。服務端能夠增長N個. 可是注意不能佔用   函數已經用的tag。不然會有問題。並且爲了保證函數的統一性。最好別這麼作。
 
六、到這裏已經很清晰了。 最後再說一次不能改tag對應的類型。
 
 

5、話外

咱們的這一套就是遠程調用rpc服務。經過咱們的序列化。

其實就能瞭解所謂的RPC服務是什麼樣的。

說白了,遠程調用就是將對象名、函數名、參數等傳遞給遠程服務器,服務器將處理結果返回給客戶端。

爲了能解析出這些信息。在入參的時候作上標識(這裏是打tag).

 

谷歌的protobuf也用過。跟thrift其實差很少可是序列化和反序列的話的具體實現是有些不一樣的。

谷歌的protobuf更節省空間

 

之前具體看過序列化的源碼。以爲序列化反序列化以及rpc很神祕。如今看了源碼才發現確實寫的確實好,

可是沒那麼神祕裏。其實就是按必定規則組包。因此仍是要多看源碼啊。

 

咱們用的thrift就是 facebook的thrift。可是改了些東西。大致是同樣的。

相關文章
相關標籤/搜索