青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品

陳碩的Blog

為什么 muduo 的 shutdown() 沒(méi)有直接關(guān)閉 TCP 連接?

陳碩 (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ù)。

posted @ 2011-02-25 21:30 陳碩 閱讀(3428) | 評(píng)論 (3)編輯 收藏

Muduo 網(wǎng)絡(luò)編程示例之四:Twisted Finger

     摘要: 陳碩 (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...  閱讀全文

posted @ 2011-02-23 21:33 陳碩 閱讀(2407) | 評(píng)論 (0)編輯 收藏

C++ 工程實(shí)踐(2):不要重載全局 ::operator new()

陳碩 (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 的提供方把它們大致分為這么幾大類:

  1. C 語(yǔ)言的標(biāo)準(zhǔn)庫(kù),也包括 Linux 編程環(huán)境提供的 Posix 系列函數(shù)。
  2. 第三方的 C 語(yǔ)言庫(kù),例如 OpenSSL。
  3. C++ 語(yǔ)言的標(biāo)準(zhǔn)庫(kù),主要是 STL。(我想沒(méi)有人在產(chǎn)品中使用 IOStream 吧?)
  4. 第三方的通用 C++ 庫(kù),例如 Boost.Regex,或者某款 XML 庫(kù)。
  5. 公司其他團(tuán)隊(duì)的人開(kāi)發(fā)的內(nèi)部基礎(chǔ) C++ 庫(kù),比如網(wǎng)絡(luò)通信和日志等基礎(chǔ)設(shè)施。
  6. 本項(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 期。

posted @ 2011-02-22 01:02 陳碩 閱讀(20209) | 評(píng)論 (12)編輯 收藏

C++ 工程實(shí)踐(1):慎用匿名 namespace

匿名 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.ccmuduo/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 有兩大不利之處:

  1. 其中的函數(shù)難以設(shè)斷點(diǎn),如果你像我一樣使用的是 gdb 這樣的文本模式 debugger。
  2. 使用某些版本的 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 替代之。

posted @ 2011-02-15 22:55 陳碩 閱讀(6828) | 評(píng)論 (3)編輯 收藏

C++ 多線程系統(tǒng)編程精要

這是一套緊湊的 PPT,基本上每一張幻燈片都可以單獨(dú)寫(xiě)一篇博客,但是我沒(méi)有那么多時(shí)間一一展開(kāi)論述,只能把結(jié)論和主要論據(jù)列了出來(lái)。

Slide1

Slide2

Slide3

Slide4

Slide5

Slide6

Slide7

Slide8

Slide9

Slide10

Slide11

Slide12

Slide13

Slide14

Slide15

Slide16

Slide17

Slide18

Slide19

Slide20

Slide21

Slide22

Slide23

Slide24

Slide25

Slide26

Slide27

Slide28

Slide29

posted @ 2011-02-12 18:49 陳碩 閱讀(4075) | 評(píng)論 (4)編輯 收藏

Muduo 網(wǎng)絡(luò)編程示例之三:定時(shí)器

     摘要: 陳碩 (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)題。文章暫定名《〈程...  閱讀全文

posted @ 2011-02-06 22:56 陳碩 閱讀(7661) | 評(píng)論 (3)編輯 收藏

Muduo 網(wǎng)絡(luò)編程示例之二:Boost.Asio 的聊天服務(wù)器

陳碩 (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ù),分包有四種方法:

  1. 消息長(zhǎng)度固定,比如 muduo 的 roundtrip 示例就采用了固定的 16 字節(jié)消息;
  2. 使用特殊的字符或字符串作為消息的邊界,例如 HTTP 協(xié)議的 headers 以 "\r\n" 為字段的分隔符;
  3. 在每條消息的頭部加一個(gè)長(zhǎng)度字段,這恐怕是最常見(jiàn)的做法,本文的聊天協(xié)議也采用這一辦法;
  4. 利用消息本身的格式來(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)用兩次:

  1. 每次收到一個(gè)字節(jié)的數(shù)據(jù),onMessage() 被調(diào)用 21 次;
  2. 數(shù)據(jù)分兩次到達(dá),第一次收到 2 個(gè)字節(jié),不足消息的長(zhǎng)度字段;
  3. 數(shù)據(jù)分兩次到達(dá),第一次收到 4 個(gè)字節(jié),剛好夠長(zhǎng)度字段,但是沒(méi)有 body;
  4. 數(shù)據(jù)分兩次到達(dá),第一次收到 8 個(gè)字節(jié),長(zhǎng)度完整,但 body 不完整;
  5. 數(shù)據(jù)分兩次到達(dá),第一次收到 9 個(gè)字節(jié),長(zhǎng)度完整,body 也完整;
  6. 數(shù)據(jù)分兩次到達(dá),第一次收到 10 個(gè)字節(jié),第一條消息的長(zhǎng)度完整、body 也完整,第二條消息長(zhǎng)度不完整;
  7. 請(qǐng)自行移動(dòng)分割點(diǎn),驗(yàn)證各種情況;
  8. 數(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ù))

posted @ 2011-02-04 08:57 陳碩 閱讀(5776) | 評(píng)論 (0)編輯 收藏

Muduo 網(wǎng)絡(luò)編程示例之一:五個(gè)簡(jiǎn)單 TCP 協(xié)議

     摘要: 這是《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),且不必使用多線程。  閱讀全文

posted @ 2011-02-02 12:33 陳碩 閱讀(3453) | 評(píng)論 (0)編輯 收藏

Muduo 網(wǎng)絡(luò)編程示例之零:前言

陳碩 (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ì)劃如下:

  1. UNP 中的簡(jiǎn)單協(xié)議,包括 echo、daytime、time、discard 等。 
  2. Boost.Asio 中的示例,包括 timer2~6、chat 等。
  3. Java Netty 中的示例,包括 discard、echo、uptime 等,其中的 discard 和 echo 帶流量統(tǒng)計(jì)功能。
  4. Python twisted 中的示例,包括 finger01~07
  5. 云風(fēng)的串并轉(zhuǎn)換連接服務(wù)器 multiplexer,包括單線程和多線程兩個(gè)版本。
  6. 用于測(cè)試兩臺(tái)機(jī)器的往返延遲的 roundtrip
  7. 用于測(cè)試兩臺(tái)機(jī)器的帶寬的 pingpong
  8. 文件傳輸
  9. 一個(gè)基于 TCP 的應(yīng)用層廣播 hub
  10. socks4a 代理服務(wù)器,包括簡(jiǎn)單的 TCP 中繼(relay)。
  11. 一個(gè) Sudoku 服務(wù)器的演變,從單線程到多線程,從阻塞到 event-based。
  12. 一個(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 LTSDebian 6.0 Squeeze,64-bit x86 硬件。

TCP 網(wǎng)絡(luò)編程本質(zhì)論

我認(rèn)為,TCP 網(wǎng)絡(luò)編程最本質(zhì)的是處理三個(gè)半事件:

  1. 連接的建立,包括服務(wù)端接受 (accept) 新連接和客戶端成功發(fā)起 (connect) 連接。
  2. 連接的斷開(kāi),包括主動(dòng)斷開(kāi) (close 或 shutdown) 和被動(dòng)斷開(kāi) (read 返回 0)。
  3. 消息到達(dá),文件描述符可讀。這是最為重要的一個(gè)事件,對(duì)它的處理方式?jīng)Q定了網(wǎng)絡(luò)編程的風(fēng)格(阻塞還是非阻塞,如何處理分包,應(yīng)用層的緩沖如何設(shè)計(jì)等等)。
  4. 消息發(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ō):

  1. 如果要主動(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) 恐怕是不行的。
  2. 如果主動(dòng)發(fā)起連接,但是對(duì)方主動(dòng)拒絕,如何定期 (帶 back-off) 重試?
  3. 非阻塞網(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 快嗎?
  4. 在非阻塞網(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ò)安全漏洞
  5. 在非阻塞網(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)題。
  6. 如果使用發(fā)送緩沖區(qū),萬(wàn)一接收方處理緩慢,數(shù)據(jù)會(huì)不會(huì)一直堆積在發(fā)送方,造成內(nèi)存暴漲?如何做應(yīng)用層的流量控制?
  7. 如何設(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ù))

posted @ 2011-02-02 01:07 陳碩 閱讀(9406) | 評(píng)論 (0)編輯 收藏

擊鼓傳花:對(duì)比 muduo 與 libevent2 的事件處理效率

前面我們比較了 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。

muduo_libevent_bench_490

從紅綠線對(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í)較少。)

