C++面试5_ TCP 粘包

 一、什么是 TCP 粘包?

TCP 是面向字节流(byte stream)的协议,它只保证数据按序传输、不丢不重,但不保证消息边界
也就是说,TCP 看到的是一串连续的字节流,而不是一条条独立的“消息”。

因此,在应用层上发送的数据,到了接收方时,可能会出现以下几种情况:

         发送方                                                                    接收方


二、为什么会发生粘包/拆包?

TCP 在传输数据时,底层会根据各种因素动态决定包的发送时机。

粘包原因:

  1. 发送方缓冲区合并

    • 如果应用层连续多次调用 send(),数据可能被 TCP 缓冲区合并为一个包一起发送。

  2. Nagle 算法

    • TCP 默认启用 Nagle 算法,小包会等待前一个包的 ACK 再合并发送,以提高效率。

  3. 接收方缓冲区一次性读取多个包

    • recv() 调用时,如果缓冲区中有多个包的数据,会一次性读出。

拆包原因:

  1. 应用层消息过大

    • 一条消息超过了 TCP 缓冲区大小或 MTU,TCP 必须拆分为多个包。

  2. 接收方读取不及时

    • recv() 一次只读了部分数据

    • 粘包本身是正常现象,根本问题是如何在接收端正确“分包”解析

粘包本身是正常现象,根本问题是如何在接收端正确“分包”解析

✅ 方法一:固定长度协议

每条消息的长度固定,例如 128 字节。

  • 优点:简单。

  • 缺点:浪费空间,不适合变长数据。

  • // 每条消息固定128字节
    recv(sock, buffer, 128, 0);

✅ 方法二:特殊分隔符

在每条消息末尾加上特定分隔符(如 \n#END# 等),接收方按分隔符切割。

// 发送端
send(sock, "Hello#END#", ...);
send(sock, "World#END#", ...);

// 接收端解析
std::string msg = recv_data();
split(msg, "#END#");

✅ 方法三:消息头 + 消息体(推荐)

在消息前加一个固定长度的“包头”,记录后续数据的长度。

例如:前 4 个字节表示消息长度。

// 发送端
uint32_t len = htonl(data.size());
send(sock, &len, 4, 0);
send(sock, data.data(), data.size(), 0);

// 接收端
uint32_t len;
recv(sock, &len, 4, MSG_WAITALL);
len = ntohl(len);

std::vector<char> buf(len);
recv(sock, buf.data(), len, MSG_WAITALL);

优点:

  • 通用、可靠、广泛使用(HTTP、MQTT、RPC 协议都类似)。

  • 能有效避免粘包和拆包问题。

🔬 四、实际开发建议

五、小结

C++未处理粘包示列:

Sever.cpp

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);

    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8080);
    addr.sin_addr.s_addr = INADDR_ANY;

    bind(server_fd, (sockaddr*)&addr, sizeof(addr));
    listen(server_fd, 1);
    std::cout << "Server listening on 8080..." << std::endl;

    int client_fd = accept(server_fd, nullptr, nullptr);
    char buffer[1024];

    while (true) {
        int len = recv(client_fd, buffer, sizeof(buffer)-1, 0);
        if (len <= 0) break;
        buffer[len] = '\0';
        std::cout << "[Server recv] " << buffer << std::endl;
    }

    close(client_fd);
    close(server_fd);
}
client.cpp

#include <iostream>
#include <string>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8080);
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);

    connect(sock, (sockaddr*)&addr, sizeof(addr));

    for (int i = 0; i < 5; ++i) {
        std::string msg = "Message_" + std::to_string(i);
        send(sock, msg.c_str(), msg.size(), 0);
        usleep(1000); // 故意短间隔
    }

    close(sock);
}
[Server recv] Message_0Message_1Message_2Message_3Message_4
说明:五条消息“粘”在一起了!

✅(正确示例)—— 包头 + 包体协议

在每个消息前添加一个 4 字节整数表示消息长度。

server.cpp

#include <iostream>
#include <vector>
#include <unistd.h>
#include <arpa/inet.h>

ssize_t read_n(int fd, void* buf, size_t n) {
    size_t total = 0;
    while (total < n) {
        ssize_t ret = recv(fd, (char*)buf + total, n - total, 0);
        if (ret <= 0) return ret;
        total += ret;
    }
    return total;
}

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8080);
    addr.sin_addr.s_addr = INADDR_ANY;
    bind(server_fd, (sockaddr*)&addr, sizeof(addr));
    listen(server_fd, 1);
    std::cout << "Server listening on 8080..." << std::endl;

    int client_fd = accept(server_fd, nullptr, nullptr);

    while (true) {
        uint32_t len_net;
        ssize_t n = read_n(client_fd, &len_net, 4);
        if (n <= 0) break;

        uint32_t len = ntohl(len_net);
        std::vector<char> buf(len + 1, 0);
        read_n(client_fd, buf.data(), len);

        std::cout << "[Server recv] " << buf.data() << std::endl;
    }

    close(client_fd);
    close(server_fd);
}
client.cpp

#include <iostream>
#include <string>
#include <unistd.h>
#include <arpa/inet.h>

void send_packet(int sock, const std::string& msg) {
    uint32_t len = htonl(msg.size());
    send(sock, &len, 4, 0);
    send(sock, msg.c_str(), msg.size(), 0);
}

int main() {
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8080);
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
    connect(sock, (sockaddr*)&addr, sizeof(addr));

    for (int i = 0; i < 5; ++i) {
        std::string msg = "Message_" + std::to_string(i);
        send_packet(sock, msg);
        usleep(1000); // 仍然很短的间隔
    }

    close(sock);
}

运行结果:
[Server recv] Message_0
[Server recv] Message_1
[Server recv] Message_2
[Server recv] Message_3
[Server recv] Message_4

每条消息完整解析,粘包问题完美解决。

方法优点缺点
固定长度简单不灵活
特殊分隔符易实现不适合二进制数据
包头 + 包体通用可靠需要协议定义

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值