跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

手写 C++ TCP 服务器:自定义协议与粘包处理实战

综述由AI生成TCP 基于字节流传输,天然存在粘包半包问题。本文通过设计长度前缀协议(Len + Content),结合 C++ Socket 编程,实现了网络计算器服务端与客户端。核心在于应用层协议封装 Encode/Decode 函数,确保数据边界明确,解决了 read 读取不完整或多次合并的问题,并展示了 Fork 多进程模型下的完整代码实现。

人间失格发布于 2026/3/27更新于 2026/6/1016 浏览
手写 C++ TCP 服务器:自定义协议与粘包处理实战

TCP 网络通信与粘包问题

TCP 是面向字节流的协议,这意味着它不保留消息边界。发送端写入的数据可能被拆分成多个数据包发送,也可能被合并后一次性接收。这导致接收端无法直接判断一个完整的'报文'何时结束。

例如,发送方连续发送两个请求,接收方可能只读到部分数据,或者将两个请求混在一起读取。这就好比往文件里放东西,如果不知道对方怎么放的,拿的时候就会出错。所以一旦我们要发送一些带有结构化的数据时,就必须制定协议,这样才能满足我们想要返回结构化数据的需求。

自定义应用层协议设计

由于 TCP 是字节流协议,一次 read() 可能读到半个数据,也可能读到多个数据。因此我们需要自定义协议来明确数据边界。

协议格式

我们采用'长度前缀'模式。协议的格式设计如下:

len\n
content\n

举个例子就是:

6\n10 + 5\n

总之就是如下的格式:|len| \n |content| \n

协议封装实现

为了简化开发,我们将编码和解码逻辑封装成工具函数。

Encode(封装报文)
std::string Encode(std::string &content) {
    std::string s;
    size_t len = content.size();
    s += std::to_string(len);
    s += "\n";
    s += content;
    s += "\n";
    return s;
}

例如输入 10 + 20,就会变为 7\n10 + 20\n。

Decode(解析报文)
bool Decode(std::string &s, std::string *content) {
    size_t left_pos = s.find("\n");
    if (left_pos == std::string::npos) return false;
    std::string content_len = s.substr(0, left_pos);
    int len = std::stoi(content_len);
    if (s.size() < content_len.size() + len + 2)  ;
    *content = s.(left_pos + , len);
    s.(, content_len.() + len + );
     ;
}
return
false
substr
1
erase
0
size
2
return
true

Decode 做了三件事:

  1. 判断是否有完整头部(长度)
  2. 判断数据是否完整
  3. 解析 + 从缓冲区移除

核心业务逻辑

这里以网络版计算器为例,展示请求与响应的结构。

请求 request

客户端发送:10 + 5

结构:

class request {
public:
    int x_;
    int y_;
    char op_;
};

序列化:"10 + 5" 反序列化:string -> request

响应 response

服务器返回:"15 0"

结构:

class response {
public:
    int result_;
    int code_;
};

code 的含义:

code含义
0成功
1除 0
2取模 0
3非法操作

核心难点:解决 TCP 粘包问题

什么是粘包?

TCP 是面向字节流的协议:

  • 发送:请求 1 + 请求 2 + 请求 3
  • 接收:可能变成 请求 1 请求 2 | 请求 3

服务器的处理方法

在收到一个报文之后,首先会调用服务器的处理方法。在处理方法中会进行解码,如果收到的报文不能够分解为类似 6\n10 + 5\n 这样的格式,我们就会返回一个空字符串。而一旦返回的是一个空字符串,我们的服务器就知道这个报文不完整,就会继续接收新的报文,直到接收到一个完整的报文且可以通过 Decode 解码成功之后,程序才会继续进行。这样就保证了我们每次服务器处理的肯定是一个正确格式的报文。

服务器核心代码

