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 因数据报特性天然避免粘包,但需处理丢包和乱序问题

相关推荐
@兽血沸腾31 分钟前
Windows系统安装Jenkins部署应用至远程Windows服务器操作指南
运维·服务器·jenkins
承渊政道4 小时前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
讯捷蓝达6 小时前
戴尔R720xd服务器卡死在 “Configuring Memory...Done“,换了内存还是卡?问题根本不在内存
运维·服务器
大白玉米7 小时前
MoxPlayer 弹幕播放器源码智能解析线路管理系统自研内核
php
数字新视界8 小时前
动环监控信创适配在智能机房管理中的应用价值与未来展望
大数据·服务器·数据库·动环监控系统·动力与环境监控系统
爱和冰阔落8 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
lisanmengmeng9 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器
严肃小白兔9 小时前
Cloudflare 更新 API Token 管理:从最小权限到自动化凭据安全
网络·安全·自动化
Android系统攻城狮10 小时前
Linux FFmpeg深度解析之av_write_frame调用流程与实战(三十三)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频
上火的金鱼妹10 小时前
k8s生产实际问题排查(网关,数据库读写等)
运维·服务器