該系列已分別介紹了服務端、客戶端的啟動流程、網路讀事件處理流程,本文將重點剖析Netty是如何封裝NIO的寫事件,
溫馨提示:本文雖然是原始碼分析,但強烈建議精讀,因為根據原始碼闡述其背后的設計哲學,也用黑體進行了標注,請特別留意,
在閱讀本篇文章之前,請稍微思考如下幾個問題:
- 寫事件需要先注冊才能往通道中寫入資料?
- 什么時候需要向通道注冊寫事件呢?
- 業務執行緒池執行業務邏輯后,是如何通過IO執行緒將資料寫入到網路中的呢?
- Netty中如何針對寫限流
1、寫事件概述
寫事件,顧名思義,就是將資料寫入網路,通過網路傳輸給接收端,通常我們知道業務都會在專屬的業務執行緒池中執行,那資料是如何通過IO執行緒寫入網路中的呢?

正如上圖中的執行緒模型,業務執行緒池是如何將資料通過IO執行緒寫入網路中的呢?
接下來我們將帶著問題,嘗試看Netty是如何封裝NIO的寫事件,
2、寫事件處理流程分析
在Netty中,寫事件的處理入口為NioEventLoop的processSelectedKey方法:

根據呼叫鏈,最終呼叫AbstractChannel的內部類AbstractUnsafe的flush0方法,

該方法有三個實作要點:
- 獲取寫快取佇列,如果寫快取佇列為空,則跳過本次寫事件,每一個通道獨享一個寫快取佇列,寫事件觸發執行的動作就是要將寫快取中的資料寫入到網路中,
- 如果通道處于未激活狀態,需要清理寫快取區
- 通過呼叫doWrite方法將寫快取中的資料寫入網路通道中,
接下來將詳細介紹上述關鍵步驟,
2.1 NIO寫事件的優雅封裝
首先,我們將寫快取區當成一個黑盒,先重點看一下doWrite方法的實作,窺探一下Netty是如何基于NIO來處理網路寫入的,

Step1:如果寫快取區中沒有可寫的資料,取消注冊寫事件,我們來看一下取消寫事件的經典實作技巧:

首先判斷一下注冊鍵是否有效,然后通過為位運算,取消寫事件,
思考題:問題來了,取消寫事件,從系統層面就無法繼續觸發寫操作了,那后續如何觸發寫事件呢?

寫事件的處理要考慮如下幾個問題:
- 本次寫快取區中資料是否寫完?
- 如果底層網路Socket快取區積壓,導致寫緩沖區未寫完如何處理?
- 如果網路快取區資料特別大又如何處理?
Netty給出的解決方案如下:
- 通過底層NIO的SocketChannel的write方法將資料寫入到Socket快取區,如果回傳值為0,表示Socket緩沖區已滿,需要暫停寫入,具體做法,注冊寫事件,等待下次繼續寫入,
- 如果寫快取區的資料全部處理完畢,可取消注冊寫事件,避免毫無意義的寫事件就緒,
- 如果寫快取區中資料很大,為了避免單個通道對其他通道的影響,默認設定單次寫事件最多呼叫底層NIO SocketChannel的write方法次數,默認為16,
寫事件的核心處理要點就介紹到這里了,
2.2 通道寫緩沖區詳解
在Netty中呼叫通道的write方法并不會立即將資料寫入到底層網路Socket中,而是寫入到“寫快取區”,為應用級別的快取區,即ChannelOutboundBuffer,這是Netty實作寫操作最重要的一個資料結構,
2.2.1 類圖
ChannelOutboundBuffer 的核心類圖如下:

核心屬性與方法簡介:
- FastThreadLocal<ByteBuffer[]> NIO_BUFFERS
可以看出是執行緒本地變數ThreadLocal的優化版本,存盤一個一個ByteBuffer陣列, - Channel channel
該寫快取區所屬的通道,每一個Channel獨享一個寫緩沖區, - Entry flushedEntry
表示第一個被重繪的Entry,在寫入時,從該Entry開始寫, - Entry unflushedEntry
在鏈表中第一個未重繪的節點(未重繪鏈表中第一個節點), - Entry tailEntry
在鏈表中尾部的節點, - int flushed
待寫入的entry個數,這個資料代表在執行一次真正的flush(flush0),將會有多少個entry中的內容會被寫入到通道,
接下來我們按照寫事件對待寫入快取區方法呼叫的順序來講解一下該方法的核心實作邏輯,
2.2.2 size方法詳解
public int size() {
return flushed;
}
回傳本次寫快取區可期望重繪的訊息個數(Entry),在NioSocketChannel的doWriter方法中,如果isEmpty回傳true,直接結束本次寫入操作,更加準確的是結束本次flush操作,
flushed該欄位代表的當時待寫入的Entry,如果為0,表示沒有待flush的Entry,但不代表ChannelOutboundBuffer中沒有Entry存在,比如呼叫Channel.writer方法,會往ChannelOutboundBuffer增加Entry,但在沒有呼叫addFlush方法之前,ChannelOutboundBuffer中的flushed 欄位的值不會增加,
2.2.3 addMessage方法詳解

向寫快取中添加訊息,方法本身的實作非常簡單,因為ChannelOutboundBuffer其內部資料結構為一個鏈表,這是一個往鏈表中添加訊息的程序,這里的關鍵點是該方法的呼叫入口為Channel的write方法,即呼叫通道的write方法只是將資料寫入到寫快取,并不會觸發真正的往網路中寫訊息,
該方法會呼叫incrementPendingOutboundBytes,我們簡單看一下該方法的實作細節:

該方法蘊含了Netty一個非常重要的機制,寫操作限流,高低水位線機制,
當快取區中存盤的資料超過了設定的高水位線(闊值),則會設定為不可寫,并向通道傳播寫狀態變更事件,
2.2.4 addMessage方法詳解

該方法并沒有真正的執行重繪動作,而是計算可刷寫的Entry個數,一次重繪動作,會將unfluedEntry開始,一直掃描到tailEntry,
同樣這里和Netty的寫限流有關,將資料刷寫后,會減少快取區中的大小,如果低于設定的低水位線,會將快取區恢復到可寫狀態,
該方法的呼叫入口為下圖:

即在呼叫通道的flush方法時會先計算本次看刷寫到Socket快取區中的資料,然后執行flush0方法執行真正的網路寫,該方法在第一部分中已詳細介紹,
本文就介紹到這里了,文章開頭的問題大家是否有了自己的理解了呢?歡迎私信或留言與我交流互動,讓我們在交流中共同進步,
一鍵三連是對我最大的支持與肯定
為了讓大家在實戰中學習Netty,筆者將RocketMQ的網路模塊單獨抽取成一個框架,可直接用于專案開發中,代碼已上傳github
Netty專案實戰代碼倉庫

推薦閱讀:
讓天下沒有難學的Netty4專欄
見字如面,我是威哥,一個從普通二本院校畢業,從未曾接觸分布式、微服務、高并發到通過技術分享實作職場蛻變,成長為RocketMQ社區優秀布道師、大廠資深架構師,出版《RocketMQ技術內幕》一書,在CSDN中記錄了我的成長歷程,歡迎大家關注,私信,一起交流進步,
分享筆者一個硬核的RocketMQ電子書:

獲取方式:微信搜索【中間件興趣圈】,回復RMQPDF即可獲取,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/271485.html
標籤:其他
上一篇:快速傅里葉變換FFT C語言實作 可用于單片機進行模擬采樣頻譜分析
下一篇:AD轉換基本原理