while (true) {
    int sockfd = listenfd_.Accept(&client_port, &client_ip);
    if (fork() == 0) {
        while (1) {
            char buffer[1280];
            ssize_t s = read(sockfd, buffer, sizeof buffer - 1);
            if (s > 0) {
                inbuffer_stream += buffer;
                while (true) {
                    std::string info = callback_(inbuffer_stream);
                    if (info.empty()) break;
                    write(sockfd, info.c_str(), info.size());
                }
            } else {
                break;
            }
        }
    }
}

完整代码

自定义协议

#include <string>
#define blank_sep " "
#define protocol_sep "\n"

class request {
public:
    request(int x, int y, char op) : x_(x), y_(y), op_(op) { }
    request() { }
    ~request() { }
    std::string serialization() {
        std::string str;
        str += std::to_string(x_);
        str += blank_sep;
        str += op_;
        str += blank_sep;
        str += std::to_string(y_);
        return str;
    }
    bool Deserialization(std::string &in) {
        size_t leftpos = in.find(blank_sep);
        if (leftpos == std::string::npos) { return false; }
        std::string str_x = in.substr(0, leftpos);
        x_ = std::stoi(str_x);
        op_ = in[leftpos + 1];
        size_t rightpos = in.rfind(blank_sep);
        if (rightpos == std::string::npos) { return false; }
        std::string str_y = in.substr(rightpos + 1);
        y_ = std::stoi(str_y);
        return true;
    }
    void DebugPrint() {
        std::cout << "新请求构建完成:" << x_ << op_ << y_ << "=?" << std::endl;
    }
public:
    int x_;
    int y_;
    char op_;
};

class response {
public:
    response() { }
    response(int result, int code) : result_(result), code_(code) { }
    ~response() { }
    std::string serialization() {
        std::string str;
        str += std::to_string(result_);
        str += blank_sep;
        str += std::to_string(code_);
        return str;
    }
    bool Deserialization(std::string &in) {
        size_t pos = in.find(blank_sep);
        if (pos == std::string::npos) { return false; }
        std::string str_result = in.substr(0, pos);
        result_ = std::stoi(str_result);
        std::string str_code = in.substr(pos + 1);
        code_ = std::stoi(str_code);
        return true;
    }
    void DebugPrint() {
        std::cout << "结果响应完成,result: " << result_ << ", code: " << code_ << std::endl;
    }
public:
    int result_;
    int code_;
};

std::string Encode(std::string &content) {
    std::string s;
    size_t len = content.size();
    s += std::to_string(len);
    s += protocol_sep;
    s += content;
    s += protocol_sep;
    return s;
}

bool Decode(std::string &s, std::string *content) {
    size_t left_pos = s.find(protocol_sep);
    if (left_pos == std::string::npos) { return false; }
    std::string content_len = s.substr(0, left_pos);
    int len = std::stoi(content_len);
    if (s.size() < content_len.size() + len + 2) { return false; }
    *content = s.substr(left_pos + 1, len);
    s.erase(0, content_len.size() + len + 2);
    return true;
}

服务端

#include <iostream>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string>
#include <functional>

enum error { SocketErr = 2, BindErr, ListenErr, ConnectErr, };

