深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

前言从学习过BIO、NIO、AIO编程之后,就能很清楚Netty编程的优势,为什么选择Netty,而不是传统的NIO编程 。本片博文是Netty的一个入门级别的教程,同时结合时序图与源码分析,以便对Netty编程有更深的理解 。
在此博文前,可以先学习了解前几篇博文:

  • 深入学习Netty(1)——传统BIO编程
  • 深入学习Netty(2)——传统NIO编程
  • 深入学习Netty(3)——传统AIO编程
参考资料《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@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {// 半包处理 ProtobufVarint32FrameDecodersocketChannel.pipeline().addLast(new ProtobufVarint32FrameDecoder());// 添加ProtobufDecoder解码器,需要解码的目标类是SubscribeReqsocketChannel.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.服务端时序图
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
(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类型
 
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
通过反射创建NioServerSocketChannel对象
  
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
通过调用无参默认的构造方法生成channel
 
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
(4)创建并初始化ChannelPipeline
本质上是一个负责处理网络事件的职责链,负责管理与执行ChannelHandler 。ChannelPipeline为ChannelHandler链提供了容器,并定义了用于在该链上传播入站和出站事件流的API 。当Channel被创建时,他会自动的分配到它专属的ChannelPipeline 。典型的网络事件包括:
  • 链路注册
  • 链路激活
  • 链路断开
  • 接收到请求消息
  • 处理请求消息
  • 发送应答消息
  • 链路发生异常
  • 发送用户自定义事件
(5)添加ChannelHandler
这是Netty提供给用户定制与扩展的关键接口,利用此可以完成大部分的功能定制 。如:码流日志打印LoggingHandler、基于长度的半包解码器LengthFiledBasedFrameDecoder...
(6)绑定并启动监听端口
将ServerSocketChannel注册到Selector上监听客户端连接
 
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
 
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
(7)Selector轮询
由Reactor线程NioEventLoop负责调度和执行Selector轮询操作,选择准备好就绪的Channel集合 。
(8)调度执行ChannelHandler
当轮询到准备就绪的Channel之后,就由Reactor线程NioEventLoop执行ChannelPipeline的相应方法,最终调度并执行ChannelHandler 。
 
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
(9)执行网络事件ChannelHandler
执行用户自定义的ChannelHandler或系统ChannelHandler,ChannelPipeline会根据事件类型,调度并执行ChannelHandler 。
    
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
3.服务端源码分析(1)创建NioEventLoopGroup线程组
 首先通过构造函数创建ServerBootstrap实例,随后创建两个EventLoopGroup:
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
NioEventLoopGroup其实就是Reactor线程池,负责调度和执行客户端接入、网络读写事件,用户自定义任务和定时任务的执行,通过ServerBootstrap的group方法传入
 
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
 其中父NioEventLoopGroup被传入父构造函数中
 
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
 该方法主要是处理各种设置I/O线程、执行和调度网络事件的读写 。
(2)创建NioServerSocketChannel
【深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门】线程组设置完成后,需要创建NioServerSocketChannel 。根据Channel的类型(channelClass)通过反射创建Channel实例(调用newInstance()方法)
  
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
      
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
    
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
 
(3)设置TCP参数
 作为服务端主要是设置TCP backlog参数:
 int listen(int sockfd, int backlog);
  
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
      
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
  backlog指定了内核为此套接口排队的最大连接个数 。在服务端要接收多个客户端发起的连接,因此必不可少要使用队列来管理这些连接 。其中在TCP三次握手中有两个队列,分别是半连接状态队列和全连接队列 。
  • 半连接状态队列:每个客户端发来的SYN报文,服务器都会把这个报文放到队列里管理,这个队列就是半连接队列,即SYN队列,此时服务器端口处于SYN_RCVD状态 。之后服务器会向客户端发送SYN+ACK报文 。
  • 全连接状态队列:当服务器接收到客户端的ACK报文后,就会将上述半连接队列里面对应的报文转移(注:其实不是同一个结构,会新建一个结构挂到全连接队列里)到另一个队列里管理,这个队列就是全连接队列,即ACCEPT队列,此时服务器端口处于ESTABLISHED状态 。
 放一张来自网络的图:
    
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
 backlog被规定为两个队列总和的最大值,Netty默认的目的backlog为200
(4)为启动辅助类和其父类分别设置Handler
childHandler是NioServerSocketChannel对应ChannelPipeline的Handler;父类中的Handler是客户端新接入的连接SocketChannel对应的ChannelPipeline的Handler
 
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
本质区别就是: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>() {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {// 处理半包的ProtobufVarint32FrameDecoder一定要在解码器前面socketChannel.pipeline().addLast(new ProtobufVarint32FrameDecoder());// 添加ProtobufDecoder解码器,需要解码的目标类是SubscribeRespsocketChannel.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 。
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
2.客户端时序图
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
(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:
 
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
         
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
 
2)设置TCP参数
创建客户端套接字的时候通常都会设置连接参数:接收和发送缓冲区大小、连接超时时间等 。
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
主要的TCP参数如下:
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
 
3)指定Channel
对于TCP客户端连接,默认使用NioSocketChannel,创建过程跟服务端是大同小异的 。
4)发起客户端连接
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
具体请看下面
(2)客户端连接操作
1)创建初始化NioSocketChannel,主要逻辑是initAndRegister方法
 
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
 
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图


2)注册到Selector上,主要逻辑是register方法
 
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
        
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
3)链路成功后发起TCP连接
先获取EventLoop线程组
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
然后进入doConnect()方法,调用NioSocketChannel异步发起connection
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
Connect操作后有三种可能:
第一是连接成功
第二种是暂时没连接上,服务端没有返回ACK,结果暂时不确定,这时候需要将selectionKey设置为OP_CONNET,监听连接结果 。
 
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
第三种是连接失败,直接抛出异常
 
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
异步连接成功以后,调用fulfillConnectPromise方法,触发链路激活事件,如果连接成功则触发ChannelActive事件
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
此时ChannelActive事件的主要作用就是将selectionKey设置为OP_READ事件
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
 
