应用层:程序员真正"写代码"的地方
我们平时写的网络程序------聊天工具、HTTP 服务器、游戏服务器、远程计算器------都工作在 应用层。
应用层不关心底层网卡怎么发比特、路由器怎么转发,它只关心一件事:
双方约定好数据长什么样,怎么解释。
这个"约定",就是应用层协议。
协议的本质:一种"约定"
socket 的 API(send / recv)读写的都是字符串(字节流) 。
但现实中我们要传输的往往是结构化数据,比如:
-
一个用户信息:姓名、年龄、城市
-
一个计算请求:左操作数、右操作数、运算符
-
一个响应:结果、状态码
问题来了:结构体不能直接塞进 socket,怎么办?
答案:把结构体按某种规则转换成字符串,对方再按同样的规则还原回来。
这就是序列化 与反序列化。
网络版计算器:两种约定方案
需求:客户端把两个加数发给服务器,服务器计算后把结果返回。
方案一:极简字符串协议
客户端发送形如:
1+2
约定:
-
两个操作数都是整数
-
中间是运算符,只支持
+ -
数字和运算符之间没有空格
服务器收到后解析、计算、返回结果。
优点 :简单直观。
缺点 :扩展性差。想支持 - * / %、多位数、浮点数、多个操作数?协议会越来越复杂,解析容易出错。
方案二:结构体 + 序列化
定义结构体表示交互信息:
cpp
class Request {
int _data_x;
int _data_y;
char _oper;
};
class Response {
int _result;
int _code;
};
发送时把结构体序列化 成字符串,接收时再反序列化回结构体。
优点 :结构清晰、易扩展、易维护。
缺点:需要设计序列化规则和报文边界。
选择方案二,并使用现成的 Jsoncpp 库来做序列化。
重新理解 read / write / recv / send 与 TCP 全双工
主机 A 与主机 B 通信:
cpp
应用层:message -> 序列化 -> "你好啊\r\n20xx-yy-zz aa:bb:cc\r\n新时代好青年"
系统调用:write(sockfd, buffer, strlen(buffer))
内核:TCP 发送缓冲区 -> 网络层 -> 传输层 -> ... -> 对端接收缓冲区
对端:read(sockfd, buffer, strlen(buffer)) -> 反序列化 -> message
关键结论
-
TCP 连接在每一端都有两个缓冲区:发送缓冲区和接收缓冲区。
-
所以一台主机可以同时发消息和收消息 ,这就是全双工。
-
这也是为什么一个 TCP sockfd 既能读又能写。
-
数据什么时候发、发多少、出错了怎么办,由 TCP 控制,所以 TCP 叫"传输控制协议"。
扩展:为什么应用层不能假设"一次 recv 就是一个完整报文"?
因为 TCP 是字节流协议,它只保证:
-
字节按顺序到达
-
不丢、不重
但它不保证消息边界 。
也就是说:
-
你调用一次
send发送 100 字节,对方可能分 3 次recv才收完; -
你调用两次
send发送两条消息,对方可能一次recv全收到(粘包)。
这就是所谓的 TCP 粘包 / 半包问题。
注意:粘包不是 TCP 的 bug,而是字节流的天然特性。
解决粘包,是应用层协议的责任。
自定义协议:报文格式设计
设计的报文格式如下:
cpp
有效载荷的长度\r\n有效载荷的内容\r\n
图示:
cpp
+----------------+--------+----------------+--------+
| 有效载荷长度 | \r\n | 有效载荷内容 | \r\n |
+----------------+--------+----------------+--------+
-
有效载荷长度:告诉对方后面有多少字节
-
\r\n:特殊分隔符,方便 debug
-
有效载荷内容:真正要传的数据(比如 JSON 字符串)
编码 Encode
cpp
const std::string LineBreakSep = "\r\n";
std::string Encode(const std::string &message) {
std::string len = std::to_string(message.size());
std::string package = len + LineBreakSep + message + LineBreakSep;
return package;
}
解码 Decode
cpp
bool Decode(std::string &package, std::string *message) {
auto pos = package.find(LineBreakSep);
if (pos == std::string::npos)
return false;
std::string lens = package.substr(0, pos);
int messageLen = std::stoi(lens);
int total = lens.size() + messageLen + 2 * LineBreakSep.size();
if (package.size() < total)
return false;
*message = package.substr(pos + LineBreakSep.size(), messageLen);
package.erase(0, total);
return true;
}
为什么这样设计能解决粘包?
因为报文是自描述的:
-
先读长度字段,直到遇到
\r\n; -
根据长度知道有效载荷有多少字节;
-
只有凑齐
长度 + 载荷 + \r\n才算一个完整报文; -
多余的字节留在缓冲区,继续处理下一个报文。
一系列"半包"状态,比如:
cpp
"l"
"len"
"len\r\n"
"len\r\nx"
"len\r\nx op"
"len\r\nx op y"
"len\r\nx op y\r\n"
以及"粘包"状态:
cpp
"len\r\nx op y\r\n""len"
"len\r\nx op y\r\n""len\n"
"len\r\nx op y\r\n""len\nx op y\r\n"
Decode 函数通过判断 package.size() < total 来区分"还没收完"和"已经完整"。
扩展:常见的报文边界方案
| 方案 | 说明 | 例子 |
|---|---|---|
| 固定长度 | 每个报文长度固定 | 定长结构体 |
| 特殊分隔符 | 用 \r\n、\0 等分隔 |
HTTP 头、课件方案 |
| 长度字段 + 载荷 | 先读长度再读内容 | 课件方案、Redis 协议 |
| 类型 + 长度 + 值 | TLV 格式 | 二进制协议 |
| 自描述格式 | JSON、XML、Protobuf | 现代 RPC |
序列化与反序列化:Jsoncpp 实战
什么是序列化?
把内存中的对象 / 结构体转换成可以存储或传输的格式(字符串、二进制)。
什么是反序列化?
把序列化后的数据还原成原来的对象 / 结构体。
为什么选 Jsoncpp?
-
开源、跨平台
-
API 直观
-
支持 JSON 标准所有类型
-
有错误信息,方便调试
安装:
cpp
# Ubuntu
sudo apt-get install libjsoncpp-dev
# CentOS
sudo yum install jsoncpp-devel
Request 的序列化与反序列化
cpp
bool Serialize(std::string *out) {
Json::Value root;
root["datax"] = _data_x;
root["datay"] = _data_y;
root["oper"] = _oper;
Json::FastWriter writer;
*out = writer.write(root);
return true;
}
bool Deserialize(std::string &in) {
Json::Value root;
Json::Reader reader;
bool res = reader.parse(in, root);
if (res) {
_data_x = root["datax"].asInt();
_data_y = root["datay"].asInt();
_oper = root["oper"].asInt();
}
return res;
}
Response 的序列化与反序列化
cpp
bool Serialize(std::string *out) {
Json::Value root;
root["result"] = _result;
root["code"] = _code;
Json::FastWriter writer;
*out = writer.write(root);
return true;
}
bool Deserialize(std::string &in) {
Json::Value root;
Json::Reader reader;
bool res = reader.parse(in, root);
if (res) {
_result = root["result"].asInt();
_code = root["code"].asInt();
}
return res;
}
Jsoncpp 常用 API 速查
序列化:
| 方法 | 特点 |
|---|---|
toStyledString() |
格式化输出,带缩进换行 |
Json::StreamWriter |
可定制缩进、换行 |
Json::FastWriter |
最快,无额外空格换行 |
Json::StyledWriter |
格式化输出 |
反序列化:
| 方法 | 特点 |
|---|---|
Json::Reader::parse() |
常用,带错误信息 |
Json::CharReader |
更精细控制,一般不推荐 |
Json::Value 常用操作:
cpp
Json::Value root;
root["name"] = "joe"; // 赋值
root["age"] = 30;
std::string s = root.toStyledString();
// 类型检查
root.isNull();
root.isInt();
root.isString();
root.isArray();
root.isObject();
// 类型转换
int age = root["age"].asInt();
std::string name = root["name"].asString();
// 数组操作
root["arr"].append(1);
root["arr"].append(2);
size_t n = root["arr"].size();
工厂模式:建造类设计模式
一个简单的工厂:
cpp
class Factory {
public:
std::shared_ptr<Request> BuildRequest() {
return std::make_shared<Request>();
}
std::shared_ptr<Request> BuildRequest(int x, int y, char op) {
return std::make_shared<Request>(x, y, op);
}
std::shared_ptr<Response> BuildResponse() {
return std::make_shared<Response>();
}
std::shared_ptr<Response> BuildResponse(int result, int code) {
return std::make_shared<Response>(result, code);
}
};
好处:
-
把对象的创建和使用分离
-
方便统一管理生命周期(
shared_ptr) -
后续扩展新协议类型时,只需改工厂
完整处理流程总结
cpp
客户端:
1. 构造 Request 对象
2. Serialize -> JSON 字符串
3. Encode -> "len\r\nJSON\r\n"
4. send 到服务器
服务器:
1. recv 到缓冲区
2. Decode -> 提取完整报文
3. Deserialize -> Request 对象
4. 计算
5. 构造 Response 对象
6. Serialize -> JSON 字符串
7. Encode -> "len\r\nJSON\r\n"
8. send 回客户端
客户端:
1. recv 到缓冲区
2. Decode -> 提取完整报文
3. Deserialize -> Response 对象
4. 得到结果