Linux网络编程:TCP 粘包与半包问题,如何正确处理 TCP 流式数据?

一、TCP 是字节流协议

之前学习 TCP 通信时,我们通常会这样写:

cpp 复制代码
send(sockfd, ...);
recv(sockfd, ...);

发送方调用 send 发送数据,接收方调用 recv 接收数据。

刚开始可能会觉得,发送方发送一条消息,接收方调用一次 recv 就能收到这条消息。

但实际上并不是这样。

TCP 是一种面向字节流的传输协议,它只保证数据能够按照顺序可靠地到达对端,并不保证发送方的一次发送操作与接收方的一次接收操作对应。

举个例子:

发送方连续发送两条消息:

text 复制代码
hello
world

接收方可能出现下面几种情况。

情况一:一次收到完整的两条消息。

text 复制代码
helloworld

情况二:第一次只收到部分数据。

text 复制代码
hel

后面再收到:

text 复制代码
loworld

情况三:先收到第一条消息,之后再收到第二条消息。

text 复制代码
hello
text 复制代码
world

这些情况都是有可能出现的。

这就是我们常说的粘包和半包问题。

二、什么是粘包和半包?

1. 粘包

发送方发送了多条消息,但是接收方一次读取时,可能将多条消息一起读取出来。

例如:

text 复制代码
发送方:

消息1:hello
消息2:world

接收方一次读取:

helloworld

此时,如果接收方不知道消息之间的边界,就无法确定哪里是第一条消息的结束位置。

2. 半包

发送方发送了一条完整的消息,但是接收方一次读取时,只读取到了其中一部分。

例如:

text 复制代码
发送方:

hello world

第一次接收:

hello

第二次接收:

 world

接收方第一次读取到的数据并不是一条完整的消息。

因此,我们不能简单地认为一次 recv 就能得到一个完整的请求。

TCP 只负责传输字节流,不负责维护应用层消息的边界。

这就需要我们在应用层自己解决。

三、如何解决粘包和半包?

既然 TCP 不会帮我们区分消息边界,那么我们就需要自己设计一种规则,让接收方能够判断一条消息什么时候结束。

常见的思路之一就是在报文中增加长度字段。

例如这样的报文格式:

text 复制代码
有效载荷长度\r\n
有效载荷内容\r\n

例如:

text 复制代码
5\r\n
hello\r\n

这里:

  • 5 表示有效载荷的长度;
  • 第一个 \r\n 用来分隔长度和内容;
  • hello 是真正需要传输的数据;
  • 最后的 \r\n 是结束分隔符。

这样,接收方就可以先读取长度,再根据长度判断当前是否已经收到完整的报文。

四、实现 Encode:给报文增加长度信息

首先实现一个编码函数:

cpp 复制代码
const std::string LineBreakSep = "\r\n";

std::string Encode(const std::string &message)
{
    std::string len = std::to_string(message.size());

    std::string package =
        len + LineBreakSep + message + LineBreakSep;

    return package;
}

这个函数主要做了三件事:

  1. 获取原始消息的长度;
  2. 将长度转换成字符串;
  3. 按照约定的格式拼接完整报文。

例如:

cpp 复制代码
std::string message = "hello";

std::string package = Encode(message);

最终得到:

text 复制代码
5\r\nhello\r\n

这样,原本没有边界信息的字符串就变成了一条具有明确格式的报文。

需要注意,长度字段记录的是有效载荷的长度,而不是整个报文的长度。

五、实现 Decode:判断报文是否完整

编码完成之后,接收方还需要实现对应的解码函数。

cpp 复制代码
bool Decode(std::string &package, std::string *message)
{
    // 查找第一个分隔符
    auto pos = package.find(LineBreakSep);

    if(pos == std::string::npos)
        return false;

    // 获取长度字符串
    std::string lens = package.substr(0, pos);

    // 转换成整数
    int messagelen = std::stoi(lens);

    // 计算完整报文的长度
    int total = lens.size()
              + messagelen
              + 2 * LineBreakSep.size();

    // 判断报文是否完整
    if(package.size() < total)
        return false;

    // 提取有效载荷
    *message = package.substr(
        pos + LineBreakSep.size(),
        messagelen
    );

    // 删除已经处理过的报文
    package.erase(0, total);

    return true;
}

这段代码是整个协议处理中比较重要的部分,需要理解每一步的作用。

1. 找到长度字段的结束位置

cpp 复制代码
auto pos = package.find(LineBreakSep);

我们约定长度和消息内容之间使用 \r\n 分隔。

因此,只要找到第一个 \r\n,就能知道长度字符串在哪里结束。

