在前面的原始碼學習中,梳理了伺服器端的啟動,以及NioEventLoop事件迴圈的工作流程,並瞭解了Netty處理網路io重要的Channel ,ChannelHandler,ChannelPipeline。
這一篇將學習伺服器端是如何構建新的連線。
當用戶端傳送的網路資料框通過網路傳輸到網路卡時,網路卡的DMA引擎將網路卡接收緩衝區中的資料拷貝到DMA環形緩衝區,資料拷貝完成後網路卡硬體觸發硬中斷,通知作業系統資料已到達。
隨後網路卡中斷處理程式將DMA環形緩衝區的資料拷貝到sk_buffer,sk_buffer位於核心中,它提供了一個緩衝區,使得網路卡中斷程式可以將他接收到的資料暫存起來,避免資料丟失和切換。
隨後發起軟中斷,網路協定棧會處理封包,對封包進行解析,路由,分發(根據目的埠號,分發給對應的應用程式,通過網路程式設計通訊端,應用程式可以監聽指定埠號,並接受網路協定棧的封包)
也就是說netty 伺服器端程式會監聽不同的網路事件,並進行處理,這也是原始碼學習的切入點!
無論是否優化,最終都是拿到就緒的SelectionKey,迴圈處理每一個就緒的網路事件,如下便是處理的邏輯:
可以看到無論是accept事件還是read事件都是呼叫AbstractNioChannel的Unsafe#read方法
Unsafe是對netty對底層網路事件處理的封裝,下面我們先看下AbstractNioChannel的類圖,可以看到NioServerSocketChannel,和NioSocketChannel都使用繼承了AbstractNioChannel,只是父類別有所不同
doBind0會呼叫Channel#bind,然後處理ChannelPipeline#bind的執行,由於bind是出站事件,將從DefaultChannelPipeline的TailContext開始執行,然後呼叫到HeadContext#bind方法,最終會呼叫NioServerSocketChannel的unsafe#bind方法
如下是NioServerSocketChannel的unsafe#bind的內容:
這裡將呼叫到Channel#read方法,最終會呼叫到HeadContext#read
前面我們說到,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
上面說到NioEventLoop讀取NioServerSocketChannel上的accept事件,將每一個新連線封裝為NioServerChannel後,將依次觸發channelRead。
如下是ServerBootstrapAcceptor#channelRead方法,可以看到它會將讀取生成的NioServerChannel註冊到childGroup,這裡的childGroup就是ServerBootstrap啟動時候指定EventLoopGroup(主從reactor模式中的從reactor)
也就是說主reactor負責處理accept事件,從reactor負責處理read事件
大多數人看到 channelReadComplete 都會認為這是 Netty 讀取了完整的資料,然而有時卻不是這樣。channelReadComplete 其實只是表明了本次從 Socket 讀了資料,該方法通常可以用來進行一些收尾工作,例如傳送響應資料或進行資源的釋放等。channelReadComplete方法在每次讀取資料完成後,即使沒有更多的資料可讀,也會被呼叫一次。
這裡其實可以看出netty對多種reactor模式(單執行緒,多執行緒,主從reactor)的支援
我們其實可以通過修改bossGroup,和workerGroup使netty使用不同的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
觸發ChannelRegistered
觸發channelActive
由於這是一個新連線,是第一次註冊到EventLoop,因此會觸發channelActive
這將呼叫到DefaultChannelPipeline的HeadContext#readIfIsAutoRead,最終就和我們第三節的【NioServerSocketChannel設定對accept事件感興趣】差不多
——HeadContext#readIfIsAutoRead會呼叫NioSockectChannel的read方法,最終呼叫到NioSockectChannel#unsafe的read方法——將註冊對read事件感興趣
筆者認為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後和女朋友遊烏鎮的風景圖