天天看點

Muduo 網絡程式設計示例之零:前言

Muduo 全系列文章清單: http://blog.csdn.net/Solstice/category/779646.aspx

我将會寫一系列文章,介紹用 ​​muduo 網絡庫​​完成常見的 TCP 網絡程式設計任務。目前計劃如下:

  1. ​​UNP​​ 中的簡單協定,包括 echo、daytime、time、discard 等。 
  2. ​​Boost.Asio​​ 中的示例,包括 timer2~6、chat 等。
  3. ​​Java Netty​​ 中的示例,包括 discard、echo、uptime 等,其中的 discard 和 echo 帶流量統計功能。
  4. ​​Python twisted​​ 中的示例,包括 finger01~07
  5. 用于測試兩台機器的往返延遲的 roundtrip
  6. 用于測試兩台機器的帶寬的 pingpong
  7. ​​雲風的串并轉換連接配接伺服器​​ multiplexer,包括單線程和多線程兩個版本。
  8. 檔案傳輸
  9. 一個基于 TCP 的應用層廣播 hub
  10. socks4a 代理伺服器,包括簡單的 TCP 中繼(relay)。
  11. 一個 Sudoku 伺服器的演變,從單線程到多線程,從阻塞到 event-based。
  12. 一個提供短址服務的 httpd 伺服器

其中前面 7 個已經放到了 muduo 代碼的 examples 目錄中,下載下傳位址是:​​http://muduo.googlecode.com/files/muduo-0.1.5-alpha.tar.gz​​ 

這些例子都比較簡單,邏輯不複雜,代碼也很短,适合摘取關鍵部分放到部落格上。其中一些有一定的代表性與針對性,比如“如何傳輸完整的檔案”估計是網絡程式設計的初學者經常遇到的問題。請注意,muduo 是設計來開發内網的網絡程式,它沒有做任何安全方面的加強措施,如果用在公網上可能會受到攻擊,在後面的例子中我會談到這一點。

本系列文章适用于 Linux 2.6.x (x > 28),主要測試發行版為 ​​Ubuntu 10.04 LTS​​ 和 ​​Debian 6.0 Squeeze​​,64-bit x86 硬體。

TCP 網絡程式設計本質論

我認為,TCP 網絡程式設計最本質的是處理三個半事件:

  1. 連接配接的建立,包括服務端接受 (accept) 新連接配接和用戶端成功發起 (connect) 連接配接。
  2. 連接配接的斷開,包括主動斷開 (close 或 shutdown) 和被動斷開 (read 傳回 0)。
  3. 消息到達,檔案描述符可讀。這是最為重要的一個事件,對它的處理方式決定了網絡程式設計的風格(阻塞還是非阻塞,如何處理分包,應用層的緩沖如何設計等等)。
  4. 消息發送完畢,這算半個。對于低流量的服務,可以不必關心這個事件;另外,這裡“發送完畢”是指将資料寫入作業系統的緩沖區,将由 TCP 協定棧負責資料的發送與重傳,不代表對方已經收到資料。

這其中有很多難點,也有很多細節需要注意,比方說:

  1. 如果要主動關閉連接配接,如何保證對方已經收到全部資料?如果應用層有緩沖(這在非阻塞網絡程式設計中是必須的,見下文),那麼如何保證先發送完緩沖區中的資料,然後再斷開連接配接。直接調用 close(2) 恐怕是不行的。
  2. 如果主動發起連接配接,但是對方主動拒絕,如何定期 (帶 back-off) 重試?
  3. 非阻塞網絡程式設計該用邊沿觸發(edge trigger)還是電平觸發(level trigger)?(這兩個中文術語有其他譯法,我選擇了一個電子工程師熟悉的說法。)如果是電平觸發,那麼什麼時候關注 EPOLLOUT 事件?會不會造成 busy-loop?如果是邊沿觸發,如何防止漏讀造成的饑餓?epoll 一定比 poll 快嗎?
  4. 在非阻塞網絡程式設計中,為什麼要使用應用層緩沖區?假如一次讀到的資料不夠一個完整的資料包,那麼這些已經讀到的資料是不是應該先暫存在某個地方,等剩餘的資料收到之後再一并處理?見 lighttpd 關于​​/r/n/r/n 分包的 bug​​。假如資料是一個位元組一個位元組地到達,間隔 10ms,每個位元組觸發一次檔案描述符可讀 (readable) 事件,程式是否還能正常工作?lighttpd 在這個問題上出過​​安全漏洞​​。
  5. 在非阻塞網絡程式設計中,如何設計并使用緩沖區?一方面我們希望減少系統調用,一次讀的資料越多越劃算,那麼似乎應該準備一個大的緩沖區。另一方面,我們系統減少記憶體占用。如果有 10k 個連接配接,每個連接配接一建立就配置設定 64k 的讀緩沖的話,将占用 640M 記憶體,而大多數時候這些緩沖區的使用率很低。muduo 用 readv 結合棧上空間巧妙地解決了這個問題。
  6. 如果使用發送緩沖區,萬一接收方處理緩慢,資料會不會一直堆積在發送方,造成記憶體暴漲?如何做應用層的流量控制?
  7. 如何設計并實作定時器?并使之與網絡 IO 共用一個線程,以避免鎖。

