深入學習Netty(4)——Netty編程入門
前言
從學習過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、高性能的序列化框架等。