如果没有找到,说明目前收到的数据还不够,暂时无法解析。

cpp 复制代码
if(pos == std::string::npos)
    return false;

2. 获取消息长度

cpp 复制代码
std::string lens = package.substr(0, pos);

int messagelen = std::stoi(lens);

例如:

text 复制代码
5\r\nhello\r\n

此时:

text 复制代码
lens = "5"
messagelen = 5

3. 判断报文是否完整

cpp 复制代码
int total = lens.size()
          + messagelen
          + 2 * LineBreakSep.size();

为什么要这样计算?

因为完整报文由三部分组成:

text 复制代码
长度字段 + 分隔符 + 有效载荷 + 结束分隔符

如果当前缓冲区中的数据长度还小于 total,就说明报文还没有接收完整。

cpp 复制代码
if(package.size() < total)
    return false;

此时不能急着解析,也不能删除缓冲区中的数据。

应该保留已经收到的内容,等待后续数据到来。

4. 提取消息并删除已处理的数据

确认报文完整之后,就可以提取有效载荷:

cpp 复制代码
*message = package.substr(
    pos + LineBreakSep.size(),
    messagelen
);

这里从第一个分隔符之后开始读取,读取长度为 messagelen 的内容。

最后:

cpp 复制代码
package.erase(0, total);

删除已经处理完成的报文。

为什么要删除?

因为缓冲区中可能不止一条消息。

例如:

text 复制代码
5\r\nhello\r\n
5\r\nworld\r\n

如果一次读取到了两条完整报文,那么解析第一条之后,还需要保留第二条,等待下一次解析。

因此这里只删除已经处理的部分,而不是直接清空整个缓冲区。

六、为什么 Recv 需要拼接数据?

前面讲过,TCP 接收数据时可能出现半包。

因此,我们不能每次 recv 完就直接解析,而应该先把收到的数据保存起来。

cpp 复制代码
bool Recv(std::string *buffer, int size)
{
    char inbuffer[size];

    ssize_t n = recv(_sockfd, inbuffer, size - 1, 0);

    if(n > 0)
    {
        inbuffer[n] = 0;

        *buffer += inbuffer;

        return true;
    }
    else if(n == 0)
        return false;
    else
        return false;
}

这里最重要的是:

cpp 复制代码
*buffer += inbuffer;

它不是直接覆盖原来的缓冲区,而是把新收到的数据拼接到原有数据后面。

例如:

第一次收到:

text 复制代码
5\r\nhe

第二次收到:

text 复制代码
llo\r\n

拼接之后:

text 复制代码
5\r\nhello\r\n

这时候再调用 Decode,就能够得到完整的消息。

如果一次收到多条消息,解码函数也可以逐条提取,每次处理完成后删除对应的数据,直到缓冲区中剩下不完整的报文。

七、完整的数据处理流程

结合前面的内容,整个 TCP 应用层数据处理流程可以整理成下面这样。

发送方:

  1. 创建 Request 对象;
  2. 将 Request 序列化成 JSON 字符串;
  3. 调用 Encode 添加报文长度和分隔符;
  4. 使用 Send 发送完整报文。

接收方:

  1. 使用 Recv 接收数据;
  2. 将数据拼接到接收缓冲区;
  3. 调用 Decode 判断报文是否完整;
  4. 如果完整,就提取有效载荷;
  5. 将 JSON 字符串反序列化成 Request 对象;
  6. 处理业务逻辑;
  7. 生成 Response,并按照相同的方式返回。

整个流程可以表示为:

text 复制代码
          客户端
             |
       创建 Request
             |
          序列化
             |
           Encode
             |
          Send
             |
             v
       TCP 网络传输
             |
             v
          服务器
             |
           Recv
             |
        拼接接收缓冲区
             |
           Decode
             |
        判断报文完整性
             |
          反序列化
             |
        处理业务逻辑
             |
        创建 Response
             |
          序列化
             |
           Encode
             |
          Send
             |
             v
          客户端

八、代码示例

cpp 复制代码
// Socket.hpp
#pragma once

#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdlib>
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

namespace SocketModule
{
    using namespace LogModule;
    const static int gbacklog = 16;
    // 模板方法模式(基类控制流程,派生类实现具体步骤)
    class Socket
    {
    public:
        virtual ~Socket() {}
        virtual void SocketOrDie() = 0;
        virtual void BindOrDie(uint16_t port) = 0;
        virtual void ListenOrDie(int backlog) = 0;
        virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
        virtual void Close() = 0;
        virtual int Recv(std::string *out) = 0;
        virtual int Send(const std::string &message) = 0;
        virtual int Connect(const std::string &server_ip, uint16_t port) = 0;

