JSON 序列化与反序列化 —— 用 Jsoncpp 打造自己的网络协议

在应用层协议设计中,如何让结构化数据在网络上「乖乖传递」(不会多读或者少读)?


一、为什么需要序列化?

当我们用 socket 发送数据时,sendrecv 操作的是一串 字节流(字符串)。

但我们的程序里处理的是 结构化的数据,比如一个计算器请求:

复制代码
int x = 10;
int y = 20;
char op = '+';

问题来了:怎么把 1020'+' 这三个东西打包成一个字符串发出去?对方收到后又怎么还原成原来的三个数据?

这就需要 序列化反序列化

术语 含义
序列化 (Serialize) 将内存中的结构化数据 → 转换成字符串 / 字节流
反序列化 (Deserialize) 将字符串 / 字节流 → 还原成内存中的结构化数据

常见的序列化方案有:JSON、XML、Protobuf、自定义格式(如 "10+20")等。

本文我们使用 JSON 格式,借助 C++ 的 jsoncpp 库来完成。


二、Jsoncpp 简介与安装

什么是 Jsoncpp?

Jsoncpp 是一个跨平台的 C++ 库,用于处理 JSON 数据。

它提供了:

  • 读写 JSON 的简单 API

  • 支持 JSON 标准中的所有类型(对象、数组、字符串、数字、布尔值、null)

  • 详细的错误提示(解析出错时告诉你第几行第几个字符)

安装

Ubuntu / Debian

复制代码
sudo apt-get install libjsoncpp-dev

CentOS / RHEL

复制代码
sudo yum install jsoncpp-devel

编译时链接

复制代码
g++ your_code.cpp -ljsoncpp

三、JSON 序列化:把对象变成字符串

我们先用一个简单的例子:把一个人的名字和性别存成 JSON 字符串。

方法一:toStyledString()

复制代码
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>

int main() {
    Json::Value root;
    root["name"] = "joe";
    root["sex"]  = "男";

    std::string s = root.toStyledString();
    std::cout << s << std::endl;
    return 0;
}

输出(格式化,带缩进):

复制代码
{
   "name" : "joe",
   "sex" : "男"
}

✅ 优点:输出美观,便于调试

❌ 缺点:多出了空格和换行,占用带宽稍多

方法二:Json::FastWriter

复制代码
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>

int main() {
    Json::Value root;
    root["name"] = "joe";
    root["sex"]  = "男";

    Json::FastWriter writer;
    std::string s = writer.write(root);
    std::cout << s << std::endl;
    return 0;
}

输出(无额外空格换行):

{"name":"joe","sex":"男"}

✅ 优点:体积小,传输快

✅ 常用在网络通信中

方法三:Json::StyledWriter

复制代码
Json::StyledWriter writer;
std::string s = writer.write(root);

输出结果和 toStyledString() 几乎一样,适合写日志或配置文件。

方法四:Json::StreamWriter

如果你需要自定义缩进、换行符等,可以使用 StreamWriterBuilder

复制代码
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>

int main() {
    Json::Value root;
    root["name"] = "joe";
    root["sex"]  = "男";

    Json::StreamWriterBuilder builder;
    std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
    std::stringstream ss;
    writer->write(root, &ss);
    std::cout << ss.str() << std::endl;
    return 0;
}

这种方式最灵活,但日常开发中用 FastWriter 就足够了。


四、JSON 反序列化:把字符串变回对象

反序列化是将序列化后的数据重新 转化为原来的 数据结构或对象。

假设我们收到了一个 JSON 字符串:

复制代码
{"name":"张三","age":30,"city":"北京"}

我们想把它还原成 C++ 中的数据。

使用 Json::Reader ------ 最常用

复制代码
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>

int main() {
    std::string json_string = R"({"name":"张三","age":30,"city":"北京"})";

    Json::Reader reader;
    Json::Value root;

    bool ok = reader.parse(json_string, root);
    if (!ok) {
        std::cout << "解析失败:" << reader.getFormattedErrorMessage() << std::endl;
        return 1;
    }

    std::string name = root["name"].asString();
    int age = root["age"].asInt();
    std::string city = root["city"].asString();

    std::cout << "姓名:" << name << "\n年龄:" << age << "\n城市:" << city << std::endl;
    return 0;
}

输出

复制代码
姓名:张三
年龄:30
城市:北京

要点

  • reader.parse() 返回 true 表示解析成功

  • root["key"] 访问成员,然后调用 .asInt(), .asString() 等转换成 C++ 类型

  • 如果 key 不存在,会返回一个 null 值,不会崩溃

错误处理很重要:

解析 JSON 时一定要检查返回值,否则一旦格式错误,程序可能异常。
reader.getFormattedErrorMessage() 会给出具体位置,非常方便调试。


五、Json::Value 常用操作大集合

Json::Value 是 jsoncpp 的核心类,**它可以表示任意 JSON 类型。**下面列出最常用的操作。

1. 类型检查(判断一个 Value 到底是什么类型)

方法 说明
isNull() 是否是 null
isBool() 是否是布尔值
isInt() / isInt64() 是否是整数(64位)
isUInt() / isUInt64() 是否是无符号整数
isDouble() 是否是浮点数
isString() 是否是字符串
isArray() 是否是数组
isObject() 是否是对象(键值对集合)
复制代码
Json::Value v;
v["count"] = 100;
if (v["count"].isInt()) {
    std::cout << "count is int" << std::endl;
}

2. 赋值与类型转换

方法 说明
operator=(bool) / operator=(int) / ... 直接赋值
asBool() 转成 bool
asInt() / asInt64() 转成整数
asUInt() / asUInt64() 转成无符号整数
asDouble() 转成 double
asString() 转成 std::string

⚠️ 注意:如果实际存储的类型不匹配(比如对字符串调用 asInt()),可能会得到 0 或触发未定义行为,务必先用 isXXX() 检查

3. 数组操作

方法 说明
size() 数组元素个数
empty() 是否为空数组
resize(n) 调整数组大小
clear() 清空数组
append(value) 在末尾添加元素
operator[](index) 通过下标访问(可修改)
复制代码
Json::Value arr;
arr.append(10);
arr.append(20);
arr[1] = 25;   // 修改第二个元素
std::cout << arr.size();  // 2

4. 对象操作

方法 说明
size() 对象中键值对的数量
empty() 是否为空对象
clear() 删除所有键值对
operator[](key) 通过 key 访问(自动创建不存在的 key)
复制代码
for (auto it = root.begin(); it != root.end(); ++it) {
    std::string key = it.key().asString();
    Json::Value value = *it;
    // ...
}

六、实战:网络计算器协议(基于 JSON)

现在我们把上面的知识串起来,动手实现一个简单的 网络版计算器协议

协议设计

  • 客户端发送一个 JSON 对象,包含三个字段:xyop

  • 服务器计算后,返回一个 JSON 对象,包含:result(计算结果)和 code(状态码,0 表示成功)

    // 保证发送的时候一定是json串,但是并不能保证json串的完整系
    // 进一步 -> 有效载荷长度(content_len) + json串
    // 50\r\n{"x":10, "y":20, "oper":"+"}\r\n
    class Request
    {
    public:
    Request() {}
    Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper)
    {
    }
    std::string Serialize()
    {
    std::string s;
    Json::Value root;
    root["x"] = _x;
    root["y"] = _y;
    root["oper"] = _oper; //

    复制代码
          Json::FastWriter writer;
          std::string s = writer.write(root);
          return s;
      }
      //{"x":10,"y":20,"oper":'+'}
      bool Deserialize(std::string &in)
      {
          Json::Value root;
          Json::Reader reader;
          bool ok = reader.parse(in, root);
          if (ok)
          {
              _x = root["x"].asInt();
              _y = root["y"].asInt();
              _oper = root["oper"].asInt();
          }
          return ok;
      }
      ~Request() {}

    private:
    int _x;
    int _y;
    char _oper; // + - * / % --》 _x _oper _y ---》 10 + 20
    };

    // server -> client
    class Response
    {
    public:
    Response() {}
    Response(int result, int code) : _result(result), _code(code)
    {
    }
    std::string Serialize()
    {
    Json::Value root;
    root["result"] = _result;
    root["code"] = _code;

    复制代码
          Json::FastWriter writer;
          return writer.write(root);
      }
      bool Deserialize(std::string &in)
      {
          Json::Value root;
          Json::Reader reader;
          bool ok = reader.parse(in, root);
          if (ok)
          {
              _result = root["result"].asInt();
              _code = root["code"].asInt();
          }
          return ok;
      }
      ~Response() {}

    private:
    int _result; // 运算结果,无法区分清楚应答是计算结果,还是异常值
    int _code; // 0:sucess , 1,2,3,4->不同的运算异常的情况
    };


七、注意事项与最佳实践

  1. 粘包处理

    实际使用 TCP 时,必须自己处理 粘包。常见做法是:

    • 固定长度

    • 使用分隔符(如 \r\n

    • 在报文前面加上长度字段

    Jsoncpp 不负责粘包,你需要自己实现 Encode / Decode 函数。

  2. 错误检查

    解析 JSON 时永远检查 parse() 的返回值。

  3. 性能
    FastWriterStyledWriter 快,网络通信中优先使用 FastWriter

  4. 版本问题

    不同版本的 jsoncpp API 有差异(比如旧版使用 Json::Reader,新版推荐 Json::CharReaderparseFromStream)。本文以最常用的老 API 为例,稳定可靠。

  5. 内存管理
    Json::Value 内部使用引用计数,可以像普通变量一样拷贝,不用担心内存泄漏。

相关推荐
@insist1231 小时前
信息安全工程师-入侵阻断与网络流量清洗技术详解
网络·安全·软考·信息安全工程师·软件水平考试
身如柳絮随风扬1 小时前
RPC 深度解析:从原理到实践,一篇讲透远程过程调用
网络协议·rpc
!沧海@一粟!1 小时前
NAT映射回流解决内网通过公网映射访问内部服务器
运维·网络
key_3_feng1 小时前
eBPF网络性能监控通用方案:构建低开销、高精度的实时洞察体系
网络
Bat U2 小时前
JavaEE|网络初识
网络·智能路由器
砍材农夫2 小时前
物联网 MQTT订阅性能优势
网络·物联网
无名的小三轮2 小时前
VMware的三种网卡模式介绍
网络·智能路由器
yyuuuzz2 小时前
国际云服务商使用的常见问题分析
运维·服务器·网络·云计算·github·aws
minji...2 小时前
Linux 网络基础(五)守护进程化,前后台进程组,作业,会话,setsid(),daemon(),端口号频繁更换问题
linux·运维·服务器·网络·c++·tcp/ip