陳碩 (giantchen_AT_gmail)
Blog.csdn.net/Solstice
Muduo 全系列文章列表: http://blog.csdn.net/Solstice/category/779646.aspx
今天收到一位網(wǎng)友來(lái)信:
在 simple 中的 daytime 示例中,服務(wù)端主動(dòng)關(guān)閉時(shí)調(diào)用的是如下函數(shù)序列,這不是只是關(guān)閉了連接上的寫(xiě)操作嗎,怎么是關(guān)閉了整個(gè)連接?
1: void DaytimeServer::onConnection(const muduo::net::TcpConnectionPtr& conn)
2: {
3: if (conn->connected())
4: {
5: conn->send(Timestamp::now().toFormattedString() + "\n");
6: conn->shutdown();
7: }
8: }
9:
10: void TcpConnection::shutdown()
11: {
12: if (state_ == kConnected)
13: {
14: setState(kDisconnecting);
15: loop_->runInLoop(boost::bind(&TcpConnection::shutdownInLoop, this));
16: }
17: }
18:
19: void TcpConnection::shutdownInLoop()
20: {
21: loop_->assertInLoopThread();
22: if (!channel_->isWriting())
23: {
24: // we are not writing
25: socket_->shutdownWrite();
26: }
27: }
28:
29: void Socket::shutdownWrite()
30: {
31: sockets::shutdownWrite(sockfd_);
32: }
33:
34: void sockets::shutdownWrite(int sockfd)
35: {
36: if (::shutdown(sockfd, SHUT_WR) < 0)
37: {
38: LOG_SYSERR << "sockets::shutdownWrite";
39: }
40: }
陳碩答復(fù)如下:
Muduo TcpConnection 沒(méi)有提供 close,而只提供 shutdown ,這么做是為了收發(fā)數(shù)據(jù)的完整性。
TCP 是一個(gè)全雙工協(xié)議,同一個(gè)文件描述符既可讀又可寫(xiě), shutdownWrite() 關(guān)閉了“寫(xiě)”方向的連接,保留了“讀”方向,這稱為 TCP half-close。如果直接 close(socket_fd),那么 socket_fd 就不能讀或?qū)懥恕?/p>
用 shutdown 而不用 close 的效果是,如果對(duì)方已經(jīng)發(fā)送了數(shù)據(jù),這些數(shù)據(jù)還“在路上”,那么 muduo 不會(huì)漏收這些數(shù)據(jù)。換句話說(shuō),muduo 在 TCP 這一層面解決了“當(dāng)你打算關(guān)閉網(wǎng)絡(luò)連接的時(shí)候,如何得知對(duì)方有沒(méi)有發(fā)了一些數(shù)據(jù)而你還沒(méi)有收到?”這一問(wèn)題。當(dāng)然,這個(gè)問(wèn)題也可以在上面的協(xié)議層解決,雙方商量好不再互發(fā)數(shù)據(jù),就可以直接斷開(kāi)連接。
等于說(shuō) muduo 把“主動(dòng)關(guān)閉連接”這件事情分成兩步來(lái)做,如果要主動(dòng)關(guān)閉連接,它會(huì)先關(guān)本地“寫(xiě)”端,等對(duì)方關(guān)閉之后,再關(guān)本地“讀”端。練習(xí):閱讀代碼,回答“如果被動(dòng)關(guān)閉連接,muduo 的行為如何?” 提示:muduo 在 read() 返回 0 的時(shí)候會(huì)回調(diào) connection callback,這樣客戶代碼就知道對(duì)方斷開(kāi)連接了。
Muduo 這種關(guān)閉連接的方式對(duì)對(duì)方也有要求,那就是對(duì)方 read() 到 0 字節(jié)之后會(huì)主動(dòng)關(guān)閉連接(無(wú)論 shutdownWrite() 還是 close()),一般的網(wǎng)絡(luò)程序都會(huì)這樣,不是什么問(wèn)題。當(dāng)然,這么做有一個(gè)潛在的安全漏洞,萬(wàn)一對(duì)方故意不不關(guān),那么 muduo 的連接就一直半開(kāi)著,消耗系統(tǒng)資源。
完整的流程是:我們發(fā)完了數(shù)據(jù),于是 shutdownWrite,發(fā)送 TCP FIN 分節(jié),對(duì)方會(huì)讀到 0 字節(jié),然后對(duì)方通常會(huì)關(guān)閉連接,這樣 muduo 會(huì)讀到 0 字節(jié),然后 muduo 關(guān)閉連接。(思考題:在 shutdown() 之后,muduo 回調(diào) connection callback 的時(shí)間間隔大約是一個(gè) round-trip time,為什么?)
另外,如果有必要,對(duì)方可以在 read() 返回 0 之后繼續(xù)發(fā)送數(shù)據(jù),這是直接利用了 half-close TCP 連接。muduo 會(huì)收到這些數(shù)據(jù),通過(guò) message callback 通知客戶代碼。
那么 muduo 什么時(shí)候真正 close socket 呢?在 TcpConnection 對(duì)象析構(gòu)的時(shí)候。TcpConnection 持有一個(gè) Socket 對(duì)象,Socket 是一個(gè) RAII handler,它的析構(gòu)函數(shù)會(huì) close(sockfd_)。這樣,如果發(fā)生 TcpConnection 對(duì)象泄漏,那么我們從 /proc/pid/fd/ 就能找到?jīng)]有關(guān)閉的文件描述符,便于查錯(cuò)。
muduo 在 read() 返回 0 的時(shí)候會(huì)回調(diào) connection callback,然后把 TcpConnection 的引用計(jì)數(shù)減一,如果 TcpConnection 的引用計(jì)數(shù)降到零,它就會(huì)析構(gòu)了。
參考:
《TCP/IP 詳解》第一卷第 18.5 節(jié),TCP Half-Close。
《UNIX 網(wǎng)絡(luò)編程》第一卷第三版第 6.6 節(jié), shutdown() 函數(shù)。
摘要: 陳碩 (giantchen_AT_gmail) Blog.csdn.net/Solstice 這是《Muduo 網(wǎng)絡(luò)編程示例》系列的第四篇文章。 Muduo 全系列文章列表: http://blog.csdn.net/Solstice/category/779646.aspx Python Twisted 是一款非常好的網(wǎng)絡(luò)庫(kù),它也采用 Reactor 作為網(wǎng)絡(luò)編程的基本模型,所以從使用上與 m...
閱讀全文
陳碩 (giantchen_AT_gmail)
Blog.csdn.net/Solstice
本文只考慮 Linux x86 平臺(tái),服務(wù)端開(kāi)發(fā)(不考慮 Windows 的跨 DLL 內(nèi)存分配釋放問(wèn)題)。本文假定讀者知道 ::operator new() 和 ::operator delete() 是干什么的,與通常用的 new/delete 表達(dá)式有和區(qū)別和聯(lián)系,這方面的知識(shí)可參考侯捷先生的文章《池內(nèi)春秋》[1],或者這篇文章。
C++ 的內(nèi)存管理是個(gè)老生常談的話題,我在《當(dāng)析構(gòu)函數(shù)遇到多線程》第 7 節(jié)“插曲:系統(tǒng)地避免各種指針錯(cuò)誤”中簡(jiǎn)單回顧了一些常見(jiàn)的問(wèn)題以及在現(xiàn)代 C++ 中的解決辦法。基本上,按現(xiàn)代 C++ 的手法(RAII)來(lái)管理內(nèi)存,你很難遇到什么內(nèi)存方面的錯(cuò)誤。“沒(méi)有錯(cuò)誤”是基本要求,不代表“足夠好”。我們常常會(huì)設(shè)法優(yōu)化性能,如果 profiling 表明 hot spot 在內(nèi)存分配和釋放上,重載全局的 ::operator new() 和 ::operator delete() 似乎是一個(gè)一勞永逸好辦法(以下簡(jiǎn)寫(xiě)為“重載 ::operator new()”),本文試圖說(shuō)明這個(gè)辦法往往行不通。
內(nèi)存管理的基本要求
如果只考慮分配和釋放,內(nèi)存管理基本要求是“不重不漏”:既不重復(fù) delete,也不漏掉 delete。也就說(shuō)我們常說(shuō)的 new/delete 要配對(duì),“配對(duì)”不僅是個(gè)數(shù)相等,還隱含了 new 和 delete 的調(diào)用本身要匹配,不要“東家借的東西西家還”。例如:
- 用系統(tǒng)默認(rèn)的 malloc() 分配的內(nèi)存要交給系統(tǒng)默認(rèn)的 free() 去釋放;
- 用系統(tǒng)默認(rèn)的 new 表達(dá)式創(chuàng)建的對(duì)象要交給系統(tǒng)默認(rèn)的 delete 表達(dá)式去析構(gòu)并釋放;
- 用系統(tǒng)默認(rèn)的 new[] 表達(dá)式創(chuàng)建的對(duì)象要交給系統(tǒng)默認(rèn)的 delete[] 表達(dá)式去析構(gòu)并釋放;
- 用系統(tǒng)默認(rèn)的 ::operator new() 分配的的內(nèi)存要交給系統(tǒng)默認(rèn)的 ::operator delete() 去釋放;
- 用 placement new 創(chuàng)建的對(duì)象要用 placement delete (為了表述方便,姑且這么說(shuō)吧)去析構(gòu)(其實(shí)就是直接調(diào)用析構(gòu)函數(shù));
- 從某個(gè)內(nèi)存池 A 分配的內(nèi)存要還給這個(gè)內(nèi)存池。
- 如果定制 new/delete,那么要按規(guī)矩來(lái)。見(jiàn) Effective C++ 相關(guān)條款。
做到以上這些不難,是每個(gè) C++ 開(kāi)發(fā)人員的基本功。不過(guò),如果你想重載全局的 ::operator new(),事情就麻煩了。
重載 ::operator new() 的理由
Effective C++ 第三版第 50 條列舉了定制 new/delete 的幾點(diǎn)理由:
- 檢測(cè)代碼中的內(nèi)存錯(cuò)誤
- 優(yōu)化性能
- 獲得內(nèi)存使用的統(tǒng)計(jì)數(shù)據(jù)
這些都是正當(dāng)?shù)男枨螅哪┪覀儗?huì)看到,不重載 ::operator new() 也能達(dá)到同樣的目的。
::operator new() 的兩種重載方式
1. 不改變其簽名,無(wú)縫直接替換系統(tǒng)原有的版本,例如:
#include <new>
void* operator new(size_t size);
void operator delete(void* p);
用這種方式的重載,使用方不需要包含任何特殊的頭文件,也就是說(shuō)不需要看見(jiàn)這兩個(gè)函數(shù)聲明。“性能優(yōu)化”通常用這種方式。
2. 增加新的參數(shù),調(diào)用時(shí)也提供這些額外的參數(shù),例如:
void* operator new(size_t size, const char* file, int line); // 其返回的指針必須能被普通的 ::operator delete(void*) 釋放
void operator delete(void* p, const char* file, int line); // 這個(gè)函數(shù)只在析構(gòu)函數(shù)拋異常的情況下才會(huì)被調(diào)用
然后用的時(shí)候是
Foo* p = new (__FILE, __LINE__) Foo; // 這樣能跟蹤是哪個(gè)文件哪一行代碼分配的內(nèi)存
我們也可以用宏替換 new 來(lái)節(jié)省打字。用這第二種方式重載,使用方需要看到這兩個(gè)函數(shù)聲明,也就是說(shuō)要主動(dòng)包含你提供的頭文件。“檢測(cè)內(nèi)存錯(cuò)誤”和“統(tǒng)計(jì)內(nèi)存使用情況”通常會(huì)用這種方式重載。當(dāng)然,這不是絕對(duì)的。
在學(xué)習(xí) C++ 的階段,每個(gè)人都可以寫(xiě)個(gè)一兩百行的程序來(lái)驗(yàn)證教科書(shū)上的說(shuō)法,重載 ::operator new() 在這樣的玩具程序里邊不會(huì)造成什么麻煩。
不過(guò),我認(rèn)為在現(xiàn)實(shí)的產(chǎn)品開(kāi)發(fā)中,重載 ::operator new() 乃是下策,我們有更簡(jiǎn)單安全的辦法來(lái)到達(dá)以上目標(biāo)。
現(xiàn)實(shí)的開(kāi)發(fā)環(huán)境
作為 C++ 應(yīng)用程序的開(kāi)發(fā)人員,在編寫(xiě)稍具規(guī)模的程序時(shí),我們通常會(huì)用到一些 library。我們可以根據(jù) library 的提供方把它們大致分為這么幾大類:
- C 語(yǔ)言的標(biāo)準(zhǔn)庫(kù),也包括 Linux 編程環(huán)境提供的 Posix 系列函數(shù)。
- 第三方的 C 語(yǔ)言庫(kù),例如 OpenSSL。
- C++ 語(yǔ)言的標(biāo)準(zhǔn)庫(kù),主要是 STL。(我想沒(méi)有人在產(chǎn)品中使用 IOStream 吧?)
- 第三方的通用 C++ 庫(kù),例如 Boost.Regex,或者某款 XML 庫(kù)。
- 公司其他團(tuán)隊(duì)的人開(kāi)發(fā)的內(nèi)部基礎(chǔ) C++ 庫(kù),比如網(wǎng)絡(luò)通信和日志等基礎(chǔ)設(shè)施。
- 本項(xiàng)目組的同事自己開(kāi)發(fā)的針對(duì)本應(yīng)用的基礎(chǔ)庫(kù),比如某三維模型的仿射變換模塊。
在使用這些 library 的時(shí)候,不可避免地要在各個(gè) library 之間交換數(shù)據(jù)。比方說(shuō) library A 的輸出作為 library B 的輸入,而 library A 的輸出本身常常會(huì)用到動(dòng)態(tài)分配的內(nèi)存(比如 std::vector<double>)。
如果所有的 C++ library 都用同一套內(nèi)存分配器(就是系統(tǒng)默認(rèn)的 new/delete ),那么內(nèi)存的釋放就很方便,直接交給 delete 去釋放就行。如果不是這樣,那就得時(shí)時(shí)刻刻記住“這一塊內(nèi)存是屬于哪個(gè)分配器,是系統(tǒng)默認(rèn)的還是我們定制的,釋放的時(shí)候不要還錯(cuò)了地方”。
(由于 C 語(yǔ)言不像 C++ 一樣提過(guò)了那么多的定制性,C library 通常都會(huì)默認(rèn)直接用 malloc/free 來(lái)分配和釋放內(nèi)存,不存在上面提到的“內(nèi)存還錯(cuò)地方”問(wèn)題。或者有的考慮更全面的 C library 會(huì)讓你注冊(cè)兩個(gè)函數(shù),用于它內(nèi)部分配和釋放內(nèi)存,這就就能完全掌控該 library 的內(nèi)存使用。這種依賴注入的方式在 C++ 里變得花哨而無(wú)用,見(jiàn)陳碩寫(xiě)的《C++ 標(biāo)準(zhǔn)庫(kù)中的allocator是多余的》。)
但是,如果重載了 ::operator new(),事情恐怕就沒(méi)有這么簡(jiǎn)單了。
重載 ::operator new() 的困境
首先,重載 ::operator new() 不會(huì)給 C 語(yǔ)言的庫(kù)帶來(lái)任何麻煩,當(dāng)然,重載它得到的三點(diǎn)好處也無(wú)法讓 C 語(yǔ)言的庫(kù)享受到。
以下僅考慮 C++ library 和 C++ 主程序。
規(guī)則 1:絕對(duì)不能在 library 里重載 ::operator new()
如果你是某個(gè) library 的作者,你的 library 要提供給別人使用,那么你無(wú)權(quán)重載全局 ::operator new(size_t) (注意這是上面提到的第一種重載方式),因?yàn)檫@非常具有侵略性:任何用到你的 library 的程序都被迫使用了你重載的 ::operator new(),而別人很可能不愿意這么做。另外,如果有兩個(gè) library 都試圖重載 ::operator new(size_t),那么它們會(huì)打架,我估計(jì)會(huì)發(fā)生 duplicated symbol link error。干脆,作為 library 的編寫(xiě)者,大家都不要重載 ::operator new(size_t) 好了。
那么第二種重載方式呢?首先,::operator new(size_t size, const char* file, int line) 這種方式得到的 void* 指針必須同時(shí)能被 ::operator delete(void*) 和 ::operator delete(void* p, const char* file, int line) 這兩個(gè)函數(shù)釋放。這時(shí)候你需要決定,你的 ::operator new(size_t size, const char* file, int line) 返回的指針是不是兼容系統(tǒng)默認(rèn)的 ::operator delete(void*)。
- 如果不兼容(也就是說(shuō)不能用系統(tǒng)默認(rèn)的 ::operator delete(void*) 來(lái)釋放內(nèi)存),那么你得重載 ::operator delete(void*),讓它的行為與你的 operator new(size_t size, const char* file, int line) 匹配。一旦你決定重載 ::operator delete(void*),那么你必須重載 ::operator new(size_t),這就回到了情況 1:你無(wú)權(quán)重載全局 ::operator new(size_t)。
- 如果選擇兼容系統(tǒng)默認(rèn)的 ::operator delete(void*),那么你在 operator new(size_t size, const char* file, int line) 里能做的事情非常有限,比方說(shuō)你不能額外動(dòng)態(tài)分配內(nèi)存來(lái)做 house keeping 或保存統(tǒng)計(jì)數(shù)據(jù)(無(wú)論顯示還是隱式),因?yàn)橄到y(tǒng)默認(rèn)的 ::operator delete(void*) 不會(huì)釋放你額外分配的內(nèi)存。(這里隱式分配內(nèi)存指的是往 std::map<> 這樣的容器里添加元素。)
看到這里估計(jì)很多人已經(jīng)暈了,但這還沒(méi)完。
其次,在 library 里重載 operator new(size_t size, const char* file, int line) 還涉及到你的重載要不要暴露給 library 的使用者(其他 library 或主程序)。這里“暴露”有兩層意思:1) 包含你的頭文件的代碼會(huì)不會(huì)用你重載的 ::operator new(),2) 重載之后的 ::operator new() 分配的內(nèi)存能不能在你的 library 之外被安全地釋放。如果不行,那么你是不是要暴露某個(gè)接口函數(shù)來(lái)讓使用者安全地釋放內(nèi)存?或者返回 shared_ptr ,利用其“捕獲”deleter 的特性?聽(tīng)上去好像挺復(fù)雜?這里就不一一展開(kāi)討論了,總之,作為 library 的作者,絕對(duì)不要?jiǎng)?#8220;重載 operator new()”的念頭。
事實(shí) 2:在主程序里重載 ::operator new() 作用不大
這不是一條規(guī)則,而是我試圖說(shuō)明這么做沒(méi)有多大意義。
如果用第一種方式重載全局 ::operator new(size_t),會(huì)影響本程序用到的所有 C++ library,這么做或許不會(huì)有什么問(wèn)題,不過(guò)我建議你使用下一節(jié)介紹的更簡(jiǎn)單的“替代辦法”。
如果用第二種方式重載 ::operator new(size_t size, const char* file, int line),那么你的行為是否惠及本程序用到的其他 C++ library 呢?比方說(shuō)你要不要統(tǒng)計(jì) C++ library 中的內(nèi)存使用情況?如果某個(gè) library 會(huì)返回它自己用 new 分配的內(nèi)存和對(duì)象,讓你用完之后自己釋放,那么是否打算對(duì)錯(cuò)誤釋放內(nèi)存做檢查?
C++ library 從代碼組織上有兩種形式:1) 以頭文件方式提供(如以 STL 和 Boost 為代表的模板庫(kù));2) 以頭文件+二進(jìn)制庫(kù)文件方式提供(大多數(shù)非模板庫(kù)以此方式發(fā)布)。
對(duì)于純以頭文件方式實(shí)現(xiàn)的 library,那么你可以在你的程序的每個(gè) .cpp 文件的第一行包含重載 ::operator new 的頭文件,這樣程序里用到的其他 C++ library 也會(huì)轉(zhuǎn)而使用你的 ::operator new 來(lái)分配內(nèi)存。當(dāng)然這是一種相當(dāng)有侵略性的做法,如果運(yùn)氣好,編譯和運(yùn)行都沒(méi)問(wèn)題;如果運(yùn)氣差一點(diǎn),可能會(huì)遇到編譯錯(cuò)誤,這其實(shí)還不算壞事;運(yùn)氣更差一點(diǎn),編譯沒(méi)有錯(cuò)誤,運(yùn)行的時(shí)候時(shí)不時(shí)出現(xiàn)非法訪問(wèn),導(dǎo)致 segment fault;或者在某些情況下你定制的分配策略與 library 有沖突,內(nèi)存數(shù)據(jù)損壞,出現(xiàn)莫名其妙的行為。
對(duì)于以庫(kù)文件方式實(shí)現(xiàn)的 library,這么做并不能讓其受惠,因?yàn)?library 的源文件已經(jīng)編譯成了二進(jìn)制代碼,它不會(huì)調(diào)用你新重載的 ::operator new(想想看,已經(jīng)編譯的二進(jìn)制代碼怎么可能提供額外的 new (__FILE__, __LINE__) 參數(shù)呢?)更麻煩的是,如果某些頭文件有 inline function,還會(huì)引起詭異的“串?dāng)_”。即 library 有的部分用了你的分配器,有的部分用了系統(tǒng)默認(rèn)的分配器,然后在釋放內(nèi)存的時(shí)候沒(méi)有給對(duì)地方,造成分配器的數(shù)據(jù)結(jié)構(gòu)被破壞。
總之,第二種重載方式看似功能更豐富,但其實(shí)與程序里使用的其他 C++ library 很難無(wú)縫配合。
綜上,對(duì)于現(xiàn)實(shí)生活中的 C++ 項(xiàng)目,重載 ::operator new() 幾乎沒(méi)有用武之地,因?yàn)楹茈y處理好與程序所用的 C++ library 的關(guān)系,畢竟大多數(shù) library 在設(shè)計(jì)的時(shí)候沒(méi)有考慮到你會(huì)重載 ::operator new() 并強(qiáng)塞給它。
如果確實(shí)需要定制內(nèi)存分配,該如何辦?
替代辦法
很簡(jiǎn)單,替換 malloc。如果需要,直接從 malloc 層面入手,通過(guò) LD_PRELOAD 來(lái)加載一個(gè) .so,其中有 malloc/free 的替代實(shí)現(xiàn)(drop-in replacement),這樣能同時(shí)為 C 和 C++ 代碼服務(wù),而且避免 C++ 重載 ::operator new() 的陰暗角落。
對(duì)于“檢測(cè)內(nèi)存錯(cuò)誤”這一用法,我們可以用 valgrind 或者 dmalloc 或者 efence 來(lái)達(dá)到相同的目的,專業(yè)的除錯(cuò)工具比自己山寨一個(gè)內(nèi)存檢查器要靠譜。
對(duì)于“統(tǒng)計(jì)內(nèi)存使用數(shù)據(jù)”,替換 malloc 同樣能得到足夠的信息,因?yàn)槲覀兛梢杂?backtrace() 函數(shù)來(lái)獲得調(diào)用棧,這比 new (__FILE__, __LINE__) 的信息更豐富。比方說(shuō)你通過(guò)分析 (__FILE__, __LINE__) 發(fā)現(xiàn) std::string 大量分配釋放內(nèi)存,有超出預(yù)期的開(kāi)銷,但是你卻不知道代碼里哪一部分在反復(fù)創(chuàng)建和銷毀 std::string 對(duì)象,因?yàn)?(__FILE__, __LINE__) 只能告訴你最內(nèi)層的調(diào)用函數(shù)。用 backtrace() 能找到真正的發(fā)起調(diào)用者。
對(duì)于“性能優(yōu)化”這一用法,我認(rèn)為這目前的多線程開(kāi)發(fā)中,自己實(shí)現(xiàn)一個(gè)能打敗系統(tǒng)默認(rèn)的 malloc 的內(nèi)存分配器是不現(xiàn)實(shí)的。一個(gè)通用的內(nèi)存分配器本來(lái)就有相當(dāng)?shù)碾y度,為多線程程序?qū)崿F(xiàn)一個(gè)安全和高效的通用(全局)內(nèi)存分配器超出了一般開(kāi)發(fā)人員的能力。不如使用現(xiàn)有的針對(duì)多核多線程優(yōu)化的 malloc,例如 Google tcmalloc 和 Intel TBB 2.2 里的內(nèi)存分配器。好在這些 allocator 都不是侵入式的,也無(wú)須重載 ::operator new()。
為單獨(dú)的 class 重載 operator new() 有問(wèn)題嗎?
與全局 ::operator new() 不同,per-class operator new() 和 operator delete () 的影響面要小得多,它只影響本 class 及其派生類。似乎重載 member operator new() 是可行的。我對(duì)此持反對(duì)態(tài)度。
如果一個(gè) class Node 需要重載 member operator new(),說(shuō)明它用到了特殊的內(nèi)存分配策略,常見(jiàn)的情況是使用了內(nèi)存池或?qū)ο蟪亍N覍幵赴堰@一事實(shí)明顯地?cái)[出來(lái),而不是改變 new Node 的默認(rèn)行為。具體地說(shuō),是用 factory 來(lái)創(chuàng)建對(duì)象,比如 static Node* Node::createNode() 或者 static shared_ptr<Node> Node::createNode();。
這可以歸結(jié)為最小驚訝原則:如果我在代碼里讀到 Node* p = new Node,我會(huì)認(rèn)為它在 heap 上分配了內(nèi)存,如果 Node class 重載了 member operator new(),那么我要事先仔細(xì)閱讀 node.h 才能發(fā)現(xiàn)其實(shí)這行代碼使用了私有的內(nèi)存池。為什么不寫(xiě)得明確一點(diǎn)呢?寫(xiě)成 Node* p = Node::createNode(),那么我能猜到 Node::createNode() 肯定做了什么與 new Node 不一樣的事情,免得將來(lái)大吃一驚。
The Zen of Python 說(shuō) explicit is better than implicit,我深信不疑。
總結(jié):重載 ::operator new() 或許在某些臨時(shí)的場(chǎng)合能應(yīng)個(gè)急,但是不應(yīng)該作為一種策略來(lái)使用。如果需要,我們可以從 malloc 層面入手,徹底而全面地替換內(nèi)存分配器。
參考文獻(xiàn):
[1] 侯捷,《池內(nèi)春秋—— Memory Pool 的設(shè)計(jì)哲學(xué)與無(wú)痛運(yùn)用》,《程序員》2002 年第 9 期。
匿名 namespace (anonymous namespace 或稱 unnamed namespace) 是 C++ 的一項(xiàng)非常有用的功能,其主要目的是讓該 namespace 中的成員(變量或函數(shù))具有獨(dú)一無(wú)二的全局名稱,避免名字碰撞 (name collisions)。一般在編寫(xiě) .cpp 文件時(shí),如果需要寫(xiě)一些小的 helper 函數(shù),我們常常會(huì)放到匿名 namespace 里。muduo 0.1.7 中的 muduo/base/Date.cc 和 muduo/base/Thread.cc 等處就用到了匿名 namespace。
我最近在工作中遇到并重新思考了這一問(wèn)題,發(fā)現(xiàn)匿名 namespace 并不是多多益善。
C 語(yǔ)言的 static 關(guān)鍵字的兩種用法
C 語(yǔ)言的 static 關(guān)鍵字有兩種用途:
1. 用于函數(shù)內(nèi)部修飾變量,即函數(shù)內(nèi)的靜態(tài)變量。這種變量的生存期長(zhǎng)于該函數(shù),使得函數(shù)具有一定的“狀態(tài)”。使用靜態(tài)變量的函數(shù)一般是不可重入的,也不是線程安全的。
2. 用在文件級(jí)別(函數(shù)體之外),修飾變量或函數(shù),表示該變量或函數(shù)只在本文件可見(jiàn),其他文件看不到也訪問(wèn)不到該變量或函數(shù)。專業(yè)的說(shuō)法叫“具有 internal linkage”(簡(jiǎn)言之:不暴露給別的 translation unit)。
C 語(yǔ)言的這兩種用法很明確,一般也不容易混淆。
C++ 語(yǔ)言的 static 關(guān)鍵字的四種用法
由于 C++ 引入了 class,在保持與 C 語(yǔ)言兼容的同時(shí),static 關(guān)鍵字又有了兩種新用法:
3. 用于修飾 class 的數(shù)據(jù)成員,即所謂“靜態(tài)成員”。這種數(shù)據(jù)成員的生存期大于 class 的對(duì)象(實(shí)體 instance)。靜態(tài)數(shù)據(jù)成員是每個(gè) class 有一份,普通數(shù)據(jù)成員是每個(gè) instance 有一份,因此也分別叫做 class variable 和 instance variable。
4. 用于修飾 class 的成員函數(shù),即所謂“靜態(tài)成員函數(shù)”。這種成員函數(shù)只能訪問(wèn) class variable 和其他靜態(tài)程序函數(shù),不能訪問(wèn) instance variable 或 instance method。
當(dāng)然,這幾種用法可以相互組合,比如 C++ 的成員函數(shù)(無(wú)論 static 還是 instance)都可以有其局部的靜態(tài)變量(上面的用法 1)。對(duì)于 class template 和 function template,其中的 static 對(duì)象的真正個(gè)數(shù)跟 template instantiation (模板具現(xiàn)化)有關(guān),相信學(xué)過(guò) C++ 模板的人不會(huì)陌生。
可見(jiàn)在 C++ 里 static 被 overload 了多次。匿名 namespace 的引入是為了減輕 static 的負(fù)擔(dān),它替換了 static 的第 2 種用途。也就是說(shuō),在 C++ 里不必使用文件級(jí)的 static 關(guān)鍵字,我們可以用匿名 namespace 達(dá)到相同的效果。(其實(shí)嚴(yán)格地說(shuō),linkage 或許稍有不同,這里不展開(kāi)討論了。)
匿名 namespace 的不利之處
在工程實(shí)踐中,匿名 namespace 有兩大不利之處:
- 其中的函數(shù)難以設(shè)斷點(diǎn),如果你像我一樣使用的是 gdb 這樣的文本模式 debugger。
- 使用某些版本的 g++ 時(shí),同一個(gè)文件每次編譯出來(lái)的二進(jìn)制文件會(huì)變化,這讓某些 build tool 失靈。
考慮下面這段簡(jiǎn)短的代碼 (anon.cc):
1: namespace
2: { 3: void foo()
4: { 5: }
6: }
7:
8: int main()
9: { 10: foo();
11: }
對(duì)于問(wèn)題 1:
gdb 的<tab>鍵自動(dòng)補(bǔ)全功能能幫我們?cè)O(shè)定斷點(diǎn),不是什么大問(wèn)題。前提是你知道那個(gè)"(anonymous namespace)::foo()"正是你想要的函數(shù)。
$ gdb ./a.out
GNU gdb (GDB) 7.0.1-debian
(gdb) b '<tab>
(anonymous namespace) __data_start _end
(anonymous namespace)::foo() __do_global_ctors_aux _fini
_DYNAMIC __do_global_dtors_aux _init
_GLOBAL_OFFSET_TABLE_ __dso_handle _start
_IO_stdin_used __gxx_personality_v0 anon.cc
__CTOR_END__ __gxx_personality_v0@plt call_gmon_start
__CTOR_LIST__ __init_array_end completed.6341
__DTOR_END__ __init_array_start data_start
__DTOR_LIST__ __libc_csu_fini dtor_idx.6343
__FRAME_END__ __libc_csu_init foo
__JCR_END__ __libc_start_main frame_dummy
__JCR_LIST__ __libc_start_main@plt int
__bss_start _edata main
(gdb) b '(<tab>
anonymous namespace) anonymous namespace)::foo()
(gdb) b '(anonymous namespace)::foo()'
Breakpoint 1 at 0x400588: file anon.cc, line 4.
麻煩的是,如果兩個(gè)文件 anon.cc 和 anonlib.cc 都定義了匿名空間中的 foo() 函數(shù)(這不會(huì)沖突),那么 gdb 無(wú)法區(qū)分這兩個(gè)函數(shù),你只能給其中一個(gè)設(shè)斷點(diǎn)。或者你使用 文件名:行號(hào) 的方式來(lái)分別設(shè)斷點(diǎn)。(從技術(shù)上,匿名 namespace 中的函數(shù)是 weak text,鏈接的時(shí)候如果發(fā)生符號(hào)重名,linker 不會(huì)報(bào)錯(cuò)。)
從根本上解決的辦法是使用普通具名 namespace,如果怕重名,可以把源文件名(必要時(shí)加上路徑)作為 namespace 名字的一部分。
對(duì)于問(wèn)題 2:
把它編譯兩次,分別生成 a.out 和 b.out:
$ g++ -g -o a.out anon.cc
$ g++ -g -o b.out anon.cc
$ md5sum a.out b.out
0f7a9cc15af7ab1e57af17ba16afcd70 a.out
8f22fc2bbfc27beb922aefa97d174e3b b.out
$ g++ --version
g++ (GCC) 4.2.4 (Ubuntu 4.2.4-1ubuntu4)
$ diff -u <(nm a.out) <(nm b.out)
--- /dev/fd/63 2011-02-15 22:27:58.960754999 +0800
+++ /dev/fd/62 2011-02-15 22:27:58.960754999 +0800
@@ -2,7 +2,7 @@
0000000000600940 d _GLOBAL_OFFSET_TABLE_
0000000000400634 R _IO_stdin_used
w _Jv_RegisterClasses
-0000000000400538 t _ZN36_GLOBAL__N_anon.cc_00000000_E2CEEB513fooEv
+0000000000400538 t _ZN36_GLOBAL__N_anon.cc_00000000_CB51498D3fooEv
0000000000600748 d __CTOR_END__
0000000000600740 d __CTOR_LIST__
0000000000600758 d __DTOR_END__
由上可見(jiàn),g++ 4.2.4 會(huì)隨機(jī)地給匿名 namespace 生成一個(gè)惟一的名字(foo() 函數(shù)的 mangled name 中的 E2CEEB51 和 CB51498D 是隨機(jī)的),以保證名字不沖突。也就是說(shuō),同樣的源文件,兩次編譯得到的二進(jìn)制文件內(nèi)容不相同,這有時(shí)候會(huì)造成問(wèn)題。比如說(shuō)拿到一個(gè)會(huì)發(fā)生 core dump 的二進(jìn)制可執(zhí)行文件,無(wú)法確定它是由哪個(gè) revision 的代碼編譯出來(lái)的。畢竟編譯結(jié)果不可復(fù)現(xiàn),具有一定的隨機(jī)性。
這可以用 gcc 的 -frandom-seed 參數(shù)解決,具體見(jiàn)文檔。
這個(gè)現(xiàn)象在 gcc 4.2.4 中存在(之前的版本估計(jì)類似),在 gcc 4.4.5 中不存在。
替代辦法
如果前面的“不利之處”給你帶來(lái)困擾,解決辦法也很簡(jiǎn)單,就是使用普通具名 namespace。當(dāng)然,要起一個(gè)好的名字,比如 boost 里就常常用 boost::detail 來(lái)放那些“不應(yīng)該暴露給客戶,但又不得不放到頭文件里”的函數(shù)或 class。
總而言之,匿名 namespace 沒(méi)什么大問(wèn)題,使用它也不是什么過(guò)錯(cuò)。萬(wàn)一它礙事了,可以用普通具名 namespace 替代之。
這是一套緊湊的 PPT,基本上每一張幻燈片都可以單獨(dú)寫(xiě)一篇博客,但是我沒(méi)有那么多時(shí)間一一展開(kāi)論述,只能把結(jié)論和主要論據(jù)列了出來(lái)。





