    public:
        void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog)
        {
            SocketOrDie();
            BindOrDie(port);
            ListenOrDie(backlog);
        }
        void BuildTcpClientSocketMethod()
        {
            SocketOrDie();
        }
    };

    const static int defaultfd = -1;
    class TcpSocket : public Socket
    {
    public:
        TcpSocket() : _sockfd(defaultfd)
        {
        }
        TcpSocket(int fd) : _sockfd(fd)
        {
        }
        ~TcpSocket() {}
        void SocketOrDie() override
        {
            _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
            {
                LOG(LogLevel::FATAL) << "socket error";
                exit(SOCKET_ERR);
            }
            LOG(LogLevel::INFO) << "socket success";
        }
        void BindOrDie(uint16_t port) override
        {
            InetAddr localaddr(port);
            int n = ::bind(_sockfd, localaddr.NetAddrPtr(), localaddr.NetAddrLen());
            if (n < 0)
            {
                LOG(LogLevel::FATAL) << "bind error";
                exit(BIND_ERR);
            }
            LOG(LogLevel::INFO) << "bind success";
        }
        void ListenOrDie(int backlog) override
        {
            int n = ::listen(_sockfd, backlog);
            if (n < 0)
            {
                LOG(LogLevel::FATAL) << "listen error";
                exit(LISTEN_ERR);
            }
            LOG(LogLevel::INFO) << "listen success";
        }
        std::shared_ptr<Socket> Accept(InetAddr *client) override
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int fd = ::accept(_sockfd, CONV(peer), &len);
            if (fd < 0)
            {
                LOG(LogLevel::WARNING) << "accept warning...";
                return nullptr;
            }
            client->SetAddr(peer);
            return std::make_shared<TcpSocket>(fd);
        }
        int Recv(std::string *out) override
        {
            char buffer[1024];
            ssize_t n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);
            if (n > 0)
            {
                buffer[n] = 0;
                *out += buffer;
            }
            return n;
        }
        int Send(const std::string &message) override
        {
            return send(_sockfd, message.c_str(), message.size(), 0);
        }
        void Close() override
        {
            if (_sockfd >= 0)
                ::close(_sockfd);
        }
        int Connect(const std::string &server_ip, uint16_t port) override
        {
            InetAddr server(server_ip, port);
            return ::connect(_sockfd, server.NetAddrPtr(), server.NetAddrLen());
        }

    private:
        int _sockfd;
    };
}
cpp 复制代码
// Protocol.hpp 
#pragma once
#include "Socket.hpp"
#include <iostream>
#include <string>
#include <memory>
#include <jsoncpp/json/json.h>
#include <functional>

// 实现一个自定义的网络版本的计算器

using namespace SocketModule;

// client -> server
class Request
{
public:
    Request() {}
    Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper)
    {
    }
    std::string Serialize()
    {
        Json::Value root;
        root["x"] = _x;
        root["y"] = _y;
        root["oper"] = _oper;

        Json::FastWriter writer;
        std::string s = writer.write(root);
        return s;
    }
    bool Deserialize(std::string &in)
    {
        Json::Value root;
        Json::Reader reader;
        bool ok = reader.parse(in, root);
        if (ok)
        {
            _x = root["x"].asInt();
            _y = root["y"].asInt();
            _oper = root["oper"].asInt();
        }
        return ok;
    }
    ~Request() {}
    int X() { return _x; }
    int Y() { return _y; }
    char Oper() { return _oper; }

private:
    int _x;
    int _y;
    char _oper; // + - * / %
};

// server -> client
class Response
{
public:
    Response() {}
    Response(int result, int code) : _result(result), _code(code)
    {
    }
    std::string Serialize()
    {
        Json::Value root;
        root["result"] = _result;
        root["code"] = _code;

        Json::FastWriter writer;
        return writer.write(root);
    }
    bool Deserialize(std::string &in)
    {
        Json::Value root;
        Json::Reader reader;
        bool ok = reader.parse(in, root);
        if (ok)
        {
            _result = root["result"].asInt();
            _code = root["code"].asInt();
        }
        return ok;
    }
    ~Response() {}
    void SetResult(int res)
    {
        _result = res;
    }
    void SetCode(int code)
    {
        _code = code;
    }
    void ShowResult()
    {
        std::cout << "计算结果是: " << _result << "[" << _code << "]" << std::endl;
    }

private:
    int _result; // 运算结果
    int _code;   // 0:success  1,2,3,4->不同的运算异常的情况
};

