找回密码
 立即注册
首页 业界区 业界 从零开始实现简易版Netty(十) MyNetty 通用编解码器解决 ...

从零开始实现简易版Netty(十) MyNetty 通用编解码器解决TCP黏包/拆包问题

师悠逸 2025-11-7 22:05:17
从零开始实现简易版Netty(十) MyNetty 通用编解码器解决TCP黏包/拆包问题

1. TCP黏包拆包问题介绍

在上一篇博客中,lab9版本的MyNetty已经实现了包括池化ByteBuf在内的绝大多数功能。按照计划,lab10中将实现通用的编解码处理器来解决tcp层面接收数据时的黏包/拆包问题。
由于本文属于系列博客,读者需要对之前的博客内容有所了解才能更好地理解本文内容。

  • lab1版本博客:从零开始实现简易版Netty(一) MyNetty Reactor模式
  • lab2版本博客:从零开始实现简易版Netty(二) MyNetty pipeline流水线
  • lab3版本博客:从零开始实现简易版Netty(三) MyNetty 高效的数据读取实现
  • lab4版本博客:从零开始实现简易版Netty(四) MyNetty 高效的数据写出实现
  • lab5版本博客:从零开始实现简易版Netty(五) MyNetty FastThreadLocal实现
  • lab6版本博客:从零开始实现简易版Netty(六) MyNetty ByteBuf实现
  • lab7版本博客:从零开始实现简易版Netty(七) MyNetty 实现Normal规格的池化内存分配
  • lab8版本博客:从零开始实现简易版Netty(八) MyNetty 实现Small规格的池化内存分配
  • lab9版本博客:从零开始实现简易版Netty(九) MyNetty 实现池化内存的线程本地缓存


操作系统实现的传输层tcp协议中,向上层的应用保证尽最大可能的(best effort delivery)、可靠的传输字节流,但并不关心实际传输的数据包是否总是符合应用层的要求。
应用层有时候会在短时间内向对端发送N个业务逻辑上独立的请求,而操作系统tcp层面出于效率的考虑并不会按照应用层的逻辑划分一个一个独立的进行消息的发送,而是会基于当前的网络负载尽可能的多的将消息数据批量发送。这使得我们在EventLoop事件循环中read读取到的数据并不总是独立、完整的,符合应用层逻辑划分的消息数据。



  • 黏包问题: 假设应用层发送的一次请求数据量比较小(比如0.01kb),tcp层可能不会在接到应用请求后立即进行传输,而是会稍微等待一小会。
    这样如果应用层在短时间内需要传输多次小数据量的请求,就可以攒在一起批量传输,传输效率会高很多。
    但这带来的问题就是接收端一次接受到的数据包内应用程序逻辑上的多次请求黏连在了一起,需要通过一些方法来将其拆分还原为一个个独立的信息给应用层。
  • 拆包问题: 假设应用层发送的一次请求数据量比较大(比如10Mb),而tcp层的数据包容量的最大值是有限的,所以应用层较大的一次请求数据会被拆分为多个包分开发送。
    这就导致接收端接受到的某个数据包其实并不是完整的应用层请求数据,没法直接交给应用程序去使用,而必须等待后续对应请求的所有数据包都接受完成后,才能组装成完整的请求对象再交给应用层处理。


当然,导致黏包拆包的场景远不止上述的那么单一,整体的网络负载变化等都可能导致黏包/拆包的现象。
可以说,黏包/拆包问题并不能看做是tcp自身的问题,而是应用层最终需求与tcp传输层功能不匹配导致的问题。
tcp出于传输效率的考虑无法很好的解决这个问题,所以黏包拆包问题最终只能在更上面的应用层自己来处理。
黏包拆包示意图

接受到的一个数据包中可能同时存在黏包问题和拆包问题(如下图所示)


1.png

黏包/拆包问题解决方案

解决黏包/拆包问题最核心的思路是如何确定一个应用层完整消息的边界。
对于黏包问题,基于边界可以独立的拆分出每一个消息;对于拆包问题,如果发现收到的数据包末尾没有边界,则继续等待新的数据包,逐渐累积直到发现边界后再一并上交给应用程序。


主流的解决黏包拆包的应用层协议设计方案有三种:


介绍优点缺点1.基于固定长度的协议每个消息都是固定的大小,如果实际上小于固定值,则需要填充简单;易于实现固定值过大,填充会浪费大量传输带宽;固定值过小则限制了可用的消息体大小2.基于特殊分隔符的协议约定一个特殊的分隔符,以这个分割符为消息边界简单;且消息体长度是可变的,性能好消息体的业务数据不允许包含这个特殊分隔符,否则会错误的拆分数据包。因此兼容性较差3.基于业务数据长度编码的协议设计一个固定大小的消息请求头(比如固定16字节、20字节大小),在消息请求头中包含实际的业务消息体长度消息体长度可变,性能好;对业务数据内容无限制,兼容性也好实现起来稍显复杂

上述这段关于黏包/拆包问题的内容基本copy自我2年前的关于手写简易rpc框架的博客:自己动手实现rpc框架(一) 实现点对点的rpc通信。
只是当时我仅仅是一个对Netty不甚了解的使用者,简单的使用Netty来实现rpc框架中基本的网络通信功能,并通过MessageToByteEncoder/ByteToMessageDecoder来实现通信协议处理黏包拆包问题。
而现在却尝试着参考Netty的源码,通过自己亲手实现这些编解码器的核心逻辑,来进一步加深对Netty的理解,这种感觉还是挺奇妙的。
2. Netty解决黏包/拆包问题的通用编解码器


  • Netty的设计者希望用户在pipeline中添加各式各样的入站和出站的Handler,组合起来共同完成复杂的业务逻辑。对发送的消息进行编码、将接收到的消息进行解码毫无疑问也是业务逻辑的一部分,所以Netty编解码器是以Handler的形式存在的。
  • Netty中解决黏包/拆包问题的编解码器是通用的,在实现基本功能的前提下也要给使用者一定的灵活性来定制自己的功能。因此Netty提供了一些基础的父类Handler完成通用的处理逻辑,并同时留下一些抽象的方法交给用户实现的子类去实现自定义的编解码业务逻辑。


