前言
從學習過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、高性能的序列化框架等。