Socket通信編程淺談及Netty框架的優勢點總結

Socket之於操作系統/進程

Socket通信在操作系統層面主要體現在I/O多路複用上,即每個進程通過一定的邏輯去檢測具體哪個文件描述符(fd)發生了I/O事件。這個邏輯主要有select、poll、epoll/kqueue這幾種。

select的缺點在於兩次拷貝耗時輪詢所有fd耗時,支持的文件描述符受限且太小,其優點在於跨平臺支持。

poll的優點在於通過鏈表存儲使得連接數(也就是文件描述符)沒有限制,但仍然存在大量拷貝,且是水平觸發,會出現當報告了fd沒有被處理,會重複報告,很耗性能。

epoll是Linux操作系統中對poll進行改進,epoll分爲ET與LT模式,具體如下:

LT延遲處理,當檢測到描述符事件通知應用程序,應用程序不立即處理該事件。那麼下次會再次通知應用程序此事件。

ET立即處理,當檢測到描述符事件通知應用程序,應用程序會立即處理。

ET模式減少了epoll被重複觸發的次數,效率比LT高。我們在使用ET的時候,必須採用非阻塞套接口,避免某文件句柄在阻塞讀或阻塞寫的時候將其他文件描述符的任務餓死。epoll的優點在於沒有最大併發連接的限制,只有活躍可用的fd纔會調用callback函數,內存拷貝是利用mmap()文件映射內存的方式加速與內核空間的消息傳遞,減少複製開銷。(內核與用戶空間共享一塊內存)。

kqueueUnix操作系統對poll進行的一些優化後的模式,kqueue與epoll非常相似,最初是2000年Jonathan Lemon在FreeBSD系統上開發的一個高性能的事件通知接口。

下面是上述函數的調用過程:

  • select函數的調用過程
  1. 將fd_set從內核空間拷貝到用戶空間
  2. 如果遍歷完所有的fd都沒有返回一個可讀寫的mask掩碼,就會讓select的進程進入休眠模式,直到發現可讀寫的資源後,重新喚醒等待隊列上休眠的進程。如果在規定時間內都沒有喚醒休眠進程,那麼進程會被喚醒重新獲得CPU,再去遍歷一次fd。
  3. poll方法會返回一個描述讀寫是否就緒的mask掩碼,根據這個mask掩碼給fd_set賦值。
  4. 調用其對應的poll方法
  5. 註冊回調函數
  6. 從用戶空間將fd_set拷貝到內核空間
  • poll函數的調用過程(與select完全一致),主要是通過鏈式結構去掉了文件描述符個數的限制
  • epoll的函數調用流程

  1. 當調用epoll_wait函數的時候,系統會創建一個epoll對象,每個對象有一個evenpoll類型的結構體與之對應

  2. 文件的fd狀態發生改變,就會觸發fd上的回調函數

  3. 回調函數將相應的fd加入到rdlist,導致rdlist不空,進程被喚醒,epoll_wait繼續執行。

  4. 有一個事件轉移函數——ep_events_transfer,它會將rdlist的數據拷貝到txlist上,並將rdlist的數據清空。

  5. ep_send_events函數,它掃描txlist的每個數據,調用關聯fd對應的poll方法去取fd中較新的事件,將取得的事件和對應的fd發送到用戶空間。如果fd是LT模式的話,會被txlist的該數據重新放回rdlist,等待下一次繼續觸發調用。

  • kqueue的函數調用過程
  1. 註冊一批socket描述符到 kqueue
  2. 當其中的描述符狀態發生變化時,kqueue 將一次性通知應用程序哪些描述符可讀、可寫或出錯了。

Socket之於JDK

JDK在1.4版本增加了NIO,1.7版本增加了NIO2.0,IO是面向流的處理,NIO是面向塊(緩衝區)的處理。

BIO是JDK最原始的IO類庫,其中的主要類有Socket、ServerSocket、各種Stream和各種Reader等,其實現都是阻塞的,且是直接對byte數組進行讀寫操作。

NIO主要有三個核心部分組成:Buffer緩衝區、Channel管道和Selector選擇器

Buffer緩衝區本質上是可以寫入數據的內存塊(類似數組),然後可以再次讀取。此內存塊包含在NIO Buffer對象中,該對象提供了一組方法,可以更輕鬆地使用內存塊。相對於直接對數組的操作,Buffer API更容易操作和管理。

Buffer三個重要屬性capacity容量、position位置、limit限制。capacity容量是作爲一個內存塊,Buffer具有一定的固定大小。position位置在寫入模式代表寫數據的位置,在讀取模式時代表讀取數據的位置。limit限制在寫入模式時等於Buffer的容量,在讀取模式時等於寫入的數據量。

ByteBuffer爲性能關鍵型代碼提供了直接內存(direct堆外)和非直接內存(heap堆)兩種實現。堆外內存會比堆內存少一次拷貝,堆外內存在GC範圍之外,降低GC壓力,而且內部有個Cleaner對象(PhantomReference)實現了自動管理,Cleaner對象被GC前會執行clean方法,從而清理了堆外內存。

