文章目录
-
- 应用层协议设计实战(二):Jsoncpp序列化与完整实现
- 一、Jsoncpp库详解
-
- [1.1 为什么选择JSON](#1.1 为什么选择JSON)
- [1.2 安装Jsoncpp](#1.2 安装Jsoncpp)
- [1.3 Json::Value基本用法](#1.3 Json::Value基本用法)
-
- [1.3.1 构造JSON对象](#1.3.1 构造JSON对象)
- [1.3.2 序列化:对象→字符串](#1.3.2 序列化:对象→字符串)
- [1.3.3 反序列化:字符串→对象](#1.3.3 反序列化:字符串→对象)
- [1.4 类型检查方法](#1.4 类型检查方法)
- 二、Request和Response序列化实现
-
- [2.1 Request序列化](#2.1 Request序列化)
-
- [2.1.1 char类型的处理](#2.1.1 char类型的处理)
- [2.1.2 测试序列化](#2.1.2 测试序列化)
- [2.2 Response序列化](#2.2 Response序列化)
-
- [2.2.1 测试序列化](#2.2.1 测试序列化)
- 三、Factory工厂模式
-
- [3.1 为什么需要Factory](#3.1 为什么需要Factory)
- [3.2 Factory类的实现](#3.2 Factory类的实现)
-
- [3.2.1 为什么用shared_ptr](#3.2.1 为什么用shared_ptr)
- [3.2.2 BuildRequest的重载](#3.2.2 BuildRequest的重载)
- 四、TcpServer完整实现
-
- [4.1 Calculator计算器类](#4.1 Calculator计算器类)
- [4.2 Service业务逻辑](#4.2 Service业务逻辑)
-
- [4.2.1 完整流程图](#4.2.1 完整流程图)
- [4.3 多线程服务器主循环](#4.3 多线程服务器主循环)
- 五、TcpClient完整实现
-
- [5.1 客户端主函数](#5.1 客户端主函数)
- [5.2 简化版:直接通信](#5.2 简化版:直接通信)
- 六、完整代码结构
-
- [6.1 Protocol.hpp(完整版)](#6.1 Protocol.hpp(完整版))
- [6.2 Makefile](#6.2 Makefile)
- 七、测试与验证
-
- [7.1 编译](#7.1 编译)
- [7.2 启动服务器](#7.2 启动服务器)
- [7.3 启动客户端](#7.3 启动客户端)
- [7.4 测试除零错误](#7.4 测试除零错误)
- [7.5 抓包验证](#7.5 抓包验证)
- 八、本篇总结
-
- [8.1 核心要点](#8.1 核心要点)
- [8.2 容易混淆的点](#8.2 容易混淆的点)
应用层协议设计实战(二):Jsoncpp序列化与完整实现
💬 开篇:上一篇完成了协议格式的设计,定义了Request和Response结构体,实现了Encode/Decode来处理报文边界和粘包问题,封装了Socket类。但Request和Response的序列化方法只是声明,还没实现。这一篇用Jsoncpp库实现序列化和反序列化,用Factory工厂模式构建对象,完整实现TcpServer和TcpClient,最后测试整个网络计算器。从库的安装、API的使用,到完整代码的实现,到测试验证,手把手带你完成一个生产级别的网络应用。
👍 点赞、收藏与分享:这篇会把所有代码都实现出来,包括Jsoncpp的详细用法、Factory模式的设计、服务器和客户端的完整逻辑。如果对你有帮助,请点赞收藏!
🚀 循序渐进:从Jsoncpp库的安装和基本用法开始,到Request/Response序列化实现,到Factory工厂模式,到TcpServer完整实现,到TcpClient完整实现,到测试验证,一步步构建完整的应用。
一、Jsoncpp库详解
1.1 为什么选择JSON
序列化方案有很多:JSON、XML、Protobuf、MessagePack等。我们选JSON的原因:
优点:
- 人类可读,易于调试(抓包能直接看懂内容)
- 跨语言支持好(几乎所有语言都有JSON库)
- 格式简单,学习成本低
- 库成熟稳定(Jsoncpp是C++最常用的JSON库之一)
缺点:
- 体积较大(文本格式,有很多冗余字符)
- 解析速度较慢(相比二进制格式如Protobuf)
对于我们的网络计算器,数据量很小,JSON的缺点不明显,但优点很突出。
1.2 安装Jsoncpp
Ubuntu:
bash
sudo apt-get install libjsoncpp-dev
CentOS:
bash
sudo yum install jsoncpp-devel
安装后,头文件在/usr/include/jsoncpp/json/,库文件是libjsoncpp.so或libjsoncpp.a。
编译时链接:
bash
g++ -o server TcpServerMain.cc -ljsoncpp -std=c++11
1.3 Json::Value基本用法
Json::Value是Jsoncpp的核心类,可以表示JSON的任意类型。
1.3.1 构造JSON对象
cpp
#include <jsoncpp/json/json.h>
Json::Value root;
root["name"] = "张三";
root["age"] = 30;
root["city"] = "北京";
这构造了一个JSON对象:
json
{
"name": "张三",
"age": 30,
"city": "北京"
}
1.3.2 序列化:对象→字符串
有三种方式:
方式一:toStyledString(格式化输出)
cpp
std::string s = root.toStyledString();
std::cout << s << std::endl;
输出(有缩进、换行):
json
{
"age" : 30,
"city" : "北京",
"name" : "张三"
}
方式二:FastWriter(紧凑输出,我们用这个)
cpp
Json::FastWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;
输出(无缩进、无换行):
json
{"age":30,"city":"北京","name":"张三"}
方式三:StreamWriter(高级定制)
cpp
Json::StreamWriterBuilder wbuilder;
std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());
std::stringstream ss;
writer->write(root, &ss);
std::cout << ss.str() << std::endl;
我们选FastWriter,因为:
- 紧凑,减少网络传输量
- 代码简单,一行搞定
- 性能好(无格式化开销)
1.3.3 反序列化:字符串→对象
cpp
std::string json_string = "{\"name\":\"张三\",\"age\":30,\"city\":\"北京\"}";
Json::Reader reader;
Json::Value root;
bool success = reader.parse(json_string, root);
if (success) {
std::string name = root["name"].asString();
int age = root["age"].asInt();
std::string city = root["city"].asString();
std::cout << "Name: " << name << std::endl;
std::cout << "Age: " << age << std::endl;
std::cout << "City: " << city << std::endl;
} else {
std::cout << "Parse error: " << reader.getFormattedErrorMessages() << std::endl;
}
关键点:
Json::Reader负责解析reader.parse()返回bool,表示是否成功root["name"].asString()提取字符串值root["age"].asInt()提取整数值- 解析失败时,
getFormattedErrorMessages()返回错误信息
1.4 类型检查方法
JSON支持多种类型,Json::Value提供了类型检查方法:
cpp
if (root["age"].isInt()) {
int age = root["age"].asInt();
}
if (root["name"].isString()) {
std::string name = root["name"].asString();
}
if (root.isObject()) {
// root是一个对象(键值对集合)
}
if (root.isArray()) {
// root是一个数组
}
常用检查方法:
isNull():是否为nullisBool():是否为布尔值isInt()、isInt64()、isUInt():是否为整数isDouble():是否为浮点数isString():是否为字符串isArray():是否为数组isObject():是否为对象
二、Request和Response序列化实现
2.1 Request序列化
cpp
class Request
{
public:
Request() : _data_x(0), _data_y(0), _oper(0) {}
Request(int x, int y, char op) : _data_x(x), _data_y(y), _oper(op) {}
bool Serialize(std::string *out)
{
Json::Value root;
root["datax"] = _data_x;
root["datay"] = _data_y;
root["oper"] = _oper; // char会被转成int(ASCII码)
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(); // 读取int再转成char
}
return res;
}
int GetX() { return _data_x; }
int GetY() { return _data_y; }
char GetOper() { return _oper; }
private:
int _data_x;
int _data_y;
char _oper;
};
2.1.1 char类型的处理
注意root["oper"] = _oper这行。_oper是char类型,JSON没有char类型,会自动转成int(ASCII码)。
例如_oper='+','+'的ASCII码是43,序列化后是:
json
{"datax":10,"datay":20,"oper":43}
反序列化时,用asInt()读取43,再赋值给char变量,自动转回'+'。
为什么不用asString()?
因为JSON中存的是43(数字),不是"+"(字符串)。如果用asString()会失败。
当然,也可以显式转成字符串存储:
cpp
// 序列化时
std::string op_str(1, _oper); // char转string
root["oper"] = op_str;
// 反序列化时
std::string op_str = root["oper"].asString();
_oper = op_str[0];
但直接用int更简单。
2.1.2 测试序列化
cpp
Request req(10, 20, '+');
std::string out;
req.Serialize(&out);
std::cout << out << std::endl;
输出:
bash
{"datax":10,"datay":20,"oper":43}
2.2 Response序列化
cpp
class Response
{
public:
Response() : _result(0), _code(0) {}
Response(int result, int code) : _result(result), _code(code) {}
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;
}
void SetResult(int res) { _result = res; }
void SetCode(int code) { _code = code; }
int GetResult() { return _result; }
int GetCode() { return _code; }
private:
int _result;
int _code;
};
和Request完全一样的模式,只是字段不同。
2.2.1 测试序列化
cpp
Response resp(30, 0);
std::string out;
resp.Serialize(&out);
std::cout << out << std::endl;
输出:
bash
{"code":0,"result":30}
三、Factory工厂模式
3.1 为什么需要Factory
创建对象时,可以直接new:
cpp
Request *req = new Request(10, 20, '+');
但这有几个问题:
- 如果构造逻辑复杂(需要初始化很多字段),代码会很乱
- 无法统一管理对象的创建(比如加日志、统计对象数量)
- 不利于后续扩展(比如改成对象池复用对象)
工厂模式把对象创建的逻辑封装起来,统一管理。
3.2 Factory类的实现
cpp
class Factory
{
public:
std::shared_ptr<Request> BuildRequest()
{
std::shared_ptr<Request> req = std::make_shared<Request>();
return req;
}
std::shared_ptr<Request> BuildRequest(int x, int y, char op)
{
std::shared_ptr<Request> req = std::make_shared<Request>(x, y, op);
return req;
}
std::shared_ptr<Response> BuildResponse()
{
std::shared_ptr<Response> resp = std::make_shared<Response>();
return resp;
}
std::shared_ptr<Response> BuildResponse(int result, int code)
{
std::shared_ptr<Response> resp = std::make_shared<Response>(result, code);
return resp;
}
};
3.2.1 为什么用shared_ptr
返回std::shared_ptr而不是裸指针的原因:
- 自动管理内存,不需要手动delete
- 避免内存泄漏
- 多个地方可以持有同一个对象,引用计数归零时自动释放
使用示例:
cpp
Factory factory;
auto req = factory.BuildRequest(10, 20, '+');
// 使用req...
// 离开作用域后,req自动释放,不需要delete
3.2.2 BuildRequest的重载
提供两个版本:
- 无参版本:创建默认对象(字段都是0)
- 有参版本:创建带初值的对象
服务器端通常用无参版本,接收到数据后反序列化填充:
cpp
auto req = factory.BuildRequest(); // 创建空对象
req->Deserialize(message); // 反序列化填充数据
客户端通常用有参版本,直接构造请求:
cpp
auto req = factory.BuildRequest(10, 20, '+'); // 直接构造
四、TcpServer完整实现
4.1 Calculator计算器类
先实现业务逻辑------计算器:
cpp
class Calculator
{
public:
std::shared_ptr<Response> Calculate(std::shared_ptr<Request> req)
{
std::shared_ptr<Response> resp = _factory.BuildResponse();
int x = req->GetX();
int y = req->GetY();
char op = req->GetOper();
switch (op) {
case '+':
resp->SetResult(x + y);
resp->SetCode(0);
break;
case '-':
resp->SetResult(x - y);
resp->SetCode(0);
break;
case '*':
resp->SetResult(x * y);
resp->SetCode(0);
break;
case '/':
if (y == 0) {
resp->SetCode(1); // 除零错误
} else {
resp->SetResult(x / y);
resp->SetCode(0);
}
break;
case '%':
if (y == 0) {
resp->SetCode(1); // 除零错误
} else {
resp->SetResult(x % y);
resp->SetCode(0);
}
break;
default:
resp->SetCode(2); // 非法运算符
break;
}
return resp;
}
private:
Factory _factory;
};
状态码定义:
- 0:成功
- 1:除零错误
- 2:非法运算符
4.2 Service业务逻辑
cpp
void Service(Socket *sock)
{
std::string inbuffer; // 接收缓冲区
while (true) {
// 1. 读取数据,追加到缓冲区
bool res = sock->Recv(&inbuffer, 1024);
if (!res) break; // 连接断开或出错
// 2. 循环解包,处理所有完整报文
std::string message;
while (Protocol::Decode(inbuffer, &message)) {
// 3. 反序列化Request
auto req = _factory.BuildRequest();
req->Deserialize(message);
// 4. 业务处理
auto resp = _cal.Calculate(req);
// 5. 序列化Response
std::string send_string;
resp->Serialize(&send_string);
// 6. 编码(加长度前缀)
send_string = Protocol::Encode(send_string);
// 7. 发送
sock->Send(send_string);
}
}
sock->CloseSocket();
}
4.2.1 完整流程图
bash
客户端发送:{"datax":10,"datay":20,"oper":43}
↓
协议编码:36\r\n{"datax":10,"datay":20,"oper":43}\r\n
↓
网络传输
↓
服务器接收:sock->Recv追加到inbuffer
↓
协议解码:Decode提取message = {"datax":10,"datay":20,"oper":43}
↓
反序列化:req->Deserialize得到Request对象(x=10, y=20, op='+')
↓
业务处理:_cal.Calculate计算result=30, code=0
↓
构造Response:Response对象(result=30, code=0)
↓
序列化:resp->Serialize得到{"code":0,"result":30}
↓
协议编码:Encode得到23\r\n{"code":0,"result":30}\r\n
↓
发送:sock->Send
↓
网络传输
↓
客户端接收
4.3 多线程服务器主循环
cpp
void Start()
{
while (true) {
std::string clientip;
uint16_t clientport;
Socket *sock = _listensock->AcceptConnection(&clientip, &clientport);
if (sock == nullptr) continue;
std::cout << "Accept new connection from " << clientip
<< ":" << clientport << std::endl;
// 创建线程处理连接
std::thread t(&TcpServer::Service, this, sock);
t.detach();
}
}
每个连接创建一个线程处理,线程detach后自动回收。
五、TcpClient完整实现
5.1 客户端主函数
cpp
int main(int argc, char *argv[])
{
if (argc != 3) {
std::cout << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
return 1;
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 1. 创建socket并连接
Socket *sock = new TcpSocket();
if (!sock->BuildConnectSocketMethod(serverip, serverport)) {
std::cerr << "Connect failed" << std::endl;
return 2;
}
std::cout << "Connect success" << std::endl;
// 2. 通信循环
Factory factory;
std::string inbuffer;
while (true) {
// 构造请求(测试用,写死几组数据)
static int x = 10, y = 20;
static char ops[] = {'+', '-', '*', '/', '%'};
static int idx = 0;
auto req = factory.BuildRequest(x, y, ops[idx % 5]);
idx++;
// 序列化+编码
std::string send_string;
req->Serialize(&send_string);
send_string = Protocol::Encode(send_string);
// 发送
sock->Send(send_string);
// 接收响应
sock->Recv(&inbuffer, 1024);
// 解码+反序列化
std::string message;
if (Protocol::Decode(inbuffer, &message)) {
auto resp = factory.BuildResponse();
resp->Deserialize(message);
std::cout << x << " " << ops[(idx-1) % 5] << " " << y
<< " = " << resp->GetResult()
<< " (code: " << resp->GetCode() << ")" << std::endl;
}
sleep(1); // 每秒发送一个请求
}
sock->CloseSocket();
delete sock;
return 0;
}
5.2 简化版:直接通信
为了测试方便,客户端写死了几组测试数据,自动循环发送。也可以改成交互式:
cpp
while (true) {
int x, y;
char op;
std::cout << "Enter expression (x op y): ";
std::cin >> x >> op >> y;
auto req = factory.BuildRequest(x, y, op);
// 发送请求
// ...
// 接收响应
// ...
std::cout << "Result: " << resp->GetResult() << std::endl;
}
六、完整代码结构
6.1 Protocol.hpp(完整版)
cpp
#pragma once
#include <iostream>
#include <memory>
#include <jsoncpp/json/json.h>
namespace Protocol
{
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;
}
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;
}
class Request
{
public:
Request() : _data_x(0), _data_y(0), _oper(0) {}
Request(int x, int y, char op) : _data_x(x), _data_y(y), _oper(op) {}
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;
}
int GetX() { return _data_x; }
int GetY() { return _data_y; }
char GetOper() { return _oper; }
private:
int _data_x;
int _data_y;
char _oper;
};
class Response
{
public:
Response() : _result(0), _code(0) {}
Response(int result, int code) : _result(result), _code(code) {}
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;
}
void SetResult(int res) { _result = res; }
void SetCode(int code) { _code = code; }
int GetResult() { return _result; }
int GetCode() { return _code; }
private:
int _result;
int _code;
};
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);
}
};
}
6.2 Makefile
makefile
.PHONY: all
all: tcp_server tcp_client
tcp_server: TcpServerMain.cc
g++ -o $@ $^ -ljsoncpp -std=c++11 -lpthread
tcp_client: TcpClientMain.cc
g++ -o $@ $^ -ljsoncpp -std=c++11
.PHONY: clean
clean:
rm -f tcp_server tcp_client
注意:
-ljsoncpp:链接Jsoncpp库-std=c++11:启用C++11(shared_ptr、thread需要)-lpthread:链接pthread库(thread需要)
七、测试与验证
7.1 编译
bash
make
生成tcp_server和tcp_client两个可执行文件。
7.2 启动服务器
bash
./tcp_server 8888
输出:
bash
[2025-02-07 10:30:00] [INFO] Server start on port 8888
7.3 启动客户端
bash
./tcp_client 127.0.0.1 8888
输出:
bash
Connect success
10 + 20 = 30 (code: 0)
10 - 20 = -10 (code: 0)
10 * 20 = 200 (code: 0)
10 / 20 = 0 (code: 0)
10 % 20 = 10 (code: 0)
10 + 20 = 30 (code: 0)
...
服务器端输出:
bash
Accept new connection from 127.0.0.1:54321
Recv request: x=10, y=20, op=+
Send response: result=30, code=0
Recv request: x=10, y=20, op=-
Send response: result=-10, code=0
...
7.4 测试除零错误
修改客户端代码,发送除零请求:
cpp
auto req = factory.BuildRequest(10, 0, '/');
客户端输出:
bash
10 / 0 = 0 (code: 1)
状态码为1,表示除零错误。
7.5 抓包验证
用tcpdump抓包:
bash
sudo tcpdump -i lo -A port 8888
看到的数据:
bash
36\r\n{"datax":10,"datay":20,"oper":43}\r\n
可以直接看懂协议内容,这就是JSON的优势。
八、本篇总结
8.1 核心要点
Jsoncpp使用:
- Json::Value构造JSON对象
- Json::FastWriter序列化(紧凑格式)
- Json::Reader反序列化
- char类型存储为int(ASCII码)
- asInt()、asString()提取值
Factory工厂模式:
- 封装对象创建逻辑
- 返回shared_ptr自动管理内存
- 提供无参和有参两个版本
服务器实现:
- Service函数:Recv→Decode→Deserialize→Calculate→Serialize→Encode→Send
- 循环Decode处理粘包
- 多线程处理并发连接
客户端实现:
- BuildConnectSocketMethod连接服务器
- 构造Request→序列化→编码→发送
- 接收→解码→反序列化→提取结果
完整流程:
bash
客户端:Request对象 → Serialize → JSON字符串 → Encode → 带长度前缀 → Send
网络传输
服务器:Recv → 拼接缓冲区 → Decode → JSON字符串 → Deserialize → Request对象
服务器:Calculate → Response对象 → Serialize → Encode → Send
网络传输
客户端:Recv → Decode → Deserialize → Response对象
8.2 容易混淆的点
-
为什么char要用asInt():JSON没有char类型,char会转成ASCII码(int),反序列化时用asInt()读取。
-
为什么要循环Decode:一次Recv可能读到多个完整报文(粘包),必须循环解析所有报文。
-
Recv为什么要拼接:一次Recv可能只读到半个报文,需要多次Recv拼接成完整数据再Decode。
-
Factory为什么返回shared_ptr:自动管理内存,避免内存泄漏,多处持有同一对象时引用计数管理生命周期。
-
为什么用FastWriter而不是toStyledString:FastWriter紧凑无格式化,减少网络传输量,提高效率。
-
状态码为什么放在Response里:让客户端能区分计算成功、除零错误、非法运算符等不同情况。
💬 总结:应用层协议设计实战系列两篇到此结束!从协议格式设计,到TCP全双工原理,到Encode/Decode实现,到Jsoncpp序列化,到Factory工厂模式,到完整的网络计算器实现,完整地走了一遍应用层协议设计的全流程。掌握了这些,你就能设计自己的网络协议,实现任意复杂的网络应用。协议设计是网络编程的核心技能,后续如果做HTTP服务器、RPC框架、游戏服务器,都要用到这些知识。
👍 点赞、收藏与分享:如果这个系列帮你理解了应用层协议设计,请点赞收藏!后续可能会讲HTTP协议解析、Protobuf使用、RPC框架设计等更高级的内容。感谢阅读!