• <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>

            陳碩的Blog

            為什么 muduo 的 shutdown() 沒有直接關閉 TCP 連接?

            陳碩 (giantchen_AT_gmail)

            Blog.csdn.net/Solstice

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

             

            今天收到一位網友來信:

            在 simple 中的 daytime 示例中,服務端主動關閉時調用的是如下函數序列,這不是只是關閉了連接上的寫操作嗎,怎么是關閉了整個連接?

               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: }

            陳碩答復如下:

            Muduo TcpConnection 沒有提供 close,而只提供 shutdown ,這么做是為了收發數據的完整性。

            TCP 是一個全雙工協議,同一個文件描述符既可讀又可寫, shutdownWrite() 關閉了“寫”方向的連接,保留了“讀”方向,這稱為 TCP half-close。如果直接 close(socket_fd),那么 socket_fd 就不能讀或寫了。

            用 shutdown 而不用 close 的效果是,如果對方已經發送了數據,這些數據還“在路上”,那么 muduo 不會漏收這些數據。換句話說,muduo 在 TCP 這一層面解決了“當你打算關閉網絡連接的時候,如何得知對方有沒有發了一些數據而你還沒有收到?”這一問題。當然,這個問題也可以在上面的協議層解決,雙方商量好不再互發數據,就可以直接斷開連接。

            等于說 muduo 把“主動關閉連接”這件事情分成兩步來做,如果要主動關閉連接,它會先關本地“寫”端,等對方關閉之后,再關本地“讀”端。練習:閱讀代碼,回答“如果被動關閉連接,muduo 的行為如何?” 提示:muduo 在 read() 返回 0 的時候會回調 connection callback,這樣客戶代碼就知道對方斷開連接了。

            Muduo 這種關閉連接的方式對對方也有要求,那就是對方 read() 到 0 字節之后會主動關閉連接(無論 shutdownWrite() 還是 close()),一般的網絡程序都會這樣,不是什么問題。當然,這么做有一個潛在的安全漏洞,萬一對方故意不不關,那么 muduo 的連接就一直半開著,消耗系統資源。

            完整的流程是:我們發完了數據,于是 shutdownWrite,發送 TCP FIN 分節,對方會讀到 0 字節,然后對方通常會關閉連接,這樣 muduo 會讀到 0 字節,然后 muduo 關閉連接。(思考題:在 shutdown() 之后,muduo 回調 connection callback 的時間間隔大約是一個 round-trip time,為什么?)

            另外,如果有必要,對方可以在 read() 返回 0 之后繼續發送數據,這是直接利用了 half-close TCP 連接。muduo 會收到這些數據,通過 message callback 通知客戶代碼。

            那么 muduo 什么時候真正 close socket 呢?在 TcpConnection 對象析構的時候。TcpConnection 持有一個 Socket 對象,Socket 是一個 RAII handler,它的析構函數會 close(sockfd_)。這樣,如果發生 TcpConnection 對象泄漏,那么我們從 /proc/pid/fd/ 就能找到沒有關閉的文件描述符,便于查錯。

            muduo 在 read() 返回 0 的時候會回調 connection callback,然后把 TcpConnection 的引用計數減一,如果 TcpConnection 的引用計數降到零,它就會析構了。

            參考:

            《TCP/IP 詳解》第一卷第 18.5 節,TCP Half-Close。

            《UNIX 網絡編程》第一卷第三版第 6.6 節, shutdown() 函數。

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

            Muduo 網絡編程示例之四:Twisted Finger

                 摘要: 陳碩 (giantchen_AT_gmail) Blog.csdn.net/Solstice 這是《Muduo 網絡編程示例》系列的第四篇文章。 Muduo 全系列文章列表: http://blog.csdn.net/Solstice/category/779646.aspx Python Twisted 是一款非常好的網絡庫,它也采用 Reactor 作為網絡編程的基本模型,所以從使用上與 m...  閱讀全文

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

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

            陳碩 (giantchen_AT_gmail)

            Blog.csdn.net/Solstice

             

            本文只考慮 Linux x86 平臺,服務端開發(不考慮 Windows 的跨 DLL 內存分配釋放問題)。本文假定讀者知道 ::operator new() 和 ::operator delete() 是干什么的,與通常用的 new/delete 表達式有和區別和聯系,這方面的知識可參考侯捷先生的文章《池內春秋》[1],或者這篇文章

            C++ 的內存管理是個老生常談的話題,我在《當析構函數遇到多線程》第 7 節“插曲:系統地避免各種指針錯誤”中簡單回顧了一些常見的問題以及在現代 C++ 中的解決辦法。基本上,按現代 C++ 的手法(RAII)來管理內存,你很難遇到什么內存方面的錯誤。“沒有錯誤”是基本要求,不代表“足夠好”。我們常常會設法優化性能,如果 profiling 表明 hot spot 在內存分配和釋放上,重載全局的 ::operator new() 和 ::operator delete() 似乎是一個一勞永逸好辦法(以下簡寫為“重載 ::operator new()”),本文試圖說明這個辦法往往行不通。

            內存管理的基本要求

            如果只考慮分配和釋放,內存管理基本要求是“不重不漏”:既不重復 delete,也不漏掉 delete。也就說我們常說的 new/delete 要配對,“配對”不僅是個數相等,還隱含了 new 和 delete 的調用本身要匹配,不要“東家借的東西西家還”。例如:

            • 用系統默認的 malloc() 分配的內存要交給系統默認的 free() 去釋放;
            • 用系統默認的 new 表達式創建的對象要交給系統默認的 delete 表達式去析構并釋放;
            • 用系統默認的 new[] 表達式創建的對象要交給系統默認的 delete[] 表達式去析構并釋放;
            • 用系統默認的 ::operator new() 分配的的內存要交給系統默認的 ::operator delete() 去釋放;
            • 用 placement new 創建的對象要用 placement delete (為了表述方便,姑且這么說吧)去析構(其實就是直接調用析構函數);
            • 從某個內存池 A 分配的內存要還給這個內存池。
            • 如果定制 new/delete,那么要按規矩來。見 Effective C++ 相關條款。

            做到以上這些不難,是每個 C++ 開發人員的基本功。不過,如果你想重載全局的 ::operator new(),事情就麻煩了。

            重載 ::operator new() 的理由

            Effective C++ 第三版第 50 條列舉了定制 new/delete 的幾點理由:

            • 檢測代碼中的內存錯誤
            • 優化性能
            • 獲得內存使用的統計數據

            這些都是正當的需求,文末我們將會看到,不重載 ::operator new() 也能達到同樣的目的。

            ::operator new() 的兩種重載方式

            1. 不改變其簽名,無縫直接替換系統原有的版本,例如:

            #include <new>

            void* operator new(size_t size);

            void operator delete(void* p);

            用這種方式的重載,使用方不需要包含任何特殊的頭文件,也就是說不需要看見這兩個函數聲明。“性能優化”通常用這種方式。

            2. 增加新的參數,調用時也提供這些額外的參數,例如:

            void* operator new(size_t size, const char* file, int line);  // 其返回的指針必須能被普通的 ::operator delete(void*) 釋放

            void operator delete(void* p, const char* file, int line);  // 這個函數只在析構函數拋異常的情況下才會被調用

            然后用的時候是

            Foo* p = new (__FILE, __LINE__) Foo;  // 這樣能跟蹤是哪個文件哪一行代碼分配的內存

            我們也可以用宏替換 new 來節省打字。用這第二種方式重載,使用方需要看到這兩個函數聲明,也就是說要主動包含你提供的頭文件。“檢測內存錯誤”和“統計內存使用情況”通常會用這種方式重載。當然,這不是絕對的。

             

            在學習 C++ 的階段,每個人都可以寫個一兩百行的程序來驗證教科書上的說法,重載 ::operator new() 在這樣的玩具程序里邊不會造成什么麻煩。

            不過,我認為在現實的產品開發中,重載 ::operator new() 乃是下策,我們有更簡單安全的辦法來到達以上目標。

            現實的開發環境

            作為 C++ 應用程序的開發人員,在編寫稍具規模的程序時,我們通常會用到一些 library。我們可以根據 library 的提供方把它們大致分為這么幾大類:

            1. C 語言的標準庫,也包括 Linux 編程環境提供的 Posix 系列函數。
            2. 第三方的 C 語言庫,例如 OpenSSL。
            3. C++ 語言的標準庫,主要是 STL。(我想沒有人在產品中使用 IOStream 吧?)
            4. 第三方的通用 C++ 庫,例如 Boost.Regex,或者某款 XML 庫。
            5. 公司其他團隊的人開發的內部基礎 C++ 庫,比如網絡通信和日志等基礎設施。
            6. 本項目組的同事自己開發的針對本應用的基礎庫,比如某三維模型的仿射變換模塊。

            在使用這些 library 的時候,不可避免地要在各個 library 之間交換數據。比方說 library A 的輸出作為 library B 的輸入,而 library A 的輸出本身常常會用到動態分配的內存(比如 std::vector<double>)。

            如果所有的 C++ library 都用同一套內存分配器(就是系統默認的 new/delete ),那么內存的釋放就很方便,直接交給 delete 去釋放就行。如果不是這樣,那就得時時刻刻記住“這一塊內存是屬于哪個分配器,是系統默認的還是我們定制的,釋放的時候不要還錯了地方”。

            (由于 C 語言不像 C++ 一樣提過了那么多的定制性,C library 通常都會默認直接用 malloc/free 來分配和釋放內存,不存在上面提到的“內存還錯地方”問題。或者有的考慮更全面的 C library 會讓你注冊兩個函數,用于它內部分配和釋放內存,這就就能完全掌控該 library 的內存使用。這種依賴注入的方式在 C++ 里變得花哨而無用,見陳碩寫的《C++ 標準庫中的allocator是多余的》。)

            但是,如果重載了 ::operator new(),事情恐怕就沒有這么簡單了。

            重載 ::operator new() 的困境

            首先,重載 ::operator new() 不會給 C 語言的庫帶來任何麻煩,當然,重載它得到的三點好處也無法讓 C 語言的庫享受到。

            以下僅考慮 C++ library 和 C++ 主程序。

            規則 1:絕對不能在 library 里重載 ::operator new()

            如果你是某個 library 的作者,你的 library 要提供給別人使用,那么你無權重載全局 ::operator new(size_t) (注意這是上面提到的第一種重載方式),因為這非常具有侵略性:任何用到你的 library 的程序都被迫使用了你重載的 ::operator new(),而別人很可能不愿意這么做。另外,如果有兩個 library 都試圖重載 ::operator new(size_t),那么它們會打架,我估計會發生 duplicated symbol link error。干脆,作為 library 的編寫者,大家都不要重載 ::operator new(size_t) 好了。

            那么第二種重載方式呢?首先,::operator new(size_t size, const char* file, int line) 這種方式得到的 void* 指針必須同時能被 ::operator delete(void*) 和 ::operator delete(void* p, const char* file, int line) 這兩個函數釋放。這時候你需要決定,你的 ::operator new(size_t size, const char* file, int line) 返回的指針是不是兼容系統默認的 ::operator delete(void*)。

            • 如果不兼容(也就是說不能用系統默認的 ::operator delete(void*) 來釋放內存),那么你得重載 ::operator delete(void*),讓它的行為與你的 operator new(size_t size, const char* file, int line) 匹配。一旦你決定重載 ::operator delete(void*),那么你必須重載 ::operator new(size_t),這就回到了情況 1:你無權重載全局 ::operator new(size_t)。
            • 如果選擇兼容系統默認的 ::operator delete(void*),那么你在 operator new(size_t size, const char* file, int line) 里能做的事情非常有限,比方說你不能額外動態分配內存來做 house keeping 或保存統計數據(無論顯示還是隱式),因為系統默認的 ::operator delete(void*) 不會釋放你額外分配的內存。(這里隱式分配內存指的是往 std::map<> 這樣的容器里添加元素。)

            看到這里估計很多人已經暈了,但這還沒完。

            其次,在 library 里重載 operator new(size_t size, const char* file, int line) 還涉及到你的重載要不要暴露給 library 的使用者(其他 library 或主程序)。這里“暴露”有兩層意思:1) 包含你的頭文件的代碼會不會用你重載的 ::operator new(),2) 重載之后的 ::operator new() 分配的內存能不能在你的 library 之外被安全地釋放。如果不行,那么你是不是要暴露某個接口函數來讓使用者安全地釋放內存?或者返回 shared_ptr ,利用其“捕獲”deleter 的特性?聽上去好像挺復雜?這里就不一一展開討論了,總之,作為 library 的作者,絕對不要動“重載 operator new()”的念頭。

            事實 2:在主程序里重載 ::operator new() 作用不大

            這不是一條規則,而是我試圖說明這么做沒有多大意義。

            如果用第一種方式重載全局 ::operator new(size_t),會影響本程序用到的所有 C++ library,這么做或許不會有什么問題,不過我建議你使用下一節介紹的更簡單的“替代辦法”。

            如果用第二種方式重載 ::operator new(size_t size, const char* file, int line),那么你的行為是否惠及本程序用到的其他 C++ library 呢?比方說你要不要統計 C++ library 中的內存使用情況?如果某個 library 會返回它自己用 new 分配的內存和對象,讓你用完之后自己釋放,那么是否打算對錯誤釋放內存做檢查?

            C++ library 從代碼組織上有兩種形式:1) 以頭文件方式提供(如以 STL 和 Boost 為代表的模板庫);2) 以頭文件+二進制庫文件方式提供(大多數非模板庫以此方式發布)。

            對于純以頭文件方式實現的 library,那么你可以在你的程序的每個 .cpp 文件的第一行包含重載 ::operator new 的頭文件,這樣程序里用到的其他 C++ library 也會轉而使用你的 ::operator new 來分配內存。當然這是一種相當有侵略性的做法,如果運氣好,編譯和運行都沒問題;如果運氣差一點,可能會遇到編譯錯誤,這其實還不算壞事;運氣更差一點,編譯沒有錯誤,運行的時候時不時出現非法訪問,導致 segment fault;或者在某些情況下你定制的分配策略與 library 有沖突,內存數據損壞,出現莫名其妙的行為。

            對于以庫文件方式實現的 library,這么做并不能讓其受惠,因為 library 的源文件已經編譯成了二進制代碼,它不會調用你新重載的 ::operator new(想想看,已經編譯的二進制代碼怎么可能提供額外的 new (__FILE__, __LINE__) 參數呢?)更麻煩的是,如果某些頭文件有 inline function,還會引起詭異的“串擾”。即 library 有的部分用了你的分配器,有的部分用了系統默認的分配器,然后在釋放內存的時候沒有給對地方,造成分配器的數據結構被破壞。

            總之,第二種重載方式看似功能更豐富,但其實與程序里使用的其他 C++ library 很難無縫配合。

             

            綜上,對于現實生活中的 C++ 項目,重載 ::operator new() 幾乎沒有用武之地,因為很難處理好與程序所用的 C++ library 的關系,畢竟大多數 library 在設計的時候沒有考慮到你會重載 ::operator new() 并強塞給它。

            如果確實需要定制內存分配,該如何辦?

            替代辦法

            很簡單,替換 malloc。如果需要,直接從 malloc 層面入手,通過 LD_PRELOAD 來加載一個 .so,其中有 malloc/free 的替代實現(drop-in replacement),這樣能同時為 C 和 C++ 代碼服務,而且避免 C++ 重載 ::operator new() 的陰暗角落。

            對于“檢測內存錯誤”這一用法,我們可以用 valgrind 或者 dmalloc 或者 efence 來達到相同的目的,專業的除錯工具比自己山寨一個內存檢查器要靠譜。

            對于“統計內存使用數據”,替換 malloc 同樣能得到足夠的信息,因為我們可以用 backtrace() 函數來獲得調用棧,這比 new (__FILE__, __LINE__) 的信息更豐富。比方說你通過分析 (__FILE__, __LINE__) 發現 std::string 大量分配釋放內存,有超出預期的開銷,但是你卻不知道代碼里哪一部分在反復創建和銷毀 std::string 對象,因為 (__FILE__, __LINE__) 只能告訴你最內層的調用函數。用 backtrace() 能找到真正的發起調用者。

            對于“性能優化”這一用法,我認為這目前的多線程開發中,自己實現一個能打敗系統默認的 malloc 的內存分配器是不現實的。一個通用的內存分配器本來就有相當的難度,為多線程程序實現一個安全和高效的通用(全局)內存分配器超出了一般開發人員的能力。不如使用現有的針對多核多線程優化的 malloc,例如 Google tcmalloc 和 Intel TBB 2.2 里的內存分配器。好在這些 allocator 都不是侵入式的,也無須重載 ::operator new()。

            為單獨的 class 重載 operator new() 有問題嗎?

            與全局 ::operator new() 不同,per-class operator new() 和 operator delete () 的影響面要小得多,它只影響本 class 及其派生類。似乎重載 member operator new() 是可行的。我對此持反對態度。

            如果一個 class Node 需要重載 member operator new(),說明它用到了特殊的內存分配策略,常見的情況是使用了內存池或對象池。我寧愿把這一事實明顯地擺出來,而不是改變 new Node 的默認行為。具體地說,是用 factory 來創建對象,比如 static Node* Node::createNode() 或者 static shared_ptr<Node> Node::createNode();。

            這可以歸結為最小驚訝原則:如果我在代碼里讀到 Node* p = new Node,我會認為它在 heap 上分配了內存,如果 Node class 重載了 member operator new(),那么我要事先仔細閱讀 node.h 才能發現其實這行代碼使用了私有的內存池。為什么不寫得明確一點呢?寫成 Node* p = Node::createNode(),那么我能猜到 Node::createNode() 肯定做了什么與 new Node 不一樣的事情,免得將來大吃一驚。

            The Zen of Python 說 explicit is better than implicit,我深信不疑。

             

            總結:重載 ::operator new() 或許在某些臨時的場合能應個急,但是不應該作為一種策略來使用。如果需要,我們可以從 malloc 層面入手,徹底而全面地替換內存分配器。

            參考文獻:

            [1] 侯捷,《池內春秋—— Memory Pool 的設計哲學與無痛運用》,《程序員》2002 年第 9 期。

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

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

            匿名 namespace (anonymous namespace 或稱 unnamed namespace) 是 C++ 的一項非常有用的功能,其主要目的是讓該 namespace 中的成員(變量或函數)具有獨一無二的全局名稱,避免名字碰撞 (name collisions)。一般在編寫 .cpp 文件時,如果需要寫一些小的 helper 函數,我們常常會放到匿名 namespace 里。muduo 0.1.7 中的 muduo/base/Date.ccmuduo/base/Thread.cc 等處就用到了匿名 namespace。

            我最近在工作中遇到并重新思考了這一問題,發現匿名 namespace 并不是多多益善。

            C 語言的 static 關鍵字的兩種用法

            C 語言的 static 關鍵字有兩種用途:

            1. 用于函數內部修飾變量,即函數內的靜態變量。這種變量的生存期長于該函數,使得函數具有一定的“狀態”。使用靜態變量的函數一般是不可重入的,也不是線程安全的。

            2. 用在文件級別(函數體之外),修飾變量或函數,表示該變量或函數只在本文件可見,其他文件看不到也訪問不到該變量或函數。專業的說法叫“具有 internal linkage”(簡言之:不暴露給別的 translation unit)。

            C 語言的這兩種用法很明確,一般也不容易混淆。

            C++ 語言的 static 關鍵字的四種用法

            由于 C++ 引入了 class,在保持與 C 語言兼容的同時,static 關鍵字又有了兩種新用法:

            3. 用于修飾 class 的數據成員,即所謂“靜態成員”。這種數據成員的生存期大于 class 的對象(實體 instance)。靜態數據成員是每個 class 有一份,普通數據成員是每個 instance 有一份,因此也分別叫做 class variable 和 instance variable。

            4. 用于修飾 class 的成員函數,即所謂“靜態成員函數”。這種成員函數只能訪問 class variable 和其他靜態程序函數,不能訪問 instance variable 或 instance method。

            當然,這幾種用法可以相互組合,比如 C++ 的成員函數(無論 static 還是 instance)都可以有其局部的靜態變量(上面的用法 1)。對于 class template 和 function template,其中的 static 對象的真正個數跟 template instantiation (模板具現化)有關,相信學過 C++ 模板的人不會陌生。

            可見在 C++ 里 static 被 overload 了多次。匿名 namespace 的引入是為了減輕 static 的負擔,它替換了 static 的第 2 種用途。也就是說,在 C++ 里不必使用文件級的 static 關鍵字,我們可以用匿名 namespace 達到相同的效果。(其實嚴格地說,linkage 或許稍有不同,這里不展開討論了。)

            匿名 namespace 的不利之處

            在工程實踐中,匿名 namespace 有兩大不利之處:

            1. 其中的函數難以設斷點,如果你像我一樣使用的是 gdb 這樣的文本模式 debugger。
            2. 使用某些版本的 g++ 時,同一個文件每次編譯出來的二進制文件會變化,這讓某些 build tool 失靈。

            考慮下面這段簡短的代碼 (anon.cc):

               1: namespace
               2: {
               3:   void foo()
               4:   {
               5:   }
               6: }
               7:  
               8: int main()
               9: {
              10:   foo();
              11: }

            對于問題 1:

            gdb 的<tab>鍵自動補全功能能幫我們設定斷點,不是什么大問題。前提是你知道那個"(anonymous namespace)::foo()"正是你想要的函數。

            $ 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.

            麻煩的是,如果兩個文件 anon.cc 和 anonlib.cc 都定義了匿名空間中的 foo() 函數(這不會沖突),那么 gdb 無法區分這兩個函數,你只能給其中一個設斷點。或者你使用 文件名:行號 的方式來分別設斷點。(從技術上,匿名 namespace 中的函數是 weak text,鏈接的時候如果發生符號重名,linker 不會報錯。)

            從根本上解決的辦法是使用普通具名 namespace,如果怕重名,可以把源文件名(必要時加上路徑)作為 namespace 名字的一部分。

            對于問題 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__

            由上可見,g++ 4.2.4 會隨機地給匿名 namespace 生成一個惟一的名字(foo() 函數的 mangled name 中的 E2CEEB51 和 CB51498D 是隨機的),以保證名字不沖突。也就是說,同樣的源文件,兩次編譯得到的二進制文件內容不相同,這有時候會造成問題。比如說拿到一個會發生 core dump 的二進制可執行文件,無法確定它是由哪個 revision 的代碼編譯出來的。畢竟編譯結果不可復現,具有一定的隨機性。

            這可以用 gcc 的 -frandom-seed 參數解決,具體見文檔。

            這個現象在 gcc 4.2.4 中存在(之前的版本估計類似),在 gcc 4.4.5 中不存在。

            替代辦法

            如果前面的“不利之處”給你帶來困擾,解決辦法也很簡單,就是使用普通具名 namespace。當然,要起一個好的名字,比如 boost 里就常常用 boost::detail 來放那些“不應該暴露給客戶,但又不得不放到頭文件里”的函數或 class。

             

            總而言之,匿名 namespace 沒什么大問題,使用它也不是什么過錯。萬一它礙事了,可以用普通具名 namespace 替代之。

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

            C++ 多線程系統編程精要

            這是一套緊湊的 PPT,基本上每一張幻燈片都可以單獨寫一篇博客,但是我沒有那么多時間一一展開論述,只能把結論和主要論據列了出來。

            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 陳碩 閱讀(4053) | 評論 (4)編輯 收藏

            Muduo 網絡編程示例之三:定時器

                 摘要: 陳碩 (giantchen_AT_gmail) Blog.csdn.net/Solstice 這是《Muduo 網絡編程示例》系列的第三篇文章。 Muduo 全系列文章列表: http://blog.csdn.net/Solstice/category/779646.aspx   程序中的時間 程序中對時間的處理是個大問題,我打算單獨寫一篇文章來全面地討論這個問題。文章暫定名《〈程...  閱讀全文

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

            Muduo 網絡編程示例之二:Boost.Asio 的聊天服務器

            陳碩 (giantchen_AT_gmail)

            Blog.csdn.net/Solstice

            這是《Muduo 網絡編程示例》系列的第二篇文章。

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

            本文講介紹一個與 Boost.Asio 的示例代碼中的聊天服務器功能類似的網絡服務程序,包括客戶端與服務端的 muduo 實現。這個例子的主要目的是介紹如何處理分包,并初步涉及 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 分包

            前面一篇《五個簡單 TCP 協議》中處理的協議沒有涉及分包,在 TCP 這種字節流協議上做應用層分包是網絡編程的基本需求。分包指的是在發生一個消息(message)或一幀(frame)數據時,通過一定的處理,讓接收方能從字節流中識別并截取(還原)出一個個消息。“粘包問題”是個偽問題。

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

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

            1. 消息長度固定,比如 muduo 的 roundtrip 示例就采用了固定的 16 字節消息;
            2. 使用特殊的字符或字符串作為消息的邊界,例如 HTTP 協議的 headers 以 "\r\n" 為字段的分隔符;
            3. 在每條消息的頭部加一個長度字段,這恐怕是最常見的做法,本文的聊天協議也采用這一辦法;
            4. 利用消息本身的格式來分包,例如 XML 格式的消息中 <root>...</root> 的配對,或者 JSON 格式中的 { ... } 的配對。解析這種消息格式通常會用到狀態機。

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

            聊天服務

            本文實現的聊天服務非常簡單,由服務端程序和客戶端程序組成,協議如下:

            • 服務端程序中某個端口偵聽 (listen) 新的連接;
            • 客戶端向服務端發起連接;
            • 連接建立之后,客戶端隨時準備接收服務端的消息并在屏幕上顯示出來;
            • 客戶端接受鍵盤輸入,以回車為界,把消息發送給服務端;
            • 服務端接收到消息之后,依次發送給每個連接到它的客戶端;原來發送消息的客戶端進程也會收到這條消息;
            • 一個服務端進程可以同時服務多個客戶端進程,當有消息到達服務端后,每個客戶端進程都會收到同一條消息,服務端廣播發送消息的順序是任意的,不一定哪個客戶端會先收到這條消息。
            • (可選)如果消息 A 先于消息 B 到達服務端,那么每個客戶端都會先收到 A 再收到 B。

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

            消息格式

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

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

            共 21 字節。

            打包的代碼

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

               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 有一個很好的功能,它在頭部預留了 8 個字節的空間,這樣第 6 行的 prepend() 操作就不需要移動已有的數據,效率較高。

            分包的代碼

            解析數據往往比生成數據復雜,分包打包也不例外。

               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 循環來反復讀取數據,直到 Buffer 中的數據不夠一條完整的消息。請讀者思考,如果換成 if (buf->readableBytes() >= kHeaderLen) 會有什么后果。

            以前面提到的兩條消息的字節流為例:

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

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

            1. 每次收到一個字節的數據,onMessage() 被調用 21 次;
            2. 數據分兩次到達,第一次收到 2 個字節,不足消息的長度字段;
            3. 數據分兩次到達,第一次收到 4 個字節,剛好夠長度字段,但是沒有 body;
            4. 數據分兩次到達,第一次收到 8 個字節,長度完整,但 body 不完整;
            5. 數據分兩次到達,第一次收到 9 個字節,長度完整,body 也完整;
            6. 數據分兩次到達,第一次收到 10 個字節,第一條消息的長度完整、body 也完整,第二條消息長度不完整;
            7. 請自行移動分割點,驗證各種情況;
            8. 數據一次就全部到達,這時必須用 while 循環來讀出兩條消息,否則消息會堆積。

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

            編解碼器 LengthHeaderCodec

            有人評論 Muduo 的接收緩沖區不能設置回調函數的觸發條件,確實如此。每當 socket 可讀,Muduo 的 TcpConnection 會讀取數據并存入 Input Buffer,然后回調用戶的函數。不過,一個簡單的間接層就能解決問題,讓用戶代碼只關心“消息到達”而不是“數據到達”,如本例中的 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* 為參數的 MessageCallback 轉換成了以 const string& 為參數的 StringMessageCallback,讓用戶代碼不必關心分包操作。客戶端和服務端都能從中受益。

            服務端的實現

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

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

             

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

               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:  
            這里有幾點值得注意,在以往的代碼里是直接把本 class 的 onMessage() 注冊給 server_;這里我們把 LengthHeaderCodec::onMessage() 注冊給 server_,然后向 codec_ 注冊了 ChatServer::onStringMessage(),等于說讓 codec_ 負責解析消息,然后把完整的消息回調給 ChatServer。這正是我前面提到的“一個簡單的間接層”,在不增加 Muduo 庫的復雜度的前提下,提供了足夠的靈活性讓我們在用戶代碼里完成需要的工作。
            另外,server_.start() 絕對不能在構造函數里調用,這么做將來會有線程安全的問題,見我在《當析構函數遇到多線程 ── C++ 中線程安全的對象回調》一文中的論述
            以下是處理連接的建立和斷開的代碼,注意它把新建的連接加入到 connections_ 容器中,把已斷開的連接從容器中刪除。這么做是為了避免內存和資源泄漏,TcpConnectionPtr 是 boost::shared_ptr<TcpConnection>,是 muduo 里唯一一個默認采用 shared_ptr 來管理生命期的對象。以后我們會談到這么做的原因。
              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:  
            以下是服務端處理消息的代碼,它遍歷整個 connections_ 容器,把消息打包發送給各個客戶連接。
              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:  
            數據成員:
              68:   typedef std::set<TcpConnectionPtr> ConnectionList;
              69:   EventLoop* loop_;
              70:   TcpServer server_;
              71:   LengthHeaderCodec codec_;
              72:   MutexLock mutex_;
              73:   ConnectionList connections_;
              74: };
              75:  
            main() 函數里邊是例行公事的代碼:
              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: }

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

             

            客戶端的實現

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

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

               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() 目前為空,客戶端的連接由操作系統在進程終止時關閉。
              37:   void disconnect()
              38:   {
              39:     // client_.disconnect();
              40:   }
              41:  
            write() 會由 main 線程調用,所以要加鎖,這個鎖不是為了保護 TcpConnection,而是保護 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() 會由 EventLoop 線程調用,所以要加鎖以保護 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:  
            把收到的消息打印到屏幕,這個函數由 EventLoop 線程調用,但是不用加鎖,因為 printf() 是線程安全的。
            注意這里不能用 cout,它不是線程安全的。
              69:   void onStringMessage(const TcpConnectionPtr&,
              70:                        const string& message,
              71:                        Timestamp)
              72:   {
              73:     printf("<<< %s\n", message.c_str());
              74:   }
              75:  
             
            數據成員:
              76:   EventLoop* loop_;
              77:   TcpClient client_;
              78:   LengthHeaderCodec codec_;
              79:   MutexLock mutex_;
              80:   TcpConnectionPtr connection_;
              81: };
              82:  
            main() 函數里除了例行公事,還要啟動 EventLoop 線程和讀取鍵盤輸入。
              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); // 注冊到 EventLoopThread 的 EventLoop 上。
              93:     client.connect();
              94:     std::string line;
              95:     while (std::getline(std::cin, line))
              96:     {
              97:       string message(line.c_str()); // 這里似乎多此一舉,可直接發送 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:  

             

            簡單測試

            開三個命令行窗口,在第一個運行

            $ ./asio_chat_server 3000

             

            第二個運行

            $ ./asio_chat_client 127.0.0.1 3000

             

            第三個運行同樣的命令

            $ ./asio_chat_client 127.0.0.1 3000

             

            這樣就有兩個客戶端進程參與聊天。在第二個窗口里輸入一些字符并回車,字符會出現在本窗口和第三個窗口中。

             

             

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

            (待續)

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

            Muduo 網絡編程示例之一:五個簡單 TCP 協議

                 摘要: 這是《Muduo 網絡編程示例》系列的第一篇文章。本文將介紹五個簡單 TCP 網絡服務協議的 muduo 實現,包括 echo、discard、chargen、daytime、time,以及 time 協議的客戶端。以上五個協議使用不同的端口,可以放到同一個進程中實現,且不必使用多線程。  閱讀全文

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

            Muduo 網絡編程示例之零:前言

            陳碩 (giantchen_AT_gmail)

            Blog.csdn.net/Solstice

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

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

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

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

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

            本系列文章適用于 Linux 2.6.x (x > 25),主要測試發行版為 Ubuntu 10.04 LTSDebian 6.0 Squeeze,64-bit x86 硬件。

            TCP 網絡編程本質論

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

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

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

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

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

            Muduo 簡介

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

            Muduo 只支持 Linux 2.6.x 下的并發非阻塞 TCP 網絡編程,它的安裝方法見陳碩的 blog 文章

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

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

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

             

             1 #ifndef MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H 
             2 #define MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H
             3 
             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 

             

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

             

             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. 實現 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 讓整個程序跑起來:

             

             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。
            這個幾十行的小程序實現了一個并發的 echo 服務程序,可以同時處理多個連接。
            對這個程序的詳細分析見下一篇博客《Muduo 網絡編程示例之一:五個簡單 TCP 協議》

            (待續)

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

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

            前面我們比較了 muduo 和 libevent2 的吞吐量,得到的結論是 muduo 比 libevent2 快 18%。有人會說,libevent2 并不是為高吞吐的應用場景而設計的,這樣的比較不公平,勝之不武。為了公平起見,這回我們用 libevent2 自帶的性能測試程序(擊鼓傳花)來對比 muduo 和 libevent2 在高并發情況下的 IO 事件處理效率。

            測試對象

            測試環境

            測試用的軟硬件環境與《muduo 與 boost asio 吞吐量對比》和《muduo 與 libevent2 吞吐量對比》相同,另外我還在自己的筆記本上運行了測試,結果也附在后面。

            測試內容

            測試的場景是:有 1000 個人圍成一圈,玩擊鼓傳花的游戲,一開始第 1 個人手里有花,他把花傳給右手邊的人,那個人再繼續把花傳給右手邊的人,當花轉手 100 次之后游戲停止,記錄從開始到結束的時間。

            用程序表達是,有 1000 個網絡連接 (socketpairs 或 pipes),數據在這些連接中順次傳遞,一開始往第 1 個連接里寫 1 個字節,然后從這個連接的另一頭讀出這 1 個字節,再寫入第 2 個連接,然后讀出來繼續寫到第 3 個連接,直到一共寫了 100 次之后程序停止,記錄所用的時間。

            以上是只有一個活動連接的場景,我們實際測試的是 100 個或 1000 個活動連接(即 100 朵花或 1000 朵花,均勻分散在人群手中),而連接總數(即并發數)從 100 到 100,000 (十萬)。注意每個連接是兩個文件描述符,為了運行測試,需要調高每個進程能打開的文件數,比如設為 256000。

            libevent2 的測試代碼位于 test/bench.c,我修復了 2.0.6-rc 版里的一個小 bug,修正后的代碼見 http://github.com/chenshuo/recipes/blob/master/pingpong/libevent/bench.c

            muduo 的測試代碼位于 examples/pingpong/bench.cc,見 http://gist.github.com/564985#file_pingpong_bench.cc

            測試結果與討論

            第一輪,分別用 100 個活動連接和 1000 個活動連接,無超時,讀寫 100 次,測試一次游戲的總時間(包含初始化)和事件處理的時間(不包含注冊 event watcher)隨連接數(并發數)變化的情況。具體解釋見 libev 的性能測試文檔 http://libev.schmorp.de/bench.html ,不同之處在于我們不比較 timer event 的性能,只比較 IO event 的性能。對每個并發數,程序循環 25 次,刨去第一次的熱身數據,后 24 次算平均值。測試用的腳本在 http://github.com/chenshuo/recipes/blob/master/pingpong/libevent/run_bench.sh 。這個腳本是 libev 的作者 Marc Lehmann 寫的,我略作改用,用于測試 muduo 和 libevent2。

            第一輪的結果,請先只看紅線和綠線。紅線是 libevent2 用的時間,綠線是 muduo 用的時間。數字越小越好。注意這個圖的橫坐標是對數的,每一個數量級的取值點為 1, 2, 3, 4, 5, 6, 7.5, 10。

            muduo_libevent_bench_490

            從紅綠線對比可以看出:

            1. libevent2 在初始化 event watcher 上面比 muduo 快 20% (左邊的兩個圖)

            2. 在事件處理方面(右邊的兩個圖):a) 在 100 個活動連接的情況下,libevent2 和 muduo 分段領先。當總連接數(并發數)小于 1000 時,二者性能差不多;當總連接數大于 30000 時,muduo 略占優;當總連接數大于 1000 小于 30000 時,libevent2 明顯領先。b) 在 1000 個活動連接的情況下,當并發數小于 10000 時,libevent2 和 muduo 得分接近;當并發數大于 10000 時,muduo 明顯占優。

            這里我們有兩個問題:1. 為什么 muduo 花在初始化上的時間比較多? 2. 為什么在一些情況下它比 libevent2 慢很多。

            我仔細分析了其中的原因,并參考了 libev 的作者 Marc Lehmann 的觀點 ( http://lists.schmorp.de/pipermail/libev/2010q2/001041.html ),結論是:在第一輪初始化時,libevent2 和 muduo 都是用 epoll_ctl(fd, EPOLL_CTL_ADD, …) 來添加 fd event watcher。不同之處在于,在后面 24 輪中,muduo 使用了 epoll_ctl(fd, EPOLL_CTL_MOD, …) 來更新已有的 event watcher;然而 libevent2 繼續調用 epoll_ctl(fd, EPOLL_CTL_ADD, …) 來重復添加 fd,并忽略返回的錯誤碼 EEXIST (File exists)。在這種重復添加的情況下,EPOLL_CTL_ADD 將會快速地返回錯誤,而 EPOLL_CTL_MOD 會做更多的工作,花的時間也更長。于是 libevent2 撿了個便宜。

            為了驗證這個結論,我改動了 muduo,讓它每次都用 EPOLL_CTL_ADD 方式初始化和更新 event watcher,并忽略返回的錯誤。

            第二輪測試結果見上圖的藍線,可見改動之后的 muduo 的初始化性能比 libevent2 更好,事件處理的耗時也有所降低(我推測是 kernel 內部的原因)。

            這個改動只是為了驗證想法,我并沒有把它放到 muduo 最終的代碼中去,這或許可以留作日后優化的余地。(具體的改動是 muduo/net/poller/EPollPoller.cc 第 115 行和 144 行,讀者可自行驗證。)

            同樣的測試在雙核筆記本電腦上運行了一次,結果如下:(我的筆記本的 CPU 主頻是 2.4GHz,高于臺式機的 1.86GHz,所以用時較少。)

            muduo_libevent_bench_6400

            結論:在事件處理效率方面,muduo 與 libevent2 總體比較接近,各擅勝場。在并發量特別大的情況下(大于 10k),muduo 略微占優。

             

             

             

            關于 muduo 的更多介紹請見《發布一個基于 Reactor 模式的 C++ 網絡庫》。muduo 的項目網站是 http://code.google.com/p/muduo ,上面有個 class diagram 可供參考。

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

            僅列出標題
            共6頁: 1 2 3 4 5 6 
            <2025年5月>
            27282930123
            45678910
            11121314151617
            18192021222324
            25262728293031
            1234567

            導航

            統計

            常用鏈接

            隨筆分類

            隨筆檔案

            相冊

            搜索

            最新評論

            閱讀排行榜

            評論排行榜

            久久精品国产免费观看| 久久成人小视频| 国产精品日韩深夜福利久久| 99热热久久这里只有精品68| 99久久综合国产精品免费| 亚洲国产精品成人久久| 国产精品xxxx国产喷水亚洲国产精品无码久久一区 | 四虎国产精品成人免费久久| 久久久久久久97| 日本精品久久久久中文字幕8 | 99久久精品国产综合一区| 久久久久se色偷偷亚洲精品av| 久久国产精品一区二区| 久久亚洲sm情趣捆绑调教| 久久精品免费观看| 97久久国产综合精品女不卡| 色综合久久久久无码专区| 亚洲&#228;v永久无码精品天堂久久| 久久精品国产精品亚洲精品| 久久播电影网| 狠狠色丁香婷婷久久综合不卡| 狠狠色狠狠色综合久久| 无码精品久久一区二区三区| 精品熟女少妇aⅴ免费久久| 国产精品久久毛片完整版| 国内高清久久久久久| 亚洲欧洲中文日韩久久AV乱码| 亚洲天堂久久精品| 99久久精品国产麻豆| 亚洲中文字幕无码久久精品1 | 久久精品国产男包| 亚洲人成电影网站久久| 久久性生大片免费观看性| 国産精品久久久久久久| 九九久久精品国产| 91精品国产高清久久久久久国产嫩草 | 亚洲国产一成人久久精品| 久久亚洲sm情趣捆绑调教| 久久亚洲AV无码精品色午夜| 国色天香久久久久久久小说| 久久婷婷五月综合国产尤物app|