Netty

Netty

Netty

  1. 一个EventLoopGroup当中会包含一个或多个EventLoop。
  2. 一个EventLoop在它的整个生命周期当中都只会与唯一一个Thread进行绑定。
  3. 所有由EventLoop所处理的各种I/O事件都将在它所关联的那个Thread上进行处理。
  4. 一个Channel在它的整个生命周期中只会注册在一个EventLoop上。
  5. 一个EventLoop在运行过程当中,会被分配给一个或者多个Channel。

重要结论:在Netty中,Channel的实现一定是线程安全的;基于此,我们可以存储一个Channel的引用,并且在需要向远程端点发送数据时,通过这个引用来调用Channel相应的方法;即便当时有很多线程都在使用它也不会出现多线程问题;而且,消息一定会按照顺序发送出去。

重要结论:我们在业务开发中,不要将长时间执行的耗时任务放入到EventLoop的执行队列中,因为它将会一直阻塞该线程所对应的所有Channle上的其他执行任务,如果我们需要进行阻塞调用或是耗时的操作(实际开发中很常见),那么我们就需要使用一个专门的EventExecutor(业务线程池)。

通常会有两种实现方式:

  1. 在ChannelHandler的回调方法中,使用自己定义的业务线程池,这样就可以实现异步调用。
  2. 借助于Netty提供的向ChannelPipeLine添加ChannelHandler时调用的addLast方法来传递EventExecutor。

说明:默认情况下(调用addLast(handler)),ChannelHandler中的回调方法都是由I/O线程所执行,如果调用了ChannelPipeline addLast(EventExecutorGroup group, ChannelHandler… handlers);方法,那么),ChannelHandler中的回调方法都是由参数中的group线程组来执行的。

JDK所提供的Future只能通过手工方式检查执行结果,而这个操作是会阻塞的;Netty则对ChannelFuture进行了增强,通过ChannelFutureListener以回调的方式来获取执行结果,去除了手工检查阻塞的操作;值得注意的是:ChannelFutureListener的operationComplete方法是有I/O线程执行的,因此要注意的是不要在这里执行耗时操作,否则需要通过另外的线程或者线程池来执行。

在Netty中有两种发送消息的方式,可以直接写到Channel中,也可以写到与ChannelHandler所关联的那个ChannelHandlerContext中。对于前一种方式来说,消息会从ChannelPipeline的末尾开始流动;对于后一种方式来说,消息从ChannelPipeline中的下一个ChannelHandler开始流动。

结论:
1.ChannelHandlerContext与ChannelHandler之间的关联绑定关系是永远都不会发生改变的,因此对其进行缓存是没有任何问题的。
2.对于与Channel的同名方法来说,ChannelHandlerContext的方法将会产生更短的事件流,所以我们应该在可能的情况下利用这个特性来提升应用性能。

使用NIO进行文件读取所涉及的步骤:

  1. 从FileInputStream对象获取到Channel对象。
  2. 创建Buffer。
  3. 将数据从Channel中读取到Buffer对象中。

0 <= mark <= position <= limit <= capacity

flip()方法

  1. 将limit值设置为当前的position。
  2. 将position设为0。

clear()方法。

  1. 将limit值设为capacity。
  2. 将position值设为0。

compact()方法

  1. 将所有未读的数据复制到buffer起始位置处。
  2. 将position设为最后一个未读元素后面。
  3. 将limit设为capacity。
  4. 现在buffer就准备好了,但是不会覆盖未读的数据。

注意:通过索引来访问Byte时并不会改变真实的读索引与写索引;我们可以通过ByteBuf的readerIndex()与writeIndex()方法分别直接修改读索引与写索引。

Netty ByteBuf所提供的3种缓冲区类型:

  1. heap buffer
  2. drect buffer
  3. composite buffer

Heap Buffer(堆缓冲区)

这是最常用的类型,ByteBuf将数据存储到JVM的堆空间中,并且将实际的数据存放到byte array中来实现。

优点:由于数据是存储在JVM的堆中,因此可以快速的创建与快速的释放,并且它提供了直接访问内部字节数组的方法。

缺点:每次读写数据时,都需要先将数据复制到直接缓冲区中再进行网络传输。

Direct Buffer(直接缓冲区)

在堆之外直接分配内存空间,直接缓冲区并不会占用堆的内容,因为它是由操作系统在本地内存进行的数据分配。

优点:在使用Socket进行数据传递时,性能非常好,因为数据直接位于操作系统的本地内存中,所以不需要从JVM将数据复制到直接缓冲区中,性能很好。

缺点:因为Direct Buffer是直接在操作系统内存中的,所以内存空间的分配与释放要比堆空间更加复杂,而且速度要慢一些。

Netty通过提供内存池来解决这个问题。直接缓冲区并不支持字节数组的方式来访问数据。

重点:对于后端的业务消息的编解码来说,推荐使用HeapByteBuf;对于I/O通信线程在读写缓冲区时,推荐使用DirectByteBuf。

Composite Buffer(复合缓冲区)

JDK的ByteBuffer与Netty的ByteBuf之间的差异比对:

  1. Netty的ByteBuf采用了读写索引分离的策略(readerIndex与writeIndex),一个初始化(里面尚未有任何数据)的ByteBuf的readerIndex与writeIndex值都为0。
  2. 当读索引与写索引处于同一个位置时,如果我们继续读取,那么就会抛出IndexOutOfBoundsException。
  3. 对于ByteBuf的任何读写错左都会分别单独维护读索引与写索引。maxCapacity最大容量默认的限制就是Integer.MAX_VALUE。

JDK的ByteBuffer的缺点:

  1. final byte[] hb;这是JDK的ByteBuffer对象中用于存储数据的对象声明;可以看到,其字节数组是被声明为final的,也就是长度是固定不变的。一般分配好后不能动态扩容与收缩;而且当待存储的数据字节很大时就很有可能出现IndexOutOfBoundersException。如果要预防这个异常,那就需要在存储之前完全确定好待存储的字节大小。如果ByteBuffer的空间不足,我们只有一种解决方案:创建一个全新的ByteBuffer对象,然后再将之前的ByteBuffer中的数据复制过去,这一切操作都需要开发者自己来手动完成。
  2. ByteBuffer只使用一个position指针来标识位置信息,在进行读写切换时需要调用flip方法或是rewind方法,使用起来很不方便。

Netty的ByteBuf的优点:

  1. 存储字节的数组是动态的,其最大默认值是Integer.MAX_VALUE。这里的动态性是体现在write方法在执行时会判断buffer的容量,如果不足则自动扩容。
  2. ByteBuf的读写索引是完全分开的,使用起来就很方便。

自旋锁。

AtomicIntegerFieldUpdater要点总结。

  1. 更新器更新的必须是int类型变量,不能是其包装类型。
  2. 更新器所更新的必须是volatile类型的变量,确保线程之间共享变量时的立即可见性。
  3. 变量不能是static的,必须要是实例变量。因为Unsafe.objectFieldOffset()方法不支持静态变量(CAS操作本质上是通过对象实例的偏移量来直接进行赋值)。
  4. 更新器只能修改它可见范围内的变量,因为更新器是通过反射来得到这个变量,如果变量不可见就会报错。

如果要更新的变量是包装类型,那么可以使用AtomicReferenceFieldUpdater来进行更新。