【Linux网络】应用层自定义协议
文章目录
- 1. 应用层
- 再谈 "协议"
- 网络版计算器
- 序列化 和 反序列化
- 2. 重新理解read、write、recv、send和tcp为什么支持全双工
- 3. Jsoncpp
- 序列化
- 反序列化
- Json::Value
1. 应用层
我们程序员写的一个个解决我们实际问题,满足我们日常需求的网络程序,都是在应用层.
应用层协议就是应用程序之间通信的规则和格式约定,让不同的程序能够理解彼此发送的数据含义。
再谈 “协议”
协议是一种 “约定”。socket api的接口,在读写数据时,都是按 “字符串” 的方式来发送接收的。如果我们要传输一些 “结构化的数据” 怎么办呢?
在使用 socket API 进行网络通信时,底层只能收发 字节流(或字符串),因此如果要在客户端‑服务器之间传递 结构化的数据,必须先把数据 序列化 为字节流,再在接收端 反序列化 回原来的结构。常见的做法包括:
步骤 | 说明 | 常用实现方式 |
---|---|---|
1️⃣ 定义数据结构 | 在双方约定好要交换的字段、类型、层次结构。可以用 结构体、类或 .proto、JSON Schema 等形式描述。 | C/C++ struct、Java class、.proto 文件等 |
2️⃣ 序列化 | 把结构体/对象转换为 字节序列(或可读的文本),并在发送前 加上长度前缀(防止粘包/半包)。 | JSON、XML、Protocol Buffers(protobuf)、MessagePack、Thrift、CBOR 等 |
3️⃣ 通过 socket 发送 | 直接 write()/send() 发送字节流;接收端使用 read()/recv() 读取指定长度的数据。 | 参考 Protobuf C++ Socket 示例 中的 write(client_sock, &msg, sizeof(msg)) 方式 |
4️⃣ 反序列化 | 接收端把字节流按照相同的规则恢复为原始结构体/对象。 | 对应的 JSON 解析库、protobuf 生成的 ParseFromArray 等 |
常见序列化方案对比
方案 | 数据形态 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
JSON | 文本(UTF‑8) | 人类可读、调试方便、跨语言支持广泛 | 数据体积相对大、解析速度慢于二进制方案 | 配置、REST API、调试阶段 |
XML | 文本(带标签) | 可自定义结构、支持命名空间 | 冗长、解析开销大 | 老旧系统、需要严格模式校验的场景 |
Protocol Buffers (protobuf) | 二进制 | 体积小、序列化/反序列化速度快、跨语言、向后兼容 | 不可直接阅读,需要 .proto 定义文件 | 高性能 RPC、移动端‑服务器、带宽受限环境 |
MessagePack | 二进制 | 类似 JSON 的结构但更紧凑 | 生态相对 protobuf 较少 | 需要兼顾可读性和效率的轻量服务 |
例如,Google 的 protobuf 将结构化数据编译成二进制字节流,传输时只占用极少的带宽,且支持多语言生成代码。在实际的 C++ socket 示例中,先把 Person 消息序列化为 std::string(或 std::vector),再通过 write() 发送;接收端使用 ParseFromArray 还原对象。
实际实现要点
-
字节序(Endian)
- 网络字节序统一为 大端。在序列化前,整数等基本类型应使用 htonl/htons(或 protobuf 自动处理)保证跨平台兼容。
-
长度前缀
- 为防止 粘包/半包,常在发送的数据前加上 4 字节的长度字段(网络字节序),接收端先读取长度,再读取完整报文。
-
错误处理
- 读取不完整或解析失败时,需要关闭连接或请求重传,防止协议状态错位。
-
协议演进
- 采用 可选字段(protobuf 的 optional/oneof)或 版本号,保证旧客户端在新服务器上仍能正常工作。
📌 其实,协议就是双方约定好的结构化的数据
网络版计算器
例如, 我们需要实现一个服务器版的加法器。我们需要客户端把要计算的两个加数发过去。 然后由服务器进行计算,最后再把结果返回给客户端.
-
约定方案一:
- 客户端发送一个形如"1+2"的字符串;
- 这个字符串中有两个操作数都是整形;
- 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
- 数字和运算符之间没有空格;
- …
-
约定方案二:
- 定义结构体来表示我们需要交互的信息;
- 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;
- 这个过程叫做 “序列化” 和 “反序列化”
序列化 和 反序列化
无论我们采用方案一, 还是方案二, 还是其他的方案,只要保证,一端发送时构造的数据,在另一端能够正确的进行解析就是ok的。这种约定就是 应用层协议
但是,为了让我们深刻理解协议,我们打算自定义实现一下协议的过程。
- 我们采用方案2,我们也要体现协议定制的细节
- 我们要引入序列化和反序列化,只不过我们直接采用现成的方案 – jsoncpp库
- 我们要对socket进行字节流的读取处理
不过我们会在下篇文章中自定义实现一下协议
2. 重新理解read、write、recv、send和tcp为什么支持全双工
-
在任何一台主机上,TCP连接既有发送缓冲区,又有接收缓冲区,所以,在内核中,可以在发消息的同时,也可以收消息,即全双工
-
实际数据什么时候发,发多少,出错了怎么办,由TCP控制,所以TCP叫做传输控制协议
- write和read是否将数据发送到网络中?
write和read操作不直接将数据发送到网络中,而是在主机内部完成数据拷贝:
- write操作:应用层将数据从应用缓冲区拷贝到内核缓冲区(发送缓冲区),再由TCP协议将内核缓冲区的数据通过网络层(如IP)发送到网络中。
- read操作:网络层将接收到的数据存入内核缓冲区(接收缓冲区),read再将内核缓冲区的数据拷贝到应用缓冲区。
因此,write和read的本质是主机内部的数据拷贝,而非直接与网络交互。
- 主机间通信的本质是什么?
主机间通信的本质是 “数据拷贝”:
- 发送方:应用层数据 → 内核缓冲区(write拷贝) → 网络层发送。
- 接收方:网络层接收 → 内核缓冲区 → 应用缓冲区(read拷贝)。
整个过程的核心是数据在“应用缓冲区-内核缓冲区-网络”之间的拷贝,而非“数据直接通过网络传输”(网络传输由TCP/IP协议栈底层完成)。
- TCP通信为何是全双工的?
TCP通信是全双工的,原因是 双方各自拥有独立的“发送缓冲区”和“接收缓冲区”:
- 每个TCP连接的两端(如主机A和主机B)都维护一对缓冲区:
- 发送缓冲区:用于暂存待发送的数据(由write拷贝而来)。
- 接收缓冲区:用于暂存接收到的数据(等待read拷贝到应用层)。
- 由于双方都有独立的发送和接收缓冲区,因此可以同时进行双向数据传输(如主机A发送数据时,主机B可以同时发送数据),从而实现全双工通信。
总结
- write/read是主机内部数据拷贝,不直接涉及网络传输;
- 主机间通信的本质是“数据拷贝”;
- TCP全双工的原因是双方拥有独立的发送/接收缓冲区,支持双向同时传输。
3. Jsoncpp
下面我们来介绍一个序列化方案Jsoncpp
Jsoncpp 是一个用于处理 JSON 数据的 C++ 库。它提供了将 JSON 数据序列化为字符串以及从字符串反序列化为 C++ 数据结构的功能。Jsoncpp 是开源的,广泛用于各种需要处理 JSON 数据的 C++ 项目中。
特性
- 简单易用:Jsoncpp 提供了直观的 API,使得处理 JSON 数据变得简单。
- 高性能:Jsoncpp 的性能经过优化,能够高效地处理大量 JSON 数据。
- 全面支持:支持 JSON 标准中的所有数据类型,包括对象、数组、字符串、数字、布尔值和 null。
- 错误处理:在解析 JSON 数据时,Jsoncpp 提供了详细的错误信息和位置,方便开发者调试。
当使用Jsoncpp库进行JSON的序列化和反序列化时,确实存在不同的做法和工具类可供选择。以下是对Jsoncpp中序列化和反序列化操作的详细介绍:
安装
ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel
序列化
序列化指的是将数据结构或对象转换为一种格式,以便在网络上传输或存储到文件中。Jsoncpp提供了多种方式进行序列化:
- 使用 Json::Value 的 toStyledString 方法:
- 优点:将 Json::Value 对象直接转换为格式化的JSON字符串。
- 示例:
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>int main()
{Json::Value root;root["name"] = "张三";root["sex"] = "男";std::string s = root.toStyledString();std::cout << s << std::endl;return 0;
}
解析:
- 创建 Json::Value 对象(根节点)
- 添加两个键值对:
- “name” → “张三”
- “sex” → “男”
- 使用 toStyledString() 方法,将JSON对象转换为格式化的字符串
- 输出结果到控制台
注意:
编译时需要链接 JsonCpp 库:
g++ -o testjson testjson.cc -ljsoncpp
运行结果:
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_network/NetCal/testJson$ g++ -o testjson testjson.cc -ljsoncpp
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_network/NetCal/testJson$ ./testjson
{"name" : "\u5f20\u4e09","sex" : "\u7537"
}
我们可以看到代码输出显示 Unicode 转义序列(如 \u5f20\u4e09)而不是中文字符,这是因为 JsonCpp 的 toStyledString() 方法默认会将非 ASCII 字符(如中文)转换为 Unicode 转义序列,这是一种符合 JSON 标准的安全处理方式,确保跨平台兼容性。不过在实际数据传输或存储中,转义序列是通用做法,解析时会自动还原为中文。
关键特点:toStyledString() 会自动添加缩进和换行,适合人类阅读
- 使用 Json::StreamWriter :
- 优点:提供了更多的定制选项,如缩进、换⾏符等。
- 示例:
int main()
{Json::Value root;root["name"] = "Bob";root["sex"] = "男";Json::StreamWriterBuilder wbuilder; // StreamWriter的工厂std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());std::stringstream ss;writer->write(root, &ss);std::cout << ss.str() << std::endl;return 0;
}
解析:
步骤分解:
-
创建Json::Value对象root,这是一个JSON对象。
-
向root中添加两个键值对:
-
“name” -> “Bob”
-
“sex” -> “男”
-
-
创建Json::StreamWriterBuilder对象wbuilder,它用于创建StreamWriter。
-
使用wbuilder创建一个StreamWriter对象,这里用到了智能指针std::unique_ptr。
-
创建一个字符串流std::stringstream对象ss。
-
使用writer的write方法将root写入到ss中。
-
将ss中的字符串输出到标准输出。
注意:这段代码与之前使用toStyledString()不同,它使用了StreamWriter来写入流。默认情况下,StreamWriter生成的JSON字符串是紧凑格式(没有换行和缩进)的,但是Json::StreamWriterBuilder可以设置多种输出格式。
运行结果:
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_network/NetCal/testJson$ g++ -o testjson testjson.cc -ljsoncpp
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_network/NetCal/testJson$
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_network/NetCal/testJson$ ./testjson
{"name" : "Bob","sex" : "\u7537"
}
可以看到输出结果默认带缩进和换行,如果需设置其他输出格式,则需要配置
以下是主要的配置参数及其说明:
- “
indentation
”:设置缩进字符串,用于美化输出(默认值为 “\t”,即制表符)。若设置为空字符串 “”,则输出为紧凑格式(无缩进和换行)。 - “
emitUTF8
”:控制是否直接输出 UTF-8 字符。默认值为 false(会将非 ASCII 字符转义为 Unicode 序列,如 \u7537),设为 true 可避免转义(直接输出原始字符)。 - “
commentStyle
”:处理 JSON 中的注释(如 // 或 /* */),可选值为 “None”(忽略注释)或其他模式。 - “
enableYAMLCompatibility
”:启用 YAML 兼容性输出(如将空值表示为 ~),默认值为 false。 - “
dropNullPlaceholders
”:控制是否忽略 null 值。若设为 true,则不会输出 null 字段。 - “
precision
”:设置浮点数输出精度(小数位数),例如 writerBuilder[“precision”] = 3 会保留 3 位小数。 - “
precisionType
”:指定精度类型(如 “decimal” 表示十进制格式)。 - “
lineBreak
”:自定义换行符(如设置为 “\n” 或 “\r\n”),影响多行输出的换行方式。
示例:
int main()
{Json::Value root;root["name"] = "Bob";root["sex"] = "男";Json::StreamWriterBuilder wbuilder; // StreamWriter的工厂wbuilder["indentation"] = ""; // 紧凑输出wbuilder["emitUTF8"] = true; // 直接输出 UTF-8 字符(避免转义)std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());std::stringstream ss;writer->write(root, &ss);std::cout << ss.str() << std::endl;return 0;
}
运行结果:
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_network/NetCal/testJson$ g++ -o testjson testjson.cc -ljsoncpp
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_network/NetCal/testJson$ ./testjson
{"name":"Bob","sex":"男"}
- 使用 Json::StyledWriter
- Json::StyledWriter 是一个简单易用的 JSON 格式化写入器,适合需要输出美观、易读的 JSON 字符串的场景。如果你需要更灵活的控制(如缩进大小、是否格式化等),则可以使用 StreamWriterBuilder 进行配置。
- 示例:
int main()
{Json::Value root;root["name"] = "Bob";root["sex"] = "男";Json::StyledWriter writer;std::string s = writer.write(root);std::cout << s << std::endl;return 0;
}
运行结果:
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_network/NetCal/testJson$ g++ -o testjson testjson.cc -ljsoncpp
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_network/NetCal/testJson$ ./testjson
{"name" : "Bob","sex" : "\u7537"
}
缺点:
-
体积较大:相比紧凑格式,输出的字符串更长
-
不适合网络传输:额外的空格和换行会增加传输数据量
-
缺乏配置选项:不能自定义缩进大小等格式细节
- 使用 Json::FastWriter :
- 优点:比 StyledWriter 更快,因为它不添加额外的空格和换行符。
- 示例:
int main()
{Json::Value root;root["name"] = "Bob";root["sex"] = "男";Json::FastWriter writer;std::string s = writer.write(root);std::cout << s << std::endl;return 0;
}
运行结果:
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_network/NetCal/testJson$ g++ -o testjson testjson.cc -ljsoncpp
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_network/NetCal/testJson$ ./testjson
{"name":"Bob","sex":"\u7537"}
反序列化
反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。Jsoncpp提供了以下方法进行反序列化:
- 使⽤ Json::Reader :
- 优点:提供详细的错误信息和位置,方便调试。
- 示例:
int main()
{// JSON 字符串std::string json_string = "{\"name\":\"张三\", \"age\":30, \"city\":\"北京\"}";// 解析 JSON 字符串Json::Reader reader;Json::Value root;// 从字符串中读取 JSON 数据bool parsingSuccessful = reader.parse(json_string, root);if (!parsingSuccessful){// 解析失败,输出错误信息std::cout << "Failed to parse JSON: " << reader.getFormattedErrorMessages() << std::endl;return 1;}// 访问 JSON 数据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;return 0;
}
解析:
-
定义一个JSON字符串。
-
创建Json::Reader对象和Json::Value对象。
-
使用Reader的parse方法解析字符串,如果解析失败,打印错误信息并返回。
-
从Json::Value对象中获取数据,注意这里使用了asString和asInt方法来转换类型。
-
打印提取的数据。
运行结果:
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_network/NetCal/testJson$ g++ -o testjson testjson.cc -ljsoncpp
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_network/NetCal/testJson$ ./testjson
Name: 张三
Age: 30
City: 北京
- 使用 Json::CharReader 的派生类(上面的已经足够了):
- 在某些情况下,你可能需要更精细地控制解析过程,可以直接使用 Json::CharReader 的派生类。
- 但通常情况下,使用 Json::parseFromStream 或 Json::Reader 的 parse 方法就足够了。
- 示例:
int main()
{std::string json_string = "{\"name\":\"张三\", \"age\":30, \"city\":\"北京\"}";Json::Value root;Json::CharReaderBuilder builder;std::unique_ptr<Json::CharReader> reader(builder.newCharReader());std::string errors;bool parsingSuccessful = reader->parse(json_string.c_str(), json_string.c_str() + json_string.length(), &root, &errors);if (!parsingSuccessful){std::cout << "Failed to parse JSON: " << errors << std::endl;return 1;}// 访问数据(同上)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;return 0;
}
运行结果:
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_network/NetCal/testJson$ g++ -o testjson testjson.cc -ljsoncpp
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_network/NetCal/testJson$ ./testjson
Name: 张三
Age: 30
City: 北京
总结
• toStyledString 、 StreamWriter 和 FastWriter 提供了不同的序列化选项,你可以根
据具体需求选择使用。
• Json::Reader 和 parseFromStream 函数是Jsoncpp中主要的反序列化工具,它们提供了强大的错误处理机制。
• 在进行序列化和反序列化时,请确保处理所有可能的错误情况,并验证输入和输出的有效性
Json::Value
Json::Value 是 Jsoncpp 库中的一个重要类,用于表示和操作 JSON 数据结构。以下是一些常用的 Json::Value 操作列表:
- 构造函数
- Json::Value() :默认构造函数,创建一个空的 Json::Value 对象。
- Json::Value(ValueType type, bool allocated = false) :根据给定的ValueType (如 nullValue , intValue , stringValue 等)创建一个Json::Value 对象。
- 访问元素
- Json::Value& operator[](const char* key) :通过键(字符串)访问对象中的元
素。如果键不存在,则创建一个新的元素。 - Json::Value& operator[](const std::string& key) :同上,但使用 std::string 类型的键。
- Json::Value& operator[](ArrayIndex index) :通过索引访问数组中的元素。如果索引超出范围,则创建一个新的元素。
- Json::Value& at(const char* key) :通过键访问对象中的元素,如果键不存在则抛出异常。
- Json::Value& at(const std::string& key) :同上,但使用 std::string 类型的键。
- 类型检查
- bool isNull() :检查值是否为 null。
- bool isBool() :检查值是否为布尔类型。
- bool isInt() :检查值是否为整数类型。
- bool isInt64() :检查值是否为 64 位整数类型。
- bool isUInt() :检查值是否为无符号整数类型。
- bool isUInt64() :检查值是否为 64 位无符号整数类型。
- bool isIntegral() :检查值是否为整数或可转换为整数的浮点数。
- bool isDouble() :检查值是否为双精度浮点数。
- bool isNumeric() :检查值是否为数字(整数或浮点数)。
- bool isString() :检查值是否为字符串。
- bool isArray() :检查值是否为数组。
- bool isObject() :检查值是否为对象(即键值对的集合)。
- 赋值和类型转换
- Json::Value& operator=(bool value) :将布尔值赋给 Json::Value 对象。
- Json::Value& operator=(int value) :将整数赋给 Json::Value 对象。
- Json::Value& operator=(unsigned int value) :将无符号整数赋给 Json::Value 对象。
- Json::Value& operator=(Int64 value) :将 64 位整数赋给 Json::Value 对象。
- Json::Value& operator=(UInt64 value) :将 64 位无符号整数赋给 Json::Value
对象。 - Json::Value& operator=(double value) :将双精度浮点数赋给 Json::Value 对象
- Json::Value& operator=(const char* value) :将 C 字符串赋给 Json::Value 对象。
- Json::Value& operator=(const std::string& value) :将 std::string 赋给 Json::Value 对象。
- bool asBool() :将值转换为布尔类型(如果可能)。
- int asInt() :将值转换为整数类型(如果可能)。
- Int64 asInt64() :将值转换为 64 位整数类型(如果可能)。
- unsigned int asUInt() :将值转换为无符号整数类型(如果可能)。
- UInt64 asUInt64() :将值转换为 64 位无符号整数类型(如果可能)。
- double asDouble() :将值转换为双精度浮点数类型(如果可能)。
- std::string asString() :将值转换为字符串类型(如果可能)。
- 数组和对象操作
- size_t size() :返回数组或对象中的元素数量。
- bool empty() :检查数组或对象是否为空。
- void resize(ArrayIndex newSize) :调整数组的大小。
- void clear() :删除数组或对象中的所有元素。
- void append(const Json::Value& value) :在数组末尾添加一个新元素。
- Json::Value& operator[](const char* key, const Json::Value& defaultValue = Json::nullValue) :在对象中插入或访问一个元素,如果键不存在则使用默认值。
- Json::Value& operator[](const std::string& key, const Json::Value&
defaultValue = Json::nullValue) :同上,但使用 std::string 类型的