深刻学习Netty(4)——Netty编程入门

2021年09月15日 阅读数:3
这篇文章主要向大家介绍深刻学习Netty(4)——Netty编程入门,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

前言

  从学习过BIO、NIO、AIO编程以后,就能很清楚Netty编程的优点,为何选择Netty,而不是传统的NIO编程。本片博文是Netty的一个入门级别的教程,同时结合时序图与源码分析,以便对Netty编程有更深的理解。html

  在此博文前,能够先学习了解前几篇博文:git

  参考资料《Netty In Action》、《Netty权威指南》(有须要的小伙伴能够评论或者私信我)github

  博文中全部的代码都已上传到Github,欢迎Star、Fork编程

 

1、服务端建立

Netty屏蔽了NIO通讯的底层细节,减小了开发成本,下降了难度。ServerBootstrap能够方便地建立Netty的服务端bootstrap

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默认的目的backlog200

 

(4)为启动辅助类和其父类分别设置Handler

  childHandler是NioServerSocketChannel对应ChannelPipeline的Handler;父类中的Handler是客户端新接入的链接SocketChannel对应的ChannelPipeline的Handler

 

  本质区别就是:ServerBootstrap中的Handler是NioServerSocketChannel使用的,全部链接该监听端口的客户端都会执行它;父类AbstractBootstrap中的Handler是个工厂类,会为每一个新接入的客户端建立一个新的Handler

2、客户端建立

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上并置selectionKeyOP_READ,监听读操做,若是没有当即成功,则多是服务端尚未马上返回ACK,因此此时将链接监听位注册到Selector上,同时selectionKeyOP_CONNECT,监听链接,等待结果

(6)注册对应的监听状态位到Selector

(7)Selector轮询各NioSocketChannel,处理链接结果

(8)若是链接成功则发送成功事件,触发ChannelPipeline执行

(9)ChannelPipeline调度执行ChannelHandler(包括系统与用户自定义),执行具体业务逻辑

3.客户端源码分析

(1)客户端链接辅助类Bootstrap

BootstrapNetty提供的客户端链接工具类,用于简化客户端的建立

1)设置I/O线程组:

客户端相对于服务端,只须要一个处理I/O读写的线程组便可。由Bootstrapgroup方法提供,主要设置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方法主要判断JDKSocketChannel链接结果

链接成功后进入fullfillConnectPromise方法,调用fulfillConnectPromise方法,触发链路激活事件,若是链接成功则触发ChannelActive事件:

(4)客户端链接超时机制

JDK没有提供链接超时机制,Netty利用定时器提供客户端链接超时控制

option方法中传入TCP超时配置

 

一旦定时器执行超时,说明客户端链接超时,这时候就构造超时异常,同时关闭客户端链接,释放句柄

  若是链接超时被设置,可是定时器执行的时候并无超时执行(在超时时间内完成),则此时connectedTimeoutFuture是不会为null的,根据此判断是否在超时时间内完成,若是完成则取消,避免再次触发定时器,实际上无论链接成功与否,只要获取到链接结果,都会删除定时器

  

3、选择Netty的好处

之因此选择Netty编程,主要Netty的如下几种优点:

(1)API使用简单,开发门槛低

(2)功能强大,预置了不少编解码功能,支持多种主流协议

(3)定制能力强,能够经过ChannelHandler对通讯框架进行灵活扩展

(4)性能高

(5)成熟、稳定,修复了已知全部的JDK NIO BUG

(6)社区活跃

(7)通过了大规模的商业应用考验

固然,这些是显而易见的优点,可是须要从源码中分析其优点,好比Netty的零拷贝、基于内存池的ByteBuf、高性能的序列化框架等。