Linux应用层自定义协议与序列化

Linux 应用层自定义协议与序列化:从报文设计到 TCP 计算器服务

摘要:TCP Socket 只能保证可靠的字节流传输,但不会告诉应用程序"一条业务消息从哪里开始、到哪里结束"。这篇文章围绕网络计算器这个例子,梳理应用层自定义协议的设计思路,并用 Jsoncpp 完成请求与响应的序列化、反序列化,再通过 length + payload 的报文格式解决 TCP 字节流边界问题。

前言

写完 TCP Socket 之后,很多人会有一个疑问:客户端 send 了一次,服务端 recv 一次,不就拿到数据了吗?简单 echo 程序看起来确实如此,但真实业务通常不会这么理想。

比如我们要实现一个网络计算器,客户端发送两个操作数和一个运算符,服务端计算后返回结果。业务数据不再是一句普通字符串,而是带有结构的请求:

text 复制代码
datax = 10
datay = 20
oper  = +

问题随之出现:

服务端如何知道这三个字段分别是什么?

一次 recv 读到的内容一定是一个完整请求吗?

如果客户端连续发送两次请求,服务端如何区分第一条和第二条?

解决这些问题的关键,就是在应用层定义一套双方都遵守的协议。

一、为什么需要应用层协议

协议本质上是一种约定。只要发送方构造的数据,接收方能够按照同样规则正确解析,这个规则就可以称为应用层协议。

对于网络计算器,可以有两种常见设计:

设计方式 示例 特点
直接传字符串 10+20 简单,但字段边界和扩展能力较弱
定义结构化请求 { "datax": 10, "datay": 20, "oper": "+" } 字段清晰,适合扩展和维护

直接传 10+20 并不是不可以,但它隐含了很多约定:两个数都是整数,中间只能有一个运算符,运算符前后没有空格,暂时不支持更多字段。随着业务变复杂,这种字符串解析会越来越脆弱。

更通用的做法是定义请求和响应结构,再把结构化数据转换为字符串进行网络传输。这个转换过程叫序列化 ;收到字符串后还原成结构化对象,叫反序列化

二、TCP 是字节流,应用层必须自己处理边界

