说一下 Tcp 粘包是怎么产生的?

TCP 粘包是什么?

TCP 粘包(TCP Packet Merging) 是指多个小的数据包在 TCP 传输过程中被合并在一起,接收方读取时无法正确分辨数据边界,导致数据解析错误。

TCP 是流式协议 ,没有数据包的概念,它只是保证数据按照字节流 的顺序传输,不保证接收方能按照原始发送时的数据边界来接收数据。因此,TCP 可能会把多个数据包合并(粘包)或者拆分(拆包)


1. TCP 粘包的两种情况

(1)发送端导致的粘包

发送方的数据量较小 ,TCP 不会立即发送 ,而是等缓冲区满了再一起发送 ,这样可以减少网络开销 。导致多个小数据包合并成一个大的数据包,产生粘包

示例

假设我们在 TCP 连接中连续发送三条消息:

复制代码
send(socket, "Hello", 5, 0);
send(socket, "World", 5, 0);
send(socket, "!!!", 3, 0);

如果 TCP 将这三次 send 的数据合并在一起,接收方可能会收到:

复制代码
HelloWorld!!!

这样就无法判断消息边界,导致解析困难。

原因

  • TCP 有 Nagle 算法 (默认开启):
    • 小数据会被合并,等待缓冲区满了才一起发送,减少小包,提高传输效率。

    • 适用于高并发场景,但会导致粘包问题。

    • 可以通过 setsockopt 关闭:

      复制代码
      int flag = 1;
      setsockopt(socket, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(int));

(2)接收端导致的粘包

接收方 读取数据不及时或者一次性读取了多个数据包 ,导致多个包的数据合并读取,形成粘包。

示例

如果发送方连续发送:

复制代码
send(socket, "Hello", 5, 0);
send(socket, "World", 5, 0);
send(socket, "!!!", 3, 0);

接收方可能这样读取:

复制代码
char buffer[20];
recv(socket, buffer, 20, 0);

如果 recv() 读取到了所有数据,buffer 里存的是:

复制代码
HelloWorld!!!

但接收方可能预期每条消息是独立的,所以会出现粘包问题。

原因

  • TCP 是 流式传输 ,没有边界概念,recv() 读取数据时,可能一次读取多个包的内容
  • 如果接收方缓冲区没满 ,但程序没有及时读取,新的数据到来后会追加到原有数据里,造成粘包。

2. TCP 拆包(包被拆分)

除了粘包,拆包(packet fragmentation) 也是常见问题。

如果单次发送的数据超过了 TCP 最大传输单元(MTU) ,TCP 会自动拆分数据包

示例

假设 send() 发送 5000 字节,而 TCP 的 MTU 设为 1500 字节,则会拆分成:

复制代码
Packet 1: 1500 bytes
Packet 2: 1500 bytes
Packet 3: 1500 bytes
Packet 4:  500 bytes

这样接收方 recv() 时可能会一次只收到部分数据 ,需要多次 recv() 才能完整还原。


3. 如何解决 TCP 粘包/拆包问题?

由于 TCP 没有消息边界 ,需要在应用层手动处理数据边界:

(1)固定长度协议

如果每条消息长度固定,可以按照固定字节数读取:

复制代码
recv(socket, buffer, 10, 0);  // 一次读取 10 字节

但这种方法仅适用于所有消息长度一致的情况


(2)特殊分隔符

在消息结尾添加特殊字符,接收方按照这个字符分割数据:

复制代码
send(socket, "Hello|", 6, 0);
send(socket, "World|", 6, 0);

接收方:

复制代码
char buffer[1024];
recv(socket, buffer, 1024, 0);

然后通过 |拆分数据

复制代码
char *token = strtok(buffer, "|");
while (token) {
    printf("Received message: %s\n", token);
    token = strtok(NULL, "|");
}

缺点

  • 需要保证 | 不会出现在正常数据中
  • 需要解析和处理数据,稍微增加了协议复杂度

(3)消息头 + 消息体(推荐)

在数据前面加上消息长度,接收方先读取长度,再读取完整数据:

复制代码
struct Message {
    uint32_t length;  // 4字节,表示消息长度
    char data[1024];  // 消息体
};

发送数据:

复制代码
uint32_t len = htonl(strlen(data));  // 转换为网络字节序
send(socket, &len, 4, 0);  // 先发送长度
send(socket, data, strlen(data), 0);  // 再发送数据

接收方:

复制代码
uint32_t len;
recv(socket, &len, 4, 0);  // 先读取 4 字节长度
len = ntohl(len);  // 转换回主机字节序
recv(socket, buffer, len, 0);  // 再读取数据

优势

  • 适用于任何长度的消息,比定长方案更灵活。
  • 不会出现边界问题,比分隔符方案更可靠。

4. 总结

粘包的原因

  1. 发送端合并小数据包(TCP 缓冲区满了才发,Nagle 算法)。
  2. 接收端一次性读取多个数据包(TCP 没有消息边界)。

如何解决

方案 适用场景 复杂度
固定长度消息 适用于消息长度固定的协议
特殊分隔符 (如 \n、` `) 适用于文本协议(如 HTTP)
消息头 + 消息体(推荐) 适用于二进制协议(如 TCP 长连接)

重点

  • TCP 是流式协议,没有边界,需要应用层协议解决粘包问题!
  • 消息头 + 消息体方式最通用,适用于大部分场景。🚀

这样就能高效避免 TCP 粘包问题啦!🎯

相关推荐
Dream Algorithm19 分钟前
DICT领域有哪些重要的技术标准和规范?
网络·物联网·边缘计算
霸气的哦尼酱30 分钟前
同一子网通信
网络·智能路由器
go_to_hacker41 分钟前
奇安信二面
网络·web安全·网络安全·渗透测试·代码审计·春招
郑州吴彦祖7721 小时前
【TCP】三次挥手,四次挥手详解--UDP和TCP协议详解
网络协议·tcp/ip·udp·三次握手
一袋米扛几楼981 小时前
【Node】Node.js环境变量配置,及下载地址
javascript·网络·node.js
He_Donglin2 小时前
从人工智能窥见网络安全的重要性
网络·人工智能·web安全
ღ星ღ2 小时前
网络编程基础
运维·服务器·网络
桃酥4032 小时前
20、HTTP的Keep-Alive是什么?TCP 的 Keepalive 和 HTTP 的 Keep-Alive 是一个东西吗?【中高频】
网络协议·tcp/ip·http
觉醒法师2 小时前
HarmonyOS NEXT - 网络请求问题(http)
前端·网络·网络协议·http·华为·harmonyos·ark-ts
the sun342 小时前
数据链路层协议
网络·智能路由器