一、为什么需要自定义协议?
之前我们已经知道,应用层协议本质上就是通信双方约定好的数据格式和处理规则。
如果只是传输简单的字符串,直接发送和接收就可以。
但如果需要传输结构化数据,比如一个计算请求:
cpp
int x;
int y;
char oper;
就需要考虑如何组织这些数据,以及接收方应该按照什么规则解析。
这时候,我们就可以通过自定义协议来解决。
本篇继续使用网络版计算器作为例子,看看如何设计一套简单的应用层协议。
二、设计 Request 和 Response
首先,我们需要明确客户端和服务器之间要交换什么数据。
对于计算器来说,客户端需要发送两个操作数和一个运算符。
因此可以定义一个请求类:
cpp
class Request
{
private:
int _data_x;
int _data_y;
char _oper;
};
其中:
_data_x:第一个操作数;_data_y:第二个操作数;_oper:运算符,支持+ - * / %。
服务器计算完成之后,还需要把结果返回给客户端。
因此可以定义一个响应类:
cpp
class Response
{
private:
int _result;
int _code;
};
其中:
_result:运算结果;_code:运算状态。
这样,请求和响应的数据结构就确定了。
但是,这还只是 C++ 内部的数据结构,我们还需要解决如何将它们转换成网络中可以传输的数据。
三、使用 Jsoncpp 实现序列化
如果让我们自己设计一套字符串格式,再手动解析每个字段,当然也可以实现。
但是这样需要处理大量字符串操作,代码也比较繁琐。
因此,可以使用现成的 JSON 库------Jsoncpp。
Jsoncpp 是一个用于处理 JSON 数据的 C++ 库,可以帮助我们完成 JSON 的序列化和反序列化。
首先,需要包含头文件:
cpp
#include <jsoncpp/json/json.h>
1. 序列化
假设现在有一个 Request 对象:
cpp
Request req(10, 20, '+');
我们希望将其中的数据转换成 JSON 字符串。
可以使用 Json::Value 创建 JSON 对象:
cpp
Json::Value root;
root["datax"] = _data_x;
root["datay"] = _data_y;
root["oper"] = _oper;
这里的 root 就是一个 JSON 对象。
通过 [] 可以设置对应的键值。
接下来使用 Json::FastWriter 将 JSON 对象转换成字符串:
cpp
Json::FastWriter writer;
*out = writer.write(root);
这样就完成了序列化。
例如,原本的结构化数据:
text
_data_x = 10
_data_y = 20
_oper = +
就可以转换成类似下面的 JSON 字符串:
json
{"datax":10,"datay":20,"oper":43}
这里的 43 是字符 '+' 对应的 ASCII 值,具体表现与字符在 JSON 中的存储方式有关。
2. 反序列化
发送方完成序列化之后,接收方就可以得到 JSON 字符串。
接下来需要把字符串还原成 Request 对象。
首先创建 JSON 对象和解析器:
cpp
Json::Value root;
Json::Reader reader;
然后调用 parse:
cpp
bool res = reader.parse(in, root);
如果解析成功,就可以取出其中的数据:
cpp
if(res)
{
_data_x = root["datax"].asInt();
_data_y = root["datay"].asInt();
_oper = root["oper"].asInt();
}
这里的 asInt() 用于将 JSON 中的值转换成整数。
这样就完成了反序列化。
整个过程可以理解为:
text
发送方:
Request
|
| Serialize
v
JSON字符串
|
| 网络传输
v
接收方:
JSON字符串
|
| Deserialize
v
Request
需要注意,反序列化之前必须判断解析是否成功,不能直接认为收到的数据一定是合法 JSON。
四、Response 同样需要序列化
Request 是客户端发送给服务器的请求,Response 则是服务器返回给客户端的响应。
因此,Response 也需要提供自己的序列化和反序列化方法。
例如:
cpp
bool Serialize(std::string *out)
{
Json::Value root;
root["result"] = _result;
root["code"] = _code;
Json::FastWriter writer;
*out = writer.write(root);
return true;
}
反序列化:
cpp
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;
}
这样,请求和响应都可以独立完成序列化与反序列化。
五、Jsoncpp 中常用的工具
除了上面使用的 Json::FastWriter 和 Json::Reader,Jsoncpp 还提供了其他工具。
1. 序列化工具
| 工具 | 作用 |
|---|---|
toStyledString() |
将 JSON 对象转换成格式化的 JSON 字符串 |
Json::StreamWriter |
提供更多格式定制选项 |
Json::FastWriter |
快速生成 JSON 字符串,输出比较紧凑 |
例如:
cpp
Json::Value root;
root["name"] = "joe";
root["age"] = 20;
std::string s = root.toStyledString();
如果使用 FastWriter:
cpp
Json::FastWriter writer;
std::string s = writer.write(root);
两种方式都可以完成序列化,只是生成的字符串格式不同。
2. 反序列化工具
cpp
Json::Reader
使用方法:
cpp
Json::Reader reader;
Json::Value root;
bool res = reader.parse(json_string, root);
如果解析成功,就可以通过键访问 JSON 中的数据。
另外,也可以通过 Json::Value 提供的类型转换函数获取对应的数据:
cpp
root["name"].asString();
root["age"].asInt();
root["score"].asDouble();
六、总结
这部分主要解决了一个问题:如何将 C++ 中的结构化数据转换成可以传输的字符串。
通过自定义 Request 和 Response,我们确定了需要传输的数据结构。
再借助 Jsoncpp,就可以方便地完成序列化和反序列化。
这里需要区分两个概念:
- 自定义协议:规定通信双方应该传输什么数据,以及数据的组织规则。
- 序列化:将结构化数据转换成约定的传输形式。
- 反序列化:将传输形式的数据还原成结构化数据。
不过,仅仅完成序列化还不够。
因为 TCP 是字节流协议,即使我们成功生成了一条完整的 JSON 字符串,也不代表一次 recv 就一定能够收到完整的数据。
这个问题就是下一篇要解决的内容。