(3)异步连接结果通知
调用processSelectedKey方法,Selector轮询客户端连接Channel
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
当服务端返回握手应答以后,对连接结果进行判断,主要调用finishConnect方法
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
进入finishConnect方法:
 
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
doFinishConnect方法主要判断JDK的SocketChannel连接结果
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
连接成功后进入fullfillConnectPromise方法,调用fulfillConnectPromise方法,触发链路激活事件,如果连接成功则触发ChannelActive事件:
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
(4)客户端连接超时机制
JDK没有提供连接超时机制,Netty利用定时器提供客户端连接超时控制
在option方法中传入TCP超时配置
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
 
一旦定时器执行超时,说明客户端连接超时,这时候就构造超时异常,同时关闭客户端连接,释放句柄
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
如果连接超时被设置,但是定时器执行的时候并没有超时执行(在超时时间内完成),则此时connectedTimeoutFuture是不会为null的,根据此判断是否在超时时间内完成,如果完成则取消,避免再次触发定时器,实际上不管连接成功与否,只要获取到连接结果,都会删除定时器 。
  
深入学习习总书记系列讲话精神 4 深入学习Netty——Netty编程入门

文章插图
三、选择Netty的好处之所以选择Netty编程,主要Netty的以下几种优势:
(1)API使用简单,开发门槛低
(2)功能强大,预置了很多编解码功能,支持多种主流协议
(3)定制能力强,可以通过ChannelHandler对通信框架进行灵活扩展
(4)性能高
(5)成熟、稳定,修复了已知所有的JDK NIO BUG
(6)社区活跃
(7)经过了大规模的商业应用考验
当然,这些是显而易见的优势,但是需要从源码中分析其优势,比如Netty的零拷贝、基于内存池的ByteBuf、高性能的序列化框架等 。