零丶引入
在前面的原始碼學習中,梳理了服務端的啟動,以及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後和女朋友遊烏鎮的風景圖