Netty:遇到TCP发送缓冲区满了 写半包操作该如何处理
Netty:遇到TCP发送缓冲区满了 写半包操作该如何处理
在TCP通信中,当发送方的数据发送速度超过接收方的处理能力时,可能会出现"写半包"的情况,即一份数据无法一次性完整发送。本文将深入探讨Netty框架中如何处理这种场景,通过源码解析揭示其背后的实现原理。
什么是写半包
写半包:一份数据,一次发送没有把他全部发送,需要循环发送,那么第一次的操作称为写半包。
什么情况下会出现写半包:
- 发送方发送200byte,但是接收方只能接受100byte,因此发送方只会发送小于100byte的数据。
这与TCP滑动窗口和消息中间件中常见的消息堆积有相似之处。总的来说,接收方顶不住来自发送方的数据压力。
对于Netty来说,就是这个时刻TCP发送缓冲区满了,无法再接收整包数据。剩下的数据则会通过Channel去监听写操作,当触发写操作的时候,再把这部分数据给带上,那么这部分数据才完整地传输。
Netty中的写半包处理
在Netty中,网络数据的读写操作都先经过ByteBuf
:
- 读操作:从
ByteBuf
中读取数据 - 写操作:将数据写入到
ByteBuf
,然后再通过其他方式把ByteBuf
的数据写入(#doWrite
)
Netty中的网络操作都是通过Channel
和里面聚合的对象Unsafe
对象进行操作。简单介绍一下:
- Channel的作用:给Netty用来进行网络数据流的处理
- JDK原生Channel:Netty采用封装了一层
Facade
(门面模式)以方便框架扩展 - 自定义Channel:支持Netty的自定义Channel来应对不同的业务场景
- EventLoop:Channel会被注册到EventLoop上,在注册时定义感兴趣的事件,采用基于事件触发的方式
回到本篇的主题:写半包
AbstractNioByteChannel
主要负责处理写半包。总的流程如下:
ChannelOutboundBuffer:环形发送数组
- 不停地从
ChannelOutboundBuffer
读取数据,看是否有可以发送的数据 - 如果有,并且是
ByteBuf
类型的,可以选择发送数据
- 如果一次发送没有发送完,则采取一定次数的循环发送(写半包)
- 数据最后还是没有发送完,则会开一条新线程专门进行剩余数据的发送
- 在最后会去同步数据写入进度
源码解析 #doWrite
不停地去环形发送数组里面取数据出来:
如果是空了,代表发送完了,把写标志位置空(
clearOpWrite
)如果不是空数据,则判断是不是
ByteBuf
数据对其进行强转,若可读字节数是0,代表消息不可读(readIndex >= writeIndex),则把他在环形发送数组中移除。
第一次读的时候,会先去获取循环发送次数writeSpinCount
。循环发送次数就是指:第一次发送没有完成时(写半包)进行循环发送的次数。给他设置一个阈值,为的就是当循环发送的时候,IO线程会一直尝试写操作,此时IO线程无法处理其他操作,相当于局部阻塞、死锁、假死的情况。
像这种处理手法非常常见,比如一般我们会给分布式锁设置一个锁的超时时间,除此之外还需要设置一个客户端的超时时间,避免客户端在拿到锁的时候,这把锁已经过期了。客户端的超时时间会比锁的超时时间要短。
然后就是进行循环发送了
消息发送操作完成时候,会调用ChannelOutboundBuffer
更新发送进度的消息,并且还会判断是否需要写半包处理
如果没有发完,则设置写半包标识位,启动专门的写半包线程继续发后续的消息
总结
写半包问题本质上是:对于接收方来说,来自发送方的数据压力太大了,因此不得不采取的一种降保护措施。
可以在发送方进行解决、也可以在接收方进行解决。Netty并没有采取说,遇到TCP缓冲区满了之后,这个数据包就等下一次再等发,而是能发多少就发多少,不够的 下次再发,是一种追求性能的选择。
像消息中间件遇到消息堆积问题,在消接收方(消费者)增大消费的速度,比如:加消费队列或扩充消费者群组等。又或者限制发送方(生产者)的发送速度,比如TCP的滑动窗口。
所以互联想的技术都是有相关联的,能看到互相的影子。