前言
從學習過BIO、NIO、AIO程式設計之後,就能很清楚Netty程式設計的優勢,為什麼選擇Netty,而不是傳統的NIO程式設計。本片博文是Netty的一個入門級別的教程,同時結合時序圖與原始碼分析,以便對Netty程式設計有更深的理解。
在此博文前,可以先學習瞭解前幾篇博文:
參考資料《Netty In Action》、《Netty權威指南》(有需要的小夥伴可以評論或者私信我)
博文中所有的程式碼都已上傳到Github,歡迎Star、Fork
一、服務端建立
Netty遮蔽了NIO通訊的底層細節,減少了開發成本,降低了難度。ServerBootstrap可以方便地建立Netty的服務端
1.服務端程式碼示例
public void bind (int port) throws Exception { // NIO 執行緒組 EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 100) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ChannelInitializer<SocketChannel>() { // Java序列化編解碼 ObjectDecoder ObjectEncoder // ObjectDecoder對POJO物件解碼,有多個建構函式,支援不同的ClassResolver,所以使用weakCachingConcurrentResolver // 建立執行緒安全的WeakReferenceMap對類載入器進行快取SubReqServer @Override protected void initChannel(SocketChannel socketChannel) throws Exception { // 半包處理 ProtobufVarint32FrameDecoder socketChannel.pipeline().addLast(new ProtobufVarint32FrameDecoder()); // 新增ProtobufDecoder解碼器,需要解碼的目標類是SubscribeReq socketChannel.pipeline().addLast( new ProtobufDecoder(SubscribeReqProto.SubscribeReq.getDefaultInstance())); socketChannel.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender()); socketChannel.pipeline().addLast(new ProtobufEncoder()); socketChannel.pipeline().addLast(new SubReqServerHandler()); } }); // 繫結埠,同步等待成功 ChannelFuture f = bootstrap.bind(port).sync(); // 等待所有服務端監聽埠關閉 f.channel().closeFuture().sync(); } finally { // 優雅退出,釋放執行緒池資源 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } }
2.服務端時序圖
(1)建立ServerBootstrap例項
是Netty服務端啟動的輔助類,提供了一系列的方法用於設定服務端自動相關引數,降低開發難度;
(2)設定並繫結Reactor執行緒池
Netty的Reactor執行緒池(I/O 複用 + 執行緒池)是EventLoopGroup,實際上就是EventLoop陣列,EventLoop處理所有註冊到本執行緒的多路複用器Selector上的Channel,Selector的輪詢操作由繫結的EventLoop執行緒run方法驅動,在一個迴圈體內迴圈執行。EventLoop不僅執行I/O事件,也能執行使用者自定義的Task和定時任務Task
(3)設定並繫結服務端Channel
需要建立ServerSocketChannel,對應的實現類就是NioServerSocketChannel。ServerBootstrap的channel方法用於指定服務端的Channel型別
通過反射建立NioServerSocketChannel物件
通過呼叫無參預設的構造方法生成channel
(4)建立並初始化ChannelPipeline
本質上是一個負責處理網路事件的職責鏈,負責管理與執行ChannelHandler。 ChannelPipeline為ChannelHandler鏈提供了容器,並定義了用於在該鏈上傳播入站和出站事件流的API。當Channel被建立時,他會自動的分配到它專屬的ChannelPipeline。典型的網路事件包括:
- 鏈路註冊
- 鏈路啟用
- 鏈路斷開
- 接收到請求訊息
- 處理請求訊息
- 傳送應答訊息
- 鏈路發生異常
- 傳送使用者自定義事件
(5)新增ChannelHandler
這是Netty提供給使用者定製與擴充套件的關鍵介面,利用此可以完成大部分的功能定製。如:碼流日誌列印LoggingHandler、基於長度的半包解碼器LengthFiledBasedFrameDecoder...
(6)繫結並啟動監聽埠
將ServerSocketChannel註冊到Selector上監聽客戶端連線
(7)Selector輪詢
由Reactor執行緒NioEventLoop負責排程和執行Selector輪詢操作,選擇準備好就緒的Channel集合。
(8)排程執行ChannelHandler
當輪詢到準備就緒的Channel之後,就由Reactor執行緒NioEventLoop執行ChannelPipeline的相應方法,最終排程並執行ChannelHandler。
(9)執行網路事件ChannelHandler
執行使用者自定義的ChannelHandler或系統ChannelHandler,ChannelPipeline會根據事件型別,排程並執行ChannelHandler。
3.服務端原始碼分析
(1)建立NioEventLoopGroup執行緒組
首先通過建構函式建立ServerBootstrap例項,隨後建立兩個EventLoopGroup:
NioEventLoopGroup其實就是Reactor執行緒池,負責排程和執行客戶端接入、網路讀寫事件,使用者自定義任務和定時任務的執行,通過ServerBootstrap的group方法傳入
其中父NioEventLoopGroup被傳入父建構函式中
該方法主要是處理各種設定I/O執行緒、執行和排程網路事件的讀寫。
(2)建立NioServerSocketChannel
執行緒組設定完成後,需要建立NioServerSocketChannel。根據Channel的型別(channelClass)通過反射建立Channel例項(呼叫newInstance()方法)
(3)設定TCP引數
作為服務端主要是設定TCP backlog引數:
int listen(int sockfd, int backlog);
backlog指定了核心為此套介面排隊的最大連線個數。在服務端要接收多個客戶端發起的連線,因此必不可少要使用佇列來管理這些連線。其中在TCP三次握手中有兩個佇列,分別是半連線狀態佇列和全連線佇列。
- 半連線狀態佇列:每個客戶端發來的SYN報文,伺服器都會把這個報文放到佇列裡管理,這個佇列就是半連線佇列,即SYN佇列,此時伺服器埠處於SYN_RCVD狀態。之後伺服器會向客戶端傳送SYN+ACK報文。
- 全連線狀態佇列:當伺服器接收到客戶端的ACK報文後,就會將上述半連線佇列裡面對應的報文轉移(注:其實不是同一個結構,會新建一個結構掛到全連線佇列裡)到另一個佇列裡管理,這個佇列就是全連線佇列,即ACCEPT佇列,此時伺服器埠處於ESTABLISHED狀態。
放一張來自網路的圖:
backlog被規定為兩個佇列總和的最大值,Netty預設的目的backlog為200
(4)為啟動輔助類和其父類分別設定Handler
childHandler是NioServerSocketChannel對應ChannelPipeline的Handler;父類中的Handler是客戶端新接入的連線SocketChannel對應的ChannelPipeline的Handler
本質區別就是:ServerBootstrap中的Handler是NioServerSocketChannel使用的,所有連線該監聽埠的客戶端都會執行它;父類AbstractBootstrap中的Handler是個工廠類,會為每個新接入的客戶端建立一個新的Handler。
二、客戶端建立
1.客戶端程式碼示例
public void connect (String host, int port) throws Exception { // NIO 執行緒組 EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group) .channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { // 處理半包的ProtobufVarint32FrameDecoder一定要在解碼器前面 socketChannel.pipeline().addLast(new ProtobufVarint32FrameDecoder()); // 新增ProtobufDecoder解碼器,需要解碼的目標類是SubscribeResp socketChannel.pipeline().addLast( new ProtobufDecoder(SubscribeRespProto.SubscribeResp.getDefaultInstance())); socketChannel.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender()); socketChannel.pipeline().addLast(new ProtobufEncoder()); socketChannel.pipeline().addLast(new SubReqClientHandler()); } }); // 發起非同步連線操作 ChannelFuture f = bootstrap.connect(host, port).sync(); // 等待所有服務端監聽埠關閉 f.channel().closeFuture().sync(); } finally { // 優雅退出,釋放執行緒池資源 group.shutdownGracefully(); } }
負責處理網路讀寫、連線和客戶端請求接入的Reactor執行緒就是NioEventLoop。
2.客戶端時序圖
(1)建立Bootstrap例項
(2)建立客戶端連線,建立執行緒組NioEventLoopGroup(執行緒數預設為CPU核心數2倍)
(3)通過ChannelFactory工廠和指定的NioSocketChannel.class型別建立用於客戶端連線的NioSocketChannel;
(4)建立預設的ChannelHandlerPipeline,用於排程與執行網路事件;
(5)非同步發起TCP連線,判斷連線結果,如果成功則將NioSocketChannel註冊到Selector上並置selectionKey為OP_READ,監聽讀操作,如果沒有立即成功,則可能是服務端還沒有立刻返回ACK,所以此時將連線監聽位註冊到Selector上,同時selectionKey為OP_CONNECT,監聽連線,等待結果;
(6)註冊對應的監聽狀態位到Selector上;
(7)Selector輪詢各NioSocketChannel,處理連線結果;
(8)如果連線成功則傳送成功事件,觸發ChannelPipeline執行;
(9)由ChannelPipeline排程執行ChannelHandler(包括系統與使用者自定義),執行具體業務邏輯。
3.客戶端原始碼分析
(1)客戶端連線輔助類Bootstrap
Bootstrap是Netty提供的客戶端連線工具類,用於簡化客戶端的建立
1)設定I/O執行緒組:
客戶端相對於服務端,只需要一個處理I/O讀寫的執行緒組即可。由Bootstrap的group方法提供,主要設定EventLoopGroup:
2)設定TCP引數
建立客戶端套接字的時候通常都會設定連線引數:接收和傳送緩衝區大小、連線超時時間等。
主要的TCP引數如下:
3)指定Channel
對於TCP客戶端連線,預設使用NioSocketChannel,建立過程跟服務端是大同小異的。
4)發起客戶端連線
具體請看下面
(2)客戶端連線操作
1)建立初始化NioSocketChannel,主要邏輯是initAndRegister方法
2)註冊到Selector上,主要邏輯是register方法
3)鏈路成功後發起TCP連線
先獲取EventLoop執行緒組
然後進入doConnect()方法,呼叫NioSocketChannel非同步發起connection
Connect操作後有三種可能:
第一是連線成功
第二種是暫時沒連線上,服務端沒有返回ACK,結果暫時不確定,這時候需要將selectionKey設定為OP_CONNET,監聽連線結果。
第三種是連線失敗,直接丟擲異常
非同步連線成功以後,呼叫fulfillConnectPromise方法,觸發鏈路啟用事件,如果連線成功則觸發ChannelActive事件
此時ChannelActive事件的主要作用就是將selectionKey設定為OP_READ事件
(3)非同步連線結果通知
呼叫processSelectedKey方法,Selector輪詢客戶端連線Channel
當服務端返回握手應答以後,對連線結果進行判斷,主要呼叫finishConnect方法
進入finishConnect方法:
doFinishConnect方法主要判斷JDK的SocketChannel連線結果
連線成功後進入fullfillConnectPromise方法,呼叫fulfillConnectPromise方法,觸發鏈路啟用事件,如果連線成功則觸發ChannelActive事件:
(4)客戶端連線超時機制
JDK沒有提供連線超時機制,Netty利用定時器提供客戶端連線超時控制
在option方法中傳入TCP超時配置
一旦定時器執行超時,說明客戶端連線超時,這時候就構造超時異常,同時關閉客戶端連線,釋放控制程式碼
如果連線超時被設定,但是定時器執行的時候並沒有超時執行(在超時時間內完成),則此時connectedTimeoutFuture是不會為null的,根據此判斷是否在超時時間內完成,如果完成則取消,避免再次觸發定時器,實際上不管連線成功與否,只要獲取到連線結果,都會刪除定時器。
三、選擇Netty的好處
之所以選擇Netty程式設計,主要Netty的以下幾種優勢:
(1)API使用簡單,開發門檻低
(2)功能強大,預置了很多編解碼功能,支援多種主流協議
(3)定製能力強,可以通過ChannelHandler對通訊框架進行靈活擴充套件
(4)效能高
(5)成熟、穩定,修復了已知所有的JDK NIO BUG
(6)社群活躍
(7)經過了大規模的商業應用考驗
當然,這些是顯而易見的優勢,但是需要從原始碼中分析其優勢,比如Netty的零拷貝、基於記憶體池的ByteBuf、高效能的序列化框架等。