天天看點

在 muduo 中實作 protobuf 編解碼器與消息分發器為什麼 Protobuf 的預設序列化格式沒有包含消息的長度與類型?什麼是編解碼器 codec?實作 ProtobufCodec消息分發器 dispatcher 有什麼用?ProtobufCodec 與 ProtobufDispatcher 的綜合運用ProtobufDispatcher 的兩種實作ProtobufCodec 和 ProtobufDispatcher 有何意義?

陳碩 (giantchen_AT_gmail)

考慮到不是每個人都安裝了 Google Protobuf,muduo 中的 protobuf 相關示例預設是不 build 的,如果你的機器上安裝了 protobuf 2.3.0 或 2.4.0a,那麼可以用 ./build.sh protobuf_all 來建構 protobuf 相關的 examples。

在介紹 codec 和 dispatcher 之前,先講講前文的一個未決問題。

Protobuf 是經過深思熟慮的消息打包方案,它的預設序列化格式沒有包含消息的長度與類型,自然有其道理。哪些情況下不需要在 protobuf 序列化得到的位元組流中包含消息的長度和(或)類型?我能想到的答案有:

如果把消息寫入檔案,一個檔案存一個消息,那麼序列化結果中不需要包含長度和類型,因為從檔案名和檔案長度中可以得知消息的類型與長度。

如果把消息寫入檔案,一個檔案存多個消息,那麼序列化結果中不需要包含類型,因為檔案名就代表了消息的類型。

如果把消息存入資料庫(或者 NoSQL),以 VARBINARY 字段儲存,那麼序列化結果中不需要包含長度和類型,因為從字段名和字段長度中可以得知消息的類型與長度。

如果把消息以 UDP 方式發生給對方,而且對方一個 UDP port 隻接收一種消息類型,那麼序列化結果中不需要包含長度和類型,因為從 port 和 UDP packet 長度中可以得知消息的類型與長度。

如果把消息以 TCP 短連接配接方式發給對方,而且對方一個 TCP port 隻接收一種消息類型,那麼序列化結果中不需要包含長度和類型,因為從 port 和 TCP 位元組流長度中可以得知消息的類型與長度。

如果把消息以 TCP 長連接配接方式發給對方,但是對方一個 TCP port 隻接收一種消息類型,那麼序列化結果中不需要包含類型,因為 port 代表了消息的類型。

如果采用 RPC 方式通信,那麼隻需要告訴對方 method name,對方自然能推斷出 Request 和 Response 的消息類型,這些可以由 protoc 生成的 RPC stubs 自動搞定。

對于最後一點,比方說 sudoku.proto 定義為:

對于上述這些情況,如果 protobuf 無條件地把長度和類型放到序列化的位元組串中,隻會浪費網絡帶寬和存儲。可見 protobuf 預設不發送長度和類型是正确的決定。Protobuf 為消息格式的設計樹立了典範,哪些該自己搞定,哪些留給外部系統去解決,這些都考慮得很清楚。

以下均隻考慮 TCP 長連接配接這一應用場景。

先談談編解碼器。

codec 是一層間接性,它位于 TcpConnection 和 ChatServer 之間,攔截處理收到的資料,在收到完整的消息之後再調用 CharServer 對應的處理函數,注意 CharServer::onStringMessage() 的參數是 std::string,不再是 muduo::net::Buffer,也就是說 LengthHeaderCodec 把 Buffer 解碼成了 string。另外,在發送消息的時候,ChatServer 通過 LengthHeaderCodec::send() 來發送 string,LengthHeaderCodec 負責把它編碼成 Buffer。這正是“編解碼器”名字的由來。

Protobuf codec 與此非常類似,隻不過消息類型從 std::string 變成了 protobuf::Message。對于隻接收處理 Query 消息的 QueryServer 來說,用 ProtobufCodec 非常友善,收到 protobuf::Message 之後 down cast 成 Query 來用就行。如果要接收處理不止一種消息,ProtobufCodec 恐怕還不能單獨完成工作,請繼續閱讀下文。

protobuf::Message 是 new 出來的對象,它的生命期如何管理?muduo 采用 shared_ptr<Message> 來自動管理對象生命期,這與其他地方的做法是一緻的。

如何處理一次收到半條消息、一條消息、一條半消息、兩條消息等等情況?這是每個 non-blocking 網絡程式中的 codec 都要面對的問題。

ProtobufCodec 在實際使用中有明顯的不足:它隻負責把 muduo::net::Buffer 轉換為具體類型的 protobuf::Message,應用程式拿到 Message 之後還有再根據其具體類型做一次分發。我們可以考慮做一個簡單通用的分發器 dispatcher,以簡化客戶代碼。

