為了提高數據傳輸的效率,Netty在寫數據時會先將數據(ByteBuf)緩存到ChannelOutboundBuffer中,直到調用flush方法才會將ChannelOutboundBuffer中的數據寫入socket緩沖區。
ChannelOutboundBuffer中有三個重要的屬性:
從它的屬性可以看出,ChannelOutboundBuffer是壹個鏈表結構,有三個指針:
ChannelOutboundBuffer中有兩個重要的方法:addMessage:以鏈表的形式緩存數據,addFlush:移動鏈表的指針,將緩存的數據標記為刷新。請註意,此時數據不會寫入套接字緩沖區。接下來,讓我們看看兩個方法的實現:
讓我們進入它的addMessage方法,並分析它如何緩存數據:
當第壹次添加數據的數量時,數據將被封裝為壹個條目。此時,tailEntry和unflushedEntry指針指向該條目,flushedEntry指針為空。每次添加數據時,都會生成壹個新的條目,tailEntry指針將指向該條目,而unflushedEntry指針將始終指向最初添加的條目。我們用圖來表示壹下:
第壹次添加:
第n次加法:
為了防止緩存數據過大,Netty限制了緩存數據的大小:
addMessage方法將最終調用incrementPendingOutboundBytes方法來記錄緩存的數據大小(totalPendingSize)。如果大小超過寫緩沖區的高水位閾值(默認為64K),將更新未寫入標誌,並且將傳播通道的不可寫入狀態已更改的事件:
移動鏈表的指針,將緩存的數據標記為刷新,並將每個數據節點的狀態設置為可取消:
執行addFlush方法後,鏈表如下所示:
通過ChannelHandlerContext # writeandFlush方法,分析Netty如何通過網絡傳輸數據:
channelhandlercontext # writeAndFlush方法最終將調用其子類abstracthandlercontext的writeAndFlush方法:
主要邏輯在寫入方法中:
write方法主要做兩件事。壹個是找到下壹個ChannelHandlerContext。第二種是調用下壹個ChannelHandlerContext的w riteAndFlush方法來傳播事件。writeAndFlush方法是壹個出站事件。如前所述,對於出站事件,事件通過ChannelHandlerContext傳播。該事件是從pipline鏈中查找當前香奈兒Handlercontext的下壹個香奈兒Handlercontext,並將其傳播到HeadContext。在此期間,我們需要壹個自定義的編碼器對傳輸過來的Java對象進行編碼,轉換成ByteBuf對象,最後事件會傳遞給HeadContext進行處理。
invokeWriteAndFlush方法主要做兩件事。壹種是調用invokeWrite0方法將數據放入Netty緩沖區,另壹種是通過NioSocketChannel調用invokeFlush0方法將緩沖區數據寫入socket緩沖區。
在內部,invokeWrite0方法將調用ChannelOutboundHandler#write方法:
如前所述,出站事件最終會傳播到HeadContext。在傳播到HeadContext之前,我們需要自定義編碼器對Java對象進行編碼,並將Java對象編碼為ByteBuf。關於編碼器的這壹章暫且不分析。我們輸入HeadContext的寫方法:
在HeadContext#write方法中調用AbstractChannelUnsafe#write方法:
這個方法主要做三件事:第壹,對數據進行過濾和轉換;第二,估算數據大小;第三,緩存數據。讓我們先來看看filterOutboundMessage方法:
1.過濾和轉換數據:
filterOutboundMessage方法會先過濾數據,如果數據不是ByteBuf或FileRegion類型,會直接拋出異常。如果數據為ByteBuf類型,則判斷數據是否為直接內存,如果不是,則轉換為直接內存以提高性能。
二、估計數據量:
第三,緩存數據:
最後,將調用通道出站緩沖區# addMessage方法將數據緩存到鏈表中。對於addmessage方法,您可以查看本文中的Netty buffer壹節。
回到abstractchannelhandlercontext # invokewriteandflush方法。在調用invokeWrite0方法將數據放入緩存後,將調用invokeFlush0方法將緩存中的數據寫入套接字緩沖區。ChannelOutboundHandler#flush方法將在invokeFlush0方法中調用:
flush方法最終會將事件傳播到HeadContext的flush方法:
在HeadContext#flush方法中調用AbstractChannelUnsafe#flush方法:
該方法主要做兩件事。壹種是調用ChannelOutboundBuffer#addFlush方法,移動鏈表指針,將緩存的數據標記為刷新。其次,調用flush0方法將緩存中的數據寫入套接字緩沖區。對於addFlush方法,請參考文章的Netty Buffer部分,我們將直接進入flush0方法:
flush0方法主要做兩件事:壹是確定是否有掛起的刷新。二:調用父類flush0方法。
首先,判斷是否有掛起的刷新。
文中提到,寫數據時,當socket緩沖區沒有空閑空間時,會設置未寫狀態,並註冊OP_WRITE事件。當套接字緩沖區中有空閑空間時,將觸發forceFlush。讓我們輸入isFlushPending方法,看看該方法是如何判斷的:
其次,調用父類flush0方法。
寫入套接字緩沖區的特定邏輯在AbstractChannel#AbstractUnsafe父類中:
核心邏輯在doWrite方法中,我們進入AbstractChannel子類NioSocketChannel的doWrite方法來看具體實現:
NioSocketChannel#doWrite方法根據nioBufferCnt的大小執行不同的寫邏輯,如果為0,則調用AbstractNioByteChannel#doWrite方法。如果nioBufferCnt為1或大於1,則調用NioSocketChannel的不同重載方法進行處理。註意,寫數據時,自旋數默認為16,也就是說,如果寫了16次仍有未完成的數據,則調用未完成寫方法,將刷新操作封裝為壹個任務放入隊列,以免阻塞其他任務。另外,如果調用NioSocketChannel#write方法後,返回的localWrittenBytes為0,說明socket緩沖區空間不足,那麽註冊OP_WRITE事件,有可用空間時會觸發該事件,然後調用forceFlush方法繼續寫數據。