📌 核心结论(TL;DR):
粘包本质:TCP 是面向字节流的,没有边界;UDP 是面向数据报的,有边界,不会粘包。
解决粘包:应用层自定义协议,常用
长度字段 + 数据,或者使用特殊分隔符。异常掉电:TCP 有 Keep-Alive 探测包。但应用层最好自己实现“心跳包”来检测死连接。
伪死连接:掉电后接收端不知道发送端已死,发送方应开启
SO_KEEPALIVE或应用层重试。
一、 什么是 TCP 粘包?
因为 TCP 是面向字节流的,应用层连续发送的数据包,在底层可能会被合并成一个大数据包发出去,或者在接收端的缓冲区里堆在一起,导致应用层一次性读到了所有数据。
二、 怎么解决粘包?
根本方法:在应用层明确数据包的边界。
定长包:规定每个包固定 100 字节,不够补空格(浪费带宽)。
特殊分隔符:比如
\r\n,但如果数据本身包含这个字符就麻烦了。长度 + 数据(最推荐):在包头预留 4 个字节,表示数据主体的长度。接收方先读 4 字节解析出长度
len,然后再读len个字节,保证能完整拆出一个包。
三、 TCP 的异常断开
正常断开:调用
close(),触发四次挥手。进程崩溃/重启:操作系统内核会自动回收资源并发送 FIN 包。
网线被拔/机器掉电:这是最致命的。连接双方没有收到任何 FIN 包,接收端会一直傻等。
TCP 保活机制:TCP 底层有 Keep-Alive 定时器,长时间没数据会发探测包。但 Linux 默认要 7200 秒(2小时)才发一次,太慢了。
应用层心跳:对于服务器,通常会应用层自己定时发“心跳包”(如 Ping),连续几次没回应就主动断开连接,释放资源。
四、 总结与扩展
TCP 协议极其复杂,因为它在“可靠、高效、安全”之间做了大量的权衡。理解了粘包,你就能看懂为什么 HTTP 协议头部需要Content-Length,为什么写 RPC 框架一定要自定义协议。这就是大厂后端工程师的必备基本功。
自我实战操作
咱们写一段极简代码,亲手把“粘包”现场复现出来,然后再用代码把它解决掉。
cd ~/linux_study/network_test实战 1:亲眼见证 TCP 粘包
vim tcp_sticky_client.c(客户端:快速发送两次数据,试图制造粘包)
#include <stdio.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> int main() { int sockfd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(9999); inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr); connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)); // 连续发送两次,中间不加 sleep send(sockfd, "Hello", 5, 0); send(sockfd, "World", 5, 0); sleep(1); close(sockfd); return 0; }vim tcp_sticky_server.c(服务端:一次性读取,观察是否读到了粘在一起的数据)
#include <stdio.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> int main() { int listen_fd = socket(AF_INET, SOCK_STREAM, 0); int opt = 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(9999); server_addr.sin_addr.s_addr = INADDR_ANY; bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)); listen(listen_fd, 5); int conn_fd = accept(listen_fd, NULL, NULL); // 缓冲区足够大,一次性把网卡收到的全读出来 char buffer[1024] = {0}; int n = read(conn_fd, buffer, sizeof(buffer) - 1); if (n > 0) { printf("服务端一次性读到了 %d 个字节,内容是: [%s]\n", n, buffer); } close(conn_fd); close(listen_fd); return 0; }编译并测试:
gcc tcp_sticky_client.c -o tcp_sticky_client gcc tcp_sticky_server.c -o tcp_sticky_server终端 A 跑服务端:
./tcp_sticky_server终端 B 跑客户端:
./tcp_sticky_client
👉现象:服务端极大概率打印出服务端一次性读到了 10 个字节,内容是: [HelloWorld]。这就是活生生的粘包!
实战 2:验证 UDP 不会粘包(选做)
你可以把上面的SOCK_STREAM换成SOCK_DGRAM,用sendto发两次,recvfrom收两次。你会发现,不管你发多快,UDP 必须收两次,一次收"Hello",一次收"World"。这就叫面向报文,有边界。