Netty原始碼學習4——伺服器端是處理新連線的&netty的reactor模式

2023-11-20 06:00:35

系列文章目錄和關於我

零丶引入

在前面的原始碼學習中,梳理了伺服器端的啟動,以及NioEventLoop事件迴圈的工作流程,並瞭解了Netty處理網路io重要的Channel ,ChannelHandler,ChannelPipeline。

這一篇將學習伺服器端是如何構建新的連線。

一丶網路包接收流程

當用戶端傳送的網路資料框通過網路傳輸到網路卡時,網路卡的DMA引擎將網路卡接收緩衝區中的資料拷貝到DMA環形緩衝區,資料拷貝完成後網路卡硬體觸發硬中斷,通知作業系統資料已到達。

隨後網路卡中斷處理程式將DMA環形緩衝區的資料拷貝到sk_buffer,sk_buffer位於核心中,它提供了一個緩衝區,使得網路卡中斷程式可以將他接收到的資料暫存起來,避免資料丟失和切換。

隨後發起軟中斷,網路協定棧會處理封包,對封包進行解析,路由,分發(根據目的埠號,分發給對應的應用程式,通過網路程式設計通訊端,應用程式可以監聽指定埠號,並接受網路協定棧的封包)

  • 當新的連線建立時,網路協定處理棧會將這個連線的通訊端標記為可讀,並生成一個accept事件,這個事件通知應用程式有新的連線需要處理
  • 當已經建立的連線上有資料到達時,網路協定處理棧會將通訊端標記為刻度,並生成一個read事件,這個事件通知應用程式有資料可供讀取
  • 當應用程式向已經建立的連線寫入資料時,如果寫緩衝區有足夠的空間,寫操作會立即完成,不會產生write事件。但如果寫緩衝區已滿,那麼寫操作將被暫停,當寫緩衝區有足夠的空間時,write事件將被觸發,通知應用程式可以繼續寫入資料。

也就是說netty 伺服器端程式會監聽不同的網路事件,並進行處理,這也是原始碼學習的切入點!

二丶伺服器端NioEventLoop處理網路IO事件

無論是否優化,最終都是拿到就緒的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

四丶伺服器端處理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

  • 觸發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後和女朋友遊烏鎮的風景圖