摘要: 陳碩 (giantchen_AT_gmail) Blog.csdn.net/Solstice 這是《Muduo 網(wǎng)絡(luò)編程示例》系列的第三篇文章。 Muduo 全系列文章列表: http://blog.csdn.net/Solstice/category/779646.aspx 程序中的時(shí)間 程序中對(duì)時(shí)間的處理是個(gè)大問(wèn)題,我打算單獨(dú)寫(xiě)一篇文章來(lái)全面地討論這個(gè)問(wèn)題。文章暫定名《〈程...
閱讀全文
陳碩 (giantchen_AT_gmail)
Blog.csdn.net/Solstice
這是《Muduo 網(wǎng)絡(luò)編程示例》系列的第二篇文章。
Muduo 全系列文章列表: http://blog.csdn.net/Solstice/category/779646.aspx
本文講介紹一個(gè)與 Boost.Asio 的示例代碼中的聊天服務(wù)器功能類似的網(wǎng)絡(luò)服務(wù)程序,包括客戶端與服務(wù)端的 muduo 實(shí)現(xiàn)。這個(gè)例子的主要目的是介紹如何處理分包,并初步涉及 Muduo 的多線程功能。Muduo 的下載地址: http://muduo.googlecode.com/files/muduo-0.1.7-alpha.tar.gz ,SHA1 873567e43b3c2cae592101ea809b30ba730f2ee6,本文的完整代碼可在線閱讀
http://code.google.com/p/muduo/source/browse/trunk/examples/asio/chat/ 。
TCP 分包
前面一篇《五個(gè)簡(jiǎn)單 TCP 協(xié)議》中處理的協(xié)議沒(méi)有涉及分包,在 TCP 這種字節(jié)流協(xié)議上做應(yīng)用層分包是網(wǎng)絡(luò)編程的基本需求。分包指的是在發(fā)生一個(gè)消息(message)或一幀(frame)數(shù)據(jù)時(shí),通過(guò)一定的處理,讓接收方能從字節(jié)流中識(shí)別并截取(還原)出一個(gè)個(gè)消息。“粘包問(wèn)題”是個(gè)偽問(wèn)題。
對(duì)于短連接的 TCP 服務(wù),分包不是一個(gè)問(wèn)題,只要發(fā)送方主動(dòng)關(guān)閉連接,就表示一條消息發(fā)送完畢,接收方 read() 返回 0,從而知道消息的結(jié)尾。例如前一篇文章里的 daytime 和 time 協(xié)議。
對(duì)于長(zhǎng)連接的 TCP 服務(wù),分包有四種方法:
- 消息長(zhǎng)度固定,比如 muduo 的 roundtrip 示例就采用了固定的 16 字節(jié)消息;
- 使用特殊的字符或字符串作為消息的邊界,例如 HTTP 協(xié)議的 headers 以 "\r\n" 為字段的分隔符;
- 在每條消息的頭部加一個(gè)長(zhǎng)度字段,這恐怕是最常見(jiàn)的做法,本文的聊天協(xié)議也采用這一辦法;
- 利用消息本身的格式來(lái)分包,例如 XML 格式的消息中 <root>...</root> 的配對(duì),或者 JSON 格式中的 { ... } 的配對(duì)。解析這種消息格式通常會(huì)用到狀態(tài)機(jī)。
在后文的代碼講解中還會(huì)仔細(xì)討論用長(zhǎng)度字段分包的常見(jiàn)陷阱。
聊天服務(wù)
本文實(shí)現(xiàn)的聊天服務(wù)非常簡(jiǎn)單,由服務(wù)端程序和客戶端程序組成,協(xié)議如下:
- 服務(wù)端程序中某個(gè)端口偵聽(tīng) (listen) 新的連接;
- 客戶端向服務(wù)端發(fā)起連接;
- 連接建立之后,客戶端隨時(shí)準(zhǔn)備接收服務(wù)端的消息并在屏幕上顯示出來(lái);
- 客戶端接受鍵盤(pán)輸入,以回車為界,把消息發(fā)送給服務(wù)端;
- 服務(wù)端接收到消息之后,依次發(fā)送給每個(gè)連接到它的客戶端;原來(lái)發(fā)送消息的客戶端進(jìn)程也會(huì)收到這條消息;
- 一個(gè)服務(wù)端進(jìn)程可以同時(shí)服務(wù)多個(gè)客戶端進(jìn)程,當(dāng)有消息到達(dá)服務(wù)端后,每個(gè)客戶端進(jìn)程都會(huì)收到同一條消息,服務(wù)端廣播發(fā)送消息的順序是任意的,不一定哪個(gè)客戶端會(huì)先收到這條消息。
- (可選)如果消息 A 先于消息 B 到達(dá)服務(wù)端,那么每個(gè)客戶端都會(huì)先收到 A 再收到 B。
這實(shí)際上是一個(gè)簡(jiǎn)單的基于 TCP 的應(yīng)用層廣播協(xié)議,由服務(wù)端負(fù)責(zé)把消息發(fā)送給每個(gè)連接到它的客戶端。參與“聊天”的既可以是人,也可以是程序。在以后的文章中,我將介紹一個(gè)稍微復(fù)雜的一點(diǎn)的例子 hub,它有“聊天室”的功能,客戶端可以注冊(cè)特定的 topic(s),并往某個(gè) topic 發(fā)送消息,這樣代碼更有意思。
消息格式
本聊天服務(wù)的消息格式非常簡(jiǎn)單,“消息”本身是一個(gè)字符串,每條消息的有一個(gè) 4 字節(jié)的頭部,以網(wǎng)絡(luò)序存放字符串的長(zhǎng)度。消息之間沒(méi)有間隙,字符串也不一定以 '\0' 結(jié)尾。比方說(shuō)有兩條消息 "hello" 和 "chenshuo",那么打包后的字節(jié)流是:
0x00, 0x00, 0x00, 0x05, 'h', 'e', 'l', 'l', 'o', 0x00, 0x00, 0x00, 0x08, 'c', 'h', 'e', 'n', 's', 'h', 'u', 'o'
共 21 字節(jié)。
打包的代碼
這段代碼把 const string& message 打包為 muduo::net::Buffer,并通過(guò) conn 發(fā)送。
1: void send(muduo::net::TcpConnection* conn, const string& message)
2: { 3: muduo::net::Buffer buf;
4: buf.append(message.data(), message.size());
5: int32_t len = muduo::net::sockets::hostToNetwork32(static_cast<int32_t>(message.size()));
6: buf.prepend(&len, sizeof len);
7: conn->send(&buf);
8: }
muduo::Buffer 有一個(gè)很好的功能,它在頭部預(yù)留了 8 個(gè)字節(jié)的空間,這樣第 6 行的 prepend() 操作就不需要移動(dòng)已有的數(shù)據(jù),效率較高。
分包的代碼
解析數(shù)據(jù)往往比生成數(shù)據(jù)復(fù)雜,分包打包也不例外。
1: void onMessage(const muduo::net::TcpConnectionPtr& conn,
2: muduo::net::Buffer* buf,
3: muduo::Timestamp receiveTime)
4: { 5: while (buf->readableBytes() >= kHeaderLen)
6: { 7: const void* data = buf->peek();
8: int32_t tmp = *static_cast<const int32_t*>(data);
9: int32_t len = muduo::net::sockets::networkToHost32(tmp);
10: if (len > 65536 || len < 0)
11: { 12: LOG_ERROR << "Invalid length " << len;
13: conn->shutdown();
14: }
15: else if (buf->readableBytes() >= len + kHeaderLen)
16: { 17: buf->retrieve(kHeaderLen);
18: muduo::string message(buf->peek(), len);
19: buf->retrieve(len);
20: messageCallback_(conn, message, receiveTime); // 收到完整的消息,通知用戶
21: }
22: else
23: { 24: break;
25: }
26: }
27: }
上面這段代碼第 7 行用了 while 循環(huán)來(lái)反復(fù)讀取數(shù)據(jù),直到 Buffer 中的數(shù)據(jù)不夠一條完整的消息。請(qǐng)讀者思考,如果換成 if (buf->readableBytes() >= kHeaderLen) 會(huì)有什么后果。
以前面提到的兩條消息的字節(jié)流為例:
0x00, 0x00, 0x00, 0x05, 'h', 'e', 'l', 'l', 'o', 0x00, 0x00, 0x00, 0x08, 'c', 'h', 'e', 'n', 's', 'h', 'u', 'o'
假設(shè)數(shù)據(jù)最終都全部到達(dá),onMessage() 至少要能正確處理以下各種數(shù)據(jù)到達(dá)的次序,每種情況下 messageCallback_ 都應(yīng)該被調(diào)用兩次:
- 每次收到一個(gè)字節(jié)的數(shù)據(jù),onMessage() 被調(diào)用 21 次;
- 數(shù)據(jù)分兩次到達(dá),第一次收到 2 個(gè)字節(jié),不足消息的長(zhǎng)度字段;
- 數(shù)據(jù)分兩次到達(dá),第一次收到 4 個(gè)字節(jié),剛好夠長(zhǎng)度字段,但是沒(méi)有 body;
- 數(shù)據(jù)分兩次到達(dá),第一次收到 8 個(gè)字節(jié),長(zhǎng)度完整,但 body 不完整;
- 數(shù)據(jù)分兩次到達(dá),第一次收到 9 個(gè)字節(jié),長(zhǎng)度完整,body 也完整;
- 數(shù)據(jù)分兩次到達(dá),第一次收到 10 個(gè)字節(jié),第一條消息的長(zhǎng)度完整、body 也完整,第二條消息長(zhǎng)度不完整;
- 請(qǐng)自行移動(dòng)分割點(diǎn),驗(yàn)證各種情況;
- 數(shù)據(jù)一次就全部到達(dá),這時(shí)必須用 while 循環(huán)來(lái)讀出兩條消息,否則消息會(huì)堆積。
請(qǐng)讀者驗(yàn)證 onMessage() 是否做到了以上幾點(diǎn)。這個(gè)例子充分說(shuō)明了 non-blocking read 必須和 input buffer 一起使用。
編解碼器 LengthHeaderCodec
有人評(píng)論 Muduo 的接收緩沖區(qū)不能設(shè)置回調(diào)函數(shù)的觸發(fā)條件,確實(shí)如此。每當(dāng) socket 可讀,Muduo 的 TcpConnection 會(huì)讀取數(shù)據(jù)并存入 Input Buffer,然后回調(diào)用戶的函數(shù)。不過(guò),一個(gè)簡(jiǎn)單的間接層就能解決問(wèn)題,讓用戶代碼只關(guān)心“消息到達(dá)”而不是“數(shù)據(jù)到達(dá)”,如本例中的 LengthHeaderCodec 所展示的那一樣。
1: #ifndef MUDUO_EXAMPLES_ASIO_CHAT_CODEC_H
2: #define MUDUO_EXAMPLES_ASIO_CHAT_CODEC_H
3:
4: #include <muduo/base/Logging.h>
5: #include <muduo/net/Buffer.h>
6: #include <muduo/net/SocketsOps.h>
7: #include <muduo/net/TcpConnection.h>
8:
9: #include <boost/function.hpp>
10: #include <boost/noncopyable.hpp>
11:
12: using muduo::Logger;
13:
14: class LengthHeaderCodec : boost::noncopyable
15: { 16: public:
17: typedef boost::function<void (const muduo::net::TcpConnectionPtr&,
18: const muduo::string& message,
19: muduo::Timestamp)> StringMessageCallback;
20:
21: explicit LengthHeaderCodec(const StringMessageCallback& cb)
22: : messageCallback_(cb)
23: { 24: }
25:
26: void onMessage(const muduo::net::TcpConnectionPtr& conn,
27: muduo::net::Buffer* buf,
28: muduo::Timestamp receiveTime)
29: { 同上 } 30:
31: void send(muduo::net::TcpConnection* conn, const muduo::string& message)
32: { 同上 } 33:
34: private:
35: StringMessageCallback messageCallback_;
36: const static size_t kHeaderLen = sizeof(int32_t);
37: };
38:
39: #endif // MUDUO_EXAMPLES_ASIO_CHAT_CODEC_H
這段代碼把以 Buffer* 為參數(shù)的 MessageCallback 轉(zhuǎn)換成了以 const string& 為參數(shù)的 StringMessageCallback,讓用戶代碼不必關(guān)心分包操作。客戶端和服務(wù)端都能從中受益。
服務(wù)端的實(shí)現(xiàn)
聊天服務(wù)器的服務(wù)端代碼小于 100 行,不到 asio 的一半。
請(qǐng)先閱讀第 68 行起的數(shù)據(jù)成員的定義。除了經(jīng)常見(jiàn)到的 EventLoop 和 TcpServer,ChatServer 還定義了 codec_ 和 std::set<TcpConnectionPtr> connections_ 作為成員,connections_ 是目前已建立的客戶連接,在收到消息之后,服務(wù)器會(huì)遍歷整個(gè)容器,把消息廣播給其中每一個(gè) TCP 連接。
首先,在構(gòu)造函數(shù)里注冊(cè)回調(diào):
1: #include "codec.h"
2:
3: #include <muduo/base/Logging.h>
4: #include <muduo/base/Mutex.h>
5: #include <muduo/net/EventLoop.h>
6: #include <muduo/net/SocketsOps.h>
7: #include <muduo/net/TcpServer.h>
8:
9: #include <boost/bind.hpp>
10:
11: #include <set>
12: #include <stdio.h>
13:
14: using namespace muduo;
15: using namespace muduo::net;
16:
17: class ChatServer : boost::noncopyable
18: { 19: public:
20: ChatServer(EventLoop* loop,
21: const InetAddress& listenAddr)
22: : loop_(loop),
23: server_(loop, listenAddr, "ChatServer"),
24: codec_(boost::bind(&ChatServer::onStringMessage, this, _1, _2, _3))
25: { 26: server_.setConnectionCallback(
27: boost::bind(&ChatServer::onConnection, this, _1));
28: server_.setMessageCallback(
29: boost::bind(&LengthHeaderCodec::onMessage, &codec_, _1, _2, _3));
30: }
31:
32: void start()
33: { 34: server_.start();
35: }
36:
這里有幾點(diǎn)值得注意,在以往的代碼里是直接把本 class 的 onMessage() 注冊(cè)給 server_;這里我們把 LengthHeaderCodec::onMessage() 注冊(cè)給 server_,然后向 codec_ 注冊(cè)了 ChatServer::onStringMessage(),等于說(shuō)讓 codec_ 負(fù)責(zé)解析消息,然后把完整的消息回調(diào)給 ChatServer。這正是我前面提到的“一個(gè)簡(jiǎn)單的間接層”,在不增加 Muduo 庫(kù)的復(fù)雜度的前提下,提供了足夠的靈活性讓我們?cè)谟脩舸a里完成需要的工作。
另外,server_.start() 絕對(duì)不能在構(gòu)造函數(shù)里調(diào)用,這么做將來(lái)會(huì)有線程安全的問(wèn)題,見(jiàn)我在《當(dāng)析構(gòu)函數(shù)遇到多線程 ── C++ 中線程安全的對(duì)象回調(diào)》一文中的論述。
以下是處理連接的建立和斷開(kāi)的代碼,注意它把新建的連接加入到 connections_ 容器中,把已斷開(kāi)的連接從容器中刪除。這么做是為了避免內(nèi)存和資源泄漏,TcpConnectionPtr 是 boost::shared_ptr<TcpConnection>,是 muduo 里唯一一個(gè)默認(rèn)采用 shared_ptr 來(lái)管理生命期的對(duì)象。以后我們會(huì)談到這么做的原因。
37: private:
38: void onConnection(const TcpConnectionPtr& conn)
39: { 40: LOG_INFO << conn->localAddress().toHostPort() << " -> "
41: << conn->peerAddress().toHostPort() << " is "
42: << (conn->connected() ? "UP" : "DOWN");
43:
44: MutexLockGuard lock(mutex_);
45: if (conn->connected())
46: { 47: connections_.insert(conn);
48: }
49: else
50: { 51: connections_.erase(conn);
52: }
53: }
54:
以下是服務(wù)端處理消息的代碼,它遍歷整個(gè) connections_ 容器,把消息打包發(fā)送給各個(gè)客戶連接。
55: void onStringMessage(const TcpConnectionPtr&,
56: const string& message,
57: Timestamp)
58: { 59: MutexLockGuard lock(mutex_);
60: for (ConnectionList::iterator it = connections_.begin();
61: it != connections_.end();
62: ++it)
63: { 64: codec_.send(get_pointer(*it), message);
65: }
66: }
67:
數(shù)據(jù)成員:
68: typedef std::set<TcpConnectionPtr> ConnectionList;
69: EventLoop* loop_;
70: TcpServer server_;
71: LengthHeaderCodec codec_;
72: MutexLock mutex_;
73: ConnectionList connections_;
74: };
75:
main() 函數(shù)里邊是例行公事的代碼:
76: int main(int argc, char* argv[])
77: { 78: LOG_INFO << "pid = " << getpid();
79: if (argc > 1)
80: { 81: EventLoop loop;
82: uint16_t port = static_cast<uint16_t>(atoi(argv[1]));
83: InetAddress serverAddr(port);
84: ChatServer server(&loop, serverAddr);
85: server.start();
86: loop.loop();
87: }
88: else
89: { 90: printf("Usage: %s port\n", argv[0]); 91: }
92: }
如果你讀過(guò) asio 的對(duì)應(yīng)代碼,會(huì)不會(huì)覺(jué)得 Reactor 往往比 Proactor 容易使用?
客戶端的實(shí)現(xiàn)
我有時(shí)覺(jué)得服務(wù)端的程序常常比客戶端的更容易寫(xiě),聊天服務(wù)器再次驗(yàn)證了我的看法。客戶端的復(fù)雜性來(lái)自于它要讀取鍵盤(pán)輸入,而 EventLoop 是獨(dú)占線程的,所以我用了兩個(gè)線程,main() 函數(shù)所在的線程負(fù)責(zé)讀鍵盤(pán),另外用一個(gè) EventLoopThread 來(lái)處理網(wǎng)絡(luò) IO。我暫時(shí)沒(méi)有把標(biāo)準(zhǔn)輸入輸出融入 Reactor 的想法,因?yàn)榉?wù)器程序的 stdin 和 stdout 往往是重定向了的。
來(lái)看代碼,首先,在構(gòu)造函數(shù)里注冊(cè)回調(diào),并使用了跟前面一樣的 LengthHeaderCodec 作為中間層,負(fù)責(zé)打包分包。
1: #include "codec.h"
2:
3: #include <muduo/base/Logging.h>
4: #include <muduo/base/Mutex.h>
5: #include <muduo/net/EventLoopThread.h>
6: #include <muduo/net/TcpClient.h>
7:
8: #include <boost/bind.hpp>
9: #include <boost/noncopyable.hpp>
10:
11: #include <iostream>
12: #include <stdio.h>
13:
14: using namespace muduo;
15: using namespace muduo::net;
16:
17: class ChatClient : boost::noncopyable
18: { 19: public:
20: ChatClient(EventLoop* loop, const InetAddress& listenAddr)
21: : loop_(loop),
22: client_(loop, listenAddr, "ChatClient"),
23: codec_(boost::bind(&ChatClient::onStringMessage, this, _1, _2, _3))
24: { 25: client_.setConnectionCallback(
26: boost::bind(&ChatClient::onConnection, this, _1));
27: client_.setMessageCallback(
28: boost::bind(&LengthHeaderCodec::onMessage, &codec_, _1, _2, _3));
29: client_.enableRetry();
30: }
31:
32: void connect()
33: { 34: client_.connect();
35: }
36:
disconnect() 目前為空,客戶端的連接由操作系統(tǒng)在進(jìn)程終止時(shí)關(guān)閉。
37: void disconnect()
38: { 39: // client_.disconnect();
40: }
41:
write() 會(huì)由 main 線程調(diào)用,所以要加鎖,這個(gè)鎖不是為了保護(hù) TcpConnection,而是保護(hù) shared_ptr。
42: void write(const string& message)
43: { 44: MutexLockGuard lock(mutex_);
45: if (connection_)
46: { 47: codec_.send(get_pointer(connection_), message);
48: }
49: }
50:
onConnection() 會(huì)由 EventLoop 線程調(diào)用,所以要加鎖以保護(hù) shared_ptr。
51: private:
52: void onConnection(const TcpConnectionPtr& conn)
53: { 54: LOG_INFO << conn->localAddress().toHostPort() << " -> "
55: << conn->peerAddress().toHostPort() << " is "
56: << (conn->connected() ? "UP" : "DOWN");
57:
58: MutexLockGuard lock(mutex_);
59: if (conn->connected())
60: { 61: connection_ = conn;
62: }
63: else
64: { 65: connection_.reset();
66: }
67: }
68:
把收到的消息打印到屏幕,這個(gè)函數(shù)由 EventLoop 線程調(diào)用,但是不用加鎖,因?yàn)?printf() 是線程安全的。
注意這里不能用 cout,它不是線程安全的。
69: void onStringMessage(const TcpConnectionPtr&,
70: const string& message,
71: Timestamp)
72: { 73: printf("<<< %s\n", message.c_str()); 74: }
75:
數(shù)據(jù)成員:
76: EventLoop* loop_;
77: TcpClient client_;
78: LengthHeaderCodec codec_;
79: MutexLock mutex_;
80: TcpConnectionPtr connection_;
81: };
82:
main() 函數(shù)里除了例行公事,還要啟動(dòng) EventLoop 線程和讀取鍵盤(pán)輸入。
83: int main(int argc, char* argv[])
84: { 85: LOG_INFO << "pid = " << getpid();
86: if (argc > 2)
87: { 88: EventLoopThread loopThread;
89: uint16_t port = static_cast<uint16_t>(atoi(argv[2]));
90: InetAddress serverAddr(argv[1], port);
91:
92: ChatClient client(loopThread.startLoop(), serverAddr); // 注冊(cè)到 EventLoopThread 的 EventLoop 上。
93: client.connect();
94: std::string line;
95: while (std::getline(std::cin, line))
96: { 97: string message(line.c_str()); // 這里似乎多此一舉,可直接發(fā)送 line。這里是
98: client.write(message);
99: }
100: client.disconnect();
101: }
102: else
103: { 104: printf("Usage: %s host_ip port\n", argv[0]); 105: }
106: }
107:
簡(jiǎn)單測(cè)試
開(kāi)三個(gè)命令行窗口,在第一個(gè)運(yùn)行
$ ./asio_chat_server 3000
第二個(gè)運(yùn)行
$ ./asio_chat_client 127.0.0.1 3000
第三個(gè)運(yùn)行同樣的命令
$ ./asio_chat_client 127.0.0.1 3000
這樣就有兩個(gè)客戶端進(jìn)程參與聊天。在第二個(gè)窗口里輸入一些字符并回車,字符會(huì)出現(xiàn)在本窗口和第三個(gè)窗口中。
下一篇文章我會(huì)介紹 Muduo 中的定時(shí)器,并實(shí)現(xiàn) Boost.Asio 教程中的 timer2~5 示例,以及帶流量統(tǒng)計(jì)功能的 discard 和 echo 服務(wù)器(來(lái)自 Java Netty)。流量等于單位時(shí)間內(nèi)發(fā)送或接受的字節(jié)數(shù),這要用到定時(shí)器功能。
(待續(xù))
摘要: 這是《Muduo 網(wǎng)絡(luò)編程示例》系列的第一篇文章。本文將介紹五個(gè)簡(jiǎn)單 TCP 網(wǎng)絡(luò)服務(wù)協(xié)議的 muduo 實(shí)現(xiàn),包括 echo、discard、chargen、daytime、time,以及 time 協(xié)議的客戶端。以上五個(gè)協(xié)議使用不同的端口,可以放到同一個(gè)進(jìn)程中實(shí)現(xiàn),且不必使用多線程。
閱讀全文
陳碩 (giantchen_AT_gmail)
Blog.csdn.net/Solstice
Muduo 全系列文章列表: http://blog.csdn.net/Solstice/category/779646.aspx
我將會(huì)寫(xiě)一系列文章,介紹用 muduo 網(wǎng)絡(luò)庫(kù)完成常見(jiàn)的 TCP 網(wǎng)絡(luò)編程任務(wù)。目前計(jì)劃如下:
- UNP 中的簡(jiǎn)單協(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è)版本。
- 用于測(cè)試兩臺(tái)機(jī)器的往返延遲的 roundtrip
- 用于測(cè)試兩臺(tái)機(jī)器的帶寬的 pingpong
- 文件傳輸
- 一個(gè)基于 TCP 的應(yīng)用層廣播 hub
- socks4a 代理服務(wù)器,包括簡(jiǎn)單的 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
這些例子都比較簡(jiǎn)單,邏輯不復(fù)雜,代碼也很短,適合摘取關(guān)鍵部分放到博客上。其中一些有一定的代表性與針對(duì)性,比如“如何傳輸完整的文件”估計(jì)是網(wǎng)絡(luò)編程的初學(xué)者經(jīng)常遇到的問(wèn)題。請(qǐng)注意,muduo 是設(shè)計(jì)來(lái)開(kāi)發(fā)內(nèi)網(wǎng)的網(wǎng)絡(luò)程序,它沒(méi)有做任何安全方面的加強(qiáng)措施,如果用在公網(wǎng)上可能會(huì)受到攻擊,在后面的例子中我會(huì)談到這一點(diǎn)。
本系列文章適用于 Linux 2.6.x (x > 25),主要測(cè)試發(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) 連接。
- 連接的斷開(kāi),包括主動(dòng)斷開(kāi) (close 或 shutdown) 和被動(dòng)斷開(kāi) (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ù)寫(xiě)入操作系統(tǒng)的緩沖區(qū),將由 TCP 協(xié)議棧負(fù)責(zé)數(shù)據(jù)的發(fā)送與重傳,不代表對(duì)方已經(jīng)收到數(shù)據(jù)。
這其中有很多難點(diǎn),也有很多細(xì)節(jié)需要注意,比方說(shuō):
- 如果要主動(dòng)關(guān)閉連接,如何保證對(duì)方已經(jīng)收到全部數(shù)據(jù)?如果應(yīng)用層有緩沖(這在非阻塞網(wǎng)絡(luò)編程中是必須的,見(jiàn)下文),那么如何保證先發(fā)送完緩沖區(qū)中的數(shù)據(jù),然后再斷開(kāi)連接。直接調(diào)用 close(2) 恐怕是不行的。
- 如果主動(dòng)發(fā)起連接,但是對(duì)方主動(dòng)拒絕,如何定期 (帶 back-off) 重試?
- 非阻塞網(wǎng)絡(luò)編程該用邊沿觸發(fā)(edge trigger)還是電平觸發(fā)(level trigger)?(這兩個(gè)中文術(shù)語(yǔ)有其他譯法,我選擇了一個(gè)電子工程師熟悉的說(shuō)法。)如果是電平觸發(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ù)收到之后再一并處理?見(jiàn) lighttpd 關(guān)于 \r\n\r\n 分包的 bug。假如數(shù)據(jù)是一個(gè)字節(jié)一個(gè)字節(jié)地到達(dá),間隔 10ms,每個(gè)字節(jié)觸發(fā)一次文件描述符可讀 (readable) 事件,程序是否還能正常工作?lighttpd 在這個(gè)問(wèn)題上出過(guò)安全漏洞。
- 在非阻塞網(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è)問(wèn)題。
- 如果使用發(fā)送緩沖區(qū),萬(wàn)一接收方處理緩慢,數(shù)據(jù)會(huì)不會(huì)一直堆積在發(fā)送方,造成內(nèi)存暴漲?如何做應(yīng)用層的流量控制?
- 如何設(shè)計(jì)并實(shí)現(xiàn)定時(shí)器?并使之與網(wǎng)絡(luò) IO 共用一個(gè)線程,以避免鎖。
這些問(wèn)題在 muduo 的代碼中可以找到答案。
Muduo 簡(jiǎn)介
我編寫(xiě) Muduo 網(wǎng)絡(luò)庫(kù)的目的之一就是簡(jiǎn)化日常的 TCP 網(wǎng)絡(luò)編程,讓程序員能把精力集中在業(yè)務(wù)邏輯的實(shí)現(xiàn)上,而不要天天和 Sockets API 較勁。借用 Brooks 的話說(shuō),我希望 Muduo 能減少網(wǎng)絡(luò)編程中的偶發(fā)復(fù)雜性 (accidental complexity)。
Muduo 只支持 Linux 2.6.x 下的并發(fā)非阻塞 TCP 網(wǎng)絡(luò)編程,它的安裝方法見(jiàn)陳碩的 blog 文章。
Muduo 的使用非常簡(jiǎn)單,不需要從指定的類派生,也不用覆寫(xiě)虛函數(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è)程序跑起來(lái):
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
完整的代碼見(jiàn) muduo/examples/simple/echo。
這個(gè)幾十行的小程序?qū)崿F(xiàn)了一個(gè)并發(fā)的 echo 服務(wù)程序,可以同時(shí)處理多個(gè)連接。
對(duì)這個(gè)程序的詳細(xì)分析見(jiàn)下一篇博客《Muduo 網(wǎng)絡(luò)編程示例之一:五個(gè)簡(jiǎn)單 TCP 協(xié)議》
(待續(xù))
前面我們比較了 muduo 和 libevent2 的吞吐量,得到的結(jié)論是 muduo 比 libevent2 快 18%。有人會(huì)說(shuō),libevent2 并不是為高吞吐的應(yīng)用場(chǎng)景而設(shè)計(jì)的,這樣的比較不公平,勝之不武。為了公平起見(jiàn),這回我們用 libevent2 自帶的性能測(cè)試程序(擊鼓傳花)來(lái)對(duì)比 muduo 和 libevent2 在高并發(fā)情況下的 IO 事件處理效率。
測(cè)試對(duì)象
測(cè)試環(huán)境
測(cè)試用的軟硬件環(huán)境與《muduo 與 boost asio 吞吐量對(duì)比》和《muduo 與 libevent2 吞吐量對(duì)比》相同,另外我還在自己的筆記本上運(yùn)行了測(cè)試,結(jié)果也附在后面。
測(cè)試內(nèi)容
測(cè)試的場(chǎng)景是:有 1000 個(gè)人圍成一圈,玩擊鼓傳花的游戲,一開(kāi)始第 1 個(gè)人手里有花,他把花傳給右手邊的人,那個(gè)人再繼續(xù)把花傳給右手邊的人,當(dāng)花轉(zhuǎn)手 100 次之后游戲停止,記錄從開(kāi)始到結(jié)束的時(shí)間。
用程序表達(dá)是,有 1000 個(gè)網(wǎng)絡(luò)連接 (socketpairs 或 pipes),數(shù)據(jù)在這些連接中順次傳遞,一開(kāi)始往第 1 個(gè)連接里寫(xiě) 1 個(gè)字節(jié),然后從這個(gè)連接的另一頭讀出這 1 個(gè)字節(jié),再寫(xiě)入第 2 個(gè)連接,然后讀出來(lái)繼續(xù)寫(xiě)到第 3 個(gè)連接,直到一共寫(xiě)了 100 次之后程序停止,記錄所用的時(shí)間。
以上是只有一個(gè)活動(dòng)連接的場(chǎng)景,我們實(shí)際測(cè)試的是 100 個(gè)或 1000 個(gè)活動(dòng)連接(即 100 朵花或 1000 朵花,均勻分散在人群手中),而連接總數(shù)(即并發(fā)數(shù))從 100 到 100,000 (十萬(wàn))。注意每個(gè)連接是兩個(gè)文件描述符,為了運(yùn)行測(cè)試,需要調(diào)高每個(gè)進(jìn)程能打開(kāi)的文件數(shù),比如設(shè)為 256000。
libevent2 的測(cè)試代碼位于 test/bench.c,我修復(fù)了 2.0.6-rc 版里的一個(gè)小 bug,修正后的代碼見(jiàn) http://github.com/chenshuo/recipes/blob/master/pingpong/libevent/bench.c
muduo 的測(cè)試代碼位于 examples/pingpong/bench.cc,見(jiàn) http://gist.github.com/564985#file_pingpong_bench.cc
測(cè)試結(jié)果與討論
第一輪,分別用 100 個(gè)活動(dòng)連接和 1000 個(gè)活動(dòng)連接,無(wú)超時(shí),讀寫(xiě) 100 次,測(cè)試一次游戲的總時(shí)間(包含初始化)和事件處理的時(shí)間(不包含注冊(cè) event watcher)隨連接數(shù)(并發(fā)數(shù))變化的情況。具體解釋見(jiàn) libev 的性能測(cè)試文檔 http://libev.schmorp.de/bench.html ,不同之處在于我們不比較 timer event 的性能,只比較 IO event 的性能。對(duì)每個(gè)并發(fā)數(shù),程序循環(huán) 25 次,刨去第一次的熱身數(shù)據(jù),后 24 次算平均值。測(cè)試用的腳本在 http://github.com/chenshuo/recipes/blob/master/pingpong/libevent/run_bench.sh 。這個(gè)腳本是 libev 的作者 Marc Lehmann 寫(xiě)的,我略作改用,用于測(cè)試 muduo 和 libevent2。
第一輪的結(jié)果,請(qǐng)先只看紅線和綠線。紅線是 libevent2 用的時(shí)間,綠線是 muduo 用的時(shí)間。數(shù)字越小越好。注意這個(gè)圖的橫坐標(biāo)是對(duì)數(shù)的,每一個(gè)數(shù)量級(jí)的取值點(diǎn)為 1, 2, 3, 4, 5, 6, 7.5, 10。
從紅綠線對(duì)比可以看出:
1. libevent2 在初始化 event watcher 上面比 muduo 快 20% (左邊的兩個(gè)圖)
2. 在事件處理方面(右邊的兩個(gè)圖):a) 在 100 個(gè)活動(dòng)連接的情況下,libevent2 和 muduo 分段領(lǐng)先。當(dāng)總連接數(shù)(并發(fā)數(shù))小于 1000 時(shí),二者性能差不多;當(dāng)總連接數(shù)大于 30000 時(shí),muduo 略占優(yōu);當(dāng)總連接數(shù)大于 1000 小于 30000 時(shí),libevent2 明顯領(lǐng)先。b) 在 1000 個(gè)活動(dòng)連接的情況下,當(dāng)并發(fā)數(shù)小于 10000 時(shí),libevent2 和 muduo 得分接近;當(dāng)并發(fā)數(shù)大于 10000 時(shí),muduo 明顯占優(yōu)。
這里我們有兩個(gè)問(wèn)題:1. 為什么 muduo 花在初始化上的時(shí)間比較多? 2. 為什么在一些情況下它比 libevent2 慢很多。
我仔細(xì)分析了其中的原因,并參考了 libev 的作者 Marc Lehmann 的觀點(diǎn) ( http://lists.schmorp.de/pipermail/libev/2010q2/001041.html ),結(jié)論是:在第一輪初始化時(shí),libevent2 和 muduo 都是用 epoll_ctl(fd, EPOLL_CTL_ADD, …) 來(lái)添加 fd event watcher。不同之處在于,在后面 24 輪中,muduo 使用了 epoll_ctl(fd, EPOLL_CTL_MOD, …) 來(lái)更新已有的 event watcher;然而 libevent2 繼續(xù)調(diào)用 epoll_ctl(fd, EPOLL_CTL_ADD, …) 來(lái)重復(fù)添加 fd,并忽略返回的錯(cuò)誤碼 EEXIST (File exists)。在這種重復(fù)添加的情況下,EPOLL_CTL_ADD 將會(huì)快速地返回錯(cuò)誤,而 EPOLL_CTL_MOD 會(huì)做更多的工作,花的時(shí)間也更長(zhǎng)。于是 libevent2 撿了個(gè)便宜。
為了驗(yàn)證這個(gè)結(jié)論,我改動(dòng)了 muduo,讓它每次都用 EPOLL_CTL_ADD 方式初始化和更新 event watcher,并忽略返回的錯(cuò)誤。
第二輪測(cè)試結(jié)果見(jiàn)上圖的藍(lán)線,可見(jiàn)改動(dòng)之后的 muduo 的初始化性能比 libevent2 更好,事件處理的耗時(shí)也有所降低(我推測(cè)是 kernel 內(nèi)部的原因)。
這個(gè)改動(dòng)只是為了驗(yàn)證想法,我并沒(méi)有把它放到 muduo 最終的代碼中去,這或許可以留作日后優(yōu)化的余地。(具體的改動(dòng)是 muduo/net/poller/EPollPoller.cc 第 115 行和 144 行,讀者可自行驗(yàn)證。)
同樣的測(cè)試在雙核筆記本電腦上運(yùn)行了一次,結(jié)果如下:(我的筆記本的 CPU 主頻是 2.4GHz,高于臺(tái)式機(jī)的 1.86GHz,所以用時(shí)較少。)
結(jié)論:在事件處理效率方面,muduo 與 libevent2 總體比較接近,各擅勝場(chǎng)。在并發(fā)量特別大的情況下(大于 10k),muduo 略微占優(yōu)。
關(guān)于 muduo 的更多介紹請(qǐng)見(jiàn)《發(fā)布一個(gè)基于 Reactor 模式的 C++ 網(wǎng)絡(luò)庫(kù)》。muduo 的項(xiàng)目網(wǎng)站是 http://code.google.com/p/muduo ,上面有個(gè) class diagram 可供參考。