2、粘包现象复现:亲手制造一个“数据混乱”现场

说实话,我刚开始学TCP编程那会儿,最头疼的就是粘包。书上讲得玄乎,什么“TCP是流式协议”、“数据没有边界”……我当时就想:你倒是让我看看它到底怎么个粘法啊!

所以这一节,咱们不扯理论,直接动手。写一个最简单的TCP服务器和客户端,故意让它们“粘”起来。你亲眼看到数据乱成一锅粥,比看十遍文档都管用。

2.1 粘包到底长什么样?

先给你打个比方。你往邮筒里扔了两封信:

  • 第一封:”你好”
  • 第二封:”世界”

结果收信人打开信箱,只看到一封信,上面写着”你好世界”。

这就是粘包——多个数据包在接收端被合并成了一个。

反过来还有一种情况:你只扔了一封长信,收信人却分两次拿,第一次拿到前半截,第二次拿到后半截。这叫拆包。

嗯,这两个问题其实是同一枚硬币的两面。咱们先看粘包怎么复现。

2.2 代码实战:故意制造粘包

我习惯用最简代码来复现问题。下面这个例子,服务器连续发送5条消息,客户端一次性接收——你看它会收到什么。

服务器端代码(server.c)

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port = htons(8888),
        .sin_addr.s_addr = INADDR_ANY
    };
    
    bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
    listen(sockfd, 5);
    
    int client = accept(sockfd, NULL, NULL);
    
    // 连续发送5条消息,故意不延时
    const char *msgs[] = {"Hello", "World", "TCP", "Sticky", "Packet"};
    for (int i = 0; i < 5; i++) {
        send(client, msgs[i], strlen(msgs[i]), 0);
        printf("发送: %s\n", msgs[i]);
    }
    
    close(client);
    close(sockfd);
    return 0;
}

客户端代码(client.c)

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port = htons(8888),
        .sin_addr.s_addr = inet_addr("127.0.0.1")
    };
    
    connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
    
    char buf[1024] = {0};
    // 只调用一次recv,试图一次性收完
    int n = recv(sockfd, buf, sizeof(buf), 0);
    printf("收到 %d 字节: %s\n", n, buf);
    
    close(sockfd);
    return 0;
}

2.3 运行结果:你猜怎么着?

先启动服务器,再启动客户端。控制台输出是这样的:

服务器端:
发送: Hello
发送: World
发送: TCP
发送: Sticky
发送: Packet

客户端:
收到 25 字节: HelloWorldTCPStickyPacket

看到了吗? 5条消息全部粘在一起了!客户端只调用了一次recv,就把所有数据一口气吞了下去。

你想想看,如果这是5个不同的业务指令,服务器根本没法区分哪段是“Hello”,哪段是“Packet”。这就是粘包带来的灾难。

核心结论:TCP是流式协议,它不关心你发送了几次数据。它只负责把字节流从A端搬到B端。至于这些字节怎么切分——那是应用层的事。

2.4 为什么偏偏是TCP会粘包?

我当年也问过这个问题:UDP怎么就不粘?

原因很简单:

  • UDP 是面向数据报的,每次sendto对应一次recvfrom,边界天然存在。
  • TCP 是面向字节流的,它内部有缓冲区,还有Nagle算法(后面会讲)。多个小包可能被合并发送,接收方也可能攒够了才通知应用层读取。

说白了,TCP的粘包不是“错误”,而是它的设计特性。你没法让TCP不粘,只能自己想办法拆包。

2.5 避坑指南:我曾经踩过的坑

我曾经在一个物联网项目里,设备上报数据时用了固定长度协议。结果设备端连续上报了3条状态,服务器端只收到一条合并后的数据,直接导致状态解析错误,设备被误判为离线。

排查了一整天,最后发现是发送间隔太短,触发了Nagle算法合并。从那以后,我养成了一个习惯:任何TCP通信,必须显式处理粘包,哪怕你只是写个Demo。

2.6 粘包与拆包的关系图

下面这张图,帮你理清粘包和拆包到底是怎么回事:

发送端(Server) 包1 包2 包3 TCP流 粘包 包1包2包3合并 接收端 (Client) 拆包 一个大包被拆成多次接收 接收端 (分2次收) 图例: 正常数据包 粘包(多个包合并) 接收端

我的小建议:初学者别急着学各种拆包方案。先亲手跑一遍上面的代码,亲眼看到粘包现象。你只有见过“敌人”长什么样,后面学“拆弹”才有感觉。

2.7 小结:这一节你学到了什么?

知识点 一句话总结
粘包现象 多个数据包在接收端被合并成一个
拆包现象 一个大数据包被拆分成多次接收
根本原因 TCP是流式协议,没有消息边界
复现方法 连续发送小数据包,接收端一次recv

嗯,粘包这事儿,说白了就是TCP的“天性”。你没法改变它,只能适应它。下一节咱们就来聊聊——怎么优雅地拆包。

可可精品资料库