這些問題在 muduo 的代碼中可以找到答案。

Muduo 簡介

我編寫 Muduo 網絡庫的目的之一就是簡化日常的 TCP 網絡程式設計,讓程式員能把精力集中在業務邏輯的實作上,而不要天天和 Sockets API 較勁。借用 Brooks 的話說,我希望 Muduo 能減少網絡程式設計中的偶發複雜性 (accidental complexity)。

Muduo 隻支援 Linux 2.6.x 下的并發非阻塞 TCP 網絡程式設計,它的安裝方法見​​陳碩的 blog 文章​​。

Muduo 的使用非常簡單,不需要從指定的類派生,也不用覆寫虛函數,隻需要注冊幾個回調函數去處理前面提到的三個半事件就行了。

以經典的 echo 回顯服務為例:

1. 定義 EchoServer class,不需要派生自任何基類:

1 #ifndef MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H 
 2  #define MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H
 3 #include <muduo/net/TcpServer.h>
 4  // RFC 862 
 5  class EchoServer 
 6 { 
 7  public: 
 8   EchoServer(muduo::net::EventLoop* loop, 
 9              const muduo::net::InetAddress& listenAddr);
10   void start();
11  private: 
12   void onConnection(const muduo::net::TcpConnectionPtr& conn);
13   void onMessage(const muduo::net::TcpConnectionPtr& conn, 
14                  muduo::net::Buffer* buf, 
15                  muduo::Timestamp time);
16   muduo::net::EventLoop* loop_; 
17   muduo::net::TcpServer server_; 
18 };
19  #endif  // MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H      

在構造函數裡注冊回調函數:

1 EchoServer::EchoServer(EventLoop* loop, 
 2                        const InetAddress& listenAddr) 
 3   : loop_(loop), 
 4     server_(loop, listenAddr, "EchoServer") 
 5 { 
 6   server_.setConnectionCallback( 
 7       boost::bind(&EchoServer::onConnection, this, _1)); 
 8   server_.setMessageCallback( 
 9       boost::bind(&EchoServer::onMessage, this, _1, _2, _3)); 
10 }
11 
12  void EchoServer::start() 
13 { 
14   server_.start(); 
15 }      

2. 實作 EchoServer::onConnection() 和 EchoServer::onMessage():

1 void EchoServer::onConnection(const TcpConnectionPtr& conn) 
 2 { 
 3   LOG_INFO << "EchoServer - " << conn->peerAddress().toHostPort() << " -> " 
 4     << conn->localAddress().toHostPort() << " is " 
 5     << (conn->connected() ? "UP" : "DOWN"); 
 6 }
 7 
 8 void EchoServer::onMessage(const TcpConnectionPtr& conn, 
 9                            Buffer* buf, 
10                            Timestamp time) 
11 { 
12   string msg(buf->retrieveAsString()); 
13   LOG_INFO << conn->name() << " echo " << msg.size() << " bytes at " << time.toString(); 
14   conn->send(msg); 
15 }      

3. 在 main() 裡用 EventLoop 讓整個程式跑起來:

1 #include "echo.h"
 2 #include <muduo/base/Logging.h> 
 3 #include <muduo/net/EventLoop.h>
 4 using namespace muduo; 
 5  using namespace muduo::net;
 6  int main() 
 7 { 
 8   LOG_INFO << "pid = " << getpid(); 
 9   EventLoop loop; 
10   InetAddress listenAddr(2007); 
11   EchoServer server(&loop, listenAddr); 
12   server.start(); 
13   loop.loop(); 
14 }      

完整的代碼見 muduo/examples/simple/echo。

這個幾十行的小程式實作了一個并發的 echo 服務程式,可以同時處理多個連接配接。

對這個程式的詳細分析見下一篇部落格《Muduo 網絡程式設計示例之一:五個簡單 TCP 協定》