Netty源码学习4——服务端是处理新连接的&netty的reactor模式
系列文章目錄和關于我
零丶引入
在前面的源碼學習中,梳理了服務端的啟動,以及NioEventLoop事件循環的工作流程,并了解了Netty處理網絡io重要的Channel ,ChannelHandler,ChannelPipeline。
這一篇將學習服務端是如何構建新的連接。
一丶網絡包接收流程
當客戶端發送的網絡數據幀通過網絡傳輸到網卡時,網卡的DMA引擎將網卡接收緩沖區中的數據拷貝到DMA環形緩沖區,數據拷貝完成后網卡硬件觸發硬中斷,通知操作系統數據已到達。
隨后網卡中斷處理程序將DMA環形緩沖區的數據拷貝到sk_buffer,sk_buffer位于內核中,它提供了一個緩沖區,使得網卡中斷程序可以將他接收到的數據暫存起來,避免數據丟失和切換。
隨后發起軟中斷,網絡協議棧會處理數據包,對數據包進行解析,路由,分發(根據目的端口號,分發給對應的應用程序,通過網絡編程套接字,應用程序可以監聽指定端口號,并接受網絡協議棧的數據包)
- 當新的連接建立時,網絡協議處理棧會將這個連接的套接字標記為可讀,并生成一個accept事件,這個事件通知應用程序有新的連接需要處理
- 當已經建立的連接上有數據到達時,網絡協議處理棧會將套接字標記為刻度,并生成一個read事件,這個事件通知應用程序有數據可供讀取
- 當應用程序向已經建立的連接寫入數據時,如果寫緩沖區有足夠的空間,寫操作會立即完成,不會產生write事件。但如果寫緩沖區已滿,那么寫操作將被暫停,當寫緩沖區有足夠的空間時,write事件將被觸發,通知應用程序可以繼續寫入數據。
也就是說netty 服務端程序會監聽不同的網絡事件,并進行處理,這也是源碼學習的切入點!
二丶服務端NioEventLoop處理網絡IO事件
如上是NioEventLoop的運行機制,在《Netty源碼學習2——NioEventLoop的執行》中我們進行了大致流程的學習,這一篇我么主要關注其run中處理網絡IO事件的部分。
無論是否優化,最終都是拿到就緒的SelectionKey,循環處理每一個就緒的網絡事件,如下便是處理的邏輯:
可以看到無論是accept事件還是read事件都是調用AbstractNioChannel的Unsafe#read方法
Unsafe是對netty對底層網絡事件處理的封裝,下面我們先看下AbstractNioChannel的類圖,可以看到NioServerSocketChannel,和NioSocketChannel都使用繼承了AbstractNioChannel,只是父類有所不同
那么NioServerSocketChannel和NioSocketChannel是什么時候Accept or read事件感興趣的昵?
三丶NioServerSocketChannel設置對accept事件感興趣
重點在ServerBootstrap#bind中,此方法會調用doBind0
doBind0會調用Channel#bind,然后處理ChannelPipeline#bind的執行,由于bind是出站事件,將從DefaultChannelPipeline的TailContext開始執行,然后調用到HeadContext#bind方法,最終會調用NioServerSocketChannel的unsafe#bind方法
如下是NioServerSocketChannel的unsafe#bind的內容:
主要完成兩部分操作:
-
調用java原生ServerSocketChannel#bind方法,進行端口綁定,這樣操作系統網絡協議棧在分發網絡數據的時候,才直到該分發到這個端口的ServerSocketChannel
-
向EventLoop中提交一個pipeline.fireChannelActive()的任務,將在pipeline上觸發channelActive方法,HeadContext#channelActive將被調用到
這里將調用到Channel#read方法,最終會調用到HeadContext#read
四丶服務端處理Accept事件
前面我們說到,NioEventLoop處理accept事件和read事件都是調用unsafe#read方法,如下是NioServerSocketChannel#unsafe的read方法
public void read() {
assert eventLoop().inEventLoop();
final ChannelConfig config = config();
final ChannelPipeline pipeline = pipeline();
final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
allocHandle.reset(config);
boolean closed = false;
Throwable exception = null;
try {
try {
do {
//讀取數據
int localRead = doReadMessages(readBuf);
if (localRead == 0) {
break;
}
if (localRead < 0) {
closed = true;
break;
}
// 計數
allocHandle.incMessagesRead(localRead);
} while (continueReading(allocHandle));
} catch (Throwable t) {
exception = t;
}
int size = readBuf.size();
for (int i = 0; i < size; i ++) {
readPending = false;
// 觸發channelRead
pipeline.fireChannelRead(readBuf.get(i));
}
readBuf.clear();
allocHandle.readComplete();
// 觸發channelReadComplete
pipeline.fireChannelReadComplete();
// 省略
} finally {
// 省略
}
}
這里出現一個RecvByteBufAllocator.Handle,這里不需要過多關注,在NioServerSocketChannel建立連接的過程中,它負責控制是否還需要繼續讀取數據
ServerSocketChannel類提供了accept()方法,用于接受客戶端的連接請求,返回一個SocketChannel代表了一個底層的TCP連接。
如上將jdk SocketChannel包裝NioSocketChannel的時候會設置SocketChannel非阻塞并在屬性readInterestOp記錄感興趣事件為read
包裝生成的NioSocketChannel會放到List中,后續每一個就緒的連接會一次傳播ChannelRead,并最終傳播ChannelReadComplete
1.channeRead事件的傳播
上面說到NioEventLoop讀取NioServerSocketChannel上的accept事件,將每一個新連接封裝為NioServerChannel后,將依次觸發channelRead。
如下是ServerBootstrapAcceptor#channelRead方法,可以看到它會將讀取生成的NioServerChannel注冊到childGroup,這里的childGroup就是ServerBootstrap啟動時候指定EventLoopGroup(主從reactor模式中的從reactor)
也就是說主reactor負責處理accept事件,從reactor負責處理read事件
2.channelReadComplete事件傳播
大多數人看到 channelReadComplete 都會認為這是 Netty 讀取了完整的數據,然而有時卻不是這樣。channelReadComplete 其實只是表明了本次從 Socket 讀了數據,該方法通常可以用來進行一些收尾工作,例如發送響應數據或進行資源的釋放等。channelReadComplete方法在每次讀取數據完成后,即使沒有更多的數據可讀,也會被調用一次。
五丶netty對多種reactor模式的支持
這里其實可以看出netty對多種reactor模式(單線程,多線程,主從reactor)的支持
我們其實可以通過修改bossGroup,和workerGroup使netty使用不同的reactor模式
六丶將NioSocketChannel注冊到從reactor
上面我們說到主reactor監聽accept事件后傳播channelRead事件,最終由ServerBootstrapAcceptor調用childGroup#register將包裝生成的NioSocketChannel注冊到從reactor(也就是workerGroup——EventLoopGroup)下面我們看看這個注冊會發生什么
首先workerGroup這個EventLoopGroup會調用next方法選擇出一個EventLoop執行register,然后
-
將NioSocketChannel中的jdk SockectChannel注冊到Selector中,并將NioSocketChannel當作附件,這樣selector#select到事件的時候,可以從附件中拿到網絡事件對應的NioSocketChannel
-
觸發handlerAdd
這一步觸發ChannelHandler#handlerAdded
最終會調用到childHandler中指定的ChannelInitializer,它會將我們指定的ServerHandler(這里可以擴展我們的業務處理邏輯)加到NioSockectChannel的pipeline中
-
觸發ChannelRegistered
-
觸發channelActive
由于這是一個新連接,是第一次注冊到EventLoop,因此會觸發channelActive
這將調用到DefaultChannelPipeline的HeadContext#readIfIsAutoRead,最終就和我們第三節的【NioServerSocketChannel設置對accept事件感興趣】差不多
——HeadContext#readIfIsAutoRead會調用NioSockectChannel的read方法,最終調用到NioSockectChannel#unsafe的read方法——將注冊對read事件感興趣
七丶再看Netty的Reactor模式
筆者認為netty的reactor有以下幾個要點
-
ServerBootstrap#bind方法
不僅僅會綁定端口,還會觸發channelActive事件,從而使DefaultChannelPipeline中的HeadContext觸發netty channel unsafe#beginRead,注冊ServerSockectChannel對accept感興趣
-
NioEventLoop處理新連接
這一步Netty 使用Selector進行IO多路復用,當accept事件產生的時候,調用
NioServerSocketChannel#unsafe的read方法,這一步會將新連接封裝NioSocketChannel,然后將對應連接的套接字注冊到Selector上,然后傳播channeRead事件 -
ServerBootstrapAcceptor 對channeRead事件的處理
筆者認為這是netty reactor模式的核心,它將NioSocketChannel注冊到從reactor上,讓子reactor負責處理NioSocketChannel上的事件,并最終注冊SocketChannel對read事件感興趣!
和tomcat的reactor(《Reactor 模式與Tomcat中的Reactor 》)有異曲同工之妙,只是netty Pipeline的設計讓整個流程更具備擴展性,當然也增加了源碼學習的復雜度doge
八丶啟下
下一篇我們將學習從reactor是如何處理read事件的,整個流程和主reactor處理accept事件類似,后續應該會設計到netty編解碼相關的知識。
這一篇是雙11結束后忙里偷閑的產物,附上一張雙11后和女朋友游烏鎮的風景圖
總結
以上是生活随笔為你收集整理的Netty源码学习4——服务端是处理新连接的&netty的reactor模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 放弃"Jenkins"的种种理由,期待更
- 下一篇: 分享我对DiscuzQ这款现代化开源轻社