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


一、前言 

 


    個人感覺序列化簡單來說就是按一定規則組包。反序列化就是按組包時的規則來接包。正常來說。序列化不會很難。不會很復雜。因為過於復雜的序列化協議會導致較長的解析時間,這可能會使得序列化和反序列化階段成為整個系統的瓶頸。就像壓縮文件、解壓文件,會占用大量cpu時間。

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

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


說序列化之前先說下平台給序列化分配的buf的空間大小

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

三、序列化步驟

 


1、我們先看下請求。

    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;
 
    }

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

最后需要序列化的東西如下是類_Cao_action_AddActionSupplier_Req

函數的入參都是我們需要序列化的內容。注意這里是rpc調用的一個關鍵點。

2、序列化開始

a)    先看下我們的thritf

如果下圖。發現我們的函數入參也是打上了tag標志的。作用跟我們在結構體中打tag標志是一樣的。為了標識一個字段的含義。

序列化的時候把這些tag序列化進去。 然后反序列化的時候靠這些tag來解析

 

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

 

 

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的。跟我的業務場景不服和。也沒必要。

 

四、序列化解析


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

1、因為tag占2個字節。所以函數入參不能大於0xffff. 一個結構體的字段個數不能大於0xffff
 
2、假如前端傳入的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進行處理
 
這也就是我們為什么能刪除字段的原因。
這樣看來我們的函數入參其實也是可以刪除的
 
3、我們服務端新增字段重新編譯。前端沒有對應的tag。根本不會序列化進來。這也是我們可以增加字段的原因。
 
4、解析的時候如果發現tag為0.則會是認為解析結束。所以我們的tag是不能為0的
 
5、這樣我們也就能為服務端函數增加入參的。 同一個函數比如前端的入參是4個。服務端可以增加N個. 但是注意不能占用   函數已經用的tag。否則會有問題。而且為了保證函數的統一性。最好別這么做。
 
6、到這里已經很清晰了。 最后再說一次不能改tag對應的類型。
 
 

五、話外

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

其實就能了解所謂的RPC服務是什么樣的。

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

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

 

谷歌的protobuf也用過。跟thrift其實差不多但是序列化和反序列的話的具體實現是有些不同的。

谷歌的protobuf更節省空間

 

以前具體看過序列化的源碼。覺得序列化反序列化以及rpc很神秘。現在看了源碼才發現確實寫的確實好,

但是沒那么神秘里。其實就是按一定規則組包。所以還是要多看源碼啊。

 

我們用的thrift就是 facebook的thrift。但是改了些東西。大體是一樣的。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM