【Netty】TCP粘包/拆包解决方案

TCP粘包/拆包是指在使用TCP传输数据时,小数据包可能被合并发送,大数据包可能被拆分的情况。这通常由Nagle算法、TCP_CORK选项和MTU/MSS限制引起。解决办法包括设定定长报文、使用分隔符或在报文头写入长度。Netty提供了解码器如DelimiterBasedFrameDecoder、LineBasedFrameDecoder和LengthFieldBasedFrameDecoder等工具来处理这个问题。案例中展示了LengthFieldBasedFrameDecoder的使用方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

image.png
TCP作为传输层(4层)协议,并不知道你的应用层(7层)数据报文的含义,因此使用TCP传输数据时,可能会出现「粘包/拆包」,需要引用层通过特定的协议去解决。

1. 什么是TCP粘包/拆包?

「粘包/拆包」在Socket编程中经常会出现,使用TCP协议传输数据时,如果对端连续发送多个小的数据包,TCP会将这些小的数据包打包,合并成一个TCP报文发送出去,这就是「粘包」。如果对端发送一个超大的数据包,TCP会根据缓冲区的情况,将这个超大数据包拆分成多个小的TCP报文发送出去,这就是「拆包」。

拿HTTP服务举例,如果一次HTTP请求的报文过大,TCP会进行拆包。因为TCP作为传输层协议,并不知道上层应用层HTTP的报文含义,它不知道单个报文的数据边界在哪里,而且一旦报文长度超过了双方约定的「可传输最大TCP报文长度」,就不得不拆包了。此时,服务端会先收到一个不完整的HTTP请求,如果服务端不做处理,那它要如何处理这个请求呢?服务端应该根据HTTP协议对请求报文和响应报文的规定,判断此次接收到的请求是否完整,如果是一个不完整的请求报文,就应该等待对端继续发送请求数据,等待读取到一个完整的请求时,再进行处理。

下图所示为粘包/拆包的场景:
在这里插入图片描述

2. 为什么会导致粘包/拆包?

清楚了「粘包/拆包」的概念,顺带了解下可能会导致TCP「粘包/拆包」的原因。

2.1 Nagle算法

TCP是面向连接的、可靠的、基于字节流的传输层协议。应用层交给TCP的数据,并不会以应用层的报文消息为单位进行传输,这些消息可能会被组合成一个数据段发送给目标主机。

Nagle是一种通过减少数据包的方式来提高TCP传输效率的算法,因为网络带宽有限,如果频繁的发送小的数据包,对带宽的压力会比较大。Nagle算法会在本地缓冲区先缓冲待发送的数据,待数据总量达到最大数据段(MSS)时,再一次性批量发送。这种方式虽然可能会使消息的发送存在延迟,但是对带宽的压力小,降低了网络拥堵的可能性并减少了额外的开销。

现在的网络资源不像几十年前那样紧张了,Linux默认是关闭Nagle算法的,即SO_NODELAY=1。

2.2 TCP_CORK

TCP有一个选项TCP_CORK也可能会导致「粘包/拆包」。
如果开启TCP_CORK,当发送的数据小于最大数据段(MSS)时,TCP会延迟20ms发送,或者等待发送缓冲区的数据达到最大数据段(MSS)才真正发送。

2.1 MTU

最大传输单元(Maximum Transmission Unit,MTU)用来通知对方所能接受数据服务单元的最大尺寸,说明发送方能够接受的有效载荷大小。

指通信协议的最大传输单元,普遍使用的网卡MTU为1500,即最大只能传输1500字节的数据帧。可以通过ifconfig命令查看各网卡的数据帧:
image.png

2.2 MSS

最大报文段长度(MSS)是TCP协议的一个选项,用于在TCP连接建立时,收发双方协商通信时每一个报文段所能承载的最大数据长度(不包括文段头)。

TCP双方建立连接后会约定可传输的最大报文长度,是TCP用来限制应用层可发送的最大字节数。如果应用层单次发送的报文长度超过了MSS,那么也会面临「拆包」。

3. 粘包/拆包的解决方案