在 TCP 中,readwriterecvsend 操作的是同一个连接对应的文件描述符。一个 TCP 连接在内核中同时维护发送缓冲区和接收缓冲区,所以同一个 socket 既可以读,也可以写,这也是 TCP 支持全双工通信的基础。
#mermaid-svg-amyOp6kJm4St07S8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-amyOp6kJm4St07S8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-amyOp6kJm4St07S8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-amyOp6kJm4St07S8 .error-icon{fill:#552222;}#mermaid-svg-amyOp6kJm4St07S8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-amyOp6kJm4St07S8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-amyOp6kJm4St07S8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-amyOp6kJm4St07S8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-amyOp6kJm4St07S8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-amyOp6kJm4St07S8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-amyOp6kJm4St07S8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-amyOp6kJm4St07S8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-amyOp6kJm4St07S8 .marker.cross{stroke:#333333;}#mermaid-svg-amyOp6kJm4St07S8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-amyOp6kJm4St07S8 p{margin:0;}#mermaid-svg-amyOp6kJm4St07S8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-amyOp6kJm4St07S8 .cluster-label text{fill:#333;}#mermaid-svg-amyOp6kJm4St07S8 .cluster-label span{color:#333;}#mermaid-svg-amyOp6kJm4St07S8 .cluster-label span p{background-color:transparent;}#mermaid-svg-amyOp6kJm4St07S8 .label text,#mermaid-svg-amyOp6kJm4St07S8 span{fill:#333;color:#333;}#mermaid-svg-amyOp6kJm4St07S8 .node rect,#mermaid-svg-amyOp6kJm4St07S8 .node circle,#mermaid-svg-amyOp6kJm4St07S8 .node ellipse,#mermaid-svg-amyOp6kJm4St07S8 .node polygon,#mermaid-svg-amyOp6kJm4St07S8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-amyOp6kJm4St07S8 .rough-node .label text,#mermaid-svg-amyOp6kJm4St07S8 .node .label text,#mermaid-svg-amyOp6kJm4St07S8 .image-shape .label,#mermaid-svg-amyOp6kJm4St07S8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-amyOp6kJm4St07S8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-amyOp6kJm4St07S8 .rough-node .label,#mermaid-svg-amyOp6kJm4St07S8 .node .label,#mermaid-svg-amyOp6kJm4St07S8 .image-shape .label,#mermaid-svg-amyOp6kJm4St07S8 .icon-shape .label{text-align:center;}#mermaid-svg-amyOp6kJm4St07S8 .node.clickable{cursor:pointer;}#mermaid-svg-amyOp6kJm4St07S8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-amyOp6kJm4St07S8 .arrowheadPath{fill:#333333;}#mermaid-svg-amyOp6kJm4St07S8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-amyOp6kJm4St07S8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-amyOp6kJm4St07S8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-amyOp6kJm4St07S8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-amyOp6kJm4St07S8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-amyOp6kJm4St07S8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-amyOp6kJm4St07S8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-amyOp6kJm4St07S8 .cluster text{fill:#333;}#mermaid-svg-amyOp6kJm4St07S8 .cluster span{color:#333;}#mermaid-svg-amyOp6kJm4St07S8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-amyOp6kJm4St07S8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-amyOp6kJm4St07S8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-amyOp6kJm4St07S8 .icon-shape,#mermaid-svg-amyOp6kJm4St07S8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-amyOp6kJm4St07S8 .icon-shape p,#mermaid-svg-amyOp6kJm4St07S8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-amyOp6kJm4St07S8 .icon-shape .label rect,#mermaid-svg-amyOp6kJm4St07S8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-amyOp6kJm4St07S8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-amyOp6kJm4St07S8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-amyOp6kJm4St07S8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 主机 B
主机 A
应用层数据
TCP 发送缓冲区
TCP 接收缓冲区
应用层数据
TCP 发送缓冲区
TCP 接收缓冲区

这里需要特别注意:TCP 保证字节可靠、有序到达,但它不保留应用层消息边界。也就是说:

text 复制代码
客户端发送:A
客户端发送:B

服务端可能读到:

text 复制代码
A

也可能读到:

text 复制代码
AB

甚至可能先读到 A 的一部分,下一次再读到剩余内容。这就是常说的"粘包/半包"问题。更准确地说,它不是 TCP 出错,而是应用层没有定义清楚如何从连续字节流中切出一条完整消息。

三、报文格式设计:长度 + 有效载荷

一个实用的协议格式可以这样设计:

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

假设有效载荷是 JSON:

json 复制代码
{"datax":10,"datay":20,"oper":"+"}

编码后的完整报文就是:

text 复制代码
35
{"datax":10,"datay":20,"oper":"+"}

这里的换行符只是分隔符,不属于业务内容。length 描述的是有效载荷长度,接收方先读出长度,再判断缓冲区中是否已经有足够多的字节组成一条完整报文。

部分 作用
length 告诉接收方有效载荷有多少字节
第一个 \n 分隔长度和正文
payload 真正的业务数据,可以是 JSON 字符串
第二个 \n 方便调试,也让报文格式更规整

这个格式的核心价值不是"换行",而是长度字段。只要拿到长度,接收方就能从 TCP 字节流中精确切出一条完整消息。

四、完整处理流程