class Socket {
public:
    Socket() { sockfd_ = socket(AF_INET, SOCK_STREAM, 0); if (sockfd_ < 0) { std::cout << "socket fail" << std::endl; exit(SocketErr); } }
    void Bind(uint16_t &port, std::string &ip) {
        struct sockaddr_in server;
        server.sin_family = AF_INET;
        server.sin_port = htons(port);
        inet_pton(AF_INET, ip.c_str(), &server.sin_addr);
        if (bind(sockfd_, (struct sockaddr *)&server, sizeof(server)) < 0) { std::cout << "server bind fail!" << std::endl; exit(BindErr); }
        std::cout << "server bind successful" << std::endl;
    }
    void Listen() {
        if (listen(sockfd_, 10) < 0) { std::cout << "server listen fail!" << std::endl; exit(ListenErr); }
        std::cout << "server listen successful" << std::endl;
    }
    int Accept(uint16_t *client_port, std::string *client_ip) {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int sockfd = accept(sockfd_, (struct sockaddr *)&client, &len);
        if (sockfd < 0) { std::cout << "accept fail!" << std::endl; return -1; }
        std::cout << "accept successful" << std::endl;
        *client_port = ntohs(client.sin_port);
        char ip[64];
        inet_ntop(AF_INET, &client.sin_addr, ip, sizeof ip);
        *client_ip = ip;
        return sockfd;
    }
    void Connect(uint16_t &server_port, std::string &server_ip) {
        struct sockaddr_in server;
        server.sin_family = AF_INET;
        server.sin_port = htons(server_port);
        inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr);
        if (connect(sockfd_, (struct sockaddr *)&server, sizeof server) < 0) { std::cout << "connect fail!" << std::endl; exit(ConnectErr); }
        std::cout << "connect successful!" << std::endl;
    }
    void Close() { close(sockfd_); }
    int fd() { return sockfd_; }
    ~Socket() { close(sockfd_); }
private:
    int sockfd_;
};

class CalculatorServer {
public:
    CalculatorServer() { }
    response CalculatorHandler(const request &req) {
        response res(0, 0);
        switch (req.op_) {
            case '+': res.result_ = req.x_ + req.y_; break;
            case '-': res.result_ = req.x_ - req.y_; break;
            case '*': res.result_ = req.x_ * req.y_; break;
            case '/': if (req.y_ == 0) { res.code_ = 1; break; } res.result_ = req.x_ / req.y_; break;
            case '%': if (req.y_ == 0) { res.code_ = 2; break; } res.result_ = req.x_ % req.y_; break;
            default: res.code_ = 3; break;
        }
        return res;
    }
    std::string Calculator(std::string& s) {
        std::string content;
        if (Decode(s, &content) == false) { return ""; }
        request req;
        bool r = req.Deserialization(content);
        if (!r) { return ""; }
        response res = CalculatorHandler(req);
        std::string ret = res.serialization();
        ret = Encode(ret);
        return ret;
    }
    ~CalculatorServer() { }
};

using func_t = std::function<std::string(std::string &)>;

class TcpServer {
public:
    TcpServer(uint16_t port, std::string ip, func_t callback) : port_(port), ip_(ip), callback_(callback) { }
    void InitServer() {
        listenfd_.Bind(port_, ip_);
        listenfd_.Listen();
        std::cout << "init server successful!" << std::endl;
    }
    void start() {
        signal(SIGCHLD, SIG_IGN);
        signal(SIGPIPE, SIG_IGN);
        while (true) {
            uint16_t client_port;
            std::string client_ip;
            int sockfd = listenfd_.Accept(&client_port, &client_ip);
            if (sockfd < 0) { continue; }
            if (fork() == 0) {
                // 子进程
                listenfd_.Close();
                std::string inbuffer_stream;
                while (1) {
                    char buffer[1280];
                    ssize_t s = read(sockfd, buffer, sizeof buffer - 1);
                    if (s > 0) {
                        buffer[s] = 0;
                        inbuffer_stream += buffer;
                        while (true) {
                            std::string info = callback_(inbuffer_stream);
                            std::cout << info << std::endl;
                            if (info.empty()) { break; }
                            write(sockfd, info.c_str(), info.size());
                        }
                    } else if (s == 0) { break; }
                    else { break; }
                }
                close(sockfd);
                exit(0);
            }
            close(sockfd);
        }
    }
    ~TcpServer() { }
private:
    Socket listenfd_;
    uint16_t port_;
    std::string ip_;
    func_t callback_;
};

int main(int argc,char* argv[]) {
    if(argc != 3) { exit(0); }
    uint16_t server_port = std::atoi(argv[2]);
    std::string server_ip = argv[1];
    CalculatorServer cal;
    TcpServer* ser = new TcpServer(server_port,server_ip,std::bind(&CalculatorServer::Calculator, &cal, std::placeholders::_1));
    ser->InitServer();
    ser->start();
    return 0;
}