常用的解决方案大致可分为三种:

  1. 数据报文定长,不足时自动填充。这种方式实现简单,但会浪费一定的带宽。
  2. 使用特定的分隔符(如换行符)将不同的报文进行分割。
  3. 在请求头中写入报文的长度。

针对这三种解决方案,Netty都提供了开箱即用的解码器,使用非常的方便。
yuque_diagram (1).jpg
DelimiterBasedFrameDecoder是一个可以自定义分隔符的解码器,Netty只有当读到指定的分隔符时才会认为是一个完整的数据报文。

LineBasedFrameDecoder是一个以「换行符」为分隔符的解码器。

FixedLengthFrameDecoder是一个定长帧的解码器,你需要指定定长的帧大小,Netty只有读到一个完整的桢时才会调用后续的ChannelRead()。

LengthFieldBasedFrameDecoder是一个将报文长度写入到请求头的解码器,Netty会根据长度字段的偏移量和长度字段占用的字节数,读取到本次报文的长度,当读到一个完整的帧时才调用后续的ChannelRead()。它的用法如下:

/**
 * @param maxFrameLength 最大帧大小
 * @param lengthFieldOffset 长度字段的偏移量
 * @param lengthFieldLength 长度字段占用的字节数,一般用int,4字节
 */
public LengthFieldBasedFrameDecoder(
        int maxFrameLength,
        int lengthFieldOffset, int lengthFieldLength) {
    this(maxFrameLength, lengthFieldOffset, lengthFieldLength, 0, 0);
}

4. 案例实战

由于LengthFieldBasedFrameDecoder比较常用,这里只演示这一个,其他解码器大家自行探索。

因为只是单纯的测试,我这里图方便,只编写一个EmbeddedChannel来测试,没有启Netty服务,案例如下:

// 读写半包 Demo
public class HalfDemo {
	public static void main(String[] args) {
		EmbeddedChannel channel = new EmbeddedChannel();
		// 最大帧1MB,0~4字节记录报文的长度
		channel.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024 * 1024, 0, 4));
		channel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
			@Override
			public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
				ByteBuf buf = (ByteBuf) msg;
				int length = buf.readInt();
				System.out.println("报文长度:" + length);
				System.out.println("收到数据:" + buf.toString(Charset.defaultCharset()));
			}
		});

		ByteBuf buf = Unpooled.buffer();
		// 写入报文的长度
		buf.writeInt(5);
		// 如果不写满5字节,控制台将不会有输出
		buf.writeBytes("hello".getBytes());
		channel.writeInbound(buf);
	}
}
Netty中的TCP拆包问题是由于底层的TCP协议无法理解上层的业务数据而导致的。为了解决这个问题,Netty提供了几种解决方案。其中,常用的解决方案有四种[1]: 1. 固定长度的拆包器(FixedLengthFrameDecoder):将每个应用层数据拆分成固定长度的大小。这种拆包器适用于应用层数据长度固定的情况。 2. 行拆包器(LineBasedFrameDecoder):将每个应用层数据以换行符作为分隔符进行分割拆分。这种拆包器适用于应用层数据以换行符作为结束符的情况。 3. 分隔符拆包器(DelimiterBasedFrameDecoder):将每个应用层数据通过自定义的分隔符进行分割拆分。这种拆包器适用于应用层数据以特定分隔符作为结束标志的情况。 4. 基于数据长度的拆包器(LengthFieldBasedFrameDecoder):将应用层数据的长度作为接收端应用层数据的拆分依据。根据应用层协议中含的数据长度进行拆包。这种拆包器适用于应用层协议中含数据长度的情况。 除了使用这些拆包器,还可以根据业界主流协议的解决方案来解决拆包问题[3]: 1. 消息长度固定:累计读取到长度和为定长LEN的报文后,就认为读取到了一个完整的信息。 2. 使用特殊的分隔符:将换行符或其他特殊的分隔符作为消息的结束标志。 3. 在消息头中定义长度字段:通过在消息头中定义长度字段来标识消息的总长度。 综上所述,Netty提供了多种解决方案来解决TCP拆包问题,可以根据具体的业务需求选择合适的解决方案[1][3]。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小潘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值