下面我们通过一个简单但又不失一般性的例子来展示Netty的通用编解码器的用法,并结合源码分析其解决黏包/拆包的具体原理。
我们首先设计一个基于业务数据长度编码的、非常简单的通信协议MySimpleProtocol,消息帧共分为3个部分,其中前4个字节是一个int类型的魔数0x2233用于解码时校验协议是否匹配,再往后的4个字节则用于标识消息体的长度,最后就是消息体的内容,消息体的内容是EchoMessageFrame对象的json字符串。
MySimpleProtocol协议示意图

2.png


  1. public class EchoMessageFrame {
  2.     /**
  3.      * 协议魔数,随便取的
  4.      * */
  5.     public static final int MAGIC = 0x2233;
  6.     /**
  7.      * 消息内容,实际消息体的json字符串
  8.      * */
  9.     private String messageContent;
  10.     /**
  11.      * 用于校验解码是否成功的属性
  12.      * */
  13.     private Integer msgLength;
  14. }
复制代码
客户端/服务端
  1. public class ClientDemo {
  2.     public static void main(String[] args) throws IOException {
  3.         DefaultChannelConfig defaultChannelConfig = new DefaultChannelConfig();
  4.         defaultChannelConfig.setInitialReceiveBufferSize(1024); // 设置小一点,方便测试
  5.         defaultChannelConfig.setAllocator(new MyPooledByteBufAllocator()); // 测试池化ByteBuf功能
  6.         MyNioClientBootstrap myNioClientBootstrap = new MyNioClientBootstrap(new InetSocketAddress(8080),new MyChannelPipelineSupplier() {
  7.             @Override
  8.             public MyChannelPipeline buildMyChannelPipeline(MyNioChannel myNioChannel) {
  9.                 MyChannelPipeline myChannelPipeline = new MyChannelPipeline(myNioChannel);
  10.                 // 解码器,解决拆包、黏包问题
  11.                 myChannelPipeline.addLast(new MyLengthFieldBasedFrameDecoder(1024 * 1024, 4, 4));
  12.                 // 注册自定义的EchoClientEventHandler
  13.                 myChannelPipeline.addLast(new EchoMessageEncoderV2());
  14.                 myChannelPipeline.addLast(new EchoMessageDecoderV2());
  15.                 myChannelPipeline.addLast(new EchoClientEventHandlerV2());
  16.                 return myChannelPipeline;
  17.             }
  18.         }, defaultChannelConfig);
  19.         myNioClientBootstrap.start();
  20.     }
  21. }
复制代码
  1. public class ServerDemo {
  2.     public static void main(String[] args) throws IOException {
  3.         DefaultChannelConfig defaultChannelConfig = new DefaultChannelConfig();
  4.         defaultChannelConfig.setInitialReceiveBufferSize(16); // 设置小一点,方便测试
  5.         defaultChannelConfig.setAllocator(new MyPooledByteBufAllocator()); // 测试池化ByteBuf功能
  6.         MyNioServerBootstrap myNioServerBootstrap = new MyNioServerBootstrap(
  7.             new InetSocketAddress(8080),
  8.             // 先简单一点,只支持childEventGroup自定义配置pipeline
  9.             new MyChannelPipelineSupplier() {
  10.                 @Override
  11.                 public MyChannelPipeline buildMyChannelPipeline(MyNioChannel myNioChannel) {
  12.                     MyChannelPipeline myChannelPipeline = new MyChannelPipeline(myNioChannel);
  13.                     // 解码器,解决拆包、黏包问题
  14.                     myChannelPipeline.addLast(new MyLengthFieldBasedFrameDecoder(1024 * 1024, 4, 4));
  15.                     // 注册自定义的EchoServerEventHandler
  16.                     myChannelPipeline.addLast(new EchoMessageEncoderV2());
  17.                     myChannelPipeline.addLast(new EchoMessageDecoderV2());
  18.                     myChannelPipeline.addLast(new EchoServerEventHandlerV2());
  19.                     return myChannelPipeline;
  20.                 }
  21.             },1,5, defaultChannelConfig);
  22.         myNioServerBootstrap.start();
  23.         LockSupport.park();
  24.     }
  25. }
复制代码
编解码流程图

3.png

Netty通用编码器Encoder原理解析

编码器Encoder简单理解就是将逻辑上的一个数据对象,从一种格式转换成另一种格式。Netty作为一个网络通信框架,其中最典型的场景就是将内存中的一个消息对象,转换成二进制的ByteBuf对象发送到对端,所对应的便是MessageToByteEncoder。
MessageToByteEncoder是一个抽象类,重写了ChannelEventHandlerAdapter的write方法。由于Netty其底层出站时只会处理ByteBuf类型对象(以及FileRegion类型),MessageToByteEncoder作为一个出站处理器,用于拦截出站的消息,将匹配条件的对象按照一定的规则转换成ByteBuf对象。
MyNetty MyMessageToByteEncoder实现

[code]/** * 基本copy自Netty的MessageToByteEncoder类,但做了一些简化 * */public abstract class MyMessageToByteEncoder<I> extends MyChannelEventHandlerAdapter {    private final TypeParameterMatcher matcher;    public MyMessageToByteEncoder(Class

相关推荐

您需要登录后才可以回帖 登录 | 立即注册