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 中,read、write、recv、send 操作的是同一个连接对应的文件描述符。一个 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 返回客户端
从执行过程看,协议处理并不只是 send 和 recv。真正关键的是:
- 业务对象如何转成字符串;
- 字符串如何封装成带边界的报文;
- 接收端如何从缓冲区中拆出完整报文;
- 拆出的正文如何还原成业务对象。
五、使用 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 |
发送请求并接收响应 |
协议层不应该关心 bind、listen、accept 这些细节。它只需要面对字符串缓冲区,完成 Encode、Decode、Serialize、Deserialize。这种分层能让代码更容易维护:
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 服务,思路会清晰很多。