天天看點

Muduo 網絡程式設計示例之二:Boost.Asio 的聊天伺服器TCP 分包聊天服務消息格式打包的代碼分包的代碼編解碼器 LengthHeaderCodec服務端的實作用戶端的實作簡單測試

陳碩 (giantchen_AT_gmail)

Blog.csdn.net/Solstice

對于短連接配接的 TCP 服務,分包不是一個問題,隻要發送方主動關閉連接配接,就表示一條消息發送完畢,接收方 read() 傳回 0,進而知道消息的結尾。例如前一篇文章裡的 daytime 和 time 協定。

對于長連接配接的 TCP 服務,分包有四種方法:

消息長度固定,比如 muduo 的 roundtrip 示例就采用了固定的 16 位元組消息;

使用特殊的字元或字元串作為消息的邊界,例如 HTTP 協定的 headers 以 "\r\n" 為字段的分隔符;

在每條消息的頭部加一個長度字段,這恐怕是最常見的做法,本文的聊天協定也采用這一辦法;

利用消息本身的格式來分包,例如 XML 格式的消息中 <root>...</root> 的配對,或者 JSON 格式中的 { ... } 的配對。解析這種消息格式通常會用到狀态機。

在後文的代碼講解中還會仔細讨論用長度字段分包的常見陷阱。

本文實作的聊天服務非常簡單,由服務端程式和用戶端程式組成,協定如下:

服務端程式中某個端口偵聽 (listen) 新的連接配接;

用戶端向服務端發起連接配接;

連接配接建立之後,用戶端随時準備接收服務端的消息并在螢幕上顯示出來;

用戶端接受鍵盤輸入,以回車為界,把消息發送給服務端;

服務端接收到消息之後,依次發送給每個連接配接到它的用戶端;原來發送消息的用戶端程序也會收到這條消息;

一個服務端程序可以同時服務多個用戶端程序,當有消息到達服務端後,每個用戶端程序都會收到同一條消息,服務端廣播發送消息的順序是任意的,不一定哪個用戶端會先收到這條消息。

(可選)如果消息 A 先于消息 B 到達服務端,那麼每個用戶端都會先收到 A 再收到 B。

這實際上是一個簡單的基于 TCP 的應用層廣播協定,由服務端負責把消息發送給每個連接配接到它的用戶端。參與“聊天”的既可以是人,也可以是程式。在以後的文章中,我将介紹一個稍微複雜的一點的例子 hub,它有“聊天室”的功能,用戶端可以注冊特定的 topic(s),并往某個 topic 發送消息,這樣代碼更有意思。

本聊天服務的消息格式非常簡單,“消息”本身是一個字元串,每條消息的有一個 4 位元組的頭部,以網絡序存放字元串的長度。消息之間沒有間隙,字元串也不一定以 '\0' 結尾。比方說有兩條消息 "hello" 和 "chenshuo",那麼打包後的位元組流是:

0x00, 0x00, 0x00, 0x05, 'h', 'e', 'l', 'l', 'o', 0x00, 0x00, 0x00, 0x08, 'c', 'h', 'e', 'n', 's', 'h', 'u', 'o'

共 21 位元組。

這段代碼把 const string& message 打包為 muduo::net::Buffer,并通過 conn 發送。

muduo::Buffer 有一個很好的功能,它在頭部預留了 8 個位元組的空間,這樣第 6 行的 prepend() 操作就不需要移動已有的資料,效率較高。

解析資料往往比生成資料複雜,分包打包也不例外。

上面這段代碼第 7 行用了 while 循環來反複讀取資料,直到 Buffer 中的資料不夠一條完整的消息。請讀者思考,如果換成 if (buf->readableBytes() >= kHeaderLen) 會有什麼後果。

以前面提到的兩條消息的位元組流為例:

假設資料最終都全部到達,onMessage() 至少要能正确處理以下各種資料到達的次序,每種情況下 messageCallback_ 都應該被調用兩次:

每次收到一個位元組的資料,onMessage() 被調用 21 次;

資料分兩次到達,第一次收到 2 個位元組,不足消息的長度字段;

資料分兩次到達,第一次收到 4 個位元組,剛好夠長度字段,但是沒有 body;

資料分兩次到達,第一次收到 8 個位元組,長度完整,但 body 不完整;

資料分兩次到達,第一次收到 9 個位元組,長度完整,body 也完整;

資料分兩次到達,第一次收到 10 個位元組,第一條消息的長度完整、body 也完整,第二條消息長度不完整;

請自行移動分割點,驗證各種情況;

資料一次就全部到達,這時必須用 while 循環來讀出兩條消息,否則消息會堆積。

請讀者驗證 onMessage() 是否做到了以上幾點。這個例子充分說明了 non-blocking read 必須和 input buffer 一起使用。

這段代碼把以 Buffer* 為參數的 MessageCallback 轉換成了以 const string& 為參數的 StringMessageCallback,讓使用者代碼不必關心分包操作。用戶端和服務端都能從中受益。

聊天伺服器的服務端代碼小于 100 行,不到 asio 的一半。

請先閱讀第 68 行起的資料成員的定義。除了經常見到的 EventLoop 和 TcpServer,ChatServer 還定義了 codec_ 和 std::set<TcpConnectionPtr> connections_ 作為成員,connections_ 是目前已建立的客戶連接配接,在收到消息之後,伺服器會周遊整個容器,把消息廣播給其中每一個 TCP 連接配接。

首先,在構造函數裡注冊回調:

如果你讀過 asio 的對應代碼,會不會覺得 Reactor 往往比 Proactor 容易使用?

我有時覺得服務端的程式常常比用戶端的更容易寫,聊天伺服器再次驗證了我的看法。用戶端的複雜性來自于它要讀取鍵盤輸入,而 EventLoop 是獨占線程的,是以我用了兩個線程,main() 函數所在的線程負責讀鍵盤,另外用一個 EventLoopThread 來處理網絡 IO。我暫時沒有把标準輸入輸出融入 Reactor 的想法,因為伺服器程式的 stdin 和 stdout 往往是重定向了的。

來看代碼,首先,在構造函數裡注冊回調,并使用了跟前面一樣的 LengthHeaderCodec 作為中間層,負責打包分包。

開三個指令行視窗,在第一個運作

$ ./asio_chat_server 3000

第二個運作

$ ./asio_chat_client 127.0.0.1 3000

第三個運作同樣的指令

這樣就有兩個用戶端程序參與聊天。在第二個視窗裡輸入一些字元并回車,字元會出現在本視窗和第三個視窗中。

下一篇文章我會介紹 Muduo 中的定時器,并實作 Boost.Asio 教程中的 timer2~5 示例,以及帶流量統計功能的 discard 和 echo 伺服器(來自 Java Netty)。流量等于機關時間内發送或接受的位元組數,這要用到定時器功能。

(待續)

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