客户端

#include <iostream>
#include <cassert>
#include <unistd.h>
#include "Protocol.hpp"
#include "Socket.hpp"

static void Usage(const std::string &proc) {
    std::cout << "\nUsage: " << proc << " serverip serverport\n" << std::endl;
}

// ./clientcal ip port
int main(int argc, char *argv[]) {
    if (argc != 3) {
        Usage(argv[0]);
        exit(0);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);
    Socket sockfd;
    sockfd.Connect(serverport, serverip);
    srand(time(nullptr) ^ getpid());
    int cnt = 1;
    const std::string opers = "+-*/%=-=&^";
    std::string inbuffer_stream;
    while (cnt <= 10) {
        std::cout << "===============第" << cnt << "次测试....., " << "===============" << std::endl;
        int x = rand() % 100 + 1;
        usleep(1234);
        int y = rand() % 100;
        usleep(4321);
        char oper = opers[rand() % opers.size()];
        request req(x, y, oper);
        req.DebugPrint();
        std::string package;
        package = req.serialization();
        package = Encode(package);
        std::cout << package << std::endl;
        write(sockfd.fd(), package.c_str(), package.size());
        char buffer[128];
        ssize_t n = read(sockfd.fd(), buffer, sizeof(buffer) - 1);
        if (n > 0) {
            buffer[n] = 0;
            inbuffer_stream += buffer;
            std::string content;
            bool r = Decode(inbuffer_stream, &content);
            assert(r);
            response resp;
            r = resp.Deserialization(content);
            assert(r);
            resp.DebugPrint();
        }
        std::cout << "=================================================" << std::endl;
        sleep(1);
        cnt++;
    }
    sockfd.Close();
    return 0;
}

到这里,我们已经完整实现了一个基于 C++ 的 TCP 计算器服务器。我们应该建立这样一个认知:TCP 编程的本质不是收发数据,而是如何正确解析数据的边界。核心思想就一句话,就是 TCP 没有消息边界,所以我们必须设计应用层协议。

目录

  1. TCP 网络通信与粘包问题
  2. 自定义应用层协议设计
  3. 协议格式
  4. 协议封装实现
  5. Encode(封装报文)
  6. Decode(解析报文)
  7. 核心业务逻辑
  8. 请求 request
  9. 响应 response
  10. 核心难点:解决 TCP 粘包问题
  11. 什么是粘包?
  12. 服务器的处理方法
  13. 服务器核心代码
  14. 完整代码
  15. 自定义协议
  16. 服务端
  17. 客户端
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • JDK 11 安装与环境变量配置教程
  • 特征学习理论框架:深度解析单模态与多模态对比学习泛化差异
  • Whisper 语音识别本地化部署实战指南
  • Stable Diffusion 3.5 云端部署方案与实战指南
  • MySQL 5.7 彻底卸载与重装全流程及常见问题解决
  • Python 基础语法与面向对象编程核心指南
  • 虚拟机 VMware 下载安装指南(Windows/Linux)
  • OpenClaw 结合 cpolar 实现本地 AI 公网访问指南
  • MCP 协议详解:与 Function Call 的区别及 Python 实战
  • 前端技术趋势:React 18、Server Components 与 AI 辅助
  • JavaScript 空值判断工具函数
  • QGIS 到 WebGIS:网络瓦片地图离线化与可视化实战
  • Ubuntu 下 Python 连接 KingbaseES 数据库实现增删改查
  • OpenClaw 本地部署与 cpolar 外网访问配置实战
  • C++ STL 容器 vector 详解:定义、访问与操作
  • 知网 AIGC 检测不通过的处理思路与步骤
  • 大模型时代:新手与程序员转型 AI 行业的最佳路径
  • Microi 吾码与 JavaScript 技术整合实战
  • SpringBoot 使用 YAML 配置实现数据脱敏方案
  • Visual C++运行库管理指南:诊断、修复与维护

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online