自定义协议 && 序列化与反序列化
网络通信中的自定义协议
什么是自定义协议
自定义协议是指在网络通信中,开发者根据特定应用需求自行设计的数据交换规则和格式。与HTTP、FTP等标准协议不同,自定义协议通常针对特定场景优化,具有更高的灵活性和效率。
自定义协议的主要特点包括:
- 专为特定应用场景设计
- 通常比通用协议更高效
- 可以根据需求自由定义数据格式
- 需要通信双方都实现协议解析逻辑
自定义协议的设计要素
设计一个良好的自定义协议需要考虑以下要素:
- 消息边界:如何确定一个完整消息的开始和结束
- 消息格式:消息头(Header)和消息体(Body)的结构
- 编解码方式:数据如何编码传输和解码读取
- 错误处理:如何处理不完整或错误的消息
- 扩展性:如何支持未来可能的协议升级
自定义协议的实现方式
常见的自定义协议实现方式包括:
- 基于TCP的二进制协议:如游戏、即时通讯等高性能场景
- 基于文本的协议:如SMTP等人类可读的协议
- 混合型协议:头部使用二进制,正文使用文本或二进制
自定义协议中的序列化和反序列化
序列化的概念与作用
序列化(Serialization)是指将数据结构或对象状态转换为可以存储或传输的格式的过程。在网络通信中,序列化主要用于:
- 将内存中的对象转换为字节流以便网络传输
- 保持数据的完整性和结构
- 实现跨平台、跨语言的数据交换
反序列化的概念与作用
反序列化(Deserialization)是序列化的逆过程,将字节流转换回原始的数据结构或对象。其主要作用包括:
- 重建发送端的数据结构
- 验证数据的完整性和正确性
- 恢复对象的完整状态
常见的序列化方式
自定义协议中常用的序列化方式包括:
二进制序列化:
- 直接内存拷贝
- Protocol Buffers
- Thrift
- FlatBuffers
文本序列化:
- JSON
- XML
- YAML
混合序列化:
- MessagePack
- BSON
简单自定义协议实现
该自定义协议中同样采用了序列化和反序列化的手段处理数据,实现了远程计算器功能,我们首先使用纯手搓的序列化及反序列化手段来实现:
首先我们实现一个 TCP 接口包装:
tcp.hpp
// tcp.hpp
#ifndef _TCP_HPP_
#define _TCP_HPP_ 1
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h> // sockaddr_in结构体定义
#include <arpa/inet.h> // 网络地址转换函数
#include <cstring> // bzero函数
#include "log.hpp" // 自定义日志头文件
// 错误码枚举定义
enum
{
socket_error = 1, // socket创建失败
bind_error, // bind失败
listen_error, // listen失败
connect_error, // connect失败
accept_error, // accept失败
};
// 线程本地存储的地址缓冲区,用于IP地址转换
inline thread_local char addr_buffer[1024];
// TCP网络操作命名空间
namespace tcp
{
// 创建TCP socket
// 返回值: 成功返回socket文件描述符,失败退出程序
int Socket()
{
// 创建IPv4 TCP socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("socket");
exit(socket_error); // 创建失败退出程序
}
return sockfd;
}
// 绑定socket到指定端口
// 参数: sockfd - socket文件描述符
// port - 要绑定的端口号
void Bind(int sockfd, int port)
{
struct sockaddr_in local; // IPv4地址结构
bzero(&local, sizeof(local)); // 清空结构体
// 设置地址族、IP地址和端口
local.sin_family = AF_INET; // IPv4
local.sin_addr.s_addr = INADDR_ANY; // 监听所有网络接口
local.sin_port = htons(port); // 主机字节序转网络字节序
// 绑定socket
int n = bind(sockfd, (struct sockaddr *)(&local), sizeof(local));
if (n != 0)
{
perror("bind");
exit(bind_error); // 绑定失败退出程序
}
}
// 从sockaddr_in结构体中提取IP和端口
// 参数: addr_in - 网络地址结构体
// ip - 输出参数,存储IP地址字符串
// port - 输出参数,存储端口号
void GetAddrAndPort(struct sockaddr_in &addr_in, std::string &ip, uint16_t &port)
{
port = ntohs(addr_in.sin_port); // 网络字节序转主机字节序
// 将IP地址转换为点分十进制字符串
inet_ntop(AF_INET, &addr_in.sin_addr, addr_buffer, sizeof(addr_buffer) - 1);
ip = addr_buffer; // 存储到输出参数
}
// 开始监听socket连接
// 参数: listen_sock - 监听socket
// backlog - 最大挂起连接数,默认为10
void Listen(int listen_sock, int backlog = 10)
{
int n = listen(listen_sock, backlog);
if (n == -1)
{
perror("listen");
exit(listen_error); // 监听失败退出程序
}
}
// 连接到服务器
// 参数: sockfd - 客户端socket
// server_ip - 服务器IP地址
// server_port - 服务器端口号
void Connect(int sockfd, const std::string &server_ip, const u_int16_t &server_port)
{
struct sockaddr_in server; // 服务器地址结构
bzero(&server, sizeof(server)); // 清空结构体
// 设置服务器地址信息
server.sin_port = htons(server_port); // 端口号转网络字节序
server.sin_family = AF_INET; // IPv4
server.sin_addr.s_addr = inet_addr(server_ip.c_str()); // IP地址转换
// 发起连接
int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
if (n == -1)
{
perror("connect");
exit(connect_error); // 连接失败退出程序
}
}
// 接受客户端连接
// 参数: listen_sockfd - 监听socket
// client_addr - 输出参数,存储客户端地址信息
// 返回值: 成功返回连接socket文件描述符,失败退出程序
int Accept(int listen_sockfd, sockaddr_in &client_addr)
{
socklen_t len = sizeof(client_addr); // 地址结构长度
// 接受连接
int n = accept(listen_sockfd, (struct sockaddr *)&client_addr, &len);
if (n == -1)
{
perror("accept");
exit(accept_error); // 接受失败退出程序
}
return n; // 返回新连接的socket描述符
}
};
#endif
然后是我们的重头戏:自定义协议实现:
protocol.hpp
#ifndef _PROTOCOL_HPP_
#define _PROTOCOL_HPP_ 1
#include <iostream>
// 协议分隔符定义
const std::string blank_space_sep = " "; // 字段间分隔符(空格)
const std::string protocol_sep = "\n"; // 协议分隔符(换行)
// 错误码枚举
enum {
divide_by_zero_error = 1, // 除零错误
operator_identify, // 操作符识别错误
};
// 协议解码函数
// 格式: "长度\n内容\n"
// 参数: package - 输入的网络数据包
// content - 输出的解码后内容
// 返回值: 解码成功返回true,失败返回false
bool Decode(std::string &package, std::string &content)
{
// 查找第一个分隔符位置
size_t pos = package.find(protocol_sep);
if(pos == std::string::npos) return false;
// 获取内容长度
size_t size = std::stoi(package.substr(0, pos));
// 查找第二个分隔符位置
size_t n_pos = package.find(protocol_sep, pos + 1);
size_t total_len = pos + size + 2; // 计算完整消息长度
// 验证消息格式是否正确
if(n_pos + 1 != total_len) {
// 格式错误,清空数据包
package.clear();
return false;
}
// 提取消息内容
content = package.substr(pos + 1, size);
// 删除已处理的部分
package.erase(0, total_len);
return true;
}
// 协议编码函数
// 格式: "长度\n内容\n"
// 参数: content - 要编码的内容
// 返回值: 编码后的字符串
std::string Encode(std::string &content)
{
std::string ret = std::to_string(content.size()); // 添加长度
ret += protocol_sep; // 添加分隔符
ret += content; // 添加内容
ret += protocol_sep; // 添加结束分隔符
return ret;
}
// 请求类(客户端→服务端)
class Request{
public:
// 构造函数,默认值表示无效请求
Request(int x = 0, int y = 0, char op = '?')
:x_(x),
y_(y),
op_(op){}
// 序列化方法:将对象转换为字符串
// 格式: "x op y"(如 "10 + 20")
std::string Serialize()
{
std::string ret = std::to_string(x_);
ret += blank_space_sep;
ret += op_;
ret += blank_space_sep;
ret += std::to_string(y_);
return ret;
}
// 反序列化方法:从字符串解析对象
// 参数: in - 输入字符串(格式必须为 "x op y")
// 返回值: 解析成功返回true,失败返回false
bool Deserialize(std::string &in)
{
// 查找第一个空格位置
size_t pos = in.find(blank_space_sep);
if(pos == std::string::npos) return false;
// 解析第一个操作数
x_ = std::stoi(in.substr(0, pos));
// 查找第二个空格位置
size_t npos = in.find(blank_space_sep, pos + 1);
if(npos == std::string::npos) return false;
// 解析操作符
op_ = in[pos + 1];
// 解析第二个操作数
y_ = std::stoi(in.substr(npos + 1));
return true;
}
public:
int x_; // 第一个操作数
int y_; // 第二个操作数
char op_; // 操作符(+,-,*,/等)
};
// 响应类(服务端→客户端)
class Response{
public:
// 构造函数
Response(int res = 0, int code = 0)
:res_(res),
code_(code){}
// 序列化方法:将对象转换为字符串
// 格式: "result code"(如 "30 0")
std::string Serialize()
{
std::string ret = std::to_string(res_);
ret += blank_space_sep;
ret += std::to_string(code_);
return ret;
}
// 反序列化方法:从字符串解析对象
// 参数: in - 输入字符串(格式必须为 "result code")
// 返回值: 解析成功返回true,失败返回false
bool Deserialize(std::string &in)
{
// 查找空格位置
size_t pos = in.find(blank_space_sep);
if(pos == std::string::npos) return false;
// 解析计算结果
res_ = std::stoi(in.substr(0, pos));
// 解析状态码
code_ = std::stoi(in.substr(pos + 1));
return true;
}
public:
int res_; // 计算结果
int code_; // 状态码(0表示成功,非0表示错误)
};
#endif
代码概述
协议格式:
- 外层协议格式:
"长度\n内容\n"(如"7\n10 + 20\n") - 请求格式:
"x op y"(如"10 + 20") - 响应格式:
"result code"(如"30 0")
- 外层协议格式:
主要功能:
Encode/Decode:处理网络传输层面的封包/解包Request:表示客户端请求,包含两个操作数和一个运算符Response:表示服务端响应,包含计算结果和状态码
特点:
- 使用简单的文本协议,便于调试
- 包含基本的错误检测机制
- 支持序列化和反序列化操作
- 使用空格和换行符作为分隔符
server_cal.hpp
以下是添加了详细注释的代码:
#ifndef _SERVER_CAL_HPP_
#define _SERVER_CAL_HPP_ 1;
#include <iostream>
#include "protocol.hpp" // 包含之前定义的自定义协议头文件
// 计算器服务端类
class ServerCal
{
public:
// 辅助计算函数,处理具体运算逻辑
// 参数: req - 包含计算请求的Request对象
// 返回值: 包含计算结果和状态的Response对象
Response CalculatorHelper(const Request &req)
{
Response resp(0, 0); // 初始化响应对象,默认结果0,状态码0(成功)
// 根据操作符执行不同运算
switch (req.op_)
{
case '+':
resp.res_ = req.x_ + req.y_; // 加法运算
break;
case '-':
resp.res_ = req.x_ - req.y_; // 减法运算
break;
case '*':
resp.res_ = req.x_ * req.y_; // 乘法运算
break;
case '/':
{
// 除法运算,检查除数是否为0
if (req.y_ == 0)
resp.code_ = divide_by_zero_error; // 除零错误
else
resp.res_ = req.x_ / req.y_; // 正常除法
}
break;
case '%':
{
// 取模运算,检查模数是否为0
if (req.y_ == 0)
resp.code_ = divide_by_zero_error; // 除零错误
else
resp.res_ = req.x_ % req.y_; // 正常取模
}
break;
default:
resp.code_ = operator_identify; // 未知操作符错误
break;
}
return resp;
}
// 主计算函数,处理协议解码和编码
// 参数: package - 网络接收到的原始数据包
// 返回值: 编码后的响应字符串,出错返回空字符串
std::string Calculator(std::string &package)
{
std::string content;
// 1. 解码网络数据包
if (!Decode(package, content)){
return ""; // 解码失败返回空字符串
}
// 2. 反序列化请求内容
Request req;
if(!req.Deserialize(content)) return ""; // 反序列化失败返回空字符串
// 3. 执行计算逻辑
Response resp = CalculatorHelper(req);
// 4. 序列化响应结果
content = resp.Serialize();
// 5. 编码为网络协议格式
content = Encode(content);
return content;
}
};
#endif
代码功能概述
这段代码实现了一个简单的网络计算器服务端,主要功能包括:
核心计算功能:
- 支持加(+)、减(-)、乘(*)、除(/)、取模(%)五种运算
- 处理除零错误和无效操作符等异常情况
协议处理流程:
- 接收网络数据包 → 解码 → 反序列化为请求对象 → 执行计算 → 序列化响应 → 编码为网络格式
- 使用之前定义的
protocol.hpp中的协议格式
错误处理:
- 解码失败时返回空字符串
- 反序列化失败时返回空字符串
- 运算错误通过Response的状态码返回
设计特点:
- 将协议处理和业务逻辑分离(Calculator和CalculatorHelper分工明确)
- 使用状态码表示运算结果状态
- 严格遵循自定义协议的格式要求
这样后,我们就可以通过将 std::string ServerCal::Calculator(std::string &package) 包装起来传递给 TCP 服务器从而使得服务器与协议功能解耦合
添加注释后的代码
#ifndef _TCPSERVERHPP_
#define _TCPSERVERHPP_ 1
#include "tcp.hpp" // TCP基础操作封装
#include "log.hpp" // 日志模块
#include <functional> // 函数对象支持
const u_int16_t default_port = 7777; // 默认服务端口
// 定义协议处理函数类型(输入请求包,返回响应包)
using func_t = std::function<std::string (std::string &)>;
class TcpServer{
public:
// 构造函数:初始化端口和协议处理回调
TcpServer(int port = default_port, func_t cal = nullptr)
:port_(port),
cal_(cal)
{}
// 初始化服务器:创建监听socket
void Init()
{
listen_sockfd_ = tcp::Socket(); // 创建socket
tcp::Bind(listen_sockfd_, port_); // 绑定端口
tcp::Listen(listen_sockfd_); // 开始监听
}
// 启动服务主循环
void Start()
{
struct sockaddr_in client; // 客户端地址信息
int client_sockfd = tcp::Accept(listen_sockfd_, client); // 接受连接
char buffer[1024]; // 接收缓冲区
std::string ip; // 客户端IP
u_int16_t port; // 客户端端口
tcp::GetAddrAndPort(client, ip, port); // 获取客户端地址
std::string package; // 累积接收的数据包
while(true)
{
// 接收客户端数据
int n = recv(client_sockfd, buffer, sizeof(buffer) - 1, 0);
if(n == -1) {
lg(Error, "recv data failed. client [%s: %d]", ip.c_str(), port);
}
else if(n == 0){ // 客户端断开连接
lg(Info, "client [%s: %d] quit", ip.c_str(), port);
close(client_sockfd);
break;
}else{
buffer[n] = 0; // 添加字符串结束符
package += buffer; // 累积数据
// 处理完整数据包(可能包含多个请求)
while(true)
{
std::string resp = cal_(package); // 调用协议处理回调
if(resp.empty()) break; // 无完整包可处理时退出
// 发送响应
if(send(client_sockfd, resp.c_str(), resp.size(), 0) == -1){
lg(Error, "send failed. client [%s: %d]", ip.c_str(), port);
}
}
}
}
}
private:
int listen_sockfd_; // 监听socket描述符
u_int16_t port_; // 服务端口
func_t cal_; // 协议处理回调函数
};
#endif
服务器解耦合设计特点
回调机制设计:
- 通过
func_t函数对象类型定义协议处理接口 - 构造函数接收外部传入的
std::string (std::string&)类型函数 - 实际处理时调用
cal_(package)触发业务逻辑
- 通过
与ServerCal的协作:
- 使用时将
ServerCal::Calculator方法绑定给cal_成员 - 服务器仅负责:接收原始数据 → 累积数据包 → 调用回调 → 返回响应
- 协议解析和业务处理完全由外部
Calculator实现
- 使用时将
优势体现:
- 服务器不感知具体协议格式(如换行分隔符等)
- 可灵活更换不同的协议处理器(只需符合函数签名)
- 业务逻辑变更不影响网络通信层
- 便于单元测试(可mock协议处理器)
数据处理流程:
flowchart TD A[网络层 TcpServer] -->|接收原始字节流| B[协议层 Decode] B -->|完整数据包| C[协议解析 Deserialize] C -->|Request对象| D[业务逻辑 CalculatorHelper] D -->|Response对象| E[协议组装 Serialize] E -->|响应字节流| F[协议层 Encode] F -->|发送数据| A
这种设计遵循了单一职责原则,使网络IO处理与协议解析分离,提高了代码的可维护性和扩展性。
客户端
以下是添加了详细注释的代码:
#include <iostream>
#include "protocol.hpp" // 自定义协议头文件
#include "tcp.hpp" // TCP网络操作头文件
using namespace std;
int main(int argc, char *argv[])
{
// 参数校验:需要服务器IP和端口
if(argc != 3)
{
cerr << "\nUsage: " << argv[0] << " server_ip server_port\n"
<< endl;
return 1;
}
// 解析命令行参数
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 创建TCP socket并连接服务器
int sockfd = tcp::Socket();
tcp::Connect(sockfd, serverip, serverport);
std::string recv_str; // 接收数据缓冲区
char buffer[1024]; // 临时接收缓冲区
// 主循环:持续与服务器交互
while(true)
{
// 1. 获取用户输入
int x, y;
char op;
cout << "Please Enter x: ";
cin >> x;
cout << "Please Enter y: ";
cin >> y;
cout << "Please Enter op: ";
cin >> op;
// 2. 构造请求并序列化
Request req(x, y, op);
string content = req.Serialize(); // 序列化为"x op y"格式
content = Encode(content); // 添加协议头尾
// 3. 发送请求到服务器
send(sockfd, content.c_str(), content.size(), 0);
// 4. 接收服务器响应
int n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if(n == -1){
perror("recv");
exit(1);
}
buffer[n] = 0; // 添加字符串结束符
recv_str += buffer; // 累积接收的数据
// 5. 解析并处理响应
Response resp;
while(true)
{
std::string content;
if(!Decode(recv_str, content)) break; // 解码协议
resp.Deserialize(content); // 反序列化响应数据
cout << "result: " << resp.res_ << ", code: " << resp.code_ << endl;
}
}
return 0;
}
代码实现思路说明
基本流程:
- 接收用户输入的两个数字和运算符
- 将输入序列化为协议格式并发送给服务器
- 接收服务器响应并解析显示结果
- 循环这个过程实现持续计算
协议处理:
- 使用
Request类序列化用户输入(格式:”x op y”) - 通过
Encode添加协议头尾(长度前缀和分隔符) - 接收时使用
Decode解析完整数据包 - 用
Response类解析服务器返回的结果和状态码
- 使用
网络通信:
- 使用TCP socket连接服务器
- 发送序列化后的请求数据
- 接收响应数据并累积(处理可能的分包情况)
特点:
- 遵循严格的协议格式
- 支持持续交互(不退出)
- 基本的错误处理(接收错误退出)
- 显示完整的计算结果和状态码
服务端
#include <iostream>
#include <memory> // 智能指针头文件
#include "server_cal.hpp" // 计算器服务实现
#include "tcpserver.hpp" // TCP服务器实现
using namespace std;
int main(int argc, char *argv[])
{
// 参数检查:需要指定服务端口
if(argc != 2){
cerr << "\nUsage: " << argv[0] << " server_port\n"
<< endl;
exit(1);
}
// 创建计算器服务实例
ServerCal sc;
// 创建TCP服务器实例:
// 1. 使用unique_ptr管理资源
// 2. 端口号从命令行参数获取
// 3. 绑定ServerCal::Calculator方法作为协议处理器
unique_ptr<TcpServer> ts(new TcpServer(
stoi(argv[1]), // 服务端口
bind(&ServerCal::Calculator, // 绑定成员函数
&sc, // 对象指针
placeholders::_1) // 占位符表示参数
));
// 初始化服务器(创建socket+绑定端口+监听)
ts->Init();
// 启动服务器主循环
ts->Start();
return 0;
}
至此,我们就讲自定义协议的计算服务实现了,下面我们来测试一下这段代码:
╭─ljx@VM-16-15-debian ~/linux_review/protocol
╰─➤ ./client_cal.o 82.156.255.140 7788
Please Enter x: 1
Please Enter y: 2
Please Enter op: +
result: 3, code: 0
Please Enter x: 3
Please Enter y: 2
Please Enter op: -
result: 1, code: 0
Please Enter x: 1
Please Enter y: 4
Please Enter op: /
result: 0, code: 0
Please Enter x: 3
Please Enter y: 0
Please Enter op: /
result: 0, code: 1
Please Enter x: 2
Please Enter y: 3
Please Enter op: %
result: 2, code: 0
Please Enter x: 2
Please Enter y: 0
Please Enter op: %
result: 0, code: 1
Please Enter x: 2
Please Enter y: 1
Please Enter op: &
result: 0, code: 2
客户端若输入的不是数字,就会导致未定义行为,因为我们对 x 和 y 的类型定义的是 int 类型,因此我们可以将 x 和 y 的类型定义为 string 类型,然后通过 std::stoi() 函数来转换为 int 类型,如果转换失败,就说明输入的不是数字,我们就可以提示用户重新输入:
string x_str, y_str;
char op;
int x = 0, y = 0;
// 输入x并验证
while (true)
{
cout << "Please Enter x: ";
cin >> x_str;
try
{
x = stoi(x_str);
break; // 转换成功则退出循环
}
catch (const exception &e)
{
cout << "Invalid number! Please enter an integer." << endl;
}
}
// 输入y并验证
while (true)
{
cout << "Please Enter y: ";
cin >> y_str;
try
{
y = stoi(y_str);
break; // 转换成功则退出循环
}
catch (const exception &e)
{
cout << "Invalid number! Please enter an integer." << endl;
}
}
而后,我们就可以正确处理客户端的输入错误了,而且这些检查都是在客户端做的,而不是让数据传递带服务端再做检查,一举两得
测试结果:
╭─ljx@VM-16-15-debian ~/linux_review/protocol
╰─➤ ./client_cal.o 82.156.255.140 7788
Please Enter x: 1
Please Enter y: +
Invalid number! Please enter an integer.
Please Enter y: %
Invalid number! Please enter an integer.
Please Enter y: 3
Please Enter op: (
result: 0, code: 2
Please Enter x: 3
Please Enter y: 2
Please Enter op: +
result: 5, code: 0
当然,序列化和反序列化是可以通过更简单的方式来处理的,我们可以利用一些比较成熟的序列化技术来更简单地实现序列化:
下面我们使用 JSON 来对数据进行序列化和反序列化:
JSON格式化实现
以下是添加了详细注释的代码:
#ifndef _PROTOCOL_HPP_
#define _PROTOCOL_HPP_ 1
#include <iostream>
#include <jsoncpp/json/json.h> // JSON库头文件
// 分隔符定义(虽然改用JSON后部分不再需要,但保留协议兼容性)
const std::string blank_space_sep = " "; // 空格分隔符
const std::string protocol_sep = "\n"; // 协议分隔符
// 错误码枚举
enum {
divide_by_zero_error = 1, // 除零错误
operator_identify, // 操作符识别错误
};
/* 协议解码函数(兼容旧协议)
* 参数: package - 输入的数据包
* content - 输出的解码后内容
* 返回值: 解码成功返回true,失败返回false
* 注意: 改用JSON后不再严格检查格式,但仍保留长度前缀机制
*/
bool Decode(std::string &package, std::string &content)
{
// 查找第一个分隔符位置
size_t pos = package.find(protocol_sep);
if(pos == std::string::npos) return false;
// 获取JSON内容长度
size_t size = std::stoi(package.substr(0, pos));
size_t total_len = pos + size + 2; // 计算总长度(长度+分隔符+内容+分隔符)
// 提取JSON内容
content = package.substr(pos + 1, size);
// 删除已处理的数据
package.erase(0, total_len);
return true;
}
/* 协议编码函数
* 参数: content - 要编码的JSON内容
* 返回值: 添加长度前缀的协议字符串
*/
std::string Encode(std::string &content)
{
std::string ret = std::to_string(content.size()); // 添加长度前缀
ret += protocol_sep; // 添加分隔符
ret += content; // 添加JSON内容
ret += protocol_sep; // 添加结束分隔符
return ret;
}
// 请求类(客户端→服务端)
class Request{
public:
// 构造函数(默认值表示无效请求)
Request(int x = 0, int y = 0, char op = '?')
:x_(x), y_(y), op_(op){}
/* 序列化为JSON字符串
* 格式: {"x":数值, "y":数值, "op":操作符}
*/
std::string Serialize()
{
Json::Value root; // 创建JSON根节点
root["x"] = x_; // 添加x字段
root["y"] = y_; // 添加y字段
root["op"] = op_; // 添加op字段
Json::StyledWriter w; // 使用格式化写入器
return w.write(root); // 生成JSON字符串
}
/* 从JSON字符串反序列化
* 参数: in - 输入的JSON字符串
* 返回值: 解析成功返回true
*/
bool Deserialize(std::string &in)
{
Json::Value root; // 创建JSON根节点
Json::Reader r; // JSON解析器
if(!r.parse(in, root)) // 解析JSON
return false;
x_ = root["x"].asInt(); // 读取x值
y_ = root["y"].asInt(); // 读取y值
op_ = root["op"].asInt(); // 读取op值(注意: 这里可能有类型问题)
return true;
}
public:
int x_; // 第一个操作数
int y_; // 第二个操作数
char op_; // 操作符
};
// 响应类(服务端→客户端)
class Response{
public:
// 构造函数
Response(int res = 0, int code = 0)
:res_(res), code_(code){}
/* 序列化为JSON字符串
* 格式: {"res":结果, "code":状态码}
*/
std::string Serialize()
{
Json::Value root;
root["res"] = res_; // 添加结果字段
root["code"] = code_; // 添加状态码字段
Json::StyledWriter w;
return w.write(root);
}
/* 从JSON字符串反序列化
* 参数: in - 输入的JSON字符串
* 返回值: 解析成功返回true
*/
bool Deserialize(std::string &in)
{
Json::Value root;
Json::Reader r;
if(!r.parse(in, root))
return false;
res_ = root["res"].asInt(); // 读取结果值
code_ = root["code"].asInt(); // 读取状态码
return true;
}
public:
int res_; // 计算结果
int code_; // 状态码
};
#endif
JSON序列化:
- 使用
Json::Value作为数据容器 StyledWriter生成格式化的JSON字符串Reader解析JSON字符串
- 使用
协议兼容性:
- 保留原有的长度前缀机制
- 外层协议格式不变,仅内容改为JSON
优势:
- 更易扩展新字段
- 数据结构更清晰
- 支持嵌套复杂数据结构
- 跨语言兼容性更好
学会合理地使用外部工具,是一个程序员的必备技能
下面是整个服务器客户端的数据处理流程图,希望对你有帮助:
flowchart LR
subgraph 客户端
A[启动客户端] --> B[建立TCP连接]
B --> C[输入x,y,op]
subgraph 接收循环[接收响应循环]
C --> D[序列化Request为JSON]
D --> E[添加长度前缀协议封装]
E --> F[发送数据]
F --> G{接收数据}
G -->|成功| H[累积到recv_buffer]
H --> I{尝试解码}
I -->|完整包| J[解析JSON响应]
I -->|不完整| C
J --> K[显示结果]
K --> I
end
end
subgraph 服务端
M[启动服务] --> N[监听端口]
N --> O[接受连接]
subgraph 处理循环[请求处理循环]
O --> P{接收数据}
P -->|成功| Q[累积到recv_buffer]
Q --> R{尝试解码}
R -->|完整包| S[解析JSON请求]
R -->|不完整| P
S --> T[执行计算逻辑]
T --> U[序列化Response为JSON]
U --> V[添加长度前缀协议封装]
V --> W[发送响应]
W --> R
P -->|失败| X[关闭连接]
end
end
客户端 -- 请求 --> 服务端
服务端 -- 响应 --> 客户端