目录
EventLoopGroup
- 一个EventLoopGroup当中会包含一个或多个EventLoop。
- 一个EventLoop在它的整个生命周期当中都只会与唯一一个Thread进行绑定。
- 所有由EventLoop所处理的各种I/O事件都将在它所关联的那个Thread上进行处理。
- 一个Channel在它的整个生命周期中只会注册在一个EventLoop上。
- 一个EventLoop在运行过程当中,会被分配给一个或者多个Channel。
重要结论
- 在netty中,Channel的实现一定是线程安全的;基于此,我们可以储存一个Channel的引用,并且在需要向远程端点发送数据时,通过这个引用来调用Channel相应的方法;即便当时有很多线程同时在使用它也不会出现线程问题;而且,消息一定会按照顺序发送出去的。
- 我们在业务开发中,不要将长时间执行的耗时任务放到EventLoop的执行队列中,因为它将会一直阻塞改线程所对应的所有Channel上的其他执行任务,如果我们需要进行阻塞调用耗时操作(实际开发中很常见),那么我们就需要使用一个专门的EventExecutor(业务线程池)。
通常会有两种实现方式:
- 在ChannelHandler的回调方法中,使用自己的业务线程池,这样就可以实现异步调用。
- 借助于netty提供的向ChannelPipeline添加ChannelHandler时调用的addLast方法来传递EventExecutor。
说明
默认情况下调用addLast(handler),ChannelHandler中的回调方法都是由I/O线程所执行,如果调用了ChannelPipeline.addLast(EventExecutorGroup group,ChannelHandler… handlers)方法,那么ChannelHandler中的回调方法就是由参数中的group线程组来执行。
Future
JDK所提供的Future只能通过手工方式检查执行结果,而这个操作是会阻塞的;Netty则对ChannelFuture进行了增强,通过ChannelFutureListener以回调的方式来获取执行结果,去除了手工检查阻塞的操作;值得注意的是:ChannelFutureListener的operationComplete方法是I/O线程执行的,因此要注意的是不要在这里执行耗时的操作,否则需要通过另外的线程或者线程池来执行。
ChannelHandler与ChannelHandlerContext
在netty中有两种发送消息的方式,可以直接是写到Channel中,也可以写到与ChannelHandler所关联的那个ChannelHandlerContext中,对于前一种方式来说,消息会从ChannelPipeline的末尾开始流动;对于后一种方式来说,消息将从ChannelPipleline中的下一个ChannelHandler开始流动。
结论
- ChannelHandlerContext与ChannelHandler之间的关联绑定的关系但是永远都不会发生改变的,因此对其进行缓存是没有任何问题的。
- 对于与Channel的同名方法来说,ChannelHandlerContext的方法会产生更短的事件流,所以我们应该在可能的情况下利用这这个特性来提升应用性能。
NIO
使用NIO进行文件读取所涉及的步骤:
3. 从FileInputStream对象获取到Channel对象。
4. 创建Buffer。
5. 将数据从Channel中读取到Buffer对象中。
0<=mark<=position<=limit<=capacity
flip()方法
- 将limit值设为当前的position。
- 将position设为0。.
clear()方法
- 将limit值设为capacity
- 将position的值设为0.
compact()方法。
- 将所有未读的数据复制到buffer起始位置处。
- 将position设为最后一个未读元素的后面。
- 将limit设为capacity
- 现在buffer就准备好了,但是不会覆盖未读的数据。
注意
通过索引来访问Byte时并不会改变真实的读索引;我么可以通过ByteBuf的readerIndex()与writeIndex()方法分别直接修改读索引与写索引。
缓冲区
Netty所提供的3种缓冲区类型
- heap buffer
- direct buffer
- composite buffer
Heap Buffer(堆缓冲区)
这是最常用的类型,ByteBuf将数据储存到JVM的堆空间中,并且将实际的数据存放到byte array中来实现。
优点
由于数据是储存在JEM的堆中,因此可以快速的创建与快速地释放,并且它提供了直接访问字节数组地方法。
缺点
每次读写数据时,都需要先将数据复制到直接缓冲区中再进行网络传输。
Diect Buffer(直接缓冲区)
在堆之外直接分配内存空间,直接缓冲区并不会占用堆的容量空间,因为它是由操作系统在本地内存
优点
在使用Socket经行数据传递的时候,性能非常好,因为数据直接位于操作系统的本地内存中,所以不需要从JVM将数据复制到直接缓冲区中,性能很好。
缺点
因为Direct Buffer是直接在操作系统的内同中的,所以内存空间的分配与释放都要比堆内存更加复杂,而且速度要慢一些。
Netty通过提供内存池来解决这个问题。
重点
对于后端的业务消息的编解码来说,推荐使用HeapByteBuf;对于I/O通信线程在读写缓冲区时,推荐使用DirectByteBuf。
Composite Buffer(复合缓冲区)
略
JDK的ByteBuffer与Netty之间的差异对比
- Netty的ByteBuf采用了读写索引分离的策略(readerIndex与writeIndex),一个初始化(里面尚未由任何数据)的ByteBuf的readerIndex与writeIndex值都为0.
- 当读索引与写索引处同一个位置的时候,如果我们继续读取,那么就会抛出IndexOutOfBoundsException。
- 对于ByteBuf的任何读写操作都会分别单独维护读索引与写索引。maxCapacity最大的容量默认的限制就是Integer.MAX_VALUE。
JDK中的ByteBuffer的缺点
- final byte[] hb;这是JDK的ByteBuffer对象中用于储存数据的对象声明;可以看到,其字节数组是被声明为final的,也就是长度是固定不变的。一旦分配好后不能动态扩容与收缩;而且当待存储的数据字很大时很有可能出现IndexOutOfBoundsException,如果要预防这个异常,那就需要在存储之前完全确定好待存储的字节大小。如果ByteBuffer的空间不足,我们只有一种解决方案:创建一个全新的ByteBuffer对象,然后再将之前的ByteBuffer对象中的数据复制过去,这一切操作都需要由开发者自己来手动完成。
- ByteBuffer只使用了一个position指针来标识位置信息,在进行读写切换时就需要flip方法或是rewind方法,使用起来很方便。
Netty的ByteBuf的优点:
- 存储字节的数组是动态的,其最大默认值是Integer.MAX_VALUE。这里的动态性是体现write方法中的,write方法在执行时会判断buffer容量,如果不足则自动扩容。
- ByteBuf的读写索引是完全分开的,使用起来很方便。
自旋锁
略
AtomicIntegerFieldUpdater要点总结
- 更新器的类型必须是int类型的变量,不能是其包装类型。
- 更新器更新的必须是volatile类型的变量,确保线程之间的共享变量时的立即可见性。
- 变量不能是static的,必须要是实例变量。因为Unsafe.objectFieldOffset()方法不支持静态变量(CAS操作本质上是通过对象实例的偏移量来直接进行赋值)。
- 更新器只能修改它可见范围内的变量,因为更新器是通过放射来得到这个变量的,如果变量不可见就会报错。
如果要更新的变量是包装类型,那么可以使用AtomicReferenceFieldUpdater来进行更新。
Netty处理器重要概念
- Netty的处理器可以分为两类:入站处理器与出站处理器。
- 入战处理器的顶层是ChannelInboundHandler,出站处理器的顶层是ChannelOutBoundHandler。
- 数据处理时常用的各种编码器本质上时处理器。
- 编解码器:无论我们向网络中写入的数据是什么类型(int、char、String、二进制),数据在网络中传递时,其都是以字节流的形式呈现的;将数据由原本的形式转换为字节流的操作称为编码(encode),将数据由字节转换成它本来的格式或者其他格式操作称为解码(decode),编解码同意称为codec。
- 编码:本质上是一种出站处理器;因此,编码一定是一种ChannelOutBoundHandler。
- 解码:本质上是一种入站处理器;因此,解码一定是一种ChannelInBoundHandler.
- 在Netty中,编解码通常以XXXEncoder命名;编码器通常以XXXDecoder命名。
结论
- 无论是编码器还是解码器,其所接收的消息类型必须要与待处理的参数类型一致,否则该编码器并不会被执行。
- 在编码器进行数据解码时,一定要记着判断缓冲区(ByteBuf)中的数据是否充足,否则将会产生一些问题。