陳碩 (giantchen_AT_gmail)
Blog.csdn.net/Solstice
Muduo 全系列文章列表: http://blog.csdn.net/Solstice/category/779646.aspx
我將會(huì)寫一系列文章,介紹用 muduo 網(wǎng)絡(luò)庫完成常見的 TCP 網(wǎng)絡(luò)編程任務(wù)。目前計(jì)劃如下:
- UNP 中的簡單協(xié)議,包括 echo、daytime、time、discard 等。
- Boost.Asio 中的示例,包括 timer2~6、chat 等。
- Java Netty 中的示例,包括 discard、echo、uptime 等,其中的 discard 和 echo 帶流量統(tǒng)計(jì)功能。
- Python twisted 中的示例,包括 finger01~07
- 云風(fēng)的串并轉(zhuǎn)換連接服務(wù)器 multiplexer,包括單線程和多線程兩個(gè)版本。
- 用于測試兩臺(tái)機(jī)器的往返延遲的 roundtrip
- 用于測試兩臺(tái)機(jī)器的帶寬的 pingpong
- 文件傳輸
- 一個(gè)基于 TCP 的應(yīng)用層廣播 hub
- socks4a 代理服務(wù)器,包括簡單的 TCP 中繼(relay)。
- 一個(gè) Sudoku 服務(wù)器的演變,從單線程到多線程,從阻塞到 event-based。
- 一個(gè)提供短址服務(wù)的 httpd 服務(wù)器
其中前面 7 個(gè)已經(jīng)放到了 muduo 代碼的 examples 目錄中,下載地址是: http://muduo.googlecode.com/files/muduo-0.1.5-alpha.tar.gz
這些例子都比較簡單,邏輯不復(fù)雜,代碼也很短,適合摘取關(guān)鍵部分放到博客上。其中一些有一定的代表性與針對(duì)性,比如“如何傳輸完整的文件”估計(jì)是網(wǎng)絡(luò)編程的初學(xué)者經(jīng)常遇到的問題。請(qǐng)注意,muduo 是設(shè)計(jì)來開發(fā)內(nèi)網(wǎng)的網(wǎng)絡(luò)程序,它沒有做任何安全方面的加強(qiáng)措施,如果用在公網(wǎng)上可能會(huì)受到攻擊,在后面的例子中我會(huì)談到這一點(diǎn)。
本系列文章適用于 Linux 2.6.x (x > 25),主要測試發(fā)行版為 Ubuntu 10.04 LTS 和 Debian 6.0 Squeeze,64-bit x86 硬件。
TCP 網(wǎng)絡(luò)編程本質(zhì)論
我認(rèn)為,TCP 網(wǎng)絡(luò)編程最本質(zhì)的是處理三個(gè)半事件:
- 連接的建立,包括服務(wù)端接受 (accept) 新連接和客戶端成功發(fā)起 (connect) 連接。
- 連接的斷開,包括主動(dòng)斷開 (close 或 shutdown) 和被動(dòng)斷開 (read 返回 0)。
- 消息到達(dá),文件描述符可讀。這是最為重要的一個(gè)事件,對(duì)它的處理方式?jīng)Q定了網(wǎng)絡(luò)編程的風(fēng)格(阻塞還是非阻塞,如何處理分包,應(yīng)用層的緩沖如何設(shè)計(jì)等等)。
- 消息發(fā)送完畢,這算半個(gè)。對(duì)于低流量的服務(wù),可以不必關(guān)心這個(gè)事件;另外,這里“發(fā)送完畢”是指將數(shù)據(jù)寫入操作系統(tǒng)的緩沖區(qū),將由 TCP 協(xié)議棧負(fù)責(zé)數(shù)據(jù)的發(fā)送與重傳,不代表對(duì)方已經(jīng)收到數(shù)據(jù)。
這其中有很多難點(diǎn),也有很多細(xì)節(jié)需要注意,比方說:
- 如果要主動(dòng)關(guān)閉連接,如何保證對(duì)方已經(jīng)收到全部數(shù)據(jù)?如果應(yīng)用層有緩沖(這在非阻塞網(wǎng)絡(luò)編程中是必須的,見下文),那么如何保證先發(fā)送完緩沖區(qū)中的數(shù)據(jù),然后再斷開連接。直接調(diào)用 close(2) 恐怕是不行的。
- 如果主動(dòng)發(fā)起連接,但是對(duì)方主動(dòng)拒絕,如何定期 (帶 back-off) 重試?
- 非阻塞網(wǎng)絡(luò)編程該用邊沿觸發(fā)(edge trigger)還是電平觸發(fā)(level trigger)?(這兩個(gè)中文術(shù)語有其他譯法,我選擇了一個(gè)電子工程師熟悉的說法。)如果是電平觸發(fā),那么什么時(shí)候關(guān)注 EPOLLOUT 事件?會(huì)不會(huì)造成 busy-loop?如果是邊沿觸發(fā),如何防止漏讀造成的饑餓?epoll 一定比 poll 快嗎?
- 在非阻塞網(wǎng)絡(luò)編程中,為什么要使用應(yīng)用層緩沖區(qū)?假如一次讀到的數(shù)據(jù)不夠一個(gè)完整的數(shù)據(jù)包,那么這些已經(jīng)讀到的數(shù)據(jù)是不是應(yīng)該先暫存在某個(gè)地方,等剩余的數(shù)據(jù)收到之后再一并處理?見 lighttpd 關(guān)于 \r\n\r\n 分包的 bug。假如數(shù)據(jù)是一個(gè)字節(jié)一個(gè)字節(jié)地到達(dá),間隔 10ms,每個(gè)字節(jié)觸發(fā)一次文件描述符可讀 (readable) 事件,程序是否還能正常工作?lighttpd 在這個(gè)問題上出過安全漏洞。
- 在非阻塞網(wǎng)絡(luò)編程中,如何設(shè)計(jì)并使用緩沖區(qū)?一方面我們希望減少系統(tǒng)調(diào)用,一次讀的數(shù)據(jù)越多越劃算,那么似乎應(yīng)該準(zhǔn)備一個(gè)大的緩沖區(qū)。另一方面,我們系統(tǒng)減少內(nèi)存占用。如果有 10k 個(gè)連接,每個(gè)連接一建立就分配 64k 的讀緩沖的話,將占用 640M 內(nèi)存,而大多數(shù)時(shí)候這些緩沖區(qū)的使用率很低。muduo 用 readv 結(jié)合棧上空間巧妙地解決了這個(gè)問題。
- 如果使用發(fā)送緩沖區(qū),萬一接收方處理緩慢,數(shù)據(jù)會(huì)不會(huì)一直堆積在發(fā)送方,造成內(nèi)存暴漲?如何做應(yīng)用層的流量控制?
- 如何設(shè)計(jì)并實(shí)現(xiàn)定時(shí)器?并使之與網(wǎng)絡(luò) IO 共用一個(gè)線程,以避免鎖。
這些問題在 muduo 的代碼中可以找到答案。
Muduo 簡介
我編寫 Muduo 網(wǎng)絡(luò)庫的目的之一就是簡化日常的 TCP 網(wǎng)絡(luò)編程,讓程序員能把精力集中在業(yè)務(wù)邏輯的實(shí)現(xiàn)上,而不要天天和 Sockets API 較勁。借用 Brooks 的話說,我希望 Muduo 能減少網(wǎng)絡(luò)編程中的偶發(fā)復(fù)雜性 (accidental complexity)。
Muduo 只支持 Linux 2.6.x 下的并發(fā)非阻塞 TCP 網(wǎng)絡(luò)編程,它的安裝方法見陳碩的 blog 文章。
Muduo 的使用非常簡單,不需要從指定的類派生,也不用覆寫虛函數(shù),只需要注冊(cè)幾個(gè)回調(diào)函數(shù)去處理前面提到的三個(gè)半事件就行了。
以經(jīng)典的 echo 回顯服務(wù)為例:
1. 定義 EchoServer class,不需要派生自任何基類:
1 #ifndef MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H
2 #define MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H
3
4 #include <muduo/net/TcpServer.h>
5
6 // RFC 862
7 class EchoServer
8 {
9 public:
10 EchoServer(muduo::net::EventLoop* loop,
11 const muduo::net::InetAddress& listenAddr);
12
13 void start();
14
15 private:
16 void onConnection(const muduo::net::TcpConnectionPtr& conn);
17
18 void onMessage(const muduo::net::TcpConnectionPtr& conn,
19 muduo::net::Buffer* buf,
20 muduo::Timestamp time);
21
22 muduo::net::EventLoop* loop_;
23 muduo::net::TcpServer server_;
24 };
25
26 #endif // MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H
27
在構(gòu)造函數(shù)里注冊(cè)回調(diào)函數(shù):
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 }
16
17
2. 實(shí)現(xiàn) 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 }
16
3. 在 main() 里用 EventLoop 讓整個(gè)程序跑起來:
1 #include "echo.h"
2
3 #include <muduo/base/Logging.h>
4 #include <muduo/net/EventLoop.h>
5
6 using namespace muduo;
7 using namespace muduo::net;
8
9 int main()
10 {
11 LOG_INFO << "pid = " << getpid();
12 EventLoop loop;
13 InetAddress listenAddr(2007);
14 EchoServer server(&loop, listenAddr);
15 server.start();
16 loop.loop();
17 }
18
完整的代碼見 muduo/examples/simple/echo。
這個(gè)幾十行的小程序?qū)崿F(xiàn)了一個(gè)并發(fā)的 echo 服務(wù)程序,可以同時(shí)處理多個(gè)連接。
對(duì)這個(gè)程序的詳細(xì)分析見下一篇博客《Muduo 網(wǎng)絡(luò)編程示例之一:五個(gè)簡單 TCP 協(xié)議》
(待續(xù))