跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表
C++算法

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

TCP 通信基于字节流,天然缺乏消息边界,易导致粘包或半包问题。通过构建网络版计算器示例,演示如何设计应用层协议解决该问题。核心方案采用长度头加内容格式,配合序列化与反序列化逻辑,确保数据完整性。代码涵盖 Socket 封装、TCP 服务端循环、请求响应解析及粘包处理机制,提供完整的 C++ 实现参考。

MongoKing发布于 2026/3/21更新于 2026/5/55 浏览
手写 C++ TCP 服务器:自定义协议与粘包处理实战

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

基于字节流的 TCP 通信中,数据边界是一个经典难题。UDP 基于数据报传输,天然具有消息边界;而 TCP 是流式协议,发送方写入的数据在接收端可能以任意片段形式返回。如果应用层不定义规则,接收方很容易读到半包、粘包或错误顺序的数据。

TCP 通信的本质挑战

TCP 负责控制何时发送、发送多少以及出错重传,但具体数据如何组织由应用层决定。我们调用 write 将数据放入内核缓冲区后,TCP 会将其拆分成多个报文段发送。接收端的 read 同样受限于内核缓冲区和网络状况,可能出现以下情况:

  • 发送方发了一个长报文,接收方只读到了前半部分。
  • 发送方连续发了两个短报文,接收方一次性读到了合并后的数据。

这就像发送方先放了一个整形,又放了一个浮点数,而接收方按照字符串去解析,必然导致数据不一致。因此,一旦涉及结构化数据传输,必须制定应用层协议来明确边界。

自定义应用层协议设计

我们以一个简单的网络版计算器为例,演示如何通过协议解决上述问题。

架构概览

Client 
  │  
  │  request (请求)
  ▼  
TcpServer 
  │  
  │  callback (回调处理)
  ▼  
CalculatorServer 
  │  
  │  result (结果)
  ▼  
response (响应)

主要模块包括 Socket 封装、TCP 服务器框架、协议封装及计算逻辑。

协议格式

为了区分消息边界,我们采用 长度头 + 内容 的格式:

len\n content\n 例如发送 10 + 5,编码后为:

6\n10 + 5\n

这种设计让接收方能先读取头部获取长度,再根据长度精确截取后续内容,从而彻底解决粘包和半包问题。

核心实现细节

协议封装(Encode/Decode)

我们需要将业务数据序列化为符合协议的字符串,并在接收端反序列化。

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

// 编码:添加长度头和分隔符
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) {
     left_pos = s.(protocol_sep);
     (left_pos == std::string::npos)  ;
    
    std::string content_len = s.(, left_pos);
     len = std::(content_len);
    
    
     (s.() < content_len.() + len + )  ;
    
    *content = s.(left_pos + , len);
    
    s.(, content_len.() + len + );
     ;
}
size_t
find
if
return
false
substr
0
int
stoi
// 检查缓冲区是否包含完整数据
if
size
size
2
return
false
substr
1
// 移除已处理的数据
erase
0
size
2
return
true

Decode 函数做了三件事:判断是否有完整头部、验证数据长度是否匹配、解析并清理缓冲区。这是处理粘包的核心逻辑。

请求与响应结构

请求 (Request) 客户端发送运算指令,如 10 + 5。

class request {
public:
    int x_;
    int y_;
    char op_;
    // ... 序列化与反序列化方法 ...
};

响应 (Response) 服务器返回计算结果及状态码。

code含义
0成功
1除零错误
2取模零错误
3非法操作
class response {
public:
    int result_;
    int code_;
    // ... 序列化与反序列化方法 ...
};

服务端核心逻辑

服务端使用 fork() 创建子进程处理每个连接,主循环监听新请求。关键在于接收缓冲区的管理:

while (true) {
    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);
                    if (info.empty()) break;
                    write(sockfd, info.c_str(), info.size());
                }
            } else if (s == 0 || s < 0) {
                break;
            }
        }
        close(sockfd);
        exit(0);
    }
    close(sockfd);
}

这里的关键在于 callback_ 内部调用了 Decode。如果收到的数据不完整,Decode 返回失败,callback_ 返回空字符串,循环继续等待更多数据。只有当缓冲区凑齐一个完整报文时,才会进行业务处理并发送响应。

完整代码示例

服务端 (main.cpp)

#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <functional>

// ... Socket 类封装 ...
// ... CalculatorServer 类封装 ...

class TcpServer {
public:
    TcpServer(uint16_t port, std::string ip, std::function<std::string(std::string &)> 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);
                            if (info.empty()) break;
                            write(sockfd, info.c_str(), info.size());
                        }
                    } else {
                        break;
                    }
                }
                close(sockfd);
                exit(0);
            }
            close(sockfd);
        }
    }
private:
    Socket listenfd_;
    uint16_t port_;
    std::string ip_;
    std::function<std::string(std::string &)> 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;
}

客户端 (client.cpp)

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

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

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 << "次测试.....\n" << 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 = 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 << "=================================================\n" << std::endl;
        sleep(1);
        cnt++;
    }
    sockfd.Close();
    return 0;
}

总结

TCP 编程的本质不是简单的收发数据,而是如何正确解析数据的边界。通过引入自定义协议(长度头 + 内容),配合序列化和反序列化机制,我们可以可靠地处理粘包和半包问题。上述代码提供了一个完整的 C++ 实现参考,涵盖了 Socket 封装、多线程模型及协议解析逻辑。在实际开发中,可根据需求扩展协议字段或更换传输介质,但核心思想保持一致。

目录

  1. 手写 C++ TCP 服务器:自定义协议与粘包处理实战
  2. TCP 通信的本质挑战
  3. 自定义应用层协议设计
  4. 架构概览
  5. 协议格式
  6. 核心实现细节
  7. 协议封装(Encode/Decode)
  8. 请求与响应结构
  9. 服务端核心逻辑
  10. 完整代码示例
  11. 服务端 (main.cpp)
  12. 客户端 (client.cpp)
  13. 总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • GPT-5.5 超高智商模型1元抵1刀ChatGPT中转购买
  • 代充Chatgpt Plus/pro 帐号了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • 基于 LLama-Factory 的游戏 NPC 对话系统优化实践
  • FANUC 机器人机架号与插槽号配置指南
  • 人形机器人躯干系统设计:结构方案、驱动布局与刚度要求
  • Java synchronized 关键字详解:从字节码到对象头与锁升级
  • 基于 Z-Image-Turbo 的虚拟现实场景资产自动化生成路径
  • EvoMap:基于基因胶囊与生物逻辑的 AI 智能体进化方案
  • HarmonyOS RcList 组件事件处理机制与应用示例
  • 昇腾 NPU 部署 Llama-2-7b:六大核心场景性能实测
  • Obsidian Copilot API 密钥配置实战:OpenRouter、Gemini 与 OpenAI 集成
  • 自然语言处理在客户服务中的实战应用
  • 博士求职复盘:华为、字节与 DeepSeek 的抉择与思考
  • C++ 类和对象(中):默认成员函数详解
  • Python 调用高德地图 MCP 服务查询天气示例
  • WSL 环境下 Git 安装与配置指南
  • 利用 Cursor AI 快速生成 SpringBoot4+Vue3 学生信息管理系统
  • 基于Flink CDC的Neo4j实时图数据库同步实战
  • Claude Skills 实战指南:从安装到自动化开发
  • YOLO11 基于 DroneVehicle 数据集的无人机视角车辆目标检测
  • OpenClaw 漏洞预警:AI 代理安全与日志审计方案
  • 基于 OpenClaw 与 Claude 的自动化写作工作流搭建实践

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • 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