零丶引入
在前面《Netty原始碼學習4——服務端是處理新連線的&netty的reactor模式》的學習中,我們瞭解到服務端是如何處理新連線的,即註冊ServerSocketChannel對accept事件感興趣,然後包裝ServerSocketChannel為NioServerSockectChannel,最後由主Reactor在迴圈中利用selector進行IO多路複用產生事件,如果產生accept事件那麼呼叫ServerSocketChannel#accept將其結果(SocketChannel)包裝為NioSockectChannel,然後傳播channelRead事件,然後由ServerBootstrapAcceptor 將NioSockectChannel註冊到子Reactor中。
也就是說ServerBootstrapAcceptor 是派活的大哥,屬於main reactor,而真正幹活的是子reactor中的NioEventLoop,它們會負責後續的資料讀寫與寫入。
這一篇我們就來學習NioSockectChannel是如何讀取資料的。
一丶子Reactor打工人NioEventLoop處理Read事件
原始碼學習的入口和服務端處理accept事件一致
區別在於這裡的Channel是NioSocketChannel,而不是NioServerSockectChannel,並且就緒的事件是READ(這是因為註冊到Selector上附件是包裝了SocketChannel的NioSocketChannel,感興趣的事件是read)並且這裡的執行緒是worker NioEventLoopGroup中的執行緒!
二丶NioSockectChannelUnsafe讀取資料
可以看到子reactor執行緒讀取客戶端傳送的資料,使用的是NioSockectChannelUnsafe#read方法。如下是read方法原始碼:
public final void read() {
final ChannelConfig config = config();
// 省略read-half相關處理
final ChannelPipeline pipeline = pipeline();
// ByteBuf分配器,預設為堆外記憶體+池化分配器
final ByteBufAllocator allocator = config.getAllocator();
// allocHandle用來控制下面讀取流程
final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
allocHandle.reset(config);
ByteBuf byteBuf = null;
boolean close = false;
try {
do {
// allocHandle使用allocator來分配記憶體給byteBuf
byteBuf = allocHandle.allocate(allocator);
// doReadBytes讀取資料到byteBuf,記錄最後一次讀取的位元組數
allocHandle.lastBytesRead(doReadBytes(byteBuf));
// 小於0==>通道已到達流結束
if (allocHandle.lastBytesRead() <= 0) {
// 釋放byteBuf
byteBuf.release();
byteBuf = null;
close = allocHandle.lastBytesRead() < 0;
if (close) {
// There is nothing left to read as we received an EOF.
readPending = false;
}
break;
}
// 記錄讀取次數+1
allocHandle.incMessagesRead(1);
readPending = false;
// 觸發channelRead
pipeline.fireChannelRead(byteBuf);
byteBuf = null;
} while (allocHandle.continueReading());//判斷是否繼續讀
allocHandle.readComplete();
// 觸發readComplete
pipeline.fireChannelReadComplete();
if (close) {
closeOnRead(pipeline);
}
} catch (Throwable t) {
// 省略
} finally {
// 省略
}
}
-
可以看到每次讀取到資料都會觸發channelRead,讀取完畢後會觸發readComplete
我們的業務邏輯就需要自己實現ChannelHandler#channelRead和channelReadComplete進行資料處理(解碼,執行業務操作,編碼,寫回)
-
可以看到是否繼續讀取客戶端傳送資料,是由allocHandle.continueReading()決定的,並且讀取客戶端的資料會存放到ByteBuf中,ByteBuf的分配是
allocHandle.allocate(allocator)
來控制
2.0 讀取客戶端傳送的資料
我們先忽略ByteBufAllocator和RecvByteBufAllocator ,直接看看doReadBytes(byteBuf)
是如何讀取的資料
最終呼叫setBytes進行讀取
下面我們看看ByteBufAllocator和RecvByteBufAllocator 在這個過程中取到了什麼作用
2.1 ByteBufAllocator
ByteBufAllocator的主要作用是分配和回收ByteBuf物件,以及管理記憶體的分配和釋放。
- 記憶體池的管理機制,用於提高記憶體分配和回收的效率。
- 支援可選的記憶體池型別,如池化和非池化等,以根據應用程式的需求進行靈活的記憶體管理。
如上是ByteBufAllocator的具體實現
-
AbstractByteBufAllocator:這是一個抽象類,提供了一些通用的方法和邏輯,用於建立和管理ByteBuf例項。它是UnpooledByteBufAllocator和PooledByteBufAllocator的基類。
-
UnpooledByteBufAllocator:這是ByteBufAllocator的預設實現。它採用非池化的方式進行記憶體分配,每次都會建立新的ByteBuf物件,不會使用記憶體池。
-
PooledByteBufAllocator:這是使用記憶體池的ByteBufAllocator實現。它透過重用記憶體池中的ByteBuf物件來提高效能和記憶體利用率。PooledByteBufAllocator可以根據需求使用池化和非池化的ByteBuf例項。
-
PreferredDirectByteBufAllocator:偏好使用直接記憶體的分配器,ByteBufAllocator#buffer並沒有說明是堆內還是堆外,PreferredDirectByteBufAllocator會優先使用堆內(裝飾器模式)
-
PreferHeapByteBufAllocator:偏好使用堆內記憶體的分配器
2.2 RecvByteBufAllocator 與 RecvByteBufAllocator.Handler
ByteBufAllocator是真正分配記憶體產生ByteBuf的分配器,但是在網路io中通常需要根據讀取資料的多少動態調整ByteBuf的。預設情況下netty在讀取客戶端資料的時候使用的是AdaptiveRecvByteBufAllocator
,顧名思義可以調整ByteBuf的RecvByteBufAllocator 實現。
也就是說,ByteBufAllocator是真正負責記憶體分配的,RecvByteBufAllocator是負責根據網路IO情況去呼叫ByteBufAllocator調整ByteBuf的。
- FixedRecvByteBufAllocator:固定分配器。該實現分配固定大小的ByteBuf,不受網路環境和應用程式需求的影響。適用於在已知資料量的情況下進行分配,不需要動態調整大小。
- AdaptiveRecvByteBufAllocator:自適應分配器。該實現根據當前的網路環境和應用程式的處理能力動態地調整ByteBuf的大小。可以根據實際情況自動增加或減少分配的大小,以最佳化效能。
- DefaultMaxMessagesRecvByteBufAllocator:根據最大訊息數量的分配器。該實現根據應用程式的需求來控制ByteBuf的分配。可以設定最大訊息數量,當達到該數量時,不再分配ByteBuf,以控制記憶體的使用。
- DefaultMaxBytesRecvByteBufAllocator:主要根據最大位元組數來控制ByteBuf的分配。它與DefaultMaxMessagesRecvByteBufAllocator類似,但是以位元組數為基礎而不是訊息數量。
- ServerChannelRecvByteBufAllocator:控制服務端接收緩衝區大小
其內部還有一個Handler,Handler才是真正實現這些邏輯的類,這樣做法的好處在於解耦合——RecvByteBufAllocator和Handler是松耦合的,多個RecvByteBufAllocator可以基於相同的Handler。
三丶 AdaptiveRecvByteBufAllocator
如下是 AdaptiveRecvByteBufAllocator#HandleImpl在讀取客戶端資料的過程中取到的作用:
3.1 分配ByteBuf&控制ByteBuf大小
可以看到真正分配ByteBuf的是ByteBufAllocator,而大小是AdaptiveRecvByteBufAllocator#HandleImpl使用guess方法猜測出來的
首次guess會返回預設的值(2048)後續該方法根據之前讀取資料的多少來“猜”這次使用多大ByteBuf比較合適
這個猜其實就是返回記憶體中記錄的下一次大小,那麼是怎麼實現猜測的過程的暱?
3.2 “猜“——動態調整ByteBuf大小
可以看到在記錄讀取數量的時候,如果是滿載而歸(比如上一次猜需要2048位元組,由於客戶端傳送資料很多,讀滿了ByteBuf)會呼叫record進行記錄和調整
可以看到容量大小記錄在了SIZE_TABLE中,SIZE_TABLE的初始化如下
可以看到AdaptiveRecvByteBufAllocator#HandlerImpl調整的策略有以下特點
- 小於512的適合,容量大小增長緩慢,大於521的容量翻倍增加
- 擴容大膽,縮容需要兩次判斷
3.3 是否繼續讀取
continueReading會判斷是否繼續讀取:需要開啟自動讀,且maybe存在更多資料需要讀取,且累計讀取訊息數小於最大訊息數,且上一次讀到了資料
-
自動讀:預設情況下,Netty的Channel是處於自動讀取模式的。這意味著當有新資料可讀時,Netty會自動觸發讀事件,從Channel中讀取資料並傳遞給下一個處理器進行處理。自動讀適合在高吞吐量的場景開啟,但是如果處理資料的速度跟不上讀取資料速度會出現資料堆積,記憶體佔用過高,rt增加的問題。
-
maybe存在更多資料需要讀取:
其實就是判斷上一次讀取的位元組數和預估的數量是否相等,也就是是否滿載而歸
-
累計讀取訊息數小於最大訊息數
雖然一個NioServerChannel只會繫結到一個執行緒,但是一個執行緒可以註冊多個NioServerChannel,so如果一個客戶端瘋狂發資料, 服務端不做干預,將導致這個執行緒上的其他Channel永遠得不到處理
so netty設定maxMessagePerRead(單次read最多可以讀取多少訊息——指迴圈讀取ServerChannel多少次)
四丶總結&啟下
1.總結
這一篇我們看了NioServerChannel是如何讀取資料的,其Unsafe依賴JDK原生的SocketChannel#read(ByteBuffer)來讀取資料,但是netty在此之上做了如下最佳化
-
使用ByteBufAllocator最佳化ByteBuf的分配,預設使用池化的直接記憶體策略
記憶體池這一篇沒用做過多學習,後續單獨學習
-
使用AdaptiveRecvByteBufAllocator對讀取過程進行最佳化
- guess會猜測多大的ByteBuf合適(每次讀取後進行擴容or縮容)
- 內部是SIZE_TABLE記錄容量大小,小於512的適合,容量大小增長緩慢,大於521的容量翻倍增加
- 擴容大膽——容量小了1次那麼下一次使用SIZE_TABLE下一個下標對應的容量,縮容需要兩次判斷,連續兩次不滿足大小才進行縮容
- 在是否繼續讀取上雨露均霑——控制最多讀取16次,並且會根據讀取資料是否滿載而歸判斷是否需要繼續讀取
2.啟下
這一篇我們看到每一次迴圈讀取NioSocketChannel資料後會觸發channelRead,讀取完畢後會觸發readComplete,
我們的業務邏輯就需要自己實現ChannelHandler#channelRead和channelReadComplete進行資料處理(解碼,執行業務操作,編碼,寫回)
那麼netty中有哪些內建的編碼解碼器暱?下一篇我們再來嘮嘮