const std::string sep = "\r\n";

using func_t = std::function<Response(Request &req)>;

class Protocol
{
public:
    Protocol()
    {
    }
    Protocol(func_t func) : _func(func)
    {
    }
    std::string Encode(const std::string &jsonstr)
    {
        std::string len = std::to_string(jsonstr.size());
        return len + sep + jsonstr + sep; // 应用层封装报头
    }
    bool Decode(std::string &buffer, std::string *package)
    {
        size_t pos = buffer.find(sep);
        if (pos == std::string::npos)
            return false;
        std::string package_len_str = buffer.substr(0, pos);
        int package_len_int = std::stoi(package_len_str);

        int target_len = package_len_str.size() + package_len_int + 2 * sep.size();
        if (buffer.size() < target_len)
            return false;

        // buffer至少有一个完整的报文
        *package = buffer.substr(pos + sep.size(), package_len_int);
        buffer.erase(0, target_len);
        return true;
    }
    void GetRequest(std::shared_ptr<Socket> &sock, InetAddr &client)
    {
        std::string buffer_queue;
        while (true)
        {
            int n = sock->Recv(&buffer_queue);
            if (n > 0)
            {
                std::string json_package;
                // 1. 解析报文, 提取完整的json请求, 如果不完整就让服务器继续读取
                while (Decode(buffer_queue, &json_package))
                {
                    LOG(LogLevel::DEBUG) << client.StringAddr() << " 请求: " << json_package;

                    // 2. 一定拿到了一个完整的报文. 对请求json串反序列化
                    Request req;
                    bool ok = req.Deserialize(json_package);
                    if (!ok)
                        continue;

                    // 3. 一定得到了一个内部属性已经被设置了的req, req->resp完成计算功能
                    Response resp = _func(req);

                    // 4. 序列化
                    std::string json_str = resp.Serialize();

                    // 5. 添加自定义长度
                    std::string send_str = Encode(json_str); // 携带长度的应答报文

                    // 6. 直接发送
                    sock->Send(send_str);
                }
            }
            else if (n == 0)
            {
                LOG(LogLevel::INFO) << "client:" << client.StringAddr() << "Quit!";
                break;
            }
            else
            {
                LOG(LogLevel::WARNING) << "client:" << client.StringAddr() << ", recv error";
                break;
            }
        }
    }
    bool GetResponse(std::shared_ptr<Socket> &client, std::string &resp_buffer, Response *resp)
    {
        while (true)
        {
            int n = client->Recv(&resp_buffer);
            if (n > 0)
            {
                std::string json_package;
                while (Decode(resp_buffer, &json_package))
                {
                    resp->Deserialize(json_package);
                }
                return true;
            }
            else if (n == 0)
            {
                std::cout << "server quit" << std::endl;
                return false;
            }
            else
            {
                std::cout << "recv error" << std::endl;
                return false;
            }
        }
    }
    std::string BuildRequestString(int x, int y, char oper)
    {
        // 1. 构建一个完整的请求
        Request req(x, y, oper);

        // 2. 序列化
        std::string json_req = req.Serialize();

        // 3. 添加长度报头
        return Encode(json_req);
    }
    ~Protocol()
    {
    }

private:
    // Request _req;
    // Response _resp;
    func_t _func;
};
cpp 复制代码
// NetCal.hpp 
#pragma once

#include "Protocol.hpp"
#include <iostream>

class Cal
{
public:
    Response Execute(Request &req)
    {
        Response resp(0, 0); // code: 0表示成功
        switch (req.Oper())
        {
        case '+':
            resp.SetResult(req.X() + req.Y());
            break;
        case '-':
            resp.SetResult(req.X() - req.Y());
            break;
        case '*':
            resp.SetResult(req.X() * req.Y());
            break;
        case '/':
        {
            if (req.Y() == 0)
            {
                resp.SetCode(1); // 1表示除零错误
            }
            else
            {
                resp.SetResult(req.X() / req.Y());
            }
        }
        break;
        case '%':
        {
            if (req.Y() == 0)
            {
                resp.SetCode(2); // 2表示 mod 0错误
            }
            else
            {
                resp.SetResult(req.X() % req.Y());
            }
        }
        break;
        default:
            resp.SetCode(3); // 非法操作
            break;
        }
        return resp;
    }
};
cpp 复制代码
// TcpServer.hpp
#include "Socket.hpp"
#include <iostream>
#include <memory>
#include <sys/wait.h>
#include <functional>

using namespace SocketModule;
using namespace LogModule;

using ioservice_t = std::function<void(std::shared_ptr<Socket> &sock, InetAddr &client)>;