Channel通道的API涵蓋了UDP/TCP網絡和文件IO,如FileChannel、DatagramChannel、SocketChannel、ServerScoektChannel。和標準IO Stream操作的區別在於:在一個通道內進行讀取和寫入,而stream通常是單向的,要麼input要目output,通道可以非阻塞讀取和寫入,通道始終讀取或寫入緩衝區。

SocketChannel用於建立TCP網絡連接,類似java.net.socket。有兩種創建socketChannel形式:

  1. 客戶端主動發起和服務器的連接。
  2. 服務器獲取的新連接。

ServerSocketChannel可以監聽新建的TCP連接通道,類似ServerSocket。serverSocketChannel.accept()是非阻塞的,默認沒有連接需要處理時直接返回null。

Selector選擇器是一個Java NIO組件,可以檢查一個或多個NIO通道,並確定哪些通道已準備好進行讀取或寫入。實現單個線程可以管理多個通道,從而管理多個網絡連接。一個線程使用Selector監聽多個channel的不同事件:四個事件分別對應SelectionKey四個常量:Connect連接(OP_CONNECT)、Accept準備就緒(OP_ACCEPT)、Read讀取(OP_READ)、Write寫入(OP_WRITE)。

Selector實現一個線程處理多個通道的核心在於事件驅動機制,在非阻塞的網絡通道下,通過Selector註冊對於通道感興趣的事件類型,線程通過監聽事件來觸發相應的代碼執行,其底層是上面提及的IO多路複用。

Socket之於Netty框架

Netty是一個高性能、高可拓展性的異步事件驅動的網絡應用程序框架,極大地簡化了TCP和UDP客戶端和服務器端開發等網絡編程,它的四個重要內容:

Reactor線程模型:一種高性能的多線程程序設計思路

重新定義的Channel概念:增強版的通道概念

ChannelPipeline責任鏈設計模式:事件處理機制

內存管理:增強的ByteBuf緩衝區

下圖爲Netty官網的結構圖,可以看出主要包含三大塊:支持Socket等多種傳輸方式,提供多種協議的編解碼實現,核心設計包含事件處理模型、API的使用、ByteBuffer的增強。

Netty實現了Reactor線程模型,Reactor模型有四個核心概念:Resources資源(請求/任務)、Synchronous Event Demultiplexer同步事件複用器、Dispatcher分配器、Request Handler請求處理器。主要是通過2個EventLoopGroup(線程組,底層是JDK的線程池)來分別處理連接和數據讀取,從而提高線程的利用率。

Netty中的Channel是一個抽象的概念,可以理解爲對JDK NIO Channel的增強和拓展。增加了很多屬性和方法。

責任鏈模式爲請求創建了一個處理對象的鏈,將發起請求和具體處理請求的過程進行解耦,責任鏈上的處理者負責處理請求,客戶只需要將請求發送到責任鏈上,無須關心請求的處理細節和請求的傳遞。

ChannelPipeline責任鏈保存了通道所有處理器信息。創建新channel時自動創建一個專有的pipeline,並且在對應入站事件(通常指I/O線程生成了入站數據,詳見ChannelInboundHandler)和出站事件(經常是指I/O線程執行實際的輸出操作,詳見ChannelOutboundHandler)時調用pipeline上的處理器。當入站事件時,執行順序是pipeline的first執行到last。當出站事件時,執行順序是pipeline的last執行到first。處理器在pipeline中的順序由添加的時候決定。

Netty自己的ByteBuf是爲解決JDK的ByteBuffer的問題,如無法動態擴容、API使用複雜。

ByteBuf實現了四個方面的增強:API操作便捷,動態擴容,多種ByteBuf實現,高效的零拷貝機制

ByteBuf的三個重要屬性:capacity容量、readerIndex讀取位置、writerIndex寫入位置。

ByteBuf的capacity默認256字節,最大值Integer.MAX_VALUE即2GB,write*方法被調用時,會先檢測是否可寫入,若不可寫入,則進行擴容,新的capacity的計算方式有2種:在沒超過4MB時,從64字節開始,每次增加一倍,直到滿足容量需求;超過4MB時,新容量=新容量最小要求/4MB*4MB+4MB。

ByteBuf在堆內堆外存儲、是否池化(是否複用ByteBuf對象、內存複用)、訪問方式是否safe三種維度進行劃分,排列組合後有8種實現類。Netty默認使用的是PooledUnsafeDirectByteBuf

Netty的零拷貝機制,是一種應用層的實現。CompositeByteBuf類將多個ByteBuf合併成一個邏輯上的ByteBuf,避免多個ByteBuf之間的拷貝。wrapedBuffer()方法,將byte[]數組包裝成ByteBuf對象。slice()方法將一個ByteBuf對象切分成多個ByteBuf對象。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章