网络计算器的一次请求处理,可以拆成以下步骤:
#mermaid-svg-MsdK6NZ8YWqTDvw9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MsdK6NZ8YWqTDvw9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .error-icon{fill:#552222;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .marker.cross{stroke:#333333;}#mermaid-svg-MsdK6NZ8YWqTDvw9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MsdK6NZ8YWqTDvw9 p{margin:0;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .cluster-label text{fill:#333;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .cluster-label span{color:#333;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .cluster-label span p{background-color:transparent;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .label text,#mermaid-svg-MsdK6NZ8YWqTDvw9 span{fill:#333;color:#333;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .node rect,#mermaid-svg-MsdK6NZ8YWqTDvw9 .node circle,#mermaid-svg-MsdK6NZ8YWqTDvw9 .node ellipse,#mermaid-svg-MsdK6NZ8YWqTDvw9 .node polygon,#mermaid-svg-MsdK6NZ8YWqTDvw9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .rough-node .label text,#mermaid-svg-MsdK6NZ8YWqTDvw9 .node .label text,#mermaid-svg-MsdK6NZ8YWqTDvw9 .image-shape .label,#mermaid-svg-MsdK6NZ8YWqTDvw9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .rough-node .label,#mermaid-svg-MsdK6NZ8YWqTDvw9 .node .label,#mermaid-svg-MsdK6NZ8YWqTDvw9 .image-shape .label,#mermaid-svg-MsdK6NZ8YWqTDvw9 .icon-shape .label{text-align:center;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .node.clickable{cursor:pointer;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .arrowheadPath{fill:#333333;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MsdK6NZ8YWqTDvw9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MsdK6NZ8YWqTDvw9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MsdK6NZ8YWqTDvw9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .cluster text{fill:#333;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .cluster span{color:#333;}#mermaid-svg-MsdK6NZ8YWqTDvw9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MsdK6NZ8YWqTDvw9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .icon-shape,#mermaid-svg-MsdK6NZ8YWqTDvw9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .icon-shape p,#mermaid-svg-MsdK6NZ8YWqTDvw9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .icon-shape .label rect,#mermaid-svg-MsdK6NZ8YWqTDvw9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MsdK6NZ8YWqTDvw9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MsdK6NZ8YWqTDvw9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MsdK6NZ8YWqTDvw9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否

客户端构造 Request
Request 序列化为 JSON
Encode 添加长度字段
send 写入 TCP 连接
服务端 recv 追加到接收缓冲区
Decode 能否切出完整报文
JSON 反序列化为 Request
执行计算逻辑
Response 序列化为 JSON
Encode 封装响应报文
send 返回客户端

从执行过程看,协议处理并不只是 sendrecv。真正关键的是:

  1. 业务对象如何转成字符串;
  2. 字符串如何封装成带边界的报文;
  3. 接收端如何从缓冲区中拆出完整报文;
  4. 拆出的正文如何还原成业务对象。

五、使用 Jsoncpp 表示请求和响应

Jsoncpp 可以把 C++ 对象转换为 JSON 字符串,也可以把 JSON 字符串解析回 Json::Value。在网络协议中,JSON 的好处是字段清晰,调试方便,扩展字段时也比较自然。

安装依赖:

bash 复制代码
sudo apt-get install libjsoncpp-dev

CentOS 系列可以使用:

bash 复制代码
sudo yum install jsoncpp-devel

下面是一个为了理解协议流程而整理的完整示例。它不启动真实网络连接,而是用一个字符串模拟 TCP 接收缓冲区:连续放入两条请求,再用 Decode 一条一条解析出来。

cpp 复制代码
#include <iostream>
#include <stdexcept>
#include <string>
#include <jsoncpp/json/json.h>

namespace Protocol {
const std::string LineBreakSep = "\n";

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

bool Decode(std::string& buffer, std::string* message) {
    size_t pos = buffer.find(LineBreakSep);
    if (pos == std::string::npos) {
        return false;
    }

    std::string len_text = buffer.substr(0, pos);
    int message_len = 0;
    try {
        message_len = std::stoi(len_text);
    } catch (...) {
        throw std::runtime_error("bad package length");
    }

    int total_len = static_cast<int>(len_text.size())
                  + static_cast<int>(LineBreakSep.size())
                  + message_len
                  + static_cast<int>(LineBreakSep.size());

    if (static_cast<int>(buffer.size()) < total_len) {
        return false;
    }

    size_t message_start = pos + LineBreakSep.size();
    *message = buffer.substr(message_start, message_len);
    buffer.erase(0, total_len);
    return true;
}

class Request {
public:
    Request() = default;
    Request(int x, int y, char op) : datax_(x), datay_(y), oper_(op) {}

