一切像霧像雨又像風
作者:CppExplore 網址:http://www.shnenglu.com/CppExplore/本人職業是linux上網絡服務器的開發,本文就網絡服務器的系統架構設計的細枝末節展開討論。歡迎任何的點評指導和討論,尤其是對文中的缺點或者更好的方案。一 系統框架概述網絡上的服務器,無論是嵌入式的網絡設備,還是pc上服務器,整體結構以及主要思想都大體相同:根據業務模型確定主要數據結構,根據數據結構確定線程模型,在各個業務線程內根據圍繞主要數據結構進行的操作確定狀態機模型,低層使用網絡層收發數據完成和其它網元的通訊。線程交互模型簡單描述如下圖:其中網絡層包括收發模塊,收數據模塊是單獨線程,而發數據模塊則被業務線程調用在其本身線程中發送數據,網絡層收到數據后也可能向多個業務線程發送消息,業務線程可能1個,也可能多個,業務線程之間可能存在消息發送,最終會調用網絡層的發送方法完成本server的功能。二 網絡層相對而言,網絡層的實現相對呆板、模式化,這個層面的要點在系統調用,實現方式要符合操作系統提供的api允許的使用方式,而不能天馬行空想當然,因此提高這部分能力的重點在于系統性的學習(《unix網絡編程》),不再于經驗。網絡層有3部分構成:連接細節、多路復用函數、協議解析。(1)連接細節。要實現各個協議的網絡層(協議棧),首先要面對的就是承載該協議的傳輸層協議,udp還是tcp,理論本身就不再多說了。簡單說下編程上的差異:udp的網絡連接簡單、收數據簡單,tcp的則網絡連接復雜、收數據需要在應用層面確定是否一個收包完畢,tcp部分可以參見《【原創】技術系列之 網絡模型(一)基礎篇》。(2)多路復用函數。除了處理udp、tcp本身網絡連接的系統調用之外,還存在和udp/tcp無關的多路復用函數(select等),它們可以監控tcp的網絡事件,也可以監控udp的網絡事件,屬于網絡層的核心驅動部分。可以參見《【原創】技術系列之 網絡模型(三)多路復用模型》(3)協議解析。這部分相對獨立,是網絡層中和網絡連接、收發消息無關的部分,主要功能則是對該協議各種消息的解包(decode)、打包(encode)。網絡層的主要線程是多路復用監控線程(select/poll/epoll_wait等),網絡消息觸發該線程的運轉,如果是收包,則調用read類函數,收包完畢,進行解包操作,之后根據需要向業務線程發送消息(也可以收包完畢后即把數據包裹在消息中發送給業務線程,由業務線程解包,單仍把解包打包操作歸在網絡層中)。性能方面:為了描述方便,引入使用場景:轉發rtp碼流,這個場景需要盡量大的并發行和實時性。(1)高性能函數。如果系統支持,使用epoll/port/kqueue等高性能多路復用函數。在此,將多路復用監控線程封裝在RtpService類中,將rtp連接,封裝在RtpConnection類中。使用模型可以參見《【原創】技術系列之 網絡模型(二)》(2)多線程支持。啟動多個RtpService示例,也既是啟動多個多路復用監控線程。將RtpConnection對象均勻的插入到各個RtpService中,同時在RtpConnection中記錄它屬于的RtpService,便于刪除的時候找到它所在的RtpService。(3)收數據線程直接轉發。處于實時性的需要,一定要在收數據的線程轉發數據,而不是向其它線程發送消息,讓其它線程完成發送。這樣做一是避免不必要的內存復制,最重要的是,線程調度引起的時間不確定性不能保證轉發的實時性。(4)讀寫鎖代替普通鎖。分發數據的時候(轉發不需要)勢必要掃描一個容器中的對象,進行分發操作,分發發生在不同的線程中,加鎖成為必然。讀寫鎖代替普通鎖,使掃描操作不必互斥,也避免(2)中的多線程不能發揮多線程的效果。注意:測試發現,linux2.6內核中的讀寫鎖,只有在靜態初時化的時候,才能寫優先,使用pthread_rwlock_init進行初始化,不管如何設置它的屬性(即便是設置屬性為寫優先),都不能實現寫優先效果,因此需要自己使用pthread_mutex_t和pthread_cond_t實現寫優先的讀寫鎖,具體實現的細節就不再多說了(可以參考《【原創】技術系列之 線程(二)》中線程消息隊列中鎖的實現),重要的是想法,不是實現。寫優先的必要性是因為轉發線程活躍頻繁,而讀線程可以一直進入讀鎖,造成寫線程永久性的處于等待狀態。(5)使用Epoll的ET模式。再此對epoll多說一點,在《【原創】技術系列之 網絡模型(三)多路復用模型》中因為我當時的測試場景是普通的http交互,得出“LT和ET性能相當”的結論,跟帖中網友bluesky給予更正,非常感謝。在這個rtp轉發的場景中,特別適合ET模式,一次觸發,必須讀盡接收緩沖區的數據,一是保證轉發實時性,一是避免剩余數據再次觸發(并發高的情況下,多路復用函數的被觸發已非常頻繁,因此要盡量減少不必要的觸發),這個場景下,多一次的讀操作微不足道。(6)減少系統調用次數。系統調用是比內存copy性能更差的操作,這個再后面的文章中會再詳細描述。網絡層中的系統可以減少的就是read/recv/recvfrom類的操作,極端化低性能的操作就是一次讀一個字節,造成系統調用的次數大幅上升,一般的做法,是開辟緩存(比如char buf[4096];),一次讀取盡可能多的字節。(7)二進制包使用結構直接解包,字符性包延遲解包。這兩點的出發點都是盡量減少內存復制。二進制解包舉例:首先根據協議規定的包結構,定義結構體。比如(注:網友powervv 跟帖指出,要點在于大小端主機序、網絡序和主機序之間的轉換、以及字節對齊問題,避免誤導讀者,舉例做出修改):
收數據到buf,解包過程則是:
完成解包,讀取seq的時候,需要ntohs轉化,tm同樣要ntohl。打包相同:
字符性包解包,則一般是預解包掃描buf,將每個字段的偏移和長度記錄下來,等需要的時候在進行內存復制操作(常用的則是立即復制出來)。通常將字段使用枚舉定義,比如有字段MAX_FIEDS_NUM個,定義開始位置和偏移結構:
則定義 FieldLoc[MAX_FIEDS_NUM],準備保存各個字段的偏移和長度。至于掃描字段引起的性能損耗和內存復制引起的性能比較將在后面闡述。(8)內存池相關、系統調用以及內存復制等的代價這些通用性能部分后面會再有描述。
Powered by: C++博客 Copyright © cppexplore