此外,目前 ProtobufCodec 的實作非常初級,它沒有充分利用 ZeroCopyInputStream 和 ZeroCopyOutputStream,而是把收到的資料作為 byte array 交給 protobuf Message 去解析,這給性能優化留下了空間。protobuf Message 不要求資料連續(像 vector 那樣),隻要求資料分段連續(像 deque 那樣),這給 buffer 管理帶來性能上的好處(避免重新配置設定記憶體,減少記憶體碎片),當然也使得代碼變複雜。muduo::net::Buffer 非常簡單,它内部是 vector<char>,我目前不想讓 protobuf 影響 muduo 本身的設計,畢竟 muduo 是個通用的網絡庫,不是為實作 protobuf RPC 而特制的。

前面提到,在使用 TCP 長連接配接,且在一個連接配接上傳遞不止一種 protobuf 消息的情況下,客戶代碼需要對收到的消息按類型做分發。比方說,收到 Logon 消息就交給 QueryServer::onLogon() 去處理,收到 Query 消息就交給 QueryServer::onQuery() 去處理。這個消息分派機制可以做得稍微有點通用性,讓所有 muduo+protobuf 程式收益,而且不增加複雜性。

換句話說,又是一層間接性,ProtobufCodec 攔截了 TcpConnection 的資料,把它轉換為 Message,ProtobufDispatcher 攔截了 ProtobufCodec 的 callback,按消息具體類型把它分派給多個 callbacks。

我寫了兩個示例代碼,client 和 server,把 ProtobufCodec 和 ProtobufDispatcher 串聯起來使用。server 響應 Query 消息,發生回 Answer 消息,如果收到未知消息類型,則斷開連接配接。client 可以選擇發送 Query 或 Empty 消息,由指令行控制。這樣可以測試 unknown message callback。

為節省篇幅,這裡就不列出代碼了,請移步閱讀

<a href="http://code.google.com/p/muduo/source/browse/trunk/examples/protobuf/codec/server.cc">http://code.google.com/p/muduo/source/browse/trunk/examples/protobuf/codec/server.cc</a>

在構造函數中,通過注冊回調函數把四方 (TcpConnection、codec、dispatcher、QueryServer) 結合起來。

要完成消息分發,那麼就是對消息做 type-switch,這似乎是一個 bad smell,但是 protobuf Message 的 Descriptor 沒有留下定制點(比如暴露一個 boost::any 成員),我們隻好硬來了。

先定義

typedef boost::function&lt;void (Message*)&gt; ProtobufMessageCallback;

注意,本節出現的不是 muduo dispatcher 真實的代碼,僅為示意,突出重點,便于畫圖。

當然,它的設計也有小小的缺陷,那就是 ProtobufMessageCallback 限制了客戶代碼隻能接受基類 Message,客戶代碼需要自己做向下轉型,比如:

如果我希望 QueryServer 這麼設計:不想每個消息處理函數自己做 down casting,而是交給 dispatcher 去處理,客戶代碼拿到的就已經是想要的具體類型。如下:

那麼該該如何實作 ProtobufDispatcher 呢?它如何與多個未知的消息類型合作?做 down cast 需要知道目标類型,難道我們要用一長串模闆類型參數嗎?

有一個辦法,把多态與模闆結合,利用 templated derived class 來提供類型上的靈活性。設計如下。

ProtobufDispatcher 有一個模闆成員函數,可以接受注冊任意消息類型 T 的回調,然後它建立一個模闆化的派生類 CallbackT&lt;T&gt;,這樣消息的類新資訊就儲存在了 CallbackT&lt;T&gt; 中,做 down casting 就簡單了。

比方說,我們有兩個具體消息類型 Query 和 Answer。

然後我們這樣注冊回調:

這樣會具現化 (instantiation) 出兩個 CallbackT 實體,如下:

ProtobufCodec 和 ProtobufDispatcher 把每個直接收發 protobuf Message 的網絡程式都會用到的功能提煉出來做成了公用的 utility,這樣以後新寫 protobuf 網絡程式就不必為打包分包和消息分發勞神了。它倆以庫的形式存在,是兩個可以拿來就當 data member 用的 class,它們沒有基類,也沒有用到虛函數或者别的什麼面向對象特征,不侵入 muduo::net 或者你的代碼。如果不這麼做,那将來每個 protobuf 網絡程式都要自己重新實作類似的功能,徒增負擔。