    bool Serialize(std::string* out) const {
        Json::Value root;
        root["datax"] = datax_;
        root["datay"] = datay_;
        root["oper"] = std::string(1, oper_);

        Json::FastWriter writer;
        *out = writer.write(root);
        return true;
    }

    bool Deserialize(const std::string& in) {
        Json::Value root;
        Json::Reader reader;

        if (!reader.parse(in, root)) {
            return false;
        }

        datax_ = root["datax"].asInt();
        datay_ = root["datay"].asInt();

        std::string op = root["oper"].asString();
        oper_ = op.empty() ? '\0' : op[0];
        return true;
    }

    int X() const { return datax_; }
    int Y() const { return datay_; }
    char Oper() const { return oper_; }

private:
    int datax_ = 0;
    int datay_ = 0;
    char oper_ = '\0';
};

class Response {
public:
    Response() = default;
    Response(int result, int code) : result_(result), code_(code) {}

    bool Serialize(std::string* out) const {
        Json::Value root;
        root["result"] = result_;
        root["code"] = code_;

        Json::FastWriter writer;
        *out = writer.write(root);
        return true;
    }

private:
    int result_ = 0;
    int code_ = 0;
};

Response Calculate(const Request& req) {
    int x = req.X();
    int y = req.Y();

    switch (req.Oper()) {
    case '+':
        return Response(x + y, 0);
    case '-':
        return Response(x - y, 0);
    case '*':
        return Response(x * y, 0);
    case '/':
        if (y == 0) return Response(0, 1);
        return Response(x / y, 0);
    case '%':
        if (y == 0) return Response(0, 2);
        return Response(x % y, 0);
    default:
        return Response(0, 3);
    }
}
}

int main() {
    using namespace Protocol;

    Request req1(10, 20, '+');
    Request req2(30, 5, '/');

    std::string body1;
    std::string body2;
    req1.Serialize(&body1);
    req2.Serialize(&body2);

    std::string tcp_buffer;
    tcp_buffer += Encode(body1);
    tcp_buffer += Encode(body2);

    std::string message;
    while (Decode(tcp_buffer, &message)) {
        Request req;
        if (!req.Deserialize(message)) {
            std::cerr << "bad request json" << std::endl;
            continue;
        }

        Response resp = Calculate(req);
        std::string response_body;
        resp.Serialize(&response_body);
        std::cout << Encode(response_body);
    }

    return 0;
}

编译运行:

bash 复制代码
g++ protocol_demo.cc -std=c++11 -ljsoncpp -o protocol_demo
./protocol_demo

可能看到类似输出:

text 复制代码
22
{"code":0,"result":30}
21
{"code":0,"result":6}

输出中的第一行 22 表示后面 JSON 响应正文的长度。第二条响应长度是 21,因为 6 只有一个字符,而 30 有两个字符。

六、关键代码解析

1. Encode:给业务数据加上边界信息

cpp 复制代码
std::string Encode(const std::string& message) {
    std::string len = std::to_string(message.size());
    return len + "\n" + message + "\n";
}

message 是已经序列化好的业务正文。Encode 不关心正文是 JSON、普通字符串还是其他格式,它只负责把正文包装成应用层报文。

很多初学者会把"序列化"和"封包"混在一起。它们其实是两层逻辑:

动作 解决的问题
序列化 结构化对象如何变成字符串
封包 一条字符串消息如何在 TCP 字节流中被识别出来

2. Decode:从缓冲区中切出完整报文

cpp 复制代码
bool Decode(std::string& buffer, std::string* message)

这里的 buffer 不是一次 recv 的临时数组,而是应用层维护的接收缓冲区。每次 recv 到新数据后,都应该追加到这个缓冲区:

