更多技術分享可關注我算法
前面的分析從Netty服務端啓動過程入手,一路走到了Netty的心臟——NioEventLoop,又總結了Netty的異步API和設計原理,如今回到Netty服務端自己,看看服務端對客戶端新鏈接接入的處理是怎麼樣的過程。數組
原文:Netty是如何處理新鏈接接入事件的?安全
首先,對於新鏈接接入,從NIO層面有一個宏觀的印象:性能優化
一、經過I/O多路複用器——Selector檢測客戶端新鏈接服務器
對應到Netty,新鏈接經過服務端的NioServerSocketChannel(底層封裝的JDK的ServerSocketChannel)綁定的I/O多路複用器(由NioEventLoop線程驅動)輪詢OP_ACCEPT(=16)事件網絡
二、輪詢到新鏈接,就建立客戶端的Channel多線程
對應到Netty就是NioSocketChannel(底層封裝JDK的SocketChannel)架構
三、爲新鏈接分配綁定新的Selector異步
對應到Netty,就是經過線程選擇器,從它的第二個線程池——worker線程池中挑選一個NIO線,在這個線程中去執行將JDK的SocketChannel註冊到新的Selector的流程,將Netty封裝的NioSocketChannel做爲附加對象也綁定到該Selectorsocket
四、向客戶端Channel綁定的Selector註冊I/O讀、或者寫事件
對應到Netty,就是默認註冊讀事件,由於Netty的設計理念是讀優先。之後本條Channel的讀寫事件就由worker線程池中的NIO線程管理
以上4步,其實就是對下面一段JDK NIO demo的抽象和封裝,並解決了一些bug的過程,以下:
接下來的幾篇文章會逐步拆解每一個步驟,並學習Netty的設計思路。
前面分析過NioEventLoopGroup和線程池對應,NioEventLoop實例和NIO線程對應,一個EventLoop實例將由一個永遠都不會改變的Thread驅動其內部的run方法(和Runnable的run不是一個)。
簡單說,Netty服務端建立的boss和worker就是兩個線程池,對於一個服務器的端口,bossGroup裏只會啓動一個NIO線程用來處理該端口上的客戶端新鏈接的檢測和接入流程。
具體的說,Netty會在服務端的Channel的pipeline上,默認建立一個新鏈接接入的handler,只用於服務端接入客戶端新鏈接,而workerGroup裏有多個NIO線程(默認2倍的CPU核數個),負責已創建的Channel上的讀寫事件的檢測、註冊或者處理,等操做。當boss線程池的那一個NIO線程檢測到新鏈接後就能夠稍作休息(或者繼續檢測處理新鏈接),此時worker線程池就開始忙碌,以下圖所示:
細節回顧能夠參考:Netty的線程調度模型分析(1)
下面開始總結,boss線程和worker線程池之間是如何配合的。
再看JDK的select方法
在總結以前,我的認爲有必要先回顧JDK的select,必須正確理解I/O多路複用器——Selector上所謂的輪詢一次,返回就緒的Channel數目的真正意義,即這個過程有一個前提是自從上次select後開始計算的。這樣乾巴巴的解釋可能不太清楚,下面舉個例子,好比有兩個已經創建的Channel,分別是A和B,並且A和B分別註冊到了一個Selector上,接着在該Selector調用select():
第一次調用select(),發現只有A有I/O事件就緒,select會當即返回1,而後處理之
第二次調用select(),發現另外一個通道B也有I/O事件就緒,此時select()仍是返回1——便是自上次select後開始計算的
還有一點注意:若是第一次輪詢後,對A沒有作任何操做,那麼就有兩個就緒的Channel。
另外還要知道,select返回後可經過其返回值判斷有沒有Channel就緒,若是有就緒的Channel,那麼可使用selectedKeys()方法拿到就緒的Channel及其一些屬性。下面看selectedKeys()的使用:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
當給Selector註冊Channel時,調用的register()方法會返回一個SelectionKey對象,這個對象表明了註冊到該Selector的Channel,能夠遍歷這個集合來訪問就緒的通道。
以上,前面的線程調度模型都分析過,回憶這個圖:
細節回顧能夠參考:
前面文章總結了NioEventLoopGroup實例化時,若是外部沒有配置,那麼會默認建立一個線程執行器——ThreadPerTaskExcutor,一個NioEventLoop組成的數組(線程池),還有一個線程選擇器——chooser。
又知道當實例化NioEventLoop並填充底層線程數組時,Netty會爲每一個NioEventLoop建立並綁定一個I/O多路複用器——Selector和一個異步任務隊列——MPSCQ,接下來又總結了Netty的NioEventLoop線程啓動的觸發時機有兩個:
宏觀上,服務端綁定端口時會觸發boss線程池裏的一個NIO線程啓動,即用戶代碼調用bind方法。若是深刻bind方法內部,那麼會發現NIO線程第一次啓動的精確時機是爲JDK的ServerSocketChannel註冊I/O多路複用器的時候——Netty會封裝這個註冊邏輯爲一個異步task,使用NIO線程驅動,若是沒有啓動,那麼就啓動之,之後的Channel綁定端口的邏輯也會被封裝爲異步task,複用已經啓動的這個NIO線程
新鏈接接入時會觸發worker線程池裏的NIO線程啓動。線程池的線程選擇器會爲新鏈接綁定一個worker裏的NIO線程,第一次接入或者線程池的線程還沒徹底啓動完畢,就會順勢啓動
總之,Netty服務端啓動後,服務端的Channel已經綁定到了boss線程池的NIO線程中,並不斷檢測是否有OP_ACCEPT事件發生,直到檢測出有該事件發生就處理之,即boss線程池裏的NioEventLoop線程只作了兩件事:
一、輪詢OP_ACCEPT事件
二、檢測到OP_ACCEPT事件後就處理該事件,處理過程其實就是客戶端Channel(新鏈接)接入的過程
下面繼續回顧NioEventLoo線程的事件循環的核心方法——run,它在NIO線程啓動時開始運行:
在這以前,先在run方法打斷點:而後啓動實驗用的最小版Netty服務端的demo,以後分別在三個客戶端使用telnet命令對其順序發送3個請求,模擬客戶端3個新鏈接接入的過程,下面進入run跟蹤源碼:
一、首先調用Netty封裝的select方法,前面分析過當有客戶端新鏈接接入,即表明已經觸發了OP_ACCEPT事件,Selector的select方法會當即返回1,以下:
這裏要理解JDK的select方法返回值究竟是什麼。select()方法會返回註冊的interest的I/O事件已經就緒的那些通道的數目,摳字眼,首先得看是哪些Channel註冊在了當前I/O多路複用器上,其次,看這些Channel上註冊的interest的I/O事件是否就緒,如上代碼的局部變量selectedKeys==1,可是我實驗的客戶端鏈接是3個,這裏可能會有疑問,selectedKeys爲什麼不是3呢?
由於當前綁定在boss線程上的I/O多路複用器只註冊了服務端的Channel,即底層只有一個ServerSocketChannel,且當前註冊的interest的I/O事件只有OP_ACCEPT,故不管多少個新鏈接接入,這裏都只會返回1。
還有一個誤區:不要認爲Selector的select返回值是已準備就緒的Channel的總數,其實它返回的是從上一個select()調用後進入就緒狀態的Channel的數量。
繼續分析:輪詢出有感興趣的I/O事件就緒的Channel後,會break循環,回到外部的run方法,開始處理這個I/O事件,這裏就是處理新鏈接的接入事件,核心方法以前也分析過,就是processSelectedKeys:
在詳細的細節能夠參考:
這個方法有兩個變體,前面文章也分析過緣由,我選擇有表明性的processSelectedKeysOptimized,看裏面的processSelectedKey(key,channel)方法,這才真正到了Netty處理I/O事件的方法入口,以下:
以下是processSelectedKey方法的實現:
首先看黃色1處,取出ServerSocketChannel的unsafe對象,前面也總結過,Netty封裝的Channel的底層都會有一個Unsafe對象與之綁定,Unsafe是個內部接口,聚合在Channel接口內部,做用是協助Channel進行網絡I/O的操做,由於它的設計初衷就是Channel的內部輔助類,不該該被Netty的使用者調用,因此被命名爲Unsafe,而不是說這個類的API都是不安全的。
繼續執行到黃色2處,會判斷當前Channel是否打開,其實就是判斷的ServerSocketChannel。一切順利繼續執行黃色3處,看到了熟悉的NIO API,下面專門看黃色3處後面的一堆代碼:
在黃色3處,k內部的readyOps集合是該Channel已經準備就緒的I/O操做的集合,OP_ACCEPT這個宏是16,因此這裏的readyOps變量爲16。
接着立刻會執行到黃色4處的if判斷邏輯,因爲readyOps爲16,這裏經過判斷,進入if內部,執行黃色5處的代碼。該處邏輯是一個read操做,很好理解。當NioEventLoop的run方法裏輪詢到ServerSocketChannel的accept事件後,服務端第一步就是對其執行讀操做,這是很天然的想法。由於這是服務端,因此下面會進入到NioMessageUnsafe實例的read方法:
在黃色1處,首先保證是NioEventLoop線程在執行,若是是外部線程執行的,那麼無效。接下來,會獲取服務端Channel的Config和默認建立的服務端Channel的pipeline。在黃色2處有一個RecvByteBufAllocator.Handle allocHandle變量,它獲取了RecvByteBuf分配器Handle,顧名思義就是設置接收的緩衝區大小,簡單說是經過二分算法獲取一個不會浪費空間,可是又足夠大小的緩衝區,是一種性能優化的策略,之後分析Netty內存圖像時在深刻。
接着在黃色2處的下一行是一個重置配置的方法,目的是重置已累積的全部計數器,併爲下一個讀取循環讀取多少消息/字節數據提供建議。Netty默認一次讀取16個新鏈接,以下:
而後繼續看NioMessageUnsafe實例的read方法,在黃色3處,進入一個do-while循環:
首先調用doReadMessages方法,在do—while循環中讀取一個個的客戶端新鏈接,並將讀取到的新鏈接用readBuf這個集合存儲,readBuf就是NioMessageUnsafe類內部的一個普通的ArrayList。
下面進入doReadMessages方法,以下該方法內部邏輯似曾相識。
首先,在黃色1處封裝了JDK的NIO API,即獲取客戶端的socket——NIO對應的是SocketChannel,完成該操做意味着TCP/IP協議棧完成了TCP的三次握手,TCP的邏輯鏈路正式創建,而後,在黃色2處,Netty將客戶端Channel封裝爲本身的客戶端channel——NioSocketChannel。由於這裏明確了是服務端在處理accept事件,故不須要反射建立NioSocketChannel,直接實例化便可,後續在詳細分析Netty的客戶端channel建立過程。最後,封裝的Channel保存到readBuf這個ArrayList中,doReadMessages方法返回1。
回到上層的do-while循環:
doReadMessages返回的localRead==1,說明本次讀取新鏈接成功,do-while的一次循環讀新鏈接完畢,會繼續讀下一個新鏈接,直到所有讀完,或者達到閾值。也就是說Netty在讀取新鏈接時也權衡了性能,若是鏈接太多,那麼Netty不會一直卡在這裏處理,它默認do-while循環處理16個,這個邏輯在黃色5處的判斷條件裏,超過閾值就退出do-while。
下面看黃色5處的判斷邏輯——即continueReading()方法,簡單看下:
Netty設計理念是讀優先,會給服務端Channel自動註冊OP_READ事件——也就是isAutoRead()方法會返回true,那個maxMessagePerRead默認配置的是16,即每一次集中處理accept事件時,最多讀取的鏈接數爲16個,是權衡了性能而設計的,這個能夠由用戶配置。
繼續回看NioMessageUnsafe實例的read方法,若是有新鏈接,那麼繼續do-while循環,直到發生異常,或者讀取的新鏈接數量達到了閾值,或者已經沒有新鏈接可讀,doReadMessages返回0,退出do-while循環。這裏說明一下,正常狀況doReadMessages裏的accept必定不會阻塞,由於只有當Channel裏有就緒的I/O事件,換句話說,有數據能夠讀,纔會進入accept環節,本質是由於Netty服務端爲NIO模型配置的是非阻塞I/O,即Netty會自動對各個Channel有以下的配置:
並且,若是服務端Channel有就緒的I/O事件,那麼accept()必定會返回客戶端Channel,除非實例化Netty的客戶端Channel——NioSocketChannel時出現異常。
若是doReadMessages返回0,那麼就會break出do-while循環,接下來大動脈——Netty的pipeline就該幹活了,以下NioMessageUnsafe實例的read方法的後面的源碼:
在黃色6處,遍歷保存客戶端新Channel的集合——readBuf,而後將每一個新鏈接傳播出去——調用pipeline.fireChannelRead(),將每條新鏈接沿着服務端Channel的pipeline傳遞,交給Channel後續的入站handler,而黃色7處,會傳播一個讀操做完成的事件——fireChannelReadComplete();後續會逐漸的拆解並詳細分析pipeline的設計,這裏知道便可。
至此,Netty服務端檢測處理客戶端新鏈接的過程分析完畢。
一、權衡性能,NIO線程一次處理的新鏈接不能太多,Netty默認是一次最多處理16個
二、Netty的pipeline機制和讀取新鏈接後的銜接過程——觸發和傳遞
三、Selector的select返回值的理解
四、深入理解同步非阻塞,即NIO模式下,accept方法爲何不會阻塞