说一下 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 粘包问题啦!🎯

相关推荐
酷熊代理13 分钟前
软路由如何屏蔽国外IP?RouterOS保姆级实战教程(附自动化脚本)
运维·网络协议·tcp/ip·自动化·智能路由器·软路由·小猫pptp
久违 °2 小时前
【渗透测试】反弹 Shell 技术详解(二)
linux·网络·网络安全
希望_睿智2 小时前
C++网络编程之字节序
c++·网络协议
热爱前端的小张2 小时前
第四章 网络层(上)
网络协议
Codingwiz_Joy3 小时前
Day07 -实例 非http/s数据包抓取工具的使用:科来 & wrieshark & 封包监听工具
网络·网络协议·安全·安全性测试
黑客呀3 小时前
软考网络安全专业
网络·安全·web安全
ssr——ssss3 小时前
网络华为HCIA+HCIP数据链路层协议-以太网协议
网络·华为
EPSDA3 小时前
介绍HTTP协议基本结构与Linux中基本实现HTTPServer
linux·运维·开发语言·c++·网络协议·tcp/ip·http
冬冬小圆帽4 小时前
axios的二次封装
网络
敲键盘的Q5 小时前
【eNSP实战】将路由器配置为DHCP服务器
网络