muduo_libevent_bench_6400

結(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 可供參考。

posted @ 2010-09-08 01:15 陳碩 閱讀(5699) | 評(píng)論 (4)編輯 收藏

僅列出標(biāo)題
共6頁(yè): 1 2 3 4 5 6 
<2011年6月>
2930311234
567891011
12131415161718
19202122232425
262728293012
3456789

導(dǎo)航

統(tǒng)計(jì)

常用鏈接

隨筆分類

隨筆檔案

相冊(cè)

搜索

最新評(píng)論

閱讀排行榜

評(píng)論排行榜

青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            亚洲第一精品福利| 欧美精品不卡| 久久精品人人做人人综合| 在线中文字幕一区| 夜久久久久久| 亚洲香蕉在线观看| 午夜精品影院在线观看| 亚洲综合社区| 久久激情视频免费观看| 噜噜噜躁狠狠躁狠狠精品视频 | 欧美xx视频| 欧美激情一区在线观看| 亚洲精品一区二区在线| 99在线精品视频| 午夜激情亚洲| 久久在线播放| 欧美三级网址| 国产一区二区三区久久悠悠色av| 精品成人一区二区| 亚洲欧洲久久| 午夜精品一区二区三区在线播放| 久久亚洲电影| 夜夜爽www精品| 久久gogo国模裸体人体| 欧美韩日精品| 国产在线视频欧美| 亚洲一本视频| 欧美1区2区| 亚洲欧美不卡| 欧美人成在线视频| 加勒比av一区二区| 亚洲视频网在线直播| 久久在线观看视频| 在线视频亚洲一区| 欧美成人小视频| 国产亚洲欧美在线| 亚洲视频axxx| 欧美黑人一区二区三区| 亚洲欧美日韩一区二区在线| 欧美日韩国产成人在线91| 激情一区二区三区| 性久久久久久久久久久久| 欧美国产一区视频在线观看| 欧美一区二区三区播放老司机| 99国产麻豆精品| 1024成人网色www| 亚洲影院免费| 亚洲大片在线| 久久国产精品一区二区| 欧美日韩和欧美的一区二区| 黄色一区二区在线| 欧美一级大片在线免费观看| 亚洲激情成人| 久久综合给合久久狠狠狠97色69| 国产日韩欧美在线观看| 亚洲一区二区综合| 日韩西西人体444www| 欧美大片网址| 亚洲国产精品国自产拍av秋霞 | 麻豆成人综合网| 亚洲小说欧美另类婷婷| 欧美日韩八区| 亚洲破处大片| 亚洲国产一区在线观看| 美女网站久久| 又紧又大又爽精品一区二区| 久久久91精品国产| 欧美主播一区二区三区| 国产日韩欧美三级| 久久精品夜色噜噜亚洲aⅴ| 亚洲欧美国产精品桃花| 国产精品免费小视频| 欧美在线3区| 欧美一区观看| 亚洲福利视频网站| 欧美激情四色 | 日韩视频在线一区| 欧美日韩亚洲一区二区三区四区| 91久久在线视频| 亚洲国产成人不卡| 欧美韩日一区二区| 中文av一区特黄| 亚洲一区二区在线| 国产一区二区中文| 欧美xart系列高清| 欧美精品亚洲一区二区在线播放| 中文av字幕一区| 欧美亚洲色图校园春色| 精品1区2区| 亚洲国产高清自拍| 欧美视频三区在线播放| 久久精品视频在线| 久久女同互慰一区二区三区| 亚洲精品一区二区三区蜜桃久| 亚洲精品亚洲人成人网| 国产精品久久久久久久久婷婷 | 亚洲第一偷拍| 国产精品久久久久永久免费观看| 久久精品国产精品亚洲综合| 久久亚洲一区| 国产精品女主播在线观看| 亚洲日本乱码在线观看| 欧美色播在线播放| 久久精品欧美| 欧美精品手机在线| 久久久久久电影| 欧美日韩国产小视频| 久久九九国产| 欧美精品免费在线观看| 欧美一区二区久久久| 欧美插天视频在线播放| 欧美亚洲免费在线| 免费不卡欧美自拍视频| 欧美影院成年免费版| 欧美激情国产日韩| 久久久久综合| 国产精品欧美久久| 亚洲国产va精品久久久不卡综合| 国产女人精品视频| 亚洲乱码精品一二三四区日韩在线 | 国产久一道中文一区| 欧美多人爱爱视频网站| 国产女精品视频网站免费 | 国产精品一区二区在线观看不卡| 久久米奇亚洲| 国产九九视频一区二区三区| 日韩一二在线观看| 亚洲欧洲一区二区在线播放| 欧美一级日韩一级| 午夜激情久久久| 欧美日韩在线播放三区| 亚洲黄一区二区| 亚洲国产免费看| 久久精品视频免费| 欧美在线www| 国产精品每日更新| 一本一本大道香蕉久在线精品| 亚洲美女av网站| 久久精品视频导航| 久久亚洲一区| 国产亚洲精品aa午夜观看| 亚洲午夜国产成人av电影男同| 一二三四社区欧美黄| 久久在线精品| 亚洲国产精品一区制服丝袜| 久久久国产一区二区三区| 另类专区欧美制服同性| 国内精品久久久久伊人av| 午夜精品久久久久久久久久久久 | 久久久精品tv| 久久综合狠狠综合久久综青草| 国产日韩欧美精品一区| 久久成人18免费观看| 久久久久www| 国产一区二区中文| 久久久久久国产精品一区| 欧美成人中文字幕| 影音先锋亚洲精品| 美女网站久久| 最近中文字幕日韩精品| 亚洲免费观看| 欧美午夜a级限制福利片| 亚洲一区日韩| 久久久高清一区二区三区| 伊人一区二区三区久久精品| 欧美成人综合一区| av成人毛片| 久久久亚洲国产天美传媒修理工| 一区二区在线视频| 欧美啪啪一区| 亚洲欧美日韩区| 欧美aⅴ99久久黑人专区| 亚洲精品免费网站| 国产精品久久久久久久第一福利 | 亚洲精品久久久久久久久| 亚洲视频精选在线| 国产亚洲成av人在线观看导航| 久久久夜精品| 一本色道久久88综合日韩精品 | 日韩一区二区精品葵司在线| 欧美在线观看你懂的| 在线日韩电影| 欧美久久久久久久久久| 亚洲一区日韩| 亚洲国产99精品国自产| 欧美一区二区三区婷婷月色| 91久久精品国产91久久| 国产精品美女久久久久久2018| 久久精品国产成人| aa亚洲婷婷| 欧美大尺度在线| 欧美一级理论性理论a| 亚洲美女免费视频| 国产亚洲激情在线| 欧美日韩精品一区二区| 久久久久久自在自线| 亚洲图片激情小说| 亚洲精品久久久久久下一站 | 亚洲制服av| 亚洲精品中文字幕有码专区|