网络编程16

netty的基本组件

  • EventLoopGroup
  • ServerBootstrap
  • NioServerSocketChannel
  • ChannelFuture
  • 各种ChannelHandler
  • ChannelPipeline
  • ChannelHandlerContext
  • byteBuf

ChannelPipeline

  • 本质是职责链模式的变形

    • 类似于servlet,servlet专门用来拼凑页面来相应的,中间会有各种filter,完全不影响servlet功能的实现
  • 把channel的管道抽象成ChannelPipeline,消息在channelPipeline上传递,传递给handler进行处理,并且在channelPipeline可以很方便的进行handler的增加和删除

  • 在channelPipeline分为两种事件,出站和入站

    • 入站:从网络到应用程序
    • 出站:从应用程序到网络,不仅是数据,还有操作的通知,比如入站事件有channelRead,但是在出站事件还有一个read,意思为读取更多数据,也是一个出站操作,不是read就一定是入站,出站事件还有绑定、发起连接、重连关闭等
  • ChannelPipeline是netty自动创建的,有一个channel连接,就会有一个独立的channelPipeline,并不是全服务器只有一个

  • channelPipeline是线程安全的,往pipeline加handler,或者删除handler都是线程安全的,做了并发控制,但是对于每一个handler自己本身,如果出现跨线程访问时,是不能保证线程安全的,这种情况发生在handler打上了sharable注解

具体的方法

  • 很多都是管理handler的方法
  • 让事件和数据在pipeline上传递和流动的相关方法
  • pipeline是一个借口,channelPipeline定义了各种行为

DefaultChannelPipeline

  • channelPipeline的缺省实现,需要对所有的定义方法提供实现

  • handler加入pipile的过程,就是链表的增删过程,而pipile的真正节点是ctx

  • addFirst

    • 链表操作
  • 所有的add和remove操作完成后都会执行callHandlerAdded0方法

    • callHandlerAdded0

      1.ctx.handler().handlerAdded(ctx);

      是一个空实现,由子类去覆盖这个方法

  • 所有以fire开头的方法,都是跟入站相关的方法,包括链路的建立、从链路上读取相关数据等等,都会产生一个事件,这个事件会在pipile中处理,入站事件是有限的,所以netty就对这些事件做了抽象

  • 包括像read、发起绑定、write这些方法是出站方法,在pipeline中有实现,但是注意pipeline本身是不做io操作的,只是handler的容器,这些出站操作最终交给channel或者channel相关的unsafe类做处理

    • 例如write方法

      @Override
      public final ChannelFuture write(Object msg, ChannelPromise promise) {
              
              
          return tail.write(msg, promise);
      }
      

      1.tail是AbstractChannelHandlerContext类

      2.在DefaultChannelPipeline构造方法中,发现head和tail构成了一个双向链表

      3.tail总是最后一个,所以channel.write是从尾巴上面开始写的,出站也就会流经所有的handler

ChannelHandlerContext

  • 代表了handler和pipeline之间的关系

具体方法

  • 和那些channel、pipeline进行了挂钩
    • channel()
    • pipeline()
  • 支持哪些事件的传播
    • fire开头的方法

AbstractChannelHandlerContext

  • ChannelHandlerContext接口的抽象类

  • 与网络传播的各种方法调用,比如说context、fire、invoke

    • 例如fireChannelRead方法

    • @Override
      public ChannelHandlerContext fireChannelRead(final Object msg) {
              
              
          invokeChannelRead(findContextInbound(), msg);
          return this;
      }
      
    • static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
              
              
          final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
          EventExecutor executor = next.executor();
          if (executor.inEventLoop()) {
              
              
              next.invokeChannelRead(m);
          } else {
              
              
              executor.execute(new Runnable() {
              
              
                  @Override
                  public void run() {
              
              
                      next.invokeChannelRead(m);
                  }
              });
          }
      }
      

      1.首先取出下一个handler

      2.交给下一个handler执行

      3.下面代码块判断当前执行invokeChannelRead方法的是不是当前线程

      if (executor.inEventLoop()) {
              
              
          next.invokeChannelRead(m);
      } else {
              
              
          executor.execute(new Runnable() {
              
              
              @Override
              public void run() {
              
              
                  next.invokeChannelRead(m);
              }
          });
      }
      

      因为eventloop和一个channel挂钩的,一个channel生命周期内所有事情都由这个eventloop进行负责,如果出现跨线程调用,netty会进行判断,是不是当前channel的同一个线程,是就直接执行,不是就投放到一个队列中去执行,投放到哪个队列?投放到当前channel所属线程里面去异步执行

  • private AbstractChannelHandlerContext findContextInbound() {
          
          
        AbstractChannelHandlerContext ctx = this;
        do {
          
          
            ctx = ctx.next;
        } while (!ctx.inbound);
        return ctx;
    }
    
    private AbstractChannelHandlerContext findContextOutbound() {
          
          
        AbstractChannelHandlerContext ctx = this;
        do {
          
          
            ctx = ctx.prev;
        } while (!ctx.outbound);
        return ctx;
    }
    

    怎么去找下一个入站或者怎么去栈前一个出站?

    循环遍历,直到找到为止。所以入站和出站之间的相对顺序不能乱,但是入站和出站的顺序可以乱,这是因为netty对handler进行了入站和出站的标记

HeadContext

  • 既是ctx,又是handler

  • 同时,既可以处理入站,又可以处理出站

  • 入站基本没有处理,只是进行简单的触发并往后面传递,因为对于入站的头一个,根本不知道怎么处理网络数据,只需要往后面传递即可

    • channelInactive
    • channelRead
    • channelActive
    • channelRegistered
  • 出站,也没有做处理,而是交给了unsafe类去做的处理,因为是出站的最后一个,直接往网络上发送即可,真正读写网络的是靠unsafe类,这是netty中的unsafe,不是jdk中的unsafe

    • bind
    • connect
    • disconnect
    • close
    • deregister
    • read
    • write
    • flush

TailContext

  • 本身是一个ctx,同时只是一个入站的最后一个handler

  • 大部分方法都是空实现

  • 有代码实现的,基本也是什么都不做,执行了onUnhandledInboundMessage方法

    • 而这个方法执行了ReferenceCountUtil.release(msg),做了资源的释放,避免造成内存泄漏

DefaultChannelHandlerContext

  • 大部分方法都已经由AbstractChannelHandlerContext实现了,所以这个方法很简单

ChannelHandler

  • 负责对io事件做拦截和处理

  • 在handler有选择的进行拦截和处理,也可以透传

  • 支持注解的

    • sharable注解,在多个channelPipeline之间共享
    • skip注解,跳过这个方法的注解

ChannelHandler接口的继承关系

  • ChannelInboundHandler接口

  • ChannelOutboundHandler接口

  • ChannelHandlerAdapter抽象类

  • ChannelInboundHandlerAdapter实体类

  • ChannelOutboundHandlerAdapter实体类

  • 为什么在接口之外还要提供一个adapter类?

    • 不用全量实现

    • 一些平庸实现,比如ChannelInboundHandlerAdapter的channelRegistered和channelUnregistered等等很多方法,实现都是透传(fire),当需要实现额外功能时,只需要重写这个类即可,不需要增加新的方法

    • ChannelOutboundHandlerAdapter不是透传,而是直接调用ctx下面的方法

    • ChannelHandlerAdapter也是,要么是空实现,要么是透传

ChannelHandler子类

  • 系统级handler
    • HeaderHandler
    • TelnetHandler
  • 编解码类handler ,ByteToByte,ByteToMessage,MessageToByte,…
  • 功能性handler,包括流量整型 Handler、读写超时 Handler、日志 Handler等
  • 协议编解码handler,ssl、http、websocket协议相关的

ByteToMessageDecoder

  • decode方法是一个抽象方法,需要自行实现

  • 在decode里面,把ByteBuf解码,可以是javaBean,转换完成后,继续在handler上传递

  • 因为是解码,一定是入站,所以关注的是channelRead方法

    • 为什么只要实现了decode,就能进行解码了?

    • channelRead方法

      1.首先判断读进来的是不是ByteBuf,如果是进行一系列处理,不是则透传

      2.如果是,创建一个CodeOutputList,往后面handler进行传递

      3.TCP协议天生就有粘包半包问题,所以引入了一个cumulation的变量,用来保存上次传输没有传输完的数据,而且专门有一个内部接口Cumulator,专门来进行合并

      4.callDecode进行实际的解码,放在一个while里面进行解码,因为一个tcp报文里面包含多个应用程序报文,所以在while循环里面调用decodeRemovalReentryProtection方法,而这个方法里面又调用我们定义的decode方法,凡是解出来的都放在CodeOutputList中

      5.只要list里面解码出来的size大于0,就往后面传递

    • 注意,这里不是直接处理粘包半包问题,解决粘包半包的一种方法FixedLengthFrameDecoder就是派生于这个类,粘包半包处理的handler总是第一个

    • 为什么要三次握手,三次握手会传递序列号,告诉对端下次传递是多少,如果不是就扔掉了,然后操作系统底层会对所有的包进行组包

MessageToByteEncoder

  • 派生自outbound,我们要实现encode这个抽象方法

  • 最关键的是write方法

    • 1.首先进行收到的msg的类型进行判定,是不是定义的message的类型,这个判定比较复杂,用了jdk里面的反射(match方法)

    • 2.进行强制转型

    • 3.交给定义好的encode方法处理

    • 4.如果编码成功,buf里面写入了数据,交给ctx继续往外面写

    • 5.如果失败,把buf释放掉,把一个null的buf往对端写

    • 编码完成后,为什么要对cast进行内存释放?

      把Message理解成javaBean,Byte理解成ByteBuf,转成ByteBuf后,应该把Message释放掉,因为已经没用了

MessageToMessageEncoder

  • 编码完成后,也要对cast进行内存释放,因为转换前的javaBean已经没用了,需要释放掉
  • 如果抛出IllegalReferenceCountException: refcnt:0, increment: 1,就要考虑这个buffer是不是已经被释放掉了
    • 要么别用这种Encode了,直接用OutBoundAdapter那个实体类

Future和Promise源码分析

  • 在写netty的应用程序时,所以的操作都是异步的,如果要获取结果,就是通过future来获取结果

juc并发包下的Future

  • get方法
  • cancel方法

netty下的future接口,继承于juc下的future

  • 除了获取结果,还加入了等待、同步、同步转异步

  • 实际过程中也是使用它的子类ChannelFuture,是io操作相关的

ChannelFuture

  • 支持增加监听器,当io操作完成后,会调用这个监听器里面的方法,可以通过监听器获取io操作的结果

  • 会有两种状态

    • 要么是完成,要么是未完成
    • 注意操作完成不代表成功,完成有三种,操作成功、操作失败、操作取消
  • 为什么不直接通过get方法去拿,而是通过监听器去拿?

    • get方法会阻塞,netty里面都是异步操作,完成时间是无法预测的,如果不设置超时时间,当前线程就会在get方法上阻塞,可能会长时间挂死,netty异步操作里面线程是很忙的,一个io线程要处理很多channel,阻塞在一个channel上了,其他的channel就没法处理的,虽然也可以使用超时,但是不知道合适的超时时间,通过异步回调机制,只要有结果来了,netty会在调用注册的监听器,执行操作完成的相关方法,通过里面的future去拿结果

    • 后者性能更优,前者有很大的性能损失

AbstractFuture

  • 实现了两个get方法

    • 第一个get

      1.调用await方法进行阻塞

      2.当io操作完成,会调用notify,唤醒阻塞的线程

      3.对获得的结果进行相应的判定

    • 第二个超时实现的get

      await方法中带入了超时时长

future总结

  • 对操作结果的获取,但是有个问题,future是获取结果,不管是哪一级的future,都没有写结果的地方,而Promise是一个可写的future,设置io操作的结果,也基础于Future

Promise

  • setSuccess
  • setFailure

DefaultPromise

  • 主要的实现类

  • 主要的工作是进行各种设置,比如当操作成功了,setSuccess方法,该方法首先设置结果,然后再唤醒相关的线程

    • setSuccess0-----设置结果的方法

      1.setValue0用了cas操作来保证线程安全

      2.checkNotifyWaiters来唤醒所有等待的线程

      3.notifyliteners来唤醒监听器

      执行监听器的operationComplete通过future来设置回调结果

  • channel中的await方法、sync方法最终都是调用的Object.wait方法

  • b.bind方法,主线程进入一种wait的状态,当有绑定完成的时候,会调用相关的notify方法把相关线程唤醒,如果不加sync()方法,很快就会跑完,不能保证主线程执行完,绑定就完成了

  • 用netty实现的rpc中(rpc-netty-server)RpcServerFrame,其中的bind方法,其中主线程不会很快跑完退出应用程序?

猜你喜欢

转载自blog.csdn.net/Markland_l/article/details/114560559