class TcpServer
{
public:
    TcpServer(uint16_t port, ioservice_t service) : _port(port),
                                                    _listensockptr(std::make_unique<TcpSocket>()),
                                                    _isrunning(false),
                                                    _service(service)
    {
        _listensockptr->BuildTcpSocketMethod(_port);
    }
    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            InetAddr client;
            auto sock = _listensockptr->Accept(&client);
            if (sock == nullptr)
            {
                continue;
            }
            LOG(LogLevel::DEBUG) << "accept success..." << client.StringAddr();

            pid_t id = fork();
            if (id < 0)
            {
                LOG(LogLevel::FATAL) << "fork error";
                exit(FORK_ERR);
            }
            else if (id == 0)
            {
                // 子进程
                _listensockptr->Close();
                if (fork() > 0)
                    exit(OK);
                // 孙子进程在执行任务, 已经是孤儿了
                _service(sock, client);
                sock->Close();
                exit(OK);
            }
            else
            {
                // 父进程
                sock->Close();
                pid_t rid = ::waitpid(id, nullptr, 0);
                (void)rid;
            }
        }
        _isrunning = false;
    }
    ~TcpServer() {}

private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensockptr;
    bool _isrunning;
    ioservice_t _service;
};
cpp 复制代码
// main.cc
#include "NetCal.hpp"
#include "Protocol.hpp"
#include "TcpServer.hpp"
#include "Daemon.hpp"

void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " port" << std::endl;
}

// ./tcpserver port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    std::cout << "服务器已经启动, 已经是一个守护进程了" << std::endl;

    Daemon(0, 0);
    // daemon(1, 1);

    // Enable_Console_Log_Strategy();
    Enable_File_Log_Strategy();

    // 1. 顶层
    std::unique_ptr<Cal> cal = std::make_unique<Cal>();

    // 2. 协议层
    std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>([&cal](Request &req)->Response{
        return cal->Execute(req);
    });

    // 3. 服务器层
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::stoi(argv[1]), 
        [&protocol](std::shared_ptr<Socket> &sock, InetAddr &client){
            protocol->GetRequest(sock, client);
    });
    
    tsvr->Start();
    return 0;
}

九、总结

这部分让我对 TCP 通信有了更深入的理解。

之前写网络程序时,很容易把一次 send 和一次 recv 当成对应的操作。

但实际上,TCP 传输的是连续的字节流,并不会保留应用层消息的边界。

因此,我们需要自己设计应用层协议,通过长度字段和分隔符判断一条消息是否完整。

这篇主要掌握三个内容:

  1. TCP 是字节流协议,存在粘包和半包问题。
  2. 可以通过在报文中添加长度字段,帮助接收方确定消息边界。
  3. 接收方需要使用缓冲区保存数据,只有收到完整报文之后才能进行解析。

另外,序列化和报文封装也不是一回事:

  • 序列化负责把结构化数据转换成字符串;
  • Encode 负责给字符串增加报文边界信息;
  • Decode 负责从 TCP 接收缓冲区中提取完整报文;
  • 反序列化负责把提取出的字符串还原成结构化数据。

把这几个过程区分开,整个应用层协议的设计思路就清楚多了。

相关推荐
AI_Gump18 分钟前
Harness篇|DeepSeek Harness 上手机:五条安卓移植路线与 App 内 Linux 拆解
android·linux·人工智能·智能手机·deepseek
星恒随风2 小时前
MySQL 从零入门(六):复合查询详解——多表查询、自连接、子查询与 UNION
linux·服务器·数据库·mysql
少司府2 小时前
嵌入式入门篇(二):初识STM32F103ZET6
linux·python·stm32·单片机·嵌入式硬件·mcu·嵌入式软件
无足鸟ICT3 小时前
【RHCA+】IO调度算法
linux·服务器
火云代理3 小时前
Python 使用代理 IP 前,先弄清 Requests 的配置来源与验证方法
网络·python·网络协议·tcp/ip·ip
wacsii3 小时前
【Linux进程(四)】命令行参数与环境变量
linux·服务器
无奈笑天下3 小时前
银河麒麟上lsblk命令教程详解
linux·运维·服务器·数据库
如夏倾城3 小时前
QUIC——HTTP/3 的唯一底层传输协议 及 0-RTT恢复
网络·网络协议·tcp/ip·安全·http
YLCHUP4 小时前
【思维&图论建模】题解:债权(debt)_图论_建模_思路转化_思维题目_C++算法竞赛
c++·笔记·算法·数学建模·贪心算法·深度优先·图论