协议和序列化反序列化
1. 再谈 “协议”
1.1 协议的概念
- “协议”本质就是一种约定,通信双方只要曾经做过某种约定,之后就可以使用这种约定来完成某种事情。而网络协议是通信计算机双方必须共同遵从的一组约定,因此我们一定要将这种约定用计算机语言表达出来,此时双方计算机才能识别约定的相关内容。
- 为了使数据在网络上能够从源到达目的,网络通信的参与方必须遵循相同的规则,我们将这套规则称为协议(protocol),而协议最终都需要通过计算机语言的方式表示出来。只有通信计算机双方都遵守相同的协议,计算机之间才能互相通信交流。
1.2 结构化数据的传输
(1)通信双方在进行网络通信时:
- 如果需要传输的数据是一个字符串,那么直接将这一个字符串发送到网络当中,此时对端也能从网络当中获取到这个字符串。
- 但是如果需要传输的是一些结构化的数据,此时就不能将这些数据一个个发送到网络当中。
(2)协议是一种 “约定”。socket api的接口,在读写数据时,都是按 “字符串” 的方式来发送接收的。如果我们要传输一些"结构化的数据" 怎么办呢?
- 比如现在要实现一个网络版的计算器,那么客户端每次给服务端发送的请求数据当中,就需要包括左操作数、右操作数以及对应需要进行的操作,此时客户端要发送的就不是一个简单的字符串,而是一组结构化的数据。类似如下的结构:
class cal { int _x; int _y; char _op; }
假设我定义了一个cal结构体对象d(10, 20, ‘+’)。我们不能直接把此结构体对象d(二进制序列)直接交给服务端。解决办法如下:
(3)我们可以把结构体序列化 + 反序列化进行处理即可
- 如上结构体对象c(10, 20, ‘+’)。我们将其按照一定的规则转成字符串10:20:+。然后再发送给服务端。
- 且你和服务端有个协议(约定):一共有三个区域,前两个是int,后一个是char,用:分割。
- 此时服务端接受数据后再按相同的规则把接收到的数据转化为结构体
上述过程中,我们把结构化数据转化成字符串或字节流序列叫做序列化。把你发过来的字符串按照一定要求转成服务器所要用到的对象叫做反序列化。
注意:
(图片来源网络,侵删)- 我们需要在定制协议的时候,序列化之后,需要将长度设置为4字节,并把长度放入序列化之后的字符串的开始之前。这就是自描述长度的协议。
- 此长度是一定要加上的。不然就好比如你给张三寄快递,张三收到了快递,但是你若不告诉张三有多少快递,张三就会一直担心快递有没有拿完。
综上可知:
- 客户端可以定制一个结构体,将需要交互的信息定义到这个结构体当中。客户端发送数据时先对数据进行序列化,服务端接收到数据后再对其进行反序列化,此时服务端就能得到客户端发送过来的结构体,进而从该结构体当中提取出对应的信息。
1.3 序列化和反序列化
(1)什么是序列化和反序列化:
- 序列化是将对象的状态信息转换为可以存储或传输的形式(字节序列)的过程。
- 反序列化是把字节序列恢复为对象的过程。
OSI七层模型中表示层的作用就是,实现设备固有数据格式和网络标准数据格式的转换。其中设备固有的数据格式指的是数据在应用层上的格式,而网络标准数据格式则指的是序列化之后可以进行网络传输的数据格式。
(2)序列化和反序列化的目的:
- 在网络传输时,序列化目的是为了方便网络数据的发送和接收,无论是何种类型的数据,经过序列化后都变成了二进制序列,此时底层在进行网络数据传输时看到的统一都是二进制序列。
- 序列化后的二进制序列只有在网络传输时能够被底层识别,上层应用是无法识别序列化后的二进制序列的,因此需要将从网络中获取到的数据进行反序列化,将二进制序列的数据转换成应用层能够识别的数据格式。
我们可以认为网络通信和业务处理处于不同的层级,在进行网络通信时底层看到的都是二进制序列的数据,而在进行业务处理时看得到则是可被上层识别的数据。如果数据需要在业务处理和网络通信之间进行转换,则需要对数据进行对应的序列化或反序列化操作。
(图片来源网络,侵删)2. 网络版计算器
在如下的代码演示中。服务器和客户端采用的是TCP网络程序(线程池版),对于服务端和客户端来说,就是固定的模式(创建套接字、绑定……)。重点还是在于网络版计算器的协议定制。
在编写网络版本计算器时先对套接字进行封装Socket.hpp:
#pragma once #include #include #include #include #include #include #include #include #include #include "Log.hpp" Log lg; enum { SocketErr = 2, BindErr, ListenErr, }; const int backlog = 10; class Sock { public: Sock() {} void Socket() { _sockfd = socket(AF_INET, SOCK_STREAM, 0); if(_sockfd
2.1 TcpServer.hpp文件
给服务端封装成一个TcpServer类。此服务端主要完成如下工作:
(1)对服务器进行初始化(Init成员函数):
- 调用socket函数,创建套接字。
- 调用bind函数,为服务端绑定一个端口号。
- 调用listen函数,将套接字设置为监听状态。
(2)启动服务器(run成员函数):
- 初始化完服务器后就可以启动服务器了,不断调用accept函数,从监听套接字当中获取新连接。创建子进程来进行任务处理,将子进程变成孤儿进程后就可以不需要管。
TcpServer.hpp:
#pragma once #include #include "Log.hpp" #include "Socket.hpp" #include using func_t = std::function; class TcpServer { public: TcpServer(uint16_t port, func_t func) :_port(port) ,callback_(func) {} bool Init() { _listensock.Socket(); _listensock.Bind(_port); _listensock.Listen(); return true; } void run() { signal(SIGCHLD, SIG_IGN); signal(SIGPIPE, SIG_IGN); while(1) { std::string clientip; uint16_t clientport; int sockfd = _listensock.Accept(clientip, clientport); if(sockfd 0) { buffer[n] = 0; inbuffer_stream += buffer; lg(Debug, "debug:\n%s", inbuffer_stream.c_str()); while (1) { std::string info = callback_(inbuffer_stream); if(info.empty()) { break; } lg(Debug, "debug, response:\n%s", info.c_str()); lg(Debug, "debug:\n%s", inbuffer_stream.c_str()); int m = write(sockfd, info.c_str(), info.size()); if (m
2.2 网络计算器任务(ServerCal类):
- 服务端收到客户端的数据,一定是经过序列化后的字符串。我们需要调用read函数进行读取。不过我们不能保证一次性将序列化后的字符串全部读取过来,因为TCP是面向字节流的,有自己的一套发送机制。就比如我们要的是完整的字符串(len\n"x op y"\n)。没有读完就只能继续读。
- 读取后调用decode函数检测是不是已经具有了一个完整的报文,若不是则继续读取。
- 读取成功后,调用Deserialize反序列化函数把序列化后的字符串转为结构化的数据。
- 通过调用执行计算任务函数Calculator将发序列化后的数据进行计算。
- 将计算好的数据(结构化的数据)调用Serialize序列化将结构化的数据转为字符串。
- 根据协议规定,还需要给序列化后的数据添加报头长度,调用encode函数完成。
- 最后调用write函数把最终结果写回客户端。
#pragma once #include #include "Protocol.hpp" enum { Div_Zero = 1, Mod_Zero, Other_Oper }; class ServerCal { public: ServerCal() {} Response CalculatorHelper(const Request& req) { Response res(0, 0); switch (req._op) { case '+': res._result = req._x + req._y; break; case '-': res._result = req._x - req._y; break; case '*': res._result = req._x * req._y; break; case '/': if(req._y == 0) { res._code = Div_Zero; break; } res._result = req._x / req._y; break; case '%': if(req._y == 0) { res._code = Mod_Zero; break; } res._result = req._x % req._y; break; default: res._code = Other_Oper; break; } return res; } std::string Calculator(std::string& package) { std::string content; bool r = Decode(package, content); //解包 if(!r) { return ""; } Request req; r = req.Deserialize(content); //反序列化 if(!r) { return ""; } content = ""; Response res = CalculatorHelper(req); res.Serialize(content); content = Encode(content); return content; } ~ServerCal() {} };
2.3 ServerCal.cpp文件
将上述两个头文件TcpServer.hpp和ServerCal.hpp包含在内,new一个TcpServer类,并且利用bind绑定Calculator函数的功能,这样服务器计算就会调用Calculator函数。
#include "TcpServer.hpp" #include "ServerCal.hpp" int main() { ServerCal cal; TcpServer* ts = new TcpServer(8080, std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1)); ts->Init(); ts->run(); return 0; }
2.4 客户端clientTcp.cpp文件
(1)执行代码逻辑如下:
- 调用socket函数,创建套接字。
- 客户端初始化完毕后需要调用connect函数连接服务端,当连接服务端成功后,客户端就可以向服务端发起计算请求了。
- 定义Request类对象req,复用makeRequest函数将输入的字符串的数据填到结构体对象req的成员变量里
- 先调用serialize函数序列化,再调用encode函数添加长度报头,返回值string类型的package对象。
- 利用write函数将package的内容写到套接字里,发送到网络里。调用read函数从套接字中读取数据存到字符串echoPackage里,注意此时的字符串是服务端encode加上长度报头后的结果,我们需要复用decode进行解码。
- 最后调用deserialize反序列化函数完成字符串到结构化数据的转变。并输出退出码和最终运算结果。
(2)ClientCal.cpp:
#include "Socket.hpp" #include "Protocol.hpp" const uint16_t port = 8080; const std::string ip = "115.159.193.163"; int main() { Sock sockfd; sockfd.Socket(); int c = sockfd.Connect(ip, port); if(c
- 初始化完服务器后就可以启动服务器了,不断调用accept函数,从监听套接字当中获取新连接。创建子进程来进行任务处理,将子进程变成孤儿进程后就可以不需要管。
- 客户端可以定制一个结构体,将需要交互的信息定义到这个结构体当中。客户端发送数据时先对数据进行序列化,服务端接收到数据后再对其进行反序列化,此时服务端就能得到客户端发送过来的结构体,进而从该结构体当中提取出对应的信息。
- 比如现在要实现一个网络版的计算器,那么客户端每次给服务端发送的请求数据当中,就需要包括左操作数、右操作数以及对应需要进行的操作,此时客户端要发送的就不是一个简单的字符串,而是一组结构化的数据。类似如下的结构:
还没有评论,来说两句吧...