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

应用层:程序员真正"写代码"的地方

我们平时写的网络程序------聊天工具、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

关键结论

  1. TCP 连接在每一端都有两个缓冲区:发送缓冲区和接收缓冲区。

  2. 所以一台主机可以同时发消息和收消息 ,这就是全双工。

  3. 这也是为什么一个 TCP sockfd 既能读又能写。

  4. 数据什么时候发、发多少、出错了怎么办,由 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;
}

为什么这样设计能解决粘包?

因为报文是自描述的:

  1. 先读长度字段,直到遇到 \r\n;

  2. 根据长度知道有效载荷有多少字节;

  3. 只有凑齐 长度 + 载荷 + \r\n 才算一个完整报文;

  4. 多余的字节留在缓冲区,继续处理下一个报文。

一系列"半包"状态,比如:

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. 得到结果
相关推荐
dyxal1 小时前
Linux Crontab 防重复执行利器:flock 文件锁实战详解(脱敏版)
linux·运维·服务器
niuTaylor1 小时前
RK3568 Linux SDK 详解:SDK 是什么、板级差异与完整构建流程
linux·运维·服务器
傻啦嘿哟1 小时前
爬虫代理IP池从0到1:构建高可用代理池,彻底解决IP被封问题
网络·爬虫·tcp/ip
拾贰_C1 小时前
【Ubuntu | port】Linux进程端口号冲突问题解决
linux·运维·ubuntu
szial1 小时前
网络爬虫与 CDP 实战(一):网页数据到底在哪?从 HTTP 到分页采集
网络·爬虫·python·网络爬虫
wdfk_prog1 小时前
Wi-Fi Direct 教程 07:第一次 P2P Scan 如何真正发出——WPS/P2P IE、radio work 与 driver request
运维·服务器·ubuntu·wifi-direct
机核研创社1 小时前
服装自动化观察:接橡筋机的超声波熔切与冷刀,差别到底在哪
运维·自动化
欣欣之王来了1 小时前
AI合规专项:AI自动化决策的合规管控要点
运维·人工智能·自动化
guo_wen_qiang1 小时前
docker安装常见软件集合
运维·docker·容器