【Net】TCP粘包与半包

文章目录

  • TCP粘包与半包
    • [1 背景](#1 背景)
    • [2 粘包(packet stick)](#2 粘包(packet stick))
    • [3 半包(packet split)](#3 半包(packet split))
    • [4 为什么会出现粘包/半包?](#4 为什么会出现粘包/半包?)
    • [5 如何解决?](#5 如何解决?)
    • [6 示例](#6 示例)
    • [7 总结](#7 总结)

TCP粘包与半包

在网络编程中,粘包半包问题是常见的 TCP 协议特有问题,尤其在基于流的传输协议中(如 TCP),它们常导致接收端无法正确还原发送端原本的一条条消息。


1 背景

TCP 是"字节流"协议,不保留消息边界,它只是一个字节流协议,只保证字节的顺序和完整性,但不关心应用层每条消息的边界。这就导致了"粘包"和"半包"的出现。


2 粘包(packet stick)

定义多条数据包被粘在一起,接收端一次接收到了多条消息数据。

举例:

客户端连续发送两条消息:

text 复制代码
[hello][world]

由于 TCP 是流式协议,可能在接收端变成:

text 复制代码
[helloworld]

此时接收端无法确定 "hello" 和 "world" 的边界。


3 半包(packet split)

定义一条完整的数据被拆成了几部分接收,接收端一次只能收到其中的一部分。

举例:

客户端发送一条 10 字节的消息:

text 复制代码
[helloworld]

可能接收端第一次 recv 只收到:

text 复制代码
[hello]

下一次再收到:

text 复制代码
[world]

也就是说,一条消息被拆成了"半包"。


4 为什么会出现粘包/半包?

  • TCP 特性导致:
    1. TCP 是字节流,不维护消息边界;
    2. Nagle 算法 会将小包合并发送(导致粘包);
    3. 接收端 buffer 缓冲区大小不确定,一次 read/recv 可能读不到完整数据(导致半包);
    4. 操作系统的发送/接收策略 也会影响包的合并与拆分。

5 如何解决?

  • 通用思路:在应用层实现消息边界的识别机制

    以下几种常见方案可以避免粘包/半包问题:

    1. 定长协议

      • 每条消息固定长度(例如每条消息都是 128 字节)。
      • 优点:实现简单;
      • 缺点:浪费带宽,不适用于变长消息。
    2. 添加分隔符

      • 每条消息结尾加特定分隔符(如 "\r\n")。
      • 接收端通过查找分隔符来拆分消息;
      • 缺点:消息内容中不能出现分隔符。
    3. 长度前缀协议(最常用)

      • 每条消息前加一个固定长度的字段表示消息体长度(如 4 字节整数):

        text 复制代码
        [4字节长度][消息体]

        示例:

        text 复制代码
        [00000005][hello]
        [00000005][world]
        • 接收端读取前 4 字节判断消息长度,再读取对应长度的消息体,精确拆包。

6 示例

C++ 实现的长度前缀协议收发逻辑示例,适用于基于 TCP 的客户端或服务器程序,用于解决粘包/半包问题。


  • 协议格式

    [4字节消息长度][消息体内容]

    • 消息长度为 uint32_t(网络字节序)

  • 核心发送/接收逻辑

发送端逻辑(附加长度前缀)

cpp 复制代码
#include <arpa/inet.h> // htonl
#include <string>
#include <unistd.h>    // write

bool sendMessage(int sockfd, const std::string& message) {
    uint32_t len = htonl(message.size()); // 转为网络字节序
    std::string packet;
    packet.append(reinterpret_cast<const char*>(&len), sizeof(len)); // 4字节长度
    packet.append(message); // 实际消息体

    size_t totalSent = 0;
    while (totalSent < packet.size()) {
        ssize_t sent = write(sockfd, packet.data() + totalSent, packet.size() - totalSent);
        if (sent <= 0) return false;
        totalSent += sent;
    }
    return true;
}

接收端逻辑(支持粘包/半包)

cpp 复制代码
#include <arpa/inet.h> // ntohl
#include <unistd.h>    // read
#include <vector>
#include <string>

bool recvExact(int sockfd, void* buffer, size_t length) {
    size_t total = 0;
    while (total < length) {
        ssize_t n = read(sockfd, (char*)buffer + total, length - total);
        if (n <= 0) return false; // 连接关闭或出错
        total += n;
    }
    return true;
}

bool recvMessage(int sockfd, std::string& outMessage) {
    uint32_t len_net;
    if (!recvExact(sockfd, &len_net, sizeof(len_net))) return false;

    uint32_t len = ntohl(len_net);
    if (len > 10 * 1024 * 1024) return false; // 限制最大消息长度,防止攻击

    std::vector<char> buffer(len);
    if (!recvExact(sockfd, buffer.data(), len)) return false;

    outMessage.assign(buffer.begin(), buffer.end());
    return true;
}

客户端完整用法

cpp 复制代码
std::string msg = "hello world";
sendMessage(sockfd, msg);

std::string received;
if (recvMessage(sockfd, received)) {
    std::cout << "Received: " << received << std::endl;
}

说明与扩展建议

项目 说明
字节序 使用 htonl/ntohl 保证跨平台兼容
粘包支持 多条消息合并也能正确拆分
半包支持 recvExact 保证完整读取
安全性 应添加最大长度检查,防止恶意攻击
异步扩展 可结合 epoll 实现非阻塞版本

7 总结

问题 表现 原因 解决方式
--------
粘包 多条消息合并 TCP 合并包 定长、分隔符、长度前缀
半包 一条消息被拆开 TCP 拆包 接收端维护 buffer,支持多次接收拼接
相关推荐
M78佐菲7 小时前
Linux学习笔记:TCP协议
linux·笔记·学习·tcp/ip·算法
weixin_416667969 小时前
【无标题】
运维·服务器·网络
予昊9 小时前
从零实现“在线五子棋对战“:WebSocket 实时通信 + 段位匹配
java·开发语言·网络·websocket
上海云盾-小余11 小时前
流量攻击复盘:为什么 WAF 完好,业务依旧瘫痪
运维·服务器·网络
戴西软件11 小时前
戴西iDWS.3DViz Suite数据轻量化可视化软件,从传统桌面软件向云端协同的重大突破
大数据·运维·网络·人工智能·机器学习·3d
芯盾时代11 小时前
《金融业网络安全管理办法(征求意见稿)》全条款深度拆解(三)
网络·安全·网络安全
米饭不加菜11 小时前
ESP-01S通过TCP直接与手机建立通信
网络·tcp/ip·智能手机
Huathy-雨落江南,浮生若梦12 小时前
Caddy 学习笔记 - 反向代理与 IP 封禁
笔记·学习·tcp/ip
闲云野鹤在人间12 小时前
OpenStack架构介绍和安装流程
linux·网络·架构·openstack
xx~t13 小时前
嵌入式——Linux软件编程——网络1
linux·c语言·网络·vscode·网络协议