07年我寫了一篇文章叫《我的網(wǎng)絡(luò)模塊設(shè)計》,姑且叫那個為第一版吧,由于持續(xù)對網(wǎng)絡(luò)模塊進(jìn)行改進(jìn),所以現(xiàn)在的實現(xiàn)和當(dāng)時有很大改變,加上上層應(yīng)用越來越多,又經(jīng)過了幾年時間考驗,現(xiàn)在的實現(xiàn)方式比之前的更靈活更有效率,也因為最近看了一些人做網(wǎng)絡(luò)程序多年竟毫無建樹,一直要用別人寫的網(wǎng)絡(luò)模塊,所以有感而寫此文,為了使得此文不受上一篇《我的網(wǎng)絡(luò)模塊設(shè)計》的影響,我決定寫之前不看原來的文章,所以此文跟原文那篇文章可能沒有太多相似性。
一個基本的網(wǎng)絡(luò)模塊,無非就是管理N個連接,快速處理每個連接的收發(fā)數(shù)據(jù)、消息等,所謂好的網(wǎng)路模塊,無非就是穩(wěn)定、高效、靈活,下面分幾部分來寫:
一、 連接管理
之所以首先寫連接管理,是因為連接管理是核心,也是最難的地方,我寫第一個網(wǎng)絡(luò)庫之前,搜索過很多當(dāng)時可以找到的例子工程,當(dāng)時幾乎找不到可穩(wěn)定運行的工程,當(dāng)然更找不到好的,于是摸索前進(jìn),期間對連接管理使用了各種方法,從最早一個cs(臨界區(qū)CriticalSection,我簡稱cs),recv send都用這個cs,到后來send用一個cs,recv用一個cs,用多個的時候還出過錯,最后使用一個cs+一個原子值ref管理一個連接,每個連接send的時候用cs,recv的時候用ref,如果該連接的消息要跨線程異步執(zhí)行,也使用ref,如此較簡單的解決了連接管理的問題。
同樣使用生存期管理方法,也有人用智能指針,雖然原理和我直接操縱生存期一樣,但實現(xiàn)方法畢竟不同,不過我為了讓實現(xiàn)依賴少一些沒有引入智能指針。
當(dāng)然我后來也發(fā)現(xiàn)很多人不是用這種方法,如有些人就id來管理連接,每個連接分個id,其他操作全部用id,每次對連接的調(diào)用先翻譯一下,如果id找得到映射目標(biāo)就調(diào)用,否則就說明該連接不存在了,這種方法簡單只是不直接,多了個查找過程,另外查找的時候可能還需要全局鎖(這依賴于連接數(shù)據(jù)組織)。
也有人使用一個線程管理連接,其他所有與該連接有關(guān)的生存期問題全部到該線程處理,這樣也是可行的,只是需要做一個較好的包裝,如果包裝好上層調(diào)用方便,如果包裝不好,可能上層調(diào)用就有一些約束。
雖然各種方法都有人使用,但我一直選擇直接的生存期管理方法,其實內(nèi)部實現(xiàn)的時候還是有很多優(yōu)化措施的,減少了大量addref、release的調(diào)用,進(jìn)一步提高了效率。
二、 線程組
我最初做網(wǎng)絡(luò)庫的時候還不是很清楚上層如何使用這個庫,后來在上面做了幾個應(yīng)用之后慢慢有了更多想法,最近的網(wǎng)絡(luò)庫是設(shè)計了這么幾組線程:io線程組、同步線程組、異步線程組、時鐘線程組、log線程組,每組線程都可開可關(guān),就算io線程組也是可關(guān)的,這只是為了整個庫更靈活適用性更廣泛,如只用同步線程組或異步線程組僅將這個線程組當(dāng)一個消息隊列使用。
Io線程組就是處理io收發(fā)的,listen recv send 以及解密解壓縮都是在這組線程,一般這組線程會開2個或2*cpu個。
同步線程組,一般這組線程開1個,用來處理logic。
異步線程組,這組線程根據(jù)需要開0個或n個,簡單應(yīng)用無db等慢速操作的應(yīng)用不開,有很多db等慢速操作的可以開很多個。
時鐘線程組,一般不開或開1個。
Log線程組,一般開1個,主要為了避免其他線程調(diào)用WriteLog的時候被磁盤io阻塞,所以弄了一個log線程。
其實還有一個主線程,我的每組線程(包括主線程)都支持事件和定時器,io線程、同步線程、異步線程組、時鐘線程組、甚至log線程組都支持事件和定時器,到去年我還只是讓每組線程都支持事件,今年為了更好的使用時鐘我給每組線程設(shè)計了定時器,現(xiàn)在定時器線程組有點雞肋的味道,一般是用不上專門的定時器線程組,不過我還沒有將它刪掉,主要在我的設(shè)計里面,它和同步異步線程組一樣,都只是一組線程,如果必要的時候可以將它用作同步線程或者異步線程組,所以繼續(xù)保留了它的存在。
這幾組線程之間都是可互發(fā)消息的,所以一個邏輯要異步到別的線程執(zhí)行是非常方便的,只要調(diào)用一下PostXXEvent(TlsInfo *ptls, DWORD dwEvent, WPARAM wParam, LPARAM lParam);我憑借這個設(shè)計使得這套網(wǎng)絡(luò)庫幾乎可以適用上層各種應(yīng)用,不管是非常簡單的網(wǎng)絡(luò)應(yīng)用還是復(fù)雜的,一框打盡。對最簡單的,一個io線程搞定,其他線程全關(guān),對于復(fù)雜的io線程+同步+異步+log全開。
三、 內(nèi)存池
內(nèi)存池其實沒有想象中的那么神秘,當(dāng)然如果要讓一個網(wǎng)絡(luò)程序持續(xù)7*24小時穩(wěn)定高效運行,內(nèi)存池幾乎必不可少的,內(nèi)存池的作用首先是減少內(nèi)存碎片,其次是為了提高速度,我想這兩點很容易想明白的,關(guān)于內(nèi)存池我之前寫了系列文章,可參考我的博客:
《內(nèi)存池之引言》 http://blog.csdn.net/oldworm/archive/2010/02/04/5288985.aspx
《單線程內(nèi)存池》 http://blog.csdn.net/oldworm/archive/2010/02/04/5289003.aspx
《多線程內(nèi)存池》 http://blog.csdn.net/oldworm/archive/2010/02/04/5289006.aspx
《dlmalloc、nedmalloc》 http://blog.csdn.net/oldworm/archive/2010/02/04/5289010.aspx
《線程關(guān)聯(lián)內(nèi)存池》 http://blog.csdn.net/oldworm/archive/2010/02/04/5289015.aspx
《線程關(guān)聯(lián)內(nèi)存池再提速》 http://blog.csdn.net/oldworm/archive/2010/02/04/5289018.aspx
四、 定時器
關(guān)于定時器,上面講線程組的時候已經(jīng)講過,我現(xiàn)在的設(shè)計是每個線程(包括主線程)都支持定時器,調(diào)用方法都是一樣的,回調(diào)函數(shù)形式也是一樣的,由于定時器放到各組線程里面,所以減少了線程之間的切換,提高了效率。
關(guān)于定時器,可參考《定時器模塊改造》 http://blog.csdn.net/oldworm/archive/2010/09/11/5877425.aspx
五、 包格式
關(guān)于包格式可參考《常用cs程序自定義數(shù)據(jù)包描述》 http://blog.csdn.net/oldworm/archive/2010/03/24/5413013.aspx
六、 Buffer
之前的文章其實我一直沒有提過我的buffer,其實我的buffer設(shè)計是很靈活的,現(xiàn)在它和pool也是有些關(guān)聯(lián)的,我的poolset其實底下就是按照各種不同大小的buffer預(yù)設(shè)的尺寸。Buffer我設(shè)計為循環(huán)式,不允許回繞,包含
Char *pbase 塊基址
Char *pread 當(dāng)前讀指針
Char *pwrite 當(dāng)前寫指針
DWORD tag;
Buffer *next;
Capacity 總分配尺寸,上面分配的時候可能只是指定了19,但實際可能分配的是32個字節(jié),所以內(nèi)部用的時候要根據(jù)capacity來最大限度的利用緩沖區(qū)。
Buffer分配還利用了一個技巧,事實上分配的時候是一次分配一個需要的大緩沖,前面為Buffer自身的數(shù)據(jù),后面為數(shù)據(jù)部分,pbase指向數(shù)據(jù)部分,這樣處理減少了一次分配,我估計很多人都在用這個技巧。
Pwrite總是不會小于pread的,但pread可能和pbase不一樣,僅當(dāng)后面空余空間不夠用的時候才可能會移動數(shù)據(jù),否則數(shù)據(jù)不會移動。
WSARecv的時候我是這么處理的,如果首次獲取了一個包的一部分,但buffer中還有足夠的空間放下包的剩余部分,我不會再分配一個buffer去recv,而是直接用原buffer指定一個合適的偏移和size去WSARecv,這樣可以最大限度的減少復(fù)制。
剛才還有朋友問到我recv的層次組織,我的網(wǎng)絡(luò)庫里面是這樣組織的,OnRecv是個虛函數(shù),最基礎(chǔ)的IocpClient的OnRecv只處理數(shù)據(jù)而不解析格式,IocpClientMsg就會認(rèn)識默認(rèn)的一種包格式,這個類的OnRecv會將m_recvbuf中的數(shù)據(jù)組織為msg,并盡可能的一次返回更多個msg,回調(diào)OnMsg函數(shù),由上層決定該消息在哪個線程處理,這樣我認(rèn)為是最靈活的,如果是個很小的server,可能直接就在io線程里面處理了,也可postevent到同步線程處理,亦可PostEvent到異步線程處理。
七、 TLSINFO
TlsInfo顧名思義就是每個線程關(guān)聯(lián)的一組數(shù)據(jù),暫時我還沒有看到別人這么設(shè)計,也許我設(shè)計得有些復(fù)雜了,在這個數(shù)據(jù)里面有一些常用的和該線程相關(guān)的數(shù)據(jù),如該線程的分配基、步長,用這兩個參數(shù)可讓每個線程制造出唯一序列,還有常用pool的地址,如tm_pool *p1k; tm_pool *p2k;… 這樣設(shè)計使得要分配的時候直接取tm_pool,最大限度的發(fā)揮了分配速度,還有一些常規(guī)參量long c; long d; DWORD a; DWORD b;… 這幾個值可理解為棧內(nèi)值,其實為了減少上層調(diào)用復(fù)雜度的,如我將一個連接的包從io線程PostEvent到同步線程處理,PostEvent首參數(shù)就是tlsinfo,PostEvent會根據(jù)tlsinfo里面的一個內(nèi)部值決定是不是要調(diào)用addref,因為我有個地方預(yù)增了2,所以大多數(shù)情況下在io發(fā)到其他線程的時候是無需調(diào)用addref的,提高了效率,tlsinfo里的其他一些值上層應(yīng)用可使用,用在邏輯處理等情況下。
八、 性能分析
*nix下有很多知名的網(wǎng)絡(luò)庫,但在win下特別是使用iocp的庫里面,一直就沒有一個能作為基準(zhǔn)的庫,即使asio也因為出來太晚不為大多數(shù)人熟悉而不能成為基準(zhǔn)庫,libevent接iocp由于采用0 buffer模擬所以也沒有發(fā)揮出足夠的性能,對比spserver我比它快70%左右,我總在想要是微軟能將他那個iocp的例子寫得更好一點就好了,至少學(xué)的人有一個更高一點的基礎(chǔ),而不至于讓http://www.codeproject.com/KB/IP/iocp_server_client.aspx這樣的垃圾代碼都能成為很多人的樣板。
九、 雜談
為了寫好一個win下穩(wěn)定高效的網(wǎng)絡(luò)庫,我07年的時候幾乎搜遍了那個時間段之前所有能找到的iocp例子,還包括通過朋友等途徑看到的如snda等網(wǎng)絡(luò)庫,可惜真沒找到好的,大多數(shù)例子是只要多線程發(fā)起幾千個連接不斷發(fā)送數(shù)據(jù)馬上就死了,偶爾幾個不死的(包括snda的)只要隨機連接并斷開就會產(chǎn)生句柄泄漏,關(guān)閉所有連接之后句柄并不關(guān)閉等,也就是說這些例子連基本的生存期管理都沒搞定,能通過生存期管理并且不死的只有有限的幾個,可惜性能又太差,杯具啊。
早年寫網(wǎng)絡(luò)庫的時候也加入了sodme在google上建的那個群,當(dāng)時群還是很熱鬧的,可惜大多數(shù)人都是摸索,所以很多問題只是討論卻從無定論,沒有誰能說服別人,也沒有人可輕易被說服,要是現(xiàn)在或許有一些很有經(jīng)驗的人,可惜那個群由于GFW現(xiàn)在雖能訪問也不大活躍了。
最近看到有些寫網(wǎng)絡(luò)程序7年甚至更久的人還在用libevent、ace等感想很復(fù)雜,可悲的是那些人還沒意識到用一個庫和寫一個庫有多大的區(qū)別,可能那些人一輩子也認(rèn)識不到寫一個庫比用一個庫難多少,那些人以為這些庫基本會用了,讓他自己去寫也基本是照這個模式,不會有什么突破,就無需自己動手了,悲哀啊。當(dāng)然,要寫一個穩(wěn)定的網(wǎng)絡(luò)庫需要耗費很多時間,特別是要寫一個能和知名庫性能接近或更好的庫,更是要費神費力,沒點耐心和持久力是不可能做好的。在中文領(lǐng)域隨便查什么稍有些名氣的代碼,總是能找到很多剖析類文章,可原創(chuàng)的東西總是很少,也不知道那些大俠怎么搞的,什么都能剖析可怎么總寫不出什么像樣的東西呢。
其實本來沒有打算寫這篇文章,可能是看了陳碩的muduo才使得我有了寫出來的沖動,大概是受到他的開源鼓勵吧。
謹(jǐn)以此文記錄本人最近3年對網(wǎng)絡(luò)模塊的修改并簡短總結(jié)。