关于netty框架

目录

使用Netty的通信流程

eventloop/eventloopgroup(每一个loop相当于NIO中的多线程多路复用中的单个worker)

channel

Future和promise

 

Handler和pipeline

Bytebuf

bytebuf的零拷贝和netty的零拷贝

粘包和半包现象


使用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事件,无参创建核心*2EventLoop

 

服务器端的优化

1、创建两个nioeventloopgroup分工成bosseventloopgroupworkereventloopgroup分别处理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方法是异步的。

 

1channelfuture.sync()起到阻塞主线程直到连接建立的作用。

2sync方法是同步等待结果,channelfuture还可以调用addlistener方法回调对象异步获取结果。由于nioeventloopgroup中的线程执行connect连接,所以addListener方法让连接建立成功后由nioeventloopgroup的线程在它的线程中返回结果,主线程不参与等待。

 

closefuture

closefuturechannelfuture的子类,专门负责把主线程阻塞,直到调用channel.close()异步方法后才正常往下执行,优雅的关闭niosocketchannel。

closefuturechannelfuture更专业化,因为它只针对close方法

1closeFuture.sync()同步关闭,一直阻塞到其他线程正常关闭后再往下执行。

2closeFuture.addListener()异步关闭,由其他执行关闭的线程在关闭后返回结果

 

 

addListener要在之前方法结束时的线程中执行其中的回调方法,怎么知道在哪个线程中执行并返回结果?

因为异步方法调用后会在主线程返回一个Future类对象,上一个异步方法返回的Future类对象执行addListener方法,这样addListener就知道是在上个异步方法的线程中执行下面的结果并返回。

 

 

Futurepromise

 

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就行。

 

 

 

Handlerpipeline

Pipeline

所有 ChannelHandler 被连成一串,就是 Pipeline ,实际上是一个由handler组成的双向链表,也有headtailhandler

 

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)

2TCP传输层滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包

3TCP传输层Nagle 算法如果发送的网络数据包太小,那么他本身会启用Nagle算法(可配置是否启用)对较小的数据包进行合并(基于此,TCP的网络延迟要UDP的高些)然后再发送(超时或者包大小足够)

半包

现象:发送 abcdef,接收 abc def

原因

1应用层:接收方 ByteBuf 小于实际发送数据量

2、TCP传输层滑动窗口:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包

3TCP传输层分段:当发送的数据超过 MSS 限制后,会将数据分段,导致接收缓冲区接收到的包的顺序不同

 

本质是因为 TCP 是面向字节流的。

 

netty中对于TCP粘包和半包的解决方案:

1、客户端采用短连接

客户端发一个包断开一次连接。缺点是效率太低

2、定长解码器

客户端在发送消息长度固定,累计读到固定长度的字节就认为获取了一个完整的消息,服务器端加入固定长度的handler解码器处理。如果收到半包消息,则会在接收端缓存,直到收到下个包进行拼包,按照固定长度拼出完整消息。

3、换行符解码器

客户端在消息中加入换行符,服务器端加入固定结尾\n换行符解码器处理

原理:

 

缺点:效率低,得一个一个字符检查,

4、LFBFD解码器

在发送消息前,约定发送消息的格式按照下面的格式,服务器接收端使用LTC解码器按照既定规则读取消息。

 

 

这个解码器可以和自定义的私有协议栈配合使用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值