应用层自定义协议与序列化
1、协议和序列化
之前写UDP和TCP的时候,本质上来说也是一种协议,比如使用UDP实现一个英汉词典的功能,我们就规定了客户端只能发送英文单词,然后服务端会根据英文单词查找中文意思返回。再比如TCP实现的远程命令执行,我们就规定了客户端必须输入Linux的指令。
在网络通信中,我们都是通过字符串来发送接受数据的,那么如果现在我们想传递一些结构化的数据呢?
比如今天我在微信群里聊天,那么我发出一条消息,我需要将头像、昵称、消息发给服务器,然后再有服务器转发给群里的其他人。那么是一条一条的发送给服务器还是整合在一起发送给服务器呢?当然是把这三条整合在一起发送给服务器最好。头像其实是字符串,因为你的头像上传到服务器了,所以就是一个路径,昵称和消息也是字符串,所以我们可以把这三个数据打包成一个大字符串发送给服务器。然后服务器再转发给其他用户,其他用户将信息提取出来。而这些信息肯定也是要在一个结构体里面保存起来的,而客户端和服务器都认识这个结构体,这就是双方约定好的协议。
那么如果是一个结构体对象数据如何传递给对方呢?

那么我们能不能直接将上图定义的data数据直接以二进制方式发送给服务器呢?可以是可以,不过可能会有问题。比如我们服务端是C/C++写的,但是客户端并不一定也是用C/C++写的,还有客户端的机器可能是32/64位的,所以会有问题。
因此我们还有下面这种更好的方式:

如图,聊天信息比如:消息、时间、昵称,这些信息是在一个结构体里面的,我们将他们提取出来转换成一个大字符串,把信息由多变一,方便网络发送,这个过程就叫做序列化。服务端在接受的时候,获取这个大字符串,然后将每个信息提取出来,把信息由一变多,方便上层处理,这个过程就叫做反序列化。
TCP全双工+面向字节流:
之前在TCP服务端HandlerRequest那里read和write是不完善的。

1、为什么TCP支持全双工?
实际上我们创建套接字sockfd后,操作系统会给我们创建两个缓冲区:发送缓冲区和接收缓冲区。而我们调用系统调用write向网络发送数据本质上是将我们的数据拷贝到发送缓冲区中,然后再由操作系统将发送缓冲区的数据发送给对方的。对方read如果没有数据会阻塞住,当数据发送到对方的接收缓冲区后,read再把数据从接收缓冲区拷贝到上层。因此write和read向网络发送或接收数据,本质上都是在拷贝数据。而当主机A将数据写入发送缓冲区然后发送给主机B的接收缓冲区的时候,主机B也可以将数据写入自己的发送缓冲区,然后发送到主机A的接收缓冲区中,因此TCP是全双工的。所以当我们将数据拷贝到发送缓冲区后,就由操作系统自主决定什么时候发送了,所以TCP叫做传输控制协议,这里的控制就是由操作系统自主来控制的。
2、TCP是面向字节流的?
由于TCP发送数据是由操作系统自己控制的,如果今天主机B迟迟没有将接收缓冲区的数据读走,上层可能还在进行数据的处理来不及读走,现在主机B的接收缓冲区可能快满了,只剩下10个字节的空间了。现在主机A要发送20字节的数据,先将这20字节数据拷贝到发送缓冲区中,然后主机A识别主机B只剩下10字节的空间了,所以就只发送过去10字节的数据。那么很不巧,这时候主机B上层将缓冲区的数据全部读走了,但是最后的数据就无法被正确解析了,因为最后只有10个字节的数据,数据是不完整的。因此TCP需要应用层自己保证报文的完整性。
TCP面向字节流,也就是说数据发送是按字节来发送的,发送方可能发送很多次,接收方可能一次就将它全部读完。就比如自来水公司给你们家通了自来水,你想用多少就用多少,你可以用瓶子接,也可以用盆接,还可以用桶接。TCP将应用层传递的数据视为无结构的字节流,不维护消息之间的边界。
而UDP是面向数据报的,所以UDP是要保证数据完整的发送给对方的。
所以,操作系统内部可能会存在大量的报文来不及处理,这么多报文操作系统要不要管理起来呢?要,如何管理?——先描述,再组织。

操作系统中描述报文的结构体就是struct sk_buff。那么网络通信需要通过文件描述符来实现,所以必定要创建struct file对象,如果是普通文件对象,struct file中的private_data就为空,如果是网络通信,那么该指针指向struct socket对象,而struct socket对象里面有个struct sock* sk,它又指向一个struct sock对象,sock里面有两个队列,一个接收队列,一个写队列,它们都是struct sk_buff_head类型的。而这两个队列结构里面又有struct sk_buff*的指针next和prev,它们指向了一个一个的报文sk_buff。
今天我们要实现一个网络版本的计算器,那么就需要制定协议。
网络功能我们就使用TCP来实现。
约定方案一:
1、客户端发送一个形如:1+2的字符串。
2、这个字符串中有两个操作数,都是整数。
3、两个整数之间有一个运算符,可以是加减乘除中的任意一个。
4、数字和运算符之间没有空格。
那么服务端接收到客户端发送过来的数据就按约定好的进行解析读取,计算后返回给客户端结果。这本质上就是在制定协议。
约定方案二:
1、定义结构体来表示我们需要交互的信息。
2、发送数据时将这个结构体按照一个规则转换成字符串,接收到数据时再按相同的规则将字符串转回结构体。这个过程就叫做序列化和反序列化。
首先我们需要给网络计算器制定协议:协议就是双方约定好的结构化的数据。

