本文主题内容
- 理解应用层协议的作用
- 理解 TCP 字节流与消息边界
- 使用长度字段设计完整报文
- 实现编码、解码与缓冲区解析
- 理解序列化与反序列化
- 使用 JSON 表示结构化数据
- 处理半包、粘包和异常输入
引言:TCP 保证字节可靠有序到达,却不知道哪些字节属于一条完整业务消息。应用层协议的核心任务,就是让通信双方能够从连续字节流中准确识别、解析并处理一条条请求与响应。
一、应用层协议
1.1 协议解决什么问题
Socket 读写的是字节序列,业务处理的却通常是结构化对象。以计算请求为例,至少包含:
- 左操作数
- 运算符
- 右操作数
- 请求版本或类型
响应可能包含:
- 计算结果
- 状态码
- 错误信息
通信双方必须约定字段的顺序、类型、长度、编码和错误处理方式,这些约定共同组成应用层协议。
结构化业务字段需要按双方约定序列化成字节串,接收端再按相同约定反序列化。

1.2 文本协议与二进制协议
文本协议可读性好,调试方便,例如 HTTP 和 JSON;二进制协议更紧凑,解析速度和类型约束通常更强。
选择协议格式时需要考虑:
- 可读性与调试成本
- 报文大小
- 解析效率
- 跨语言能力
- 版本兼容
- 安全边界
二、TCP 字节流与消息边界
TCP 的每一端都有独立的发送缓冲区和接收缓冲区,write、send 把字节复制到发送缓冲区,read、recv 从接收缓冲区取出字节,因此一条连接可以同时收发;这些缓冲区只保存连续字节,不保存应用层消息边界。