cpp 复制代码
std::string inbuffer;
recv(sockfd, temp, sizeof(temp), 0);
inbuffer += temp;

然后循环调用 Decode

cpp 复制代码
while (Decode(inbuffer, &message)) {
    // 处理一条完整消息
}

这样可以同时处理三种情况:

TCP 缓冲区状态 Decode 行为
只有半条报文 返回 false,等待下一次读取
正好一条完整报文 返回 true,解析并清理缓冲区
多条报文粘在一起 多次返回 true,循环处理

3. Request 和 Response:业务字段要清晰

请求对象包含两个操作数和一个运算符:

cpp 复制代码
Request(int x, int y, char op)

响应对象包含计算结果和状态码:

cpp 复制代码
Response(int result, int code)

code 用来表达业务状态,例如:

code 含义
0 成功
1 除法除数为 0
2 取模除数为 0
3 不支持的运算符

这里还有一个容易踩的点:如果直接把 char 赋给 Json::Value,它可能以整数形式保存,调试时不够直观。示例中把运算符转为长度为 1 的字符串:

cpp 复制代码
root["oper"] = std::string(1, oper_);

这样 JSON 正文会更接近人的阅读习惯:

json 复制代码
{"datax":10,"datay":20,"oper":"+"}

七、Socket 封装在这里承担什么职责

在完整网络程序中,Socket 层一般负责连接建立和基础收发:

服务端动作 说明
socket 创建 TCP 套接字
bind 绑定本地端口
listen 进入监听状态
accept 获取与客户端通信的新 socket
recv/send 在连接上收发字节流

客户端通常是:

客户端动作 说明
socket 创建 TCP 套接字
connect 连接服务器
send/recv 发送请求并接收响应

协议层不应该关心 bindlistenaccept 这些细节。它只需要面对字符串缓冲区,完成 EncodeDecodeSerializeDeserialize。这种分层能让代码更容易维护:

text 复制代码
网络层:负责把字节送过去
协议层:负责把字节切成完整消息
业务层:负责处理 Request 并生成 Response

八、常见问题与易错点

1. 认为一次 recv 就等于一条完整消息

这是最常见的问题。TCP 面向字节流,没有消息边界。正确做法是维护应用层缓冲区,读到数据后追加进去,再循环尝试解包。

2. 只做 JSON 序列化,不设计报文边界

JSON 只能解决"结构化数据如何表达",不能解决"TCP 字节流里一条 JSON 从哪里结束"。所以 JSON 外面还需要一层报文格式,例如 length + payload

3. 长度字段和正文长度不一致

length 必须描述有效载荷长度,不应该把分隔符算进去。发送端和接收端只要有一处计算规则不一致,就会导致解包错位。

4. 没有处理反序列化失败

网络传来的数据不一定可信。reader.parse 失败时不能继续取字段,否则业务层会拿到无意义数据。

5. 忽略除零、非法运算符等业务错误

协议不仅要传成功结果,也要能表达失败原因。响应中的 code 字段就是为了让客户端区分"计算结果为 0"和"计算失败"。

6. 对多个协议类型没有预留空间

如果后续不只做计算器,还要支持登录、心跳、文件传输等功能,可以在报文中增加协议号或命令字段。例如:

text 复制代码
protocol_code\nlength\npayload\n

这样接收方可以先根据协议号分发给不同处理函数。

九、总结

应用层自定义协议解决的是"双方如何理解同一段网络字节"的问题。对于 TCP 程序来说,可靠传输只是基础,业务程序还必须自己定义消息格式、字段含义和边界规则。

这篇文章围绕网络计算器完成了一条比较完整的链路:Request/Response 表达业务数据,Jsoncpp 负责序列化和反序列化,Encode/Decode 负责封包和拆包,Socket 只承担字节收发。把这几层分清楚后,再去写更复杂的 TCP 服务,思路会清晰很多。