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

相关推荐
其实防守也摸鱼2 小时前
KMP全栈开发:从Android到AI Agent的技术演进与实践
android·运维·网络·人工智能·学习·安全·macos
treesforest10 小时前
平时上网留下的IP地址,到底能被查到什么?
网络·网络协议·tcp/ip·ip地址·ip查询·定位服务
多巴胺梦想家11 小时前
06 代数系统:运算背后的结构之美
网络·拓扑学
小锋学长生活大爆炸11 小时前
【福利】最新免费领取云服务器和虚拟主机攻略
网络·github
81250353312 小时前
第 60 篇:IP选项:那些被遗忘的功能
网络·tcp/ip·智能路由器
IT小盘13 小时前
04-大模型流式输出原理-SSE与Python实现
开发语言·网络·人工智能·python
网络研究院14 小时前
2026年上半年十大网络攻击和数据泄露事件
网络·安全·漏洞·风险·数据·泄露·事件
巨量HTTP16 小时前
Python爬虫动态换IP实战,彻底解决IP403封禁、限流问题(附完整代码)
爬虫·python·tcp/ip·http
我星期八休息16 小时前
网络编程—UDP与TCP
linux·运维·网络·数据库·网络协议·tcp/ip·udp
玖玥拾16 小时前
Unity 3D 笔记(十四)Unity/C# Socket 网络笔记3
服务器·网络·unity·c#