2.1 为什么会出现半包与粘包
TCP 不保留 send 调用边界。发送方连续发送两条消息,接收方可能出现:
- 第一次只读到半条消息
- 一次读到一条半消息
- 一次读到多条完整消息
- 一条消息被分多次读取
这不是 TCP 异常,而是字节流协议的正常行为。
2.2 常见定界方案
| 方案 | 优点 | 注意事项 |
|---|---|---|
| 固定长度 | 解析简单 | 浪费空间,扩展困难 |
| 分隔符 | 可读性好 | 正文出现分隔符时需要转义 |
| 长度字段 | 通用、高效 | 必须限制最大长度 |
| 自描述格式 | 扩展性好 | 解析成本较高 |
本文使用长度字段:
text
正文长度\r\n正文\r\n
例:
text
6\r\n10 + 5\r\n
其中长度只统计正文,不统计两个 \r\n。
长度字段解决的是从 TCP 字节流中提取完整报文,序列化解决的是结构化对象怎样转换成正文。
三、请求与响应对象
3.1 Request
例:
cpp
class Request
{
public:
Request() = default;
Request(int x, int y, char op)
: x_(x), y_(y), op_(op)
{
}
int X() const
{
return x_;
}
int Y() const
{
return y_;
}
char Op() const
{
return op_;
}
private:
int x_ = 0;
int y_ = 0;
char op_ = '+';
};
3.2 Response
例:
cpp
class Response
{
public:
int result = 0;
int code = 0;
std::string message = "ok";
};
状态码可以约定为:
| 状态码 | 含义 |
|---|---|
| 0 | 成功 |
| 1 | 除零错误 |
| 2 | 非法运算符 |
| 3 | 请求格式错误 |
四、序列化与反序列化
4.1 序列化
序列化是把内存中的对象转换成适合存储或传输的字节序列。不能直接发送 C++ 对象内存,因为对象可能包含指针、填充字节和平台相关布局。
使用 JSON 表示请求:
json
{
"x": 10,
"y": 5,
"op": "+"
}
4.2 使用 JsonCpp
例:
cpp
#include <json/json.h>
#include <memory>
#include <string>
bool SerializeRequest(const Request &req, std::string *out)
{
Json::Value root;
root["x"] = req.X();
root["y"] = req.Y();
root["op"] = std::string(1, req.Op());
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
*out = Json::writeString(builder, root);
return true;
}
反序列化必须检查类型和字段:
cpp
bool DeserializeRequest(const std::string &text, Request *req)
{
Json::CharReaderBuilder builder;
std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
Json::Value root;
std::string errors;
if (!reader->parse(text.data(), text.data() + text.size(), &root, &errors))
{
return false;
}
if (!root["x"].isInt() || !root["y"].isInt() || !root["op"].isString())
{
return false;
}
std::string op = root["op"].asString();
if (op.size() != 1)
{
return false;
}
*req = Request(root["x"].asInt(), root["y"].asInt(), op[0]);
return true;
}
注意:反序列化成功只表示 JSON 语法正确,还要继续验证字段是否存在、类型是否正确、数值是否越界。
五、报文编码
5.1 Encode
例:
cpp
const std::string kLineBreak = "\r\n";
std::string Encode(const std::string &payload)
{
return std::to_string(payload.size()) + kLineBreak
+ payload + kLineBreak;
}
5.2 为什么需要最大长度
长度字段来自不可信网络。如果攻击者声明一个极大长度,服务器可能持续缓存数据并耗尽内存。
例:
cpp
constexpr size_t kMaxPayload = 1024 * 1024;
解析长度后必须确认:
- 长度字符串只包含数字
- 数值转换没有溢出
- 长度不超过上限
- 缓冲区确实包含完整正文和尾部分隔符
六、从缓冲区解析完整报文
服务器为每条连接维护输入缓冲区。每次收到数据后追加到缓冲区,再循环提取零条、一条或多条完整报文。
例:
cpp
#include <charconv>
#include <string>
enum class DecodeResult
{
Ok,
NeedMore,
BadMessage
};
DecodeResult DecodeOne(std::string *buffer, std::string *payload)
{
size_t pos = buffer->find("\r\n");
if (pos == std::string::npos)
{
return DecodeResult::NeedMore;
}
if (pos == 0 || pos > 20)
{
return DecodeResult::BadMessage;
}
size_t length = 0;
const char *begin = buffer->data();
const char *end = begin + pos;
auto [ptr, ec] = std::from_chars(begin, end, length);
if (ec != std::errc{} || ptr != end || length > kMaxPayload)
{
return DecodeResult::BadMessage;
}
size_t frameSize = pos + 2 + length + 2;
if (buffer->size() < frameSize)
{
return DecodeResult::NeedMore;
}
size_t payloadBegin = pos + 2;
if (buffer->compare(payloadBegin + length, 2, "\r\n") != 0)
{
return DecodeResult::BadMessage;
}
*payload = buffer->substr(payloadBegin, length);
buffer->erase(0, frameSize);
return DecodeResult::Ok;
}
处理循环:
cpp
for (;;)
{
std::string payload;
DecodeResult result = DecodeOne(&inbuffer, &payload);
if (result == DecodeResult::NeedMore)
{
break;
}
if (result == DecodeResult::BadMessage)
{
CloseConnection();
break;
}
HandleRequest(payload);
}
这个循环同时处理半包和粘包:不完整就保留缓冲区,完整就提取并继续解析下一条。
七、业务处理
例:
cpp
Response Calculate(const Request &req)
{
Response resp;
switch (req.Op())
{
case '+':
resp.result = req.X() + req.Y();
break;
case '-':
resp.result = req.X() - req.Y();
break;
case '*':
resp.result = req.X() * req.Y();
break;
case '/':
if (req.Y() == 0)
{
resp.code = 1;
resp.message = "division by zero";
}
else
{
resp.result = req.X() / req.Y();
}
break;
default:
resp.code = 2;
resp.message = "unsupported operator";
break;
}
return resp;
}
完整处理链路是:
text
recv -> 追加输入缓冲区 -> 解码完整报文 -> 反序列化
-> 业务处理 -> 序列化 -> 编码 -> 追加输出缓冲区 -> send
八、协议设计中的工程问题
8.1 版本兼容
协议应预留版本或消息类型字段。新增字段时,接收端应能忽略未知可选字段,必要字段缺失则明确报错。
8.2 部分写
输出缓冲区不一定一次发送完。send 返回部分字节后,只能删除已经发送的前缀,剩余内容继续等待写就绪。
8.3 超时与资源限制
需要限制:
- 单条报文最大长度
- 单连接输入缓冲区上限
- 空闲连接时间
- 单次解析的报文数量
- JSON 嵌套深度和字段数量
8.4 错误响应
协议错误、业务错误和服务器错误应该使用不同状态码。严重格式错误通常应关闭连接,避免解析状态失去同步。
九、总结
应用层协议把业务对象转换成双方都能理解的报文。TCP 是字节流,不能依赖一次收发对应一条消息,必须通过固定长度、分隔符或长度字段建立消息边界。
长度前缀协议通常经历编码、接收缓冲、解码、反序列化、业务处理、序列化和发送缓冲等阶段。每一步都必须处理不完整数据和异常输入,并限制报文长度与资源占用。
协议设计的正确性不只体现在正常请求能够解析,更体现在半包、粘包、恶意长度、字段缺失和部分写出现时仍能保持清晰状态。