下一篇文章講《分布式程式的自動回歸測試》會介紹利用 protobuf 的跨語言特性,采用 Java 為 C++ 服務程式編寫 test harness。

在 muduo 中實作 protobuf 編解碼器與消息分發器為什麼 Protobuf 的預設序列化格式沒有包含消息的長度與類型?什麼是編解碼器 codec?實作 ProtobufCodec消息分發器 dispatcher 有什麼用?ProtobufCodec 與 ProtobufDispatcher 的綜合運用ProtobufDispatcher 的兩種實作ProtobufCodec 和 ProtobufDispatcher 有何意義?

<a href="http://home.cnblogs.com/u/Solstice/">陳碩</a>

<a href="http://home.cnblogs.com/u/Solstice/followees">關注 - 0</a>

<a href="http://home.cnblogs.com/u/Solstice/followers">粉絲 - 1091</a>

<a>+加關注</a>

3

<a></a>

好文章,近斷時間正在了解這方面的知識。

非常好的東西,頂一個!

我們也做了一個消息模型,用自己的二進制序列化器。當時也考慮過Protobuf,那是個好東西,隻是在控制方面還不夠靈活,不能滿足要求。

我們的消息開頭就是編号,用7位壓縮編碼寫的,接收者就是根據這個編号來識别是哪一種消息類型,然後根據該類型進行反序列化。

因為指定了類型,是以資料裡面不需要指定長度。

如果遇到需要使用動态長度資料的消息,消息實作本身要加一個字段表明長度,然後擴充反序列化,告訴Reader,後面這個家夥的長度是多少。

為了減少傳輸資料的長度,我們的二進制序列化器,不寫長度,不寫類型,不寫成員名稱,整數一律采用7位編碼存儲。

說到底,就跟C++裡面一個對象的記憶體模型一樣,這邊把對象的記憶體資料拿走,接收者把資料放入記憶體,成為一個對象

....ls 的。用7位壓縮編碼寫的,接收者就是根據這個編号來識别是哪一種消息類型,然後根據該類型進行反序列化。拿到序列号就能解析出消息長度嗎?不能,除非消息是定長的。否則你的解析器需要做到非常智能,比如解析http協定那樣的解析器。否則如果一段的資料發生了錯誤,你的解析器能立刻發現問題嗎?不行,因為你缺少一些邊界校驗。

至于lz 這篇文章。隻是用map&lt;Descriptor*, ProtobufMessageCallback&gt; 成員 代替了 switch case 的做法。用 mether string 代替了手工的編号技術。lz的組包方式,已經拍過磚了 就不多說了。

關于lz 自己一直宣揚自己的 vector&lt;char&gt; 做buff, 我隻能保留自己的意見。 一個buff 居然需要使用 vector 您不嫌太重了嗎?lz 曾經寫過一篇批判ACE的文章,老夫挺有同感,現在看來 lz 在ACE 的道路上繼續前行!

我想請問下關于關于google probuf的問題,如果我擁有的字段是可變的,比如TLV編碼,中L(length)是可變的,可以是一個位元組或者兩個位元組,用C++ ungigned表示,當一個位元組時,長度範圍是(0-127),是以連個位元組就是&gt;127,如果自己寫代碼就判斷下第一個位元組是否為1來判斷;但是我想如果自己寫解碼比較麻煩,還不知道probuf中能否有自定義解碼規則,您知道嗎,謝謝啦!

既然使用了protobuf,為什麼你還要關心它如何編碼呢?

字段直接定義成 int 類型就可以,protobuf 對整數采用變長編碼。

請教一下。複雜的message結構,許多個optional。。對性能影響大嗎?optional項可能每次内容不一樣。。

你做一下 benchmark 不就知道了。

好吧。确實有點偷懶了。謝謝部落客回複。

基本上沒影響,這個是按照id做的一個KV隊,如果optional很多,就是KV大一點而已,隻要别搞個幾十萬條,其他的無所謂

ProtoBuf使用,主要注意兩點:

1, bytes和string的資料長度别超過 ( 508 * 1024 - 49 ) ,否則序列号時會有一個數量級的性能降低;因為記憶體管理方式上的不同,直接把資料打包入PB中,比使用帶外資料要快一些;不過如果考慮通用性,大資料最好都走帶外資料;

2, PB不具有邊界性,一定要注意保持他的完整性

    本文轉自 陳碩  部落格園部落格,原文連結:http://www.cnblogs.com/Solstice/archive/2011/04/13/2014362.html,如需轉載請自行聯系原作者

繼續閱讀