Netty网络编程第八卷

Netty网络编程第八卷,第1张

Netty网络编程第八卷

Netty网络编程第八卷

整体架构

ByteBufChannelEventLoop和EventLoopGroupChannelFutureChannelHandler和ChannelPipelineSimpleChannelInboundHandler关于入站和出站处理器是如何识别的问题BootStrap


本卷在于对ChannelHandler,ChannelHandlerContext,ChannelPipeline三大组件的详细讲解,力争做到从架构到源码的深入剖析


整体架构

Netty整体由以下几大组件组成:

  • ByteBuf
  • Channel
  • EventLoop和EventLoopGroup
  • ChannelFuture
  • ChannelHandler和ChannelPipeline
  • Bootstrap
    ByteBuf

    ByteBuf之前几卷中都进行过详细分析,这里只做简单的回顾

    从开始学习Java网络编程开始,不知道大家有没有发现API所规定的的数据传输最小单元就是字节,比如NIO中的IntBuffer,LongBuffer等等都是基于ByteBuffer而来的,因此Netty中对NIO中的ByteBuffer类进行了进一步的封装和优化。

    ByteBuf的构造如下图所示,它维护了两个指针,一个是读指针,一个是写指针,如果是数据读取 *** 作读指针会自动后移,如果是写 *** 作,写指针会自动后移。因此,这样就不用手动的进行flip() *** 作了,减少了 *** 作Buffer的复杂性。


    它的一些 *** 作方法一般都ByteBuf接口中有所规定的。

    ByteBuf的几种模式:

      堆缓冲区: 将数据存在JVM堆里的一个字节数组,在没有使用内存池的情况下可以提供快速的分配和释放,也被成为支撑数组(backing array)直接缓冲区:直接缓冲区模式可以避免不必要的中间内存拷贝,它的内存分配并不在JVM堆里,相对于堆来说它的分配和释放代价比较大,而且如果要对它里面的数据进行 *** 作的话还需要将里面的内容拷贝到一个数组里 *** 作。它比较适合大数据量的传输且不带有数据处理的一些 *** 作。复合缓冲区:它可以看做是多个ByteBuf聚合后的视图,可以根据需要进行ByteBuf实例的添加和删除(这个在JDK中的复合缓冲区是没有这个特性的),Netty中通过CompositeByteBuf(ByteBuf的子类)实现了这个模式,里面可以同时包含有直接缓冲区和非直接缓冲区。它的结构如下图所示,相当于是一个链表结构,它的内部也带有迭代器。但是它也有缺点,也就是它不支持直接访问数组,和直接缓冲区一样,需要先转化为数组,才能进行 *** 作。


    Channel

    在传统的BIO编程中,我们都会使用Socket进行端口绑定,连接等 *** 作,但是在NIO中我们使用的是SocketChannel(可以简单的理解为Socket+Channel),它也可以进行绑定,连接,读写等 *** 作,也可以完成Channel的关闭 *** 作,因此不难发现Channel的一些增强类提供了一些API让我们不需要直接去使用Socket,减小了开发的复杂性。

    下图为netty下的Channel接口里的方法,可以看到它直接调用Unsafe方法来完成Socket的功能

    如果对jdk源码有所了解的小伙伴,应该知道jdk底层也有一个Unsafe对象,用来直接和底层 *** 作系统打交道,分配内存的,但是jdk底层的Unsafe对象和这里不是一个对象,不要混淆

    进入io.netty.channel包下可以看到它有明显的包结构划分,这也就是常说的Netty给提供的一些数据传输方式

  • Embedded:不需要一个真正的基于网络的传输,但是可以使用ChannelHandler,一般用来测试ChannelHandler。
  • Epoll:它是完全非阻塞IO,比普通NIO传输要快,只能在支持Linux环境下应用
  • ChannelGroup:是一个线程安全的集合,里面包含一些开放的Channel并可以对这些Channel实施批量 *** 作,可以将一些满足某些条件的Channel放到一个Group,关闭Channel的时候自动会从set里移除,因此它可以用来进行群发(广播)
  • Local:可以在虚拟机内部通过管道进行本地传输通信
  • NIO:在java.nio.channels包基础下,使用选择器方式(Selector)
  • OIO:在java.net包的基础下,使用阻塞IO流的方式
  • ChannelPool:就是Channel池(通道连接池),实现连接复用,此实现对通道池中的通道使用后进先出顺序。
  • RXTX:可以实现Java与串口应用的通信
  • SCTP:流控制传输协议(SCTP,Stream Control Transmission Protocol)是一种在网络连接两端之间同时传输多个数据流的协议,所提供的服务类似于TCP和UDP
  • Socket:里面包含nio的一些Socket还有oio的一些Socket,比如NioServerSocketChannel,OioServerSocketChannel等等
  • UDT:UDP-based Data Transfer Protocol,简称UDT,UDT建于UDP之上,并引入新的拥塞控制和数据可靠性控制机制,因此是一个面向连接的。它同时支持可靠的数据流传输和部分可靠的数据报传输。
  • Unix:下面放的是一些Unix系统下支持传输方式
    EventLoop和EventLoopGroup

    用于处理连接生命周期内所发生的的事件,就不用手动注册和消除事件监听和处理逻辑的调用了。

    下图为EventLoop的UML图(后面进行解释)


    先说说Channel,EventLoop,EventLoopGroup之间的关系,如下图所示:

    可以对上图之间的包含关系做一个整理:

  • 一个EventLoopGroup包含一个或多个EventLoop
  • 一个EventLoop在它生命周期中只和一个Thread绑定
  • 所有有EventLoop处理的IO事件都将在它专有线程中被处理(因此就不会有线程同步问题)
  • 可以被分配给一个或多个Channel(regist方法),但一个Channel在它的生命周期中只能注册一个到EventLoop
    因此,在前文说到EventLoop很像是一个Selector。
  • 接下来就来看看EventLoop的类图,我借助书上的一张图(和上面的图是一样的,只不过书上的图将不同的类别进行了分块,易于阅读)


    可以从上图中很明显的看到,它的实现是借助了java.util.concurrent包下的线程池来完成的,线程池主要就是用来提供任务执行器,因此netty的EventLoopGroup是netty与jdk的协同设计。

    下面这句话是摘抄于书上(将的很透彻):

    在这个模型中,一个EventLoop 将由一个永远都不会改变的 Thread 驱动,可以将任务(Runnable 或者Callable)直接提交给EventLoop,以及执行或者调度执行。根据配置和可用核心的不 同,可能会创建多个EventLoop 实例用以优化资源的使用,并且单 个EventLoop 可能会被指派用于服务多个Channel 。


    ChannelFuture

    ChannelFuture可以看做是一个线程执行结果的占位符,因为它的执行不确定性因素十分大,谁也不能确定异步信息什么时候会得到结果。

    在前文也提到过,在ChannelFuture中扩展了J.U.C的Future,它可以使用addListener()注册一个ChannelFutureListener的一个监听器,以便于可以在某个 *** 作完成之后得到结果(无论是成功还是失败)。

    在同一个Channel的异步任务是可以保证它们的顺序调用执行


    ChannelHandler和ChannelPipeline

    ChannelHandler,它可以对出站入站的数据进行处理,相应的网络事件的出发也就伴随着相应的ChannelHandler的执行。

  • ChannelInboundHandler:对入站的数据处理
  • ChannelOutboundHandler:对出站的数据处理

    ChannelPipeline则是它的容器,在ChannelPipeline中包含有处理对应Channel的所有ChannelHandler,在这个ChannelPipeline中规定有数据的进站和出站处理的一些列ChannelHandler,如下图所示


    采用的是责任链模式,这种模式简化了手动的逻辑判断,比如在Tomcat中也是使用到了责任链模式


    下面详细剖析一下这些组件:

    首先先分析一下ChannelHandler,ChannelHandler是我们日常开发中使用最多的组件了,大概我们平时写的最多的组件就是Handler了,继承图如下


    我们平时继承的最多的就是ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter,这两个不是接口也不是抽象类,所以我们可以仅仅重写我们需要的方法,没有必须要实现的方法,当然我们也会使用SimpleChannelInboundHandler,这个类我们上个小节也稍微讲了它的优缺点,这里不赘述

    ChannelHandler,ChannelHandlerContext,ChannelPipeline这三者的关系很特别,相辅相成,

    一个ChannelPipeline中可以有多个ChannelHandler实例,而每一个ChannelHandler实例与ChannelPipeline之间的桥梁就是ChannelHandlerContext实例,如图所示:


    看图就知道,ChannelHandlerContext的重要性了,如果你获取到了ChannelHandlerContext的实例的话,你可以获取到你想要的一切,你可以根据ChannelHandlerContext执行ChannelHandler中的方法,我们举个例子来说,我们可以看下ChannelHandlerContext部分API:


    这几个API都是使用比较频繁的,都是调用当前handler之后同一类型的channel中的某个方法,这里的同一类型指的是同一个方向,比如inbound调用inbound,outbound调用outbound类型的channel,一般来说,都是一个channel的ChannnelActive方法中调用fireChannelActive来触发调用下一个handler中的ChannelActive方法

    ChannelHandlerContext负责包装一个ChannelHandler对象,然后pipeline使用双向链表的形式将这一个一个ChannelHandlerContext串联在一起,每当有一个客户端连接传入时,所有工作线程共享这一套pipeline工作流体系

    这里我们追踪一下源码:





    可以知道findContextOutbound返回的是拥有下一个channelHanlder的channelHanlderContext对象


    next.invokeChannelActive()这里当前的this对象已经发生了改变,这在下面条件不满足时,继续寻找链表上下一个ChannelHandlerContext的findContextInBound()方法中起到了作用


    不满足时会继续找下一个ContextHandlerContext.,然后执行其内部维护的ChannelHandler的channelActive方法


    上面以active事件为切入点进行了调用链分析,其他事件类似,大家可以参考

    分析了那么多,下面讲讲pipeline的作用体现


    我们下面看看是怎么放进去的




    剩余代码不做分析,大家可以自行去看源码

    目前来说这样做的好处:

    1)每一个handler只需要关注自己要处理的方法,如果你不关注channelActive方法时,你自定义的channelhandler就不需要重写channelActive方法

    2)异常处理,如果 exceptionCaught方法每个handler都重写了,只需有一个类捕捉到然后做处理就可以了,不需要每个handler都处理一遍

    3)灵活性。例如如下图所示:

    如图所示在业务逻辑处理中,也许左侧第一个ChannelHandler根本不需要管理某个业务逻辑,但是从第二个ChannelHandler就需要关注处理某个业务需求了,那么就可以很灵活地从第二个ChannelHandler开始处理业务,不需要从channel中的第一个ChannelHandler开始处理,这样会使代码显得让人看不懂~

    初步看懂的ChannelHandler,ChannelHandlerContext,ChannelPipeline之间的关系就是如上总结的

    这里纠正一下上面源码剖析的一个错误结论:每个客户端的socketChannel对象都会创建一个自己的piepline,并且互相拥有对方的引用,下面源码证明:

    每个客户端连接上来后,都会用NioSocktChannel包装原生的SocketChannel引用,下面源码论证:

    详细的accept事件源码流程,可以参考第五卷


    SimpleChannelInboundHandler

    为什么要单独把这个拿出来说一下,是因为这里有坑,很多人会踩到

    坑: 使用的channelRead0这个方法,结果服务器端就是不打印,服务器返回的结果,当时客户端是这样写的

    import io.netty.buffer.ByteBuf;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.SimpleChannelInboundHandler;
     
    public class baseClientHandler extends SimpleChannelInboundHandler{
        
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
            System.out.println("Client channelRead0 received:" + msg);
        }
        
    //    @Override
    //    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    //        System.out.println("Client channelRead received:" + msg);
    //        
    //    }
      
         @Override
         public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
             cause.printStackTrace();
             ctx.close();
         }
    }
    

    原因:SimpleChannelInboundHandler是继承于ChannelInboundHandlerAdapter,重写了channelRead方法

    @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            boolean release = true;
            try {
                if (acceptInboundMessage(msg)) {
                    @SuppressWarnings("unchecked")
                    I imsg = (I) msg;
                    channelRead0(ctx, imsg);
                } else {
                    release = false;
                    ctx.fireChannelRead(msg);
                }
            } finally {
                if (autoRelease && release) {
                    ReferenceCountUtil.release(msg);
                }
            }
        }
    

    SimpleChannelInboundHandler后面指定了处理类型,也就是源码中的"I",acceptInboundMessage方法判断msg是不是SimpleChannelInboundHandler中指定的类型,我们这边指定的是ByteBuf,感觉没啥问题啊,但是我们忽略了一个问题,我们客户端中有3个处理器,两个inbound类型的处理器,其中一个就是HelloWorldClientHandler,还有一个就是StringDecoder,上一个处理器已经把服务器端的信息转化成String,还用ByteBuf来接收,显然不能处理

    这里如果想处理,把类型换成String即可,下面我们来看看书上是怎么说的

    SimpleChannelInboundHandler的channelRead0还有一个好处就是你不用关心释放资源,因为源码中已经帮你释放了,所以如果你保存获取的信息的引用,是无效的~


    关于入站和出站处理器是如何识别的问题

    首先pipeline将所有入站和出站处理器串联在一起,并没有搞出两条链表进行区分,那么pipeline是如何识别入站和出站处理器的呢?


    下面分析:

    上面分析过active事件,这里是触发read事件的时候,会挨个调用每个handler的channelRead方法,但是别忘了这里有我们之前没讲的一个方法:


    分析完毕


    BootStrap

    BootStrap就是常常听到的引导,比如SpringBoot中的启动类上的注解带有BootStrap,因此可以知道BootStrap是一个程序的启动入口,也就是引导的意思。在Netty中引导可以分为两种:服务端引导和客户端引导

  • ServerBootStrap:作用于服务端,可以绑定到一个本地端口
  • BootStrap:作用于客户端,可以连接一个远程主机和端口

    也可以从对照下图

    BootStrap里只有一个EventLoopGroup,而ServerBootStrap中有两个EventLoopGroup,这是为什么呢?

  • 因为服务器需要对本地端口进行绑定,因此它会需要一个专用的ServerChannel来连接正在开放监听的端口(这是一个EventLoopGroup,里面只包含一个ServerChannel,因此相应的ServerChannel也就只使用到一个EventLoop)。
  • 它还要进行客户端的连接处理,因此第二组包含的是已创建的用来处理客户端连接的Channel。

    而客户端不需要本地端口绑定,因此只有连接的Channel,这样更进一步的应征了上一篇文章说得到的ChannelGroup,它可以将不同类型的Channel分为一组。

    服务端的两个EventLoopGroup工作流程如下

    总结: 在编写服务端的时候需要使用ServerBootStrap来绑定端口和配置一些其他信息,编写客户端是需要使用BootStrap指明发送链接的目的地和其他配置,一个Channel只能绑定一个EventLoop,一个EventLoop可以给多个Channel绑定,通过ChannelFuture来进行异步事件的通知,当事件被触发后,在ChannelPipeline中会有相应的ChannelHandler可以对当前数据进行处理。


    欢迎分享,转载请注明来源:内存溢出

    原文地址: http://outofmemory.cn/zaji/5717770.html

  • (0)
    打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
    上一篇 2022-12-18
    下一篇 2022-12-18

    发表评论

    登录后才能评论

    评论列表(0条)

    保存