那么我们定义两个结构体Request和Response。Request里面有两个整形变量和一个字符,将来就通过x oper y来计算结果返回给客户端。Response里面有result和code,result表示计算结果,而由于除0是有问题的,所以我们通过code来表示结果是否可信,比如0表示成功,1表示除0结果不可信。那么客户端服务器都约定好,将来客户端发送数据给服务器,那么服务器也能根据数据提取出Request对象,再将计算结果写入Response对象发送给客户端,客户端接收后也能获取Response对象然后获得计算结果。这就是在制定协议。
其次我们要选择序列化的方案:
1、自己做:我们可以自己来做序列化和反序列化的方案,比如我们约定将来客户端发送过来的数据都是"xopery",但是由于TCP是面向字节流的,因此用户层需要判断报文的完整性。我们在该数据前面加上数据长度和\r\n,在该数据后面再加上\r\n。也就是下面这个样子:

发送过来的数据为11+22,长度为5,所以前面加上5\r\n,后面加上\r\n。这样将来如果发送过来的是多个报文,我只要找到\r\n,然后前面就是有效载荷的长度,我们将前面长度提取出来,根据长度获取后面的数据即可。
2、使用工具:有xml && json && protobuf。
我们可以使用工具,我们选择jsoncpp。

如图,它会将数据转换成上图这种格式,左边这种是有\n的,右边是直接使用,间隔的。序列化后转换成这种格式的字符串后发送给对方,将来对方拿着这个数据再反序列化就可以获取数据。
2、Jsoncpp
Jsoncpp是一个用于处理 JSON 数据的 C++ 库。它提供了将JSON数据序列化为字符串以及从字符串反序列化为C++数据结构的功能。 Jsoncpp是开源的,广泛用于各种需要处理JSON数据的C++项目中。
特性:
1.简单易用:Jsoncpp 提供了直观的 API,使得处理 JSON 数据变得简单。
2.高性能:Jsoncpp 的性能经过优化,能够高效地处理大量 JSON 数据。
3.全面支持:支持 JSON 标准中的所有数据类型,包括对象、数组、字符串、数字、布尔值和 null。
4.错误处理:在解析 JSON 数据时, Jsoncpp 提供了详细的错误信息和位置,方便开发者调试。
首先需要安装jsoncpp:
ubuntu: sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel

安装后/usr/include目录下就有个jsoncpp文件,里面的json就包含了需要引入的头文件,将来在使用的时候需要引入如:#include <jsoncpp/json/reader.h>,因为系统默认只能找到/usr/include。
序列化:
1、使用Json::Value的toStyledString方法:
优点:将Json::Value对象直接转换为格式化的JSON字符串。
#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;
}

编译后报错,需要指明链接文件:-ljsoncpp

2、使用Json::StreamWriter:
优点:提供了更多的定制选项,如缩进、换行符等。
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
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;
}

3、使用Json::FastWriter:
优点:比StyledWriter更快,因为它不添加额外的空格和换行符。
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
Json::FastWriter writer1;
Json::StyledWriter writer2;
std::string s1 = writer1.write(root);
std::string s2 = writer2.write(root);
std::cout << s1 << std::endl;
std::cout << s2 << std::endl;
return 0;
}

反序列化:使用 Json::Reader
优点:提供详细的错误信息和位置,方便调试。
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root1;
root1["name"] = "joe";
root1["sex"] = "男";
Json::StreamWriterBuilder wbuilder;
std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());
std::stringstream ss;
writer->write(root1, &ss);
std::cout << ss.str() << std::endl;
std::string json_string = ss.str();
Json::Reader reader;
Json::Value root2;
bool parsingSuccessful = reader.parse(json_string, root2);
if (!parsingSuccessful)
{
std::cout << "Failed to parse JSON: " << reader.getFormattedErrorMessages() << std::endl;
return 1;
}
std::string name = root2["name"].asString();
std::string sex = root2["sex"].asString();
std::cout << "name: " << name << std::endl;
std::cout << "sex: " << sex << std::endl;
return 0;
}

3、使用Jsoncpp实现网络版计算器
直接将之前写的TCP远程命令执行拿过来,去掉CommandExec.hpp,还需要添加Calculator.hpp和Protocol.hpp。

下面我们先实现Protocol.hpp,如下:
#pragma once
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
bool Encode(std::string& message)
{
}
bool Decode(std::string& package, std::string* content)
{
}
class Request
{
public:
Request()
:_x(0)
,_y(0)
,_oper(0)
{
}
Request(int x, int y, char oper)
:_x(x)
,_y(y)
,_oper(oper)
{
}
bool Serialize(std::string& out_string)
{
}
bool Deserialize(std::string& in_string)
{
}
void Print()
{
std::cout << _x << std::endl;
std::cout << _y << std::endl;
std::cout << _oper << std::endl;
}
int X() const {
return _x; }
int Y() const {
return _y; }
char Oper() const {
return

最低0.47元/天 解锁文章
1711

被折叠的 条评论
为什么被折叠?



