TCP 粘包

一、粘包问题详解

1. 粘包的概念
  • 定义
    指在 TCP 通信中,由于发送方和接收方的读写速度、数据量不一致,导致多个数据包被错误地合并成一个数据包处理的现象。
  • 产生原因
    • TCP 是流式协议(无边界),数据以字节流形式传输,内核缓冲区可能累积多包数据。
    • 发送方连续发送多个小数据包,接收方未及时读取,导致数据在缓冲区中粘连。
  • 关键区别
    UDP 是数据报协议(有边界),每个recvfrom返回一个完整数据报,不会出现粘包
2. 解决方法:封包与拆包
  • 核心思想 :在应用层为数据添加长度字段,明确数据包边界。
    1. 封包(发送方)
      将数据分为 "长度字段 + 数据内容" 两部分,长度字段通常占 4 字节(uint32_t),存储数据内容的字节数。
    2. 拆包(接收方)
      先读取长度字段,再根据长度读取对应字节数的数据内容。
  • 实现步骤
    1. 发送方将数据长度转换为网络字节序htonl),与数据一同发送。
    2. 接收方先读取 4 字节长度字段(ntohl转换为本地字节序),再循环读取指定长度的数据。

二、粘包处理代码示例(TCP)

1. 服务器端(拆包逻辑)

c

复制代码
#include "head.h"

int main(int argc, const char *argv[]) {
    // 1. 创建套接字
    int sfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sfd == -1) PRINTF_ERROR("socket error");

    // 2. 绑定端口
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port = htons(8888),
        .sin_addr.s_addr = INADDR_ANY
    };
    if (bind(sfd, (struct sockaddr*)&addr, sizeof(addr)) == -1)
        PRINTF_ERROR("bind error");

    // 3. 监听连接
    if (listen(sfd, 5) == -1) PRINTF_ERROR("listen error");

    // 4. 接受客户端连接
    struct sockaddr_in cliaddr;
    socklen_t cli_len = sizeof(cliaddr);
    int fd = accept(sfd, (struct sockaddr*)&cliaddr, &cli_len);
    if (fd == -1) PRINTF_ERROR("accept error");

    // 5. 接收数据(拆包逻辑)
    char buf[1024] = {0};
    while (1) {
        // 先接收4字节长度字段
        int ret = recv(fd, buf, 4, 0);
        if (ret <= 0) {
            printf("客户端关闭\n");
            break;
        }
        int data_len = ntohl(*(unsigned int*)buf); // 转换为本地字节序
        printf("接收数据长度: %d\n", data_len);

        // 再接收指定长度的数据内容
        int count = 0;
        while (count < data_len) {
            ret = recv(fd, buf + count, data_len - count, 0);
            if (ret <= 0) {
                PRINTF_ERROR("recv error");
                break;
            }
            count += ret;
        }
        if (count == data_len) {
            printf("数据接收完毕: [%s]\n", buf);
            memset(buf, 0, sizeof(buf)); // 清空缓冲区
        }
    }
    close(fd);
    close(sfd);
    return 0;
}
2. 客户端(封包逻辑)

c

复制代码
#include "head.h"

int main(int argc, const char *argv[]) {
    // 1. 创建套接字
    int sfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sfd == -1) PRINTF_ERROR("socket error");

    // 2. 连接服务器
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port = htons(8888),
        .sin_addr.s_addr = inet_addr("0.0.0.0")
    };
    if (connect(sfd, (struct sockaddr*)&addr, sizeof(addr)) == -1)
        PRINTF_ERROR("connect error");

    // 3. 模拟发送文件数据(粘包演示)
    char *msg = (char*)malloc(1024);
    int file_fd = open("./66.txt", O_RDONLY);
    if (file_fd == -1) PRINTF_ERROR("open error");

    while (1) {
        char buf[128] = {0};
        int size = read(file_fd, buf, rand() % 30); // 随机读取1-30字节
        if (size <= 0) {
            printf("文件读取完成\n");
            break;
        }

        // 封包:4字节长度 + 数据内容
        *((unsigned int*)msg) = htonl(size); // 存储长度(网络字节序)
        memcpy(msg + 4, buf, size); // 复制数据内容

        // 发送封包
        int count = 0;
        while (count < size + 4) {
            ret = send(sfd, msg + count, size + 4 - count, 0);
            if (ret == -1) PRINTF_ERROR("send error");
            count += ret;
        }
        printf("发送数据长度: %d | 内容: [%s]\n", size, buf);
        sleep(1); // 模拟发送间隔
    }
    free(msg);
    close(sfd);
    return 0;
}

三、关键技术点

1. 字节序转换
  • 问题 :不同主机可能采用不同字节序(大端 / 小端),需通过htonl/ntohl统一为网络字节序(大端)。

    c

    复制代码
    uint32_t len = htonl(data_length); // 主机字节序 → 网络字节序(发送方)
    uint32_t len = ntohl(*(uint32_t*)buf); // 网络字节序 → 主机字节序(接收方)
2. 循环读写
  • 原因recv/send可能无法一次性读写完整数据,需循环处理直到完成指定长度。

    c

    复制代码
    // 接收循环
    int count = 0;
    while (count < data_len) {
        ret = recv(fd, buf + count, data_len - count, 0);
        count += ret;
    }
    
    // 发送循环
    int count = 0;
    while (count < total_len) {
        ret = send(sfd, buf + count, total_len - count, 0);
        count += ret;
    }
3. UDP 无粘包特性
  • 原理 :UDP 以数据报为单位传输,每个recvfrom返回一个完整数据包,天然支持边界划分。

    c

    复制代码
    // UDP接收(无需处理粘包)
    struct sockaddr_in cliaddr;
    socklen_t cli_len = sizeof(cliaddr);
    int n = recvfrom(udp_fd, buf, sizeof(buf), 0, &cliaddr, &cli_len);

四、总结

特性 TCP 粘包 UDP 无粘包
协议类型 流式协议(无边界) 数据报协议(有边界)
问题根源 发送 / 接收速度不一致、缓冲区累积 每个数据报独立,内核自动维护边界
解决方法 应用层封包(长度字段 + 数据) 无需处理,直接按数据报接收
典型场景 文件传输、消息通信(需自定义协议) 实时数据传输(如 DNS、视频流)

注意:TCP 粘包是应用层问题,需通过协议设计解决;UDP 因数据报特性天然避免粘包,但需处理丢包和乱序问题

相关推荐
BingoGo1 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack1 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo2 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack2 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
Sinclair3 天前
简单几步,安卓手机秒变服务器,安装 CMS 程序
android·服务器
JaguarJack3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
Rockbean4 天前
用40行代码搭建自己的无服务器OCR
服务器·python·deepseek
茶杯梦轩4 天前
CompletableFuture 在 项目实战 中 创建异步任务 的核心优势及使用场景
服务器·后端·面试
JaguarJack4 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel