Skip to content

002_limou_2024_04_17_序列库

约 3873 字大约 13 分钟

2025-04-09

1.1.json 格式文档

JSON 是一种数据交换格式,常用于网络应用编程中的序列和反序列化(尤其是用于网络请求和网络响应的正文部分)。

JSON 的数据类型只有如下几种:

  • 对象,使用 {} 包含
  • 数组,使用 [] 包含
  • 字符串,使用 "" 包含
  • 数字(包含整数和浮点),直接使用就行
  • 布尔值,truefalse
  • 空值,null

而一份 .json 文件内就包含由上述数据类型构成的一份文档,当需要发送时就压缩和序列化成字符串发送出去,对端读取后再进行反序列进行阅读。

补充:可以 前往 MDN 查看 JSON 的相关内容...

1.2.json 第三方库

1.2.1.jsoncpp

1.2.1.1.查看源代码

json-cpp 可用于实现 json 格式的序列化和反序列化,主要依靠三个类和少量函数。您可以使用 sudo yum install jsoncpp-devel 下载内部相关文件,也可以去克隆官方 git 仓库(https://github.com/open-source-parsers/jsoncpp)。

1.2.1.2.使用源代码

由于比较简单,这里直接上代码进行使用。

//使用 jsoncpp 的相关接口
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <cstring>
    #include <jsoncpp/json/json.h>

int main()
{
    //1.制作 JSON 内容
    std::cout << "1.制作 JSON 内容" << std::endl;
    //创建一个空的 JSON 对象
    Json::Value root;

    //添加键值对
    root["name"] = "John"; //实际上 operator[]() 重载返回的也是 Json::Value, 这里还重载了 operator=()
    root["age"] = 30;
    root["isStudent"] = true;
    root["email"] = Json::nullValue;

    //添加一个数组
    Json::Value hobbies(Json::arrayValue);
    hobbies.append("reading");
    hobbies.append("music");
    root["hobbies"] = hobbies;

    //添加一个嵌套的 JSON 对象
    Json::Value address;
    address["city"] = "New York";
    address["zipcode"] = "10001";
    root["address"] = address;

    
    //2.访问 JSON 对象
    std::cout << "2.访问 JSON 对象" << std::endl;
    //访问整个 JSON 对象
    std::cout << "JSON Object:" << root << std::endl;

    //访问 JSON 对象的值
    std::string name = root["name"].asString();
    int age = root["age"].asInt();
    bool isStudent = root["isStudent"].asBool();
    std::string email = root["email"].isNull() ? "null" : "no null";

    //访问数组元素
    std::cout << "hobby[]: ";
    for (const auto& hobby : root["hobbies"]) {
        std::cout << hobby.asString() << " ";
    }
    std::cout << std::endl;

    //访问内嵌 JSON 对象值
    std::string city = root["address"]["city"].asString();
    std::string zipcode = root["address"]["zipcode"].asString();

    //迭代 JSON 对象
    auto it = root.begin();
    auto key = it.key();
    auto value = (*it);
    std::cout << "类型: " << typeid(key).name() << " and " << typeid(value).name() << std::endl;
    
    for (auto it = root.begin(); it != root.end(); ++it)
    {
        auto key = it.key();
        auto value = (*it);
        std::cout << key << "----" << value << std::endl;
    }
    std::cout << std::endl;

    
    //3.序列化操作
    std::cout << "3.序列化操作" << std::endl;

    std::cout << "低版本:" << std::endl;
    //空白字符经过压缩的序列化
    Json::FastWriter aFastWriter;
    std::string jsonString = aFastWriter.write(root);
    std::cout << jsonString << std::endl;
    
    //带有缩进和换行的序列化
    Json::StyledWriter aStyledWriter;
    std::string jsonStringStyle = aStyledWriter.write(root);
    std::cout << jsonStringStyle << std::endl;

    //高版本的序列化
    //class StreamWriter
    //{
    //    int write(Value const& root, std::ostream * sou); //sou 是输出型参数
    //};

    //但是由于虚继承关系, 必须按照下面方式进行调用
    //using namespace Json
    //Value val = ...;
    //StreamWriterBuilder builder;
    //builder['...'] = ...
    //std::unique_ptr<StreamWriter> writer(builder.newStreamWriter());
    //writer->write(value, &std::cout); //这会输出到流中
    std::cout << "高版本:" << std::endl;
    Json::StreamWriterBuilder swb;
    std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
    std::stringstream ss;
    sw->write(root, &ss);
    std::cout << ss.str() << std::endl;


    //4.反序列化操作
    std::cout << "4.反序列化操作" << std::endl;

    std::cout << "低版本" << std::endl;
    Json::Value newRoot_1;
    Json::Value newRoot_2;
    Json::Reader aReader;
    bool parsingSuccessful;
    parsingSuccessful = aReader.parse(jsonString, newRoot_1);
    parsingSuccessful = aReader.parse(jsonStringStyle, newRoot_2);

    std::cout << newRoot_1 << std::endl;
    std::cout << newRoot_2 << std::endl;

    //高版本的反序列化
    //class CharReader
    //{
    //  bool parse(char const* beginDoc, char const* endDoc, Value* root, std::string* errs); //root 输出型参数
    //};

    //但是由于虚继承关系, 必须按照类似 StreamWriterBuilder 的方式来使用 CharReaderBuilder
    std::cout << "高版本" << std::endl;
    Json::CharReaderBuilder crb;
    std::unique_ptr<Json::CharReader> cr(crb.newCharReader());

    Json::Value getRoot;
    const char* s = jsonString.c_str();
    std::string err;
    bool res = cr->parse(s, s + strlen(s), &getRoot, &err);
    std::cout << getRoot << std::endl;
    
    return 0;
}

不过如果是我的话,我会选择使用较高版本的接口,总结起来就是以下的接口。

//常用高频接口
//制作 JSON 数据
Json::Value root; //创建 JSON 对象
root["key"] = value; //添加普通键值对或者嵌套 JSON 对象
Json::Value arr(Json::arrayValue); //添加数组对象
hobbies.append("arr[0]");
hobbies.append("arr[1]");
root["array"] = arr;


//序列化操作
Json::StreamWriterBuilder swb;
swb["indentation"] = ""; //设置缩进为空字符串, 实现压缩(还有一些其他选项您可以查阅以下, 这里不再展开)
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());

std::stringstream ss;

sw->write(root, &ss); //写入流
std::cout << ss.str() << std::endl;


//反序列化操作
Json::CharReaderBuilder crb;
std::unique_ptr<Json::CharReader> cr(crb.newCharReader());

Json::Value getRoot;
const char* s = jsonString.c_str();
std::string err;

cr->parse(s, s + strlen(s), &getRoot, &err); //读取字符串
std::cout << getRoot << std::endl;

2.XML 解析

2.1.XML 格式文档

除了 JSON,另外一种组织数据的文档就是 XML 格式的文档,这种文档和 HTML 很像,但是两者区别还是比较大的,一个是做网站页面骨架,一个是做数据传输的中间文件。

<!-- 一份 xml 文档 -->
<?xml version="1.0" encoding="UTF-8"?> <!-- 这是 XML 的版本和编码声明 -->

<!-- <?xml-stylesheet type="text/css" href="stylesheet.css"?> XML 也可以类似 HTML 一样引入外部样式表文件, 方便做展示, 不过使用现代浏览器默认风格也挺直观的 -->

<!-- 定义实体 entity, 类似宏替换 -->
<!DOCTYPE body [
  <!ENTITY warning "Warning: Something bad happened... please refresh and try again.">
  <!ENTITY Debug "Debug: This is a debug code...">
]>

<body>
  <message> &warning; </message>
  <message> &Debug; </message>
</body>

上述的 XML 格式文档也可以转化为 JSON,这里我给您找了 一个在线转化 JSON-XML 的工具

补充:可以 前往 MDN 查看 XML 的相关内容...

2.2.XML 第三方库

待补充...

3.Protobuf 解析

3.1.Protobuf 格式文档

3.1.1.Protobuf 是什么

rotocol Buffers (Protobuf) 是一种由 Google 开发的数据序列化格式和相关的工具集,广泛用于定义数据结构和高效地序列化/反序列化数据。它可以用于数据存储、通信协议、数据交换等场景,Protobuf 是一种轻量级且高效的结构化数据序列化机制,比 JSON、XML 等格式更加高效。

3.1.2.Protobuf 的特点

  1. 高效性Protobuf 序列化后的数据体积小,解析速度快,适合在带宽和性能受限的环境中使用
  2. 跨平台Protobuf 支持多种编程语言,包括但不限于 C++, Java, Python, Go, C#, Ruby, JavaScript
  3. 兼容性:可以对现有的数据结构进行扩展,而不会破坏现有的协议

一般都是编写一个 .proto 格式文档,然后使用 protoc 编译器编译该文件,生成处理 massage 的一系列接口方法(例如操作、访问、序列、反序列化方法),交给业务代码进行依赖。

3.1.3.Protobuf 的规范

  • 创建 .proto 文件时,文件命名应该使用全小写字母命名,多个字母之间⽤ _ 连接
  • 书写 .proto 文件时,应使⽤ 2 个空格的缩进(因此我不推荐使用 [Tab] 进行)
  • 可以向文件添加注释,使用 //... 或者 /* ... */
  • 最好指定 proto3 语法,Protocol Buffers 语言版本 3 简称 proto3.proto ⽂件最新的语法版本。proto3 简化了 Protocol Buffers 语⾔,既易于使用,又可以在更广泛的编程语言中使用。可以用 syntax = "proto3"; 来指定文件语法,必须写在除去注释内容的第⼀行(如果没有指定编译器会使用 proto2 语法)
  • 最好使用 package 声明符来定义一个命名空间,防止消息出现冲突
  • 每一句完整语句都需要使用分号进行结尾

3.1.4.Protobuf 的消息

message 中我们可以定义其属性字段,字段定义格式为:字段类型 字段名 = 字段唯一编号,合起来就是:

message 消息名称 { // 后续生成的文件的类名会和消息名保持一致
	字段类型 字段名 = 字段唯一编号;
}
  • 字段类型分为:标量数据类型和特殊类型(包括枚举、其他消息类型等)
  • 字段名称命名规范:全小写字母,多个字母之间用 _ 连接
  • 字段唯一编号:用来标识字段,一旦开始使用就不能够再改变

常见的字段类型如下:

.proto TypeNotesC++ Type
doubledouble
floatfloat
int32使用变长编码,负数的编码效率较低,若字段可能为负值,应使用 sint32 代替int32
int64使用变长编码,负数的编码效率较低,若字段可能为负值,应使用 sint64 代替int64
uint32使用变长编码uint32
uint64使用变长编码uint64
sint32使用变长编码,符号整型,负值的编码效率高于常规的 int32 类型int32
sint64使用变长编码,符号整型,负值的编码效率高于常规的 int64 类型int64
fixed32定长 4 字节,若值常大于 2^28 则会比 uint32 更高效uint32
fixed64定长 8 字节,若值常大于 2^56 则会比 uint64 更高效uint64
sfixed32定长 4 字节int32
sfixed64定长 8 字节int64
boolbool
string包含 UTF-8ASCII 编码的字符串,长度不能超过 2^32string
bytes可包含任意的字节序列但长度不能超过 2^32string

补充:int32uint32sint32 各自有不同的用途:

  1. int32:表示有符号的 32 位整数,可以表示负数和正数。适合需要负数的情况
  2. uint32:表示无符号的 32 位整数,只能表示非负数(0及正数),其取值范围更大,适用于只需要正数的场景
  3. sint32:表示可变长度的有符号整数,使用 ZigZag 编码,能有效压缩负数和小正数,节省空间,适合需要频繁使用小范围负数的情况

补充:变长编码是指经过 Protobuf 编码后,原本 4 字节或 8 字节的数可能会被变为其他字节数。

补充:字段唯⼀编号的范围为 1~536,870,911(即 2^29-1) ,其中 19,000~19,999 不可⽤。在 Protobuf 协议的实现中,对这些数进行了预留。如果⾮要在 .proto ⽂件中使用这些预留标识号,例如将 name 字段的编号设置为19000,编译时就会报警。之所以使用字段唯⼀编号,主要有三个原因:

  • 标识性:每个字段都有一个唯一编号,确保在序列化和反序列化时能够正确识别和匹配字段
  • 兼容性:当消息结构发生变化(如添加或删除字段)时,唯一编号确保旧版本和新版本之间的兼容性,使得数据可以在不同版本的应用程序中无缝传输
  • 空间效率:在二进制序列化中,使用唯一编号比使用字段名称更节省空间,因为数字表示比字符串更紧凑

3.2.Protobuf 第三方库

3.2.1.查看源代码

可以直接 去 google 的 github 链接下找到 Protobuf 编译器和相关依赖文件...

# Centos 或 Ubuntu 安装 Protobuf
$ sudo yum install autoconf automake libtool curl make gcc-c++ unzip # 安装依赖库
$ wget https://github.com/protocolbuffers/protobuf/releases/download/v3.20.2/protobufall-3.20.2.tar.gz # 下载安装包(如果是 Ubuntu 尝试使用新版 wget https://github.com/protocolbuffers/protobuf/releases/download/v27.0/protobuf-27.0.tar.gz)
$ tar -zxf protobuf-all-3.20.2.tar.gz # 解压安装包(如果是 Ubuntu 尝试使用 tar -zxf protobuf-27.0.tar.gz)
$ cd protobuf-3.20.2 # 进入源代码文件(如果是 Ubuntu 尝试使用 cd protobuf-27.0)
$ ./autogen.sh # 检测环境
$ ./configure # 设置配置
$ make # 编译
$ sudo make install # 使用 root 进行安装
$ protoc --version # 检查版本

3.2.2.使用源代码

然后尝试编写一下相关代码。

// contacts.proto
syntax = "proto3";

package contacts; // 这是命名空间

message PeopleInfo {
    string name = 1;
    int32 age = 2;
}

使用 protoc 编译器 proto 文件。

protoc [--proto_path=IMPORT_PATH] --cpp_out=OUT_DIR <file.proto>
  • --proto_path=IMPORT_PATH:指定被编译的 .proto 文件所在目录。可以多次指定。简写为 -I IMPORT_PATH。如果不指定该参数,则在当前目录进行搜索。当某个 .proto 文件 import 其他 .proto 文件时,或者需要编译的 .proto 文件不在当前目录下时,就需要用 -I 来指定搜索目录。
  • --cpp_out=OUT_DIR:指定编译后的文件为 c++ 文件,OUT_DIR 为编译后生成文件的目标路径。
  • path/to/file.proto:需要编译的 .proto 文件的路径。
# 生成依赖
$ protoc --cpp_out=. contacts.proto 
$ ls
contacts.pb.cc  contacts.pb.h  contacts.proto

编写包含 protobuf 接口的 c++ 代码。

// test.cpp
#include <iostream>
#include <iomanip>
#include "contacts.pb.h"
void PrintHex(const std::string& data) {
    for (unsigned char c : data) {
        std::cout << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(c) << " ";
    }
    std::cout << std::dec;
}

int main() {
    // 创建 PeopleInfo 消息对象
    contacts::PeopleInfo person;
    
    // 设置字段值并且查看
    person.set_name("John");
    person.set_age(30);
    std::cout << "查看字段值 " << person.name() << " and " << person.age() << std::endl;
    
    // 序列化
    int size = person.ByteSizeLong(); // 获取序列化后的消息大小
    std::cout << "获取序列化后的消息大小 " << size << " bytes" << std::endl;

    std::string serialized_data;
    person.SerializeToString(&serialized_data); // 获取序列化后的结果(打印出来没有意义, 本身是二进制存储的)
    std::cout << "输出序列化后的结果 ";
    PrintHex(serialized_data);
    std::cout << std::endl;

    // 反序列化
    contacts::PeopleInfo copy_person;
    copy_person.ParseFromString(serialized_data);
    std::cout << "输出反序列化后的结果 " << copy_person.name() << " and " << copy_person.age() << std::endl; // 输出反序列化后的结果
    
    // 清理字段值
    person.clear_name();
    person.clear_age();
    return 0;
}
# 编译运行
$ g++ -std=c++11 test.cpp contacts.pb.cc -lprotobuf
$ ./a.out 
查看字段值 John and 30
获取序列化后的消息大小 8 bytes
输出序列化后的结果 0a 04 4a 6f 68 6e 10 1e 
输出反序列化后的结果 John and 30

也可以读取二进制文件中的消息进行解析。

// 从流中输入消息数据和解析消息数据 
#include <iostream>
#include <fstream>
#include "contacts.pb.h"

int main() {
    // 创建 PeopleInfo 消息对象并设置字段值
    contacts::PeopleInfo person;
    person.set_name("John");
    person.set_age(30);

    // 序列化并写入文件
    std::ofstream output_file("person_data.bin", std::ios::binary);
    if (output_file.is_open()) {
        if (person.SerializeToOstream(&output_file)) { // 把序列化结果写入文件中
            std::cout << "Serialization successful!" << std::endl;
        } else {
            std::cerr << "Serialization failed!" << std::endl;
        }
        output_file.close();
    } else {
        std::cerr << "Failed to open output file!" << std::endl;
        return 1;
    }

    // 从文件中读取并反序列化
    std::ifstream input_file("person_data.bin", std::ios::binary);
    if (input_file.is_open()) {
        contacts::PeopleInfo copy_person;
        if (copy_person.ParseFromIstream(&input_file)) {
            std::cout << "Deserialization successful!" << std::endl;
            std::cout << "Name: " << copy_person.name() << std::endl;
            std::cout << "Age: " << copy_person.age() << std::endl;
        } else {
            std::cerr << "Deserialization failed!" << std::endl;
        }
        input_file.close();
    } else {
        std::cerr << "Failed to open input file!" << std::endl;
        return 1;
    }

    return 0;
}
$ g++ -std=c++11 test.cpp contacts.pb.cc -lprotobuf
$ ./a.out 
Serialization successful!
Deserialization successful!
Name: John
Age: 30

这里总结出一些常见的方法:

  • 字段相关
    • set_<field_name>(value) 设置某个字段的值
    • get_<field_name>() 获取某个字段的值
  • 序列化
    • SerializeToString(&output_string) 将消息序列化为字符串
    • SerializeToArray(output_array, size) 将消息序列化为字节数组(方便操作)
    • SerializeToOstream(output_stream) 将消息序列化到输出流中(方便操作)
  • 反序列化
    • ParseFromString(input_string) 从字符串解析消息
    • ParseFromArray(input_array, size) 从字节数组解析消息
    • ParseFromIstream(input_stream) 从输入流解析消息
  • 获取消息大小
    • ByteSizeLong() 返回序列化后消息的大小
  • 清理字段值
    • clear_<field_name>() 清空某个字段的值
  • 检查字段是否设置
    • has_<field_name>() 检查某个字段是否被设置
  • 添加和访问重复字段
    • add_<field_name>(value) 向重复字段添加一个值
    • mutable_<field_name>(index) 获取可变的重复字段值引用
    • get_<field_name>(index) 获取指定索引的重复字段值
    • field_size() 获取重复字段的数量
  • 复制和比较
    • CopyFrom(other_message) 复制另一个消息的内容
    • IsInitialized() 检查消息是否完全初始化
  • 字符串表示
    • DebugString() 返回消息的调试字符串表示
    • ShortDebugString() 返回消息的简短调试字符串表示

补充:更多方法可以前往 Google 编写的 Protocol Buffers Documentation 文档查看更多 API 接口的使用方法

更新日志

2025/11/24 07:31
查看所有更新日志
  • 0843b-修改语言目录结构,同时归纳新的语言目录
  • 6fa6e-迁移所有有效的文章
  • 01111-保存所有的学习
  • 7ffa6-学习使用 Sentinel、Sa-token、Nacos、ES
  • 1a85c-临时迁移文件,开始部署博客
  • b7955-修改资源目录
  • 1d7f9-数据备份
  • 9c3ea-数据备份

本站公告

本网站正在不断升级中,若有 bug 可以在本开发者的对应项目下提交 issues