目录
eventloop/eventloopgroup(每一个loop相当于NIO中的多线程多路复用中的单个worker)
使用Netty的通信流程
服务器端接收数据
客户端发送数据
整体流程
注意12步调用初始化方法只是为了添加各自的handler处理器但并不调用。
对整体流程的理解
一开始需要树立正确的观念
把 channel 理解为数据的通道
把 msg 理解为流动的数据,最开始输入是 ByteBuf,但经过 pipeline 的加工,会变成其它类型对象,最后输出又变成 ByteBuf
把 handler 理解为数据的处理工序,工序有多道,合在一起就是 pipeline,pipeline 负责发布事件(读、读取完成...)传播给每个 handler, handler 对自己感兴趣的事件进行处理(重写了相应事件处理方法)
把 eventLoop 理解为处理数据的工人
工人可以管理多个 channel 的 io 操作,并且一旦工人负责了某个 channel,就要负责到底(绑定)
工人既可以执行 io 操作,也可以进行任务处理,每位工人有任务队列,队列里可以堆放多个 channel 的待处理任务,任务分为普通任务、定时任务
工人按照 pipeline 顺序,依次按照 handler 的规划(代码)处理数据,可以为每道工序指定不同的工人
eventloop/eventloopgroup(每一个loop相当于NIO中的多线程多路复用中的单个worker)
事件循环对象
EventLoop 内部维护了一个单线程,同时维护了一个 Selector,里面有 run 方法处理 Channel 上源源不断的 io 事件。因为继承了ScheduledExecutorService,因此可以作为一个线程池使用。
事件循环组
eventloopgroup由多个EventLoop组成。Channel 一般会调用 EventLoopGroup 的 register 方法来绑定其中一个 EventLoop,后续这个 Channel 上的 io 事件都由此 EventLoop 来处理(保证了 io 事件处理时的线程安全)
通常用NIOEventLoopGroup,区别在它能多处理IO事件,无参创建核心*2个EventLoop。
服务器端的优化
1、创建两个nioeventloopgroup分工成bosseventloopgroup和workereventloopgroup分别处理accept事件和IO读写事件,因为nioserversocketchannel就一个,所以bosseventloopgroup中只分配了一个bosseventloop;每有一个执行io任务的连接就会在workereventloopgroup中按照负载均衡分配一个workereventloop,并绑定此客户端,负责到底。
2、增加一个eventloopgroup专门处理耗时间长的事件,否则在开始的workereventloopgroup中会耽误它关联的其他客户端连接。具体做法是作为自定义handler放在pipeline管道中。
在eventloopgroup中handler怎么往下执行
如果两个 handler 绑定的是同一个eventloop,那么就当前eventloop直接调用。
否则,把要调用的代码封装为一个任务对象,由下一个 handler 的eventloop来调用。
channel
channelfuture
channelfuture是客户端connect方法返回的对象,作用是用来返回建立的niosocketchannel。
为什么在客户端要执行sync方法?
因为connect方法是异步的。
1、channelfuture.sync()起到阻塞主线程直到连接建立的作用。
2、sync方法是同步等待结果,channelfuture还可以调用addlistener方法回调对象异步获取结果。由于nioeventloopgroup中的线程执行connect连接,所以addListener方法让连接建立成功后由nioeventloopgroup的线程在它的线程中返回结果,主线程不参与等待。
closefuture
closefuture是channelfuture的子类,专门负责把主线程阻塞,直到调用channel.close()异步方法后才正常往下执行,优雅的关闭niosocketchannel。
closefuture比channelfuture更专业化,因为它只针对close方法。
1、closeFuture.sync()同步关闭,一直阻塞到其他线程正常关闭后再往下执行。
2、closeFuture.addListener()异步关闭,由其他执行关闭的线程在关闭后返回结果
addListener要在之前方法结束时的线程中执行其中的回调方法,怎么知道在哪个线程中执行并返回结果?
因为异步方法调用后会在主线程返回一个Future类对象,上一个异步方法返回的Future类对象执行addListener方法,这样addListener就知道是在上个异步方法的线程中执行下面的结果并返回。
Future和promise
Future不能主动独立创建,只能由异步方法返回。
Promise可以主动创建,在创建的时候显式的指定其同步等待或异步参与的线程是哪个。promise可以异步盛放结果。
Promise
Promise也有同步等待结果和异步返回结果两种方式
同步成功或失败:promise.get()或promise.sync()或promise.await()和promise.setSuccess(xx)和promise.setFailure(xx),因为setSuccess在其他线程中获取到成功的结果了,还要同步返回到主线程,所以还要get或sync或await方法。(await和sync的区别是await不管成功结果还是失败结果不会在主线程抛异常,会往下执行,通常搭配isSuccess…………)
异步成功或失败:只有promise.setSuccess(xx)和promise.setFailure(xx),因为不用返回到主线程,所以只用在其他线程中setSuccess或setFailure就行。
Handler和pipeline
Pipeline
所有 ChannelHandler 被连成一串,就是 Pipeline ,实际上是一个由handler组成的双向链表,也有head和tail的handler。
ChannelHandler
分为入站和出站处理器。
入站处理器通常是 和ChannelInboundHandlerAdapter 的子类(含有解码器),主要用来读取客户端数据,通过handler加工成最后结果。
出站处理器通常是 ChannelOutboundHandlerAdapter 的子类(含有编码器),主要对写回结果进行加工,并发送最终结果给channel
handler的执行顺序:
入站处理器是按照 addLast 的顺序执行的,而 出站处理器 是按照 addLast 的逆序执行的。(这里的顺序对普通netty是指服务器的顺序,客户端不会在入站handler中加入write操作。但是在加入心跳设置后客户端也会加入。)
handlercontext的作用就是在出入站处理器之间传递加工结果,在最后一个入站handler要调用write方法才能正常传递到出站handler。注意如果在最后一个入站handler调用channel.write是从tail向前切换到出站handler,如果调用ctx.write则是从最后一个入站handler向前寻找出站handler。
Bytebuf
创建
可创建基于池化和非池化的直接内存中或堆内存中的bytebuf。
直接内存 vs 堆内存
缺点:直接内存创建和销毁耗时较长,要注意及时主动释放。
优点:直接内存对 GC 压力小,因为这部分内存不受 JVM 垃圾回收的管理;读写性能高(少一次内存复制),适合配合池化功能一起用
池化 vs 非池化
池化的最大意义在于可以重用 ByteBuf,优点有
没有池化,则每次都得创建新的 ByteBuf 实例,这个操作对直接内存代价昂贵,就算是堆内存,也会增加 GC 压力
有了池化,则可以重用池中 ByteBuf 实例,节约内存,减少内存溢出的可能
写入数据(跟ByteBuffer不同,不用channel写入,而是自己写入)
读取数据
readxxx
组成结构
可扩容部分起到按需使用的作用。最开始读写指针都在 0 位置
bytebuf的扩容
扩容规则是
如果写入后数据大小未超过 512,则选择下一个 16 的整数倍,例如写入后大小为 12 ,则扩容后 capacity 是 16
如果写入后数据大小超过 512,则选择下一个 2^n,例如写入后大小为 513,则扩容后 capacity 是 2^10=1024(2^9=512 已经不够了)
扩容不能超过 max capacity 会报错
内存回收
由于 Netty 中有堆外内存的 ByteBuf 实现,堆外内存最好是手动来释放,而不是等 GC 垃圾回收。
UnpooledHeapByteBuf 使用的是 JVM 内存,只需等 GC 回收内存即可
UnpooledDirectByteBuf 使用的就是直接内存了,需要特殊的方法来回收内存
PooledByteBuf 和它的子类使用了池化机制,需要更复杂的规则来回收内存
Netty 采用了引用计数法来控制回收内存,每个 ByteBuf 都实现了 ReferenceCounted 接口
每个 ByteBuf 对象的初始计数为 1
调用 release 方法计数减 1,如果计数为 0,ByteBuf 内存被回收
调用 retain 方法计数加 1,表示调用者没用完之前,其它 handler 即使调用了 release 也不会造成回收
当计数为 0 时,底层内存会被回收,这时即使 ByteBuf 对象还在,其各个方法均无法正常使用
我们不能草率地在服务器端第一个接收的handler使用完后就直接release释放内存,因为不符合handler的传递性。应该谁是最后使用者,谁负责 release。具体流程如下:
入站 ByteBuf 处理原则
如果 ByteBuf 没有成功传递到下一个 ChannelHandler,则要在当前handler中 release;假设Bytebuf一直向后传,那么 TailContext 会负责释放未处理消息(原始的 ByteBuf)
出站 ByteBuf 处理原则
出站消息最终都会转为 ByteBuf 输出进channel,由 HeadContext 在写入完成后 release
异常处理原则
如果不清楚 ByteBuf 被引用了多少次,但又必须彻底释放,可以循环调用 release 直到返回 true
▲不能因为head和tail能释放就不去主动释放,因为head和tail作用的情况它们接收到bytebuf的时候才会释放,如果已经被加工成其他对象,就不能调用其release方法了
bytebuf的零拷贝和netty的零拷贝
slice方法:【零拷贝】的体现之一,对原始 ByteBuf 进行切片成多个 ByteBuf,切片后的 ByteBuf 没有发生内存复制,还是使用原始 ByteBuf 的内存,相当于几个子切片和原bytebuf共享内存。
并且切片后的 ByteBuf 维护独立的 read,write 指针,但是子切片的capacity固定为其大小,不能写入超过其大小的数据。
原ByteBuf 读写数据不影响子切片,但是二者修改数据则会同时修改。
netty的零拷贝????????????????????????????????
Netty 的零拷贝主要包含三个方面:
- Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行 Socket 读写,JVM 会将堆内存 Buffer 拷贝一份到直接内存中,然后才写入 Socket 中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
- Netty 提供了组合 Buffer 对象,可以聚合多个 ByteBuffer 对象,用户可以像操作一个 Buffer 那样方便的对组合 Buffer 进行操作,避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的 Buffer。
- Netty 的文件传输采用了 transferTo 方法,它可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过循环 write 方式导致的内存拷贝问题。
粘包和半包现象
粘包
现象:发送 abc def,接收 abcdef
原因:
1、应用层:接收方 ByteBuf 设置太大(Netty 默认 1024)
2、TCP传输层滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包
3、TCP传输层Nagle 算法:如果发送的网络数据包太小,那么他本身会启用Nagle算法(可配置是否启用)对较小的数据包进行合并(基于此,TCP的网络延迟要UDP的高些)然后再发送(超时或者包大小足够)
半包
现象:发送 abcdef,接收 abc def
原因
1、应用层:接收方 ByteBuf 小于实际发送数据量
2、TCP传输层滑动窗口:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包
3、TCP传输层分段:当发送的数据超过 MSS 限制后,会将数据分段,导致接收缓冲区接收到的包的顺序不同。
本质是因为 TCP 是面向字节流的。
netty中对于TCP粘包和半包的解决方案:
1、客户端采用短连接
客户端发一个包断开一次连接。缺点是效率太低
2、定长解码器
客户端在发送消息长度固定,累计读到固定长度的字节就认为获取了一个完整的消息,服务器端加入固定长度的handler解码器处理。如果收到半包消息,则会在接收端缓存,直到收到下个包进行拼包,按照固定长度拼出完整消息。
3、换行符解码器
客户端在消息中加入换行符,服务器端加入固定结尾\n换行符解码器处理
原理:
缺点:效率低,得一个一个字符检查,
4、LFBFD解码器
在发送消息前,约定发送消息的格式按照下面的格式,服务器接收端使用LTC解码器按照既定规则读取消息。
这个解码器可以和自定义的私有协议栈配合使用。