应用场景
使用 Netty 创建 TCP 服务器,和底层硬件进行TCP 交互,底层每次传输1026 个字节。
问题描述及复现
但是Netty的TCP服务端接收数据时,第一包只能接收1024个字节,第二包接收2字节。于是猜测是不是 Netty 有什么配置,将字节缓冲区设置成了 1024。于是到百度上查,有的人说将ChannelOption.SO_BACKLOG 设置成单次包传输的字节大小(在我这就是1026)。实际测试情况表明这并不是问题的解决办法。 于是我尝试着发送2056个字节 过去,并打印每次接收的数据大小,结果如下
第一条日志是 1024 字节,第二条日志是 1032 字节。这两次实际上是底层硬件的第一次上传,被 Netty 分包了。 第三条日志对应客户端第二次上传数据。 第四条日志对应客户端第三次上传数据。
追踪问题原因
可以看到,对于客户端的三次数据上传,Netty 分配的缓冲区大小不是固定的!猜测可能是 Netty 为了节省内存开销而设计的这种机制,接下来进入断点定位问题。 可以看到这里有个 read 方法,估计和 jdk的 ServerSocket的read方法一个作用,点过去看看 这里分配了一个 ByteBuf 正好是 1024 字节的容量,应该是在 allocHandle.allocate 方法中分配的空间。点进这个方法看看是如何分配的空间。 可以看到 ioBuffer 是根据参数来分配空间的,这样就可以定位到 guess() 方法的返回值是关键。 按F7 步进到方法内部,可以看到这是一个AdaptiveRecvByteBufAllocator 的内部类,它返回一个成员属性nextReceiveBufferSize
它在被构建的时候指定了三个参数。而nextReceiveBufferSize 就是在此处被初始化的。 而 getSizeTableIndex() 是什么作用呢?可以到AdaptiveRecvByteBufAllocator 的 Doc 文档上看到这么一句话
这里可以看到,这个类就是实现可变缓冲区大小的。如果上次填满缓冲区,则下次会创建一个更大的缓冲区。Netty 在初始化时,创建了一个缓冲区大小空间值的数组。
在 Channel被创建时,会调用 AdaptiveRecvByteBufAllocator 的 newHandle 方法。此时指定的缓冲区大小默认为 1024
解决思路
如果说 Netty 默认提供了一个可变的缓冲区大小分配方案,那么我们可不可以改变这个策略呢?从AdaptiveRecvByteBufAllocator 开始向上找到根类型,可以最终找到 RecvByteBufAllocator 接口上,查看这个接口的子类,应该会有其他缓冲区大小分配方案。 这里有一个固定的接收数组空间分配器,现在只要想办法把默认的 AdaptiveRecvByteBufAllocator 换成 FixedRecvByteBufAllocator 就可以解决问题了。
现在回到 read 方法中,guess 是在 allocate 方法中调用的,而 allocate 则是一个RecvByteBufAllocator.Handle 类型。
此时我们找到 recvBufAllocHandle() 是如何创建这个 allocHandle 的。
首先调用 config 方法,然后调用getRecvByteBufAllocator 来创建这个allocHandle 。看下 config 方法哪来的?
原来是在 Channel 中设置的呀,那既然有getRecvByteBufAllocator 方法,那么肯定也有setRecvByteBufAllocator 方法,我们在ChannelInitializer 中来调用下setRecvByteBufAllocator 方法,并 new 一个FixedRecvByteBufAllocator 来替换AdaptiveRecvByteBufAllocator
再次重启 Netty 的 Tcp 服务器,发现每次能够完整的接收到 1026字节,把 maxLen 改成 2056之后也是如此,问题解决!
|