Netty-2

  • 【Netty-2】Netty 核心的几个概念
    1. 一个EventLoopGroup当中包含一个或多个EventLoop
    2. 一个EventLoop在它的整个生命周期当中都只会与唯一一个Thread进行绑定
    3. 所有由EventLoop所处理的各种I/O事件都将在它所关联的那个Thread上进行处理
    4. 一个Channel在它的整个生命周期中只会注册在一个EventLoop上
    5. 一个EventLoop在运行过程当中,会被分配到多个Channel
    - 结论:
    1. 在Netty中,Channel的实现一定是线程安全的,基于此,我们可以存储一个Channel的引用,并且在需要向远端发送数据时,通过整个引用来调用Channel的相应方法;即便当时有很多线程都在使用它也不会出现线程问题;而且消息一定会按照顺序发送出去。
    2. 我们在业务开发中,不要将长时间执行的耗时任务放入到EventLoop的执行队列中,因为它将会一直阻塞该线程所对应的所有Channel上的其他执行任务,如果我们需要进行阻塞调用或是耗时的操作,那么我们就需要使用一个专门的EventExecutor(业务线程池)
    3. JDK所提供的Future只能通过手工的方式检查执行结果,而这个操作是会阻塞的;Netty则对ChannelFuture进行了增强,通过ChannelFutureListener以回调的方式获取结果,去除了手工检查的操作(观察者模式); 值得注意的是:ChannelFutureListener的operationComplete方法是由I/O线程执行的,因此要注意的是不要在这里执行耗时的操作,否则需要通过另外的线程或线程池来执行。
    4. 在Netty中有两种发送消息的方式,可以直接写到Channel中,也可以写到ChannelHandler所关联的那个ChannelHandlerContext中。对于前一种方式来说,消息会从ChannelPipline的末尾开始流动;对于后一种方式来说,消息将从ChannelPipline中的下一个ChannelHandler开始流动。
    5. ChannelHandlerContext与ChannelHandler之间的关联绑定关系是永远都不会发生改变的,因此对其进行缓存是没有任何问题的。
    6. 对于Channel的同名方法来说,ChannelHandlerContext的方法将会产生更短的事件流,所以我们应该在可能的情况下利用这个特性来提升应用的性能。
    7. 在实际的开发中我们经常会遇到一个服务端可能会去要调用另外一个客户端,这时这个服务端的角色就相当于即作为服务端也作为客户端。这时我们需要注意在我们作为客户端时我们应该将对服务端和客户端的channel绑定在同一个eventLoop上;
    - Netty 提供的三种缓冲区类型
    1. heap buffer 堆缓冲区
    - 优点:由于数据是存储在JVM的堆中,因此可以快速的创建于快速的释放,并且他提供了直接访问内部字节数组的方法
    - 缺点:每次的读写操作,都需要先将数据复制到直接缓冲区中在进行网络传输
    2. direct buffer 直接缓冲区(在堆之外直接分配内存空间,直接缓冲区不会占用堆的容量空间,因为他是由操作系统在本地内存进行的数据分配)
    - 优点:在使用Sockte进行数据传递时,性能非常好,因为数据直接位于操作系统的本地内存中,所以不需要从JVM将数据复制到直接缓冲区,性能很好。
    - 缺点:因为Direct Buffer是直接在操作系统内存中,所以内存空间的分配与释放要比堆空间更加复杂,而且速度慢一些。(Netty通过提供内存池来解决这个问题)
    - 注意:直接缓冲区不支持通过字节数组的方式来直接访问数据(对于后端的业务消息的编解码来说,推荐使用HeapByteBuf;对于I/O通信线程在读写缓冲区时,推荐使用DirectByteBuf)
    3. composite buffer 复合缓冲区
    - JDK的ByteBuffer和Netty的ByteBuf的差异比对
    1. Netty的ByteBuf采用读写索引分离的策略(readerIndex与writerIndex),一个初始化(里面尚未有任何数据)的ByteBuf的readerIndex与witerIndex值都为0
    2. 当读索引和写索引处于同一个位置时,如果我们继续读取,那么就会抛出IndexOutofBoundsException
    3. 对于ByteBuf的任何读写操作都会分别单独维护读索引与写索引。maxCapacity最大的容量默认是 Integer.MAX_VALUE。
    - JDK的ByteBuffer的缺点 1. final byte[] hb; 这是JDK的ByteBuffer对象中用于存储数据的对象声明;可以看到,其字节数组是被声明为final的,也就是长度是固定不变的。一旦分配好就不能动态扩容与收缩。而且当待存储的数据字节很大时就很有可能出现 IndexOutofBoundsException 。如果需要预防这个异常,那就需要在存储之前完全确定好待存储的字节大小。如果ByteBuffer的空间不足,我们只有一种解决方案;创建一个全新的ByteBuffer对象,然后将之间的ByteBuffer中的数据复制过去,这一切的操作都需要开发者自己手动完成。 2. ByteBuffer只使用一个position指针来标识位置信息,在进行读写切换时就需要调用flip方法或是rewind方法,使用起来不方便。- Netty的ByteBuf的优点 1. 存储的字节数组是动态的,其最大值的默认是Integer.MAX_VALUE。这里的动态性体现在write方法中的。write方法在执行时会判断buffer的容量,如果不足则自动扩容。 2. ByteBuf的读写索引是完全分开的,使用起来就很方便。- 引用计数 1. netty采用自旋锁的方式来进行操作(io.netty.util.AbstractReferenceCounted) ```java // 更新特定的字段 private static final AtomicIntegerFieldUpdater refCntUpdater = AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCounted.class, "refCnt"); private volatile int refCnt = 1; ...private ReferenceCounted retain0(int increment) { for (; ; ) { int refCnt = this.refCnt; final int nextCnt = refCnt + increment; // Ensure we not resurrect (which means the refCnt was 0) and also that we encountered an overflow. // 这里的判断有两个意义 // nextCnt==increment 表示refCnt=0 这在netty中对于引用计数的定义是:如果一个对象的引用计数为0则这个引用对象不能使用 // nextCnt < increment 成立则表示 nextCnt是负数 说明已经超过最大的整数值了 if (nextCnt <= increment) { throw new IllegalReferenceCountException(refCnt, increment); } if (refCntUpdater.compareAndSet(this, refCnt, nextCnt)) { break; } } return this; }```

    1. AtomicIntegerFieldUpdater 要点总结
      1. 更新器更新的必须是int类型变量,不能是其包装类型
      2. 更新器更新的必须是 volatile类型变量,确保线程之间共享变量时的立即可见性。
      3. 变量不能是static的,必须是实例变量。因为Unsafe.objectFiledOffset()方法不支持静态变量(CAS操作本质上是通过对象实例的偏移量来直接进行赋值)
      4. 更新器只能修改它可见范围的变量,因为更新器是通过反射来得到这个变量,如果变量不可见就会报错。
      5. 如果需要更新的变量时包装类型或者是其他类型,可以使用 AtomicReferenceFieldUpdater来进行更新
      3. Netty内存泄漏检测
      4. Netty的处理器和编解码器
      1. Netty的处理器分为两类 入站处理器、出站处理器
      2. 入站处理器的顶层是 ChannelInboundHandler,出站处理器的顶层是ChannelOutboundHandler
      3. 数据处理时常用的各种编解码器本质上都是处理器。
      4. 编解码器:编码器,解码器。无论我们想网络中写入的数据是什么类型(int,char,String,二进制等),数据在网络中传递时,其都是以字节流的形式呈现的; 将数据由原来的形式转换为字节流的操作称为编码(encode),将数据由字节转换成它原本的格式或是其他格式的操作称为解码(decode),编解码统一称为codec。
      5. 编码:本质上是一种出站处理器。因此,编码一定是 ChannelOutboundHandler。
      6. 解码:本质上是一种入站处理器,因此解码一定是 ChannelInboundHandler。
      7. 在Netty中,编码器通常以 XXXEncoder命名,解码器通常以XXXDecoder命名。
      8. TCP粘包与拆包
      5. 结论:
      1. 无论是编码器还是解码器,其所接收的消息类型必须要与待处理的参数类型一致,否则该编码器或解码器并不会被执行。
      2. 在解码器进行数据解码时,一定要记得判断缓冲(ByteBuf)中的数据是否足够,否则将会产生一些问题。

    推荐阅读