目录
1、重谈 "协议"
协议的概念
结构化数据的传输
序列化和反序列化
2、网络版计算器
2.1、服务端serverTcp.cc文件
服务端serverTcp.cc总代码
2.2、客户端clientTcp.cc文件
客户端clientTcp.cc总代码
2.3、协议定制Protocol.hpp文件
服务端需要的协议
客户端需要的协议
协议定制Protocol.hpp总代码
2.4、测试结果
3、json序列化和反序列化
3.1、安装json库
3.2、request请求类中的json
request的json序列化
request的json反序列化
3.3、reponse响应类中的json
3.4、Makefile中的json操作
-D命令行定义宏
-ljsoncpp 包json对应的第三方库
3.5、代码测试
3.6、Json::FastWriter与Json::StyledWriter两种显示风格
4、源码gitee链接
1、重谈 "协议"
协议的概念
- “协议”本质就是一种约定,通信双方只要曾经做过某种约定,之后就可以使用这种约定来完成某种事情。而网络协议是通信计算机双方必须共同遵从的一组约定,因此我们一定要将这种约定用计算机语言表达出来,此时双方计算机才能识别约定的相关内容
- 为了使数据在网络上能够从源到达目的,网络通信的参与方必须遵循相同的规则,我们将这套规则称为协议(protocol),而协议最终都需要通过计算机语言的方式表示出来。只有通信计算机双方都遵守相同的协议,计算机之间才能互相通信交流。
结构化数据的传输
通信双方在进行网络通信时:
- 如果需要传输的数据是一个字符串,那么直接将这一个字符串发送到网络当中,此时对端也能从网络当中获取到这个字符串。
- 但如果需要传输的是一些结构化的数据,此时就不能将这些数据一个个发送到网络当中。
协议是一种 "约定"。socket api的接口,在读写数据时,都是按 "字符串" 的方式来发送接收的。如果我们要传输一些"结构化的数据" 怎么办呢?
- 比如现在要实现一个网络版的计算器,那么客户端每次给服务端发送的请求数据当中,就需要包括左操作数、右操作数以及对应需要进行的操作,此时客户端要发送的就不是一个简单的字符串,而是一组结构化的数据。类似如下的结构:
struct data { int x; int y; char op; }
- 假设我定义了一个data结构体对象d(10, 20, '+')。我们不能直接把此结构体对象d(二进制序列)直接交给服务端。解决办法如下。
序列化 + 反序列化
- 如上结构体对象d(10, 20, '+')。我们将其按照一定的规则转成字符串10:20:+。然后再发送给服务端。
- 且你和服务端有个协议(约定):一共有三个区域,前两个是int,后一个是char,用:分割。
- 此时服务端接受数据后再按相同的规则把接收到的数据转化为结构体
上述过程中,我们把结构化数据转化成字符串或字节流序列叫做序列化。把你发过来的字符串按照一定要求转成服务器所要用到的对象叫做反序列化。
注意:
- 我们需要在定制协议的时候,序列化之后,需要将长度设置为4字节,并把长度放入序列化之后的字符串的开始之前。这就是自描述长度的协议。
- 此长度是一定要加上的。不然就好比如你给张三寄快递,张三收到了快递,但是你若不告诉张三有多少快递,张三就会一直担心快递有没有拿完。
综上:
- 客户端可以定制一个结构体,将需要交互的信息定义到这个结构体当中。客户端发送数据时先对数据进行序列化,服务端接收到数据后再对其进行反序列化,此时服务端就能得到客户端发送过来的结构体,进而从该结构体当中提取出对应的信息。
序列化和反序列化
序列化和反序列化:
- 序列化是将对象的状态信息转换为可以存储或传输的形式(字节序列)的过程。
- 反序列化是把字节序列恢复为对象的过程。
OSI七层模型中表示层的作用就是,实现设备固有数据格式和网络标准数据格式的转换。其中设备固有的数据格式指的是数据在应用层上的格式,而网络标准数据格式则指的是序列化之后可以进行网络传输的数据格式。
序列化和反序列化的目的:
- 在网络传输时,序列化目的是为了方便网络数据的发送和接收,无论是何种类型的数据,经过序列化后都变成了二进制序列,此时底层在进行网络数据传输时看到的统一都是二进制序列。
- 序列化后的二进制序列只有在网络传输时能够被底层识别,上层应用是无法识别序列化后的二进制序列的,因此需要将从网络中获取到的数据进行反序列化,将二进制序列的数据转换成应用层能够识别的数据格式。
我们可以认为网络通信和业务处理处于不同的层级,在进行网络通信时底层看到的都是二进制序列的数据,而在进行业务处理时看得到则是可被上层识别的数据。如果数据需要在业务处理和网络通信之间进行转换,则需要对数据进行对应的序列化或反序列化操作。
2、网络版计算器
在下面的代码演示中。我的服务器和客户端采用的是前面博文的TCP网络程序(线程池版),对于服务端和客户端来说,就是那样固定的板子(创建套接字、绑定……)。下面简要提下。重点还是在于网络版计算器的协议定制。
2.1、服务端serverTcp.cc文件
这里我们给服务端封装成一个ServerTcp类,我们将此服务端设计成线程池版。此服务端主要完成如下工作:
1、对服务器进行初始化(init成员函数):
- 调用socket函数,创建套接字。
- 调用bind函数,为服务端绑定一个端口号。
- 调用listen函数,将套接字设置为监听状态。
- 加载线程池
2、启动服务器(loop成员函数):
- 初始化完服务器后就可以启动服务器了,服务器启动后要做的就是先启动线程池,然后不断调用accept函数,从监听套接字当中获取新连接。这里的服务端我们引入了线程池,所以将来在运行服务端时,服务端预先创建一批线程,当有客户端请求连接时就让这些线程为客户端提供服务,此时客户端一来就有线程为其提供服务,而不是当客户端来了才创建对应的服务线程。
class ServerTcp { public: ServerTcp(uint16_t port, const string &ip = "") {} ~ServerTcp() {} public: // 初始化 void init() { // 1、创建socket listensock_ = socket(AF_INET, SOCK_STREAM, 0); if (listensock_ < 0) { logMessage(FATAL, "socket: %s", strerror(errno)); // 创建失败,打印日志 exit(SOCKET_ERR); } logMessage(DEBUG, "socket: %s, %d", strerror(errno), listensock_); // 2、bind绑定 // 2.1、填充服务器信息 struct sockaddr_in local; // 用户栈 memset(&local, 0, sizeof(local)); local.sin_family = PF_INET; local.sin_port = htons(port_); ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr)); // 2.2、将本地socket信息,写入listensock_对应的内核区域 if (bind(listensock_, (const struct sockaddr *)&local, sizeof(local)) == -1) { logMessage(FATAL, "bind: %s", strerror(errno)); // 绑定失败,打印日志 exit(BIND_ERR); } logMessage(DEBUG, "bind: %s, %d", strerror(errno), listensock_); // 3、监听socket if (listen(listensock_, 5) < 0) { logMessage(FATAL, "listen: %s", strerror(errno)); // 监听失败,打印日志 exit(LISTEN_ERR); } logMessage(DEBUG, "listen: %s, %d", strerror(errno), listensock_); // 允许别人连接你了 // 4、加载线程池 tp_ = ThreadPool<Task>::getInstance(); } // 启动服务端 void loop() { // 启动线程池 tp_->start(); logMessage(DEBUG, "thread pool start success, thread num: %d", tp_->threadNum()); signal(SIGCHLD, SIG_IGN); // 忽略SIGCHLD信号 while (!quit_) { // 4、获取连接 struct sockaddr_in peer; socklen_t len = sizeof(peer); int serviceSock = accept(listensock_, (struct sockaddr *)&peer, &len); if (quit_) break; if (serviceSock < 0) { logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock); // 获取连接失败 continue; } // 4.1、获取客户端基本信息 uint16_t peerPort = ntohs(peer.sin_port); string peerIp = inet_ntoa(peer.sin_addr); logMessage(DEBUG, "accept: %s | %s:[%d], socket fd: %d", strerror(errno), peerIp.c_str(), peerPort, serviceSock); // 5.3 v3.3 Task t(serviceSock, peerIp, peerPort, netCal); tp_->push(t); } } // 安全退出 bool quitServer() {} private: int listensock_; // 监听套接字socket uint16_t port_; // port string ip_; // ip // 引入线程池 ThreadPool<Task> *tp_; // 安全退出 bool quit_; };
3、网络计算器任务(netCal函数):
- 我服务端收到客户端的数据,一定是经过序列化后的字符串。我们需要调用read函数进行读取。不过我们不能保证一次性将序列化后的字符串全部读取过来,因为TCP是面向字节流的,有自己的一套发送机制。就比如我们要的是完整的字符串(9\r\n100 + 200\r\n)。没有读完就只能继续读。
- 读取后调用decode函数检测是不是已经具有了一个完整的报文,若不是则继续读取。
- 读取成功后,调用deserialize反序列化函数把序列化后的字符串转为结构化的数据。
- 通过调用执行计算任务函数calculator将发序列化后的数据进行计算。
- 将计算好的数据(结构化的数据)调用serialize序列化将结构化的数据转为字符串。
- 根据协议规定,还需要给序列化后的数据添加报头长度,调用encode函数完成。
- 最后调用write函数把最终结果写回客户端。
static Response calculator(const Request &req) { Response resp; switch (req.op_) { case '+': resp.result_ = req.x_ + req.y_; break; case '-': resp.result_ = req.x_ - req.y_; break; case '*': resp.result_ = req.x_ * req.y_; break; case '/': { // x_ / y_ if (req.y_ == 0) resp.exitCode_ = -1; // -1:除0错误 else resp.result_ = req.x_ / req.y_; } break; case '%': { // x_ % y_ if (req.y_ == 0) resp.exitCode_ = -2; // -2:模0错误 else resp.result_ = req.x_ % req.y_; } break; default: resp.exitCode_ = -3; // -3:非法操作符 break; } return resp; } // 网络计算器任务 void netCal(int sock, const string &clientIp, uint16_t clientPort) { assert(sock >= 0); assert(!clientIp.empty()); assert(clientPort >= 1024); // 9\r\n100 + 200\r\n 9\r\n100 + 200\r\n string inbuffer; // 一次不一定能读完 while (true) { Request req; char buff[128]; ssize_t s = read(sock, buff, sizeof(buff) - 1); if (s == 0) { logMessage(NOTICE, "client[%s:%d] close sock, service done", clientIp.c_str(), clientPort); break; } else if (s < 0) { logMessage(WARINING, "read client[%s:%d] error, errorcode: %d, errormessage: %s", clientIp.c_str(), clientPort, errno, strerror(errno)); break; } // read success buff[s] = 0; inbuffer += buff; // 1、检测inbuffer是不是已经具有了一个完整的报文strPackage uint32_t packageLen = 0; string package = req.decode(inbuffer, &packageLen); if (packageLen == 0) continue; // 无法提取一个完整的报文,继续读取 // 2、已经获得一个完整的package if (req.deserialize(package)) { req.debug(); // 3、处理逻辑,输入的是一个req,得到一个resp Response resp = calculator(req); // resp是一个结构化的数据 // 4、对resp进行序列化 string respPackage; resp.serialize(&respPackage); // 5、对报文进行encode respPackage = resp.encode(respPackage, respPackage.size()); // 6、简单进行发送 write(sock, respPackage.c_str(), respPackage.size()); } } }
服务端serverTcp.cc总代码
#include "utli.hpp" #include "ThreadPool.hpp" #include "Task.hpp" #include "daemon.hpp" #include "Protocol.hpp" #include <signal.h> #include <sys/types.h> #include <sys/wait.h> #include <pthread.h> static Response calculator(const Request &req) { Response resp; switch (req.op_) { case '+': resp.result_ = req.x_ + req.y_; break; case '-': resp.result_ = req.x_ - req.y_; break; case '*': resp.result_ = req.x_ * req.y_; break; case '/': { // x_ / y_ if (req.y_ == 0) resp.exitCode_ = -1; // -1:除0错误 else resp.result_ = req.x_ / req.y_; } break; case '%': { // x_ % y_ if (req.y_ == 0) resp.exitCode_ = -2; // -2:模0错误 else resp.result_ = req.x_ % req.y_; } break; default: resp.exitCode_ = -3; // -3:非法操作符 break; } return resp; } // 网络计算器任务 void netCal(int sock, const string &clientIp, uint16_t clientPort) { assert(sock >= 0); assert(!clientIp.empty()); assert(clientPort >= 1024); // 9\r\n100 + 200\r\n 9\r\n100 + 200\r\n string inbuffer; // 一次不一定能读完 while (true) { Request req; char buff[128]; ssize_t s = read(sock, buff, sizeof(buff) - 1); if (s == 0) { logMessage(NOTICE, "client[%s:%d] close sock, service done", clientIp.c_str(), clientPort); break; } else if (s < 0) { logMessage(WARINING, "read client[%s:%d] error, errorcode: %d, errormessage: %s", clientIp.c_str(), clientPort, errno, strerror(errno)); break; } // read success buff[s] = 0; inbuffer += buff; // 1、检测inbuffer是不是已经具有了一个完整的报文strPackage uint32_t packageLen = 0; string package = decode(inbuffer, &packageLen); if (packageLen == 0) continue; // 无法提取一个完整的报文,继续读取 // 2、已经获得一个完整的package if (req.deserialize(package)) { req.debug(); // 3、处理逻辑,输入的是一个req,得到一个resp Response resp = calculator(req); // resp是一个结构化的数据 // 4、对resp进行序列化 string respPackage; resp.serialize(&respPackage); // 5、对报文进行encode respPackage = encode(respPackage, respPackage.size()); // 6、简单进行发送 write(sock, respPackage.c_str(), respPackage.size()); } } } class ServerTcp { public: ServerTcp(uint16_t port, const string &ip = "") : port_(port), ip_(ip), listensock_(-1), tp_(nullptr) { quit_ = false; } ~ServerTcp() { if (listensock_ >= 0) close(listensock_); } public: // 初始化 void init() { // 1、创建socket listensock_ = socket(AF_INET, SOCK_STREAM, 0); if (listensock_ < 0) { logMessage(FATAL, "socket: %s", strerror(errno)); // 创建失败,打印日志 exit(SOCKET_ERR); } logMessage(DEBUG, "socket: %s, %d", strerror(errno), listensock_); // 2、bind绑定 // 2.1、填充服务器信息 struct sockaddr_in local; // 用户栈 memset(&local, 0, sizeof(local)); local.sin_family = PF_INET; local.sin_port = htons(port_); ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr)); // 2.2、将本地socket信息,写入listensock_对应的内核区域 if (bind(listensock_, (const struct sockaddr *)&local, sizeof(local)) == -1) { logMessage(FATAL, "bind: %s", strerror(errno)); // 绑定失败,打印日志 exit(BIND_ERR); } logMessage(DEBUG, "bind: %s, %d", strerror(errno), listensock_); // 3、监听socket if (listen(listensock_, 5) < 0) { logMessage(FATAL, "listen: %s", strerror(errno)); // 监听失败,打印日志 exit(LISTEN_ERR); } logMessage(DEBUG, "listen: %s, %d", strerror(errno), listensock_); // 允许别人连接你了 // 4、加载线程池 tp_ = ThreadPool<Task>::getInstance(); } // 启动服务端 void loop() { // 启动线程池 tp_->start(); logMessage(DEBUG, "thread pool start success, thread num: %d", tp_->threadNum()); signal(SIGCHLD, SIG_IGN); // 忽略SIGCHLD信号 while (!quit_) { // 4、获取连接 struct sockaddr_in peer; socklen_t len = sizeof(peer); int serviceSock = accept(listensock_, (struct sockaddr *)&peer, &len); if (quit_) break; if (serviceSock < 0) { logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock); // 获取连接失败 continue; } // 4.1、获取客户端基本信息 uint16_t peerPort = ntohs(peer.sin_port); string peerIp = inet_ntoa(peer.sin_addr); logMessage(DEBUG, "accept: %s | %s:[%d], socket fd: %d", strerror(errno), peerIp.c_str(), peerPort, serviceSock); // 5.3 v3.3 Task t(serviceSock, peerIp, peerPort, netCal); tp_->push(t); } } bool quitServer() { quit_ = true; } private: int listensock_; // 监听套接字socket uint16_t port_; // port string ip_; // ip // 引入线程池 ThreadPool<Task> *tp_; // 安全退出 bool quit_; }; static void Usage(string proc) { cerr << "Usage:\n\t" << proc << "port ip" << endl; cerr << "Example:\n\t" << proc << "8080 127.0.0.1\n" << endl; } ServerTcp *svrp = nullptr; void sigHandler(int signo) { if (signo == 3 && svrp != nullptr) svrp->quitServer(); logMessage(DEBUG, "server quit safe!"); } // ./ServerTcp local_port local_ip int main(int argc, char *argv[]) { if (argc != 2 && argc != 3) { Usage(argv[0]); exit(USAGE_ERR); } uint16_t port = atoi(argv[1]); string ip; if (argc == 3) ip = argv[2]; signal(3, sigHandler); ServerTcp svr(port, ip); svr.init(); svrp = &svr; svr.loop(); return 0; }
2.2、客户端clientTcp.cc文件
内部执行代码逻辑如下:
- 调用socket函数,创建套接字。
- 客户端初始化完毕后需要调用connect函数连接服务端,当连接服务端成功后,客户端就可以向服务端发起计算请求了。
- 定义Request类对象req,复用makeRequest函数将输入的字符串的数据填到结构体对象req的成员变量里
- 先调用serialize函数序列化,再调用encode函数添加长度报头,返回值string类型的package对象。
- 利用write函数将package的内容写到套接字里,发送到网络里。调用read函数从套接字中读取数据存到字符串echoPackage里,注意此时的字符串是服务端encode加上长度报头后的结果,我们需要复用decode进行解码。
- 最后调用deserialize反序列化函数完成字符串到结构化数据的转变。并输出退出码和最终运算结果。
客户端clientTcp.cc总代码
#include "utli.hpp" #include "Protocol.hpp" #include <cstdio> volatile bool quit = false; static void Usage(string proc) { cerr << "Usage:\n\t" << proc << "serverIp serverPort" << endl; cerr << "Example:\n\t" << proc << "127.0.0.1 8080\n" << endl; } // ./clientTcp serverIp serverPort int main(int argc, char *argv[]) { if (argc != 3) { Usage(argv[0]); exit(USAGE_ERR); } string serverIp = argv[1]; uint16_t serverPort = atoi(argv[2]); // 1、创建socket int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) { cerr << "socket: " << strerror(errno) << endl; exit(SOCKET_ERR); } // 2、connect, 向服务器发起连接请求 // 2.1、先填充需要连接的远端主机的基本信息 struct sockaddr_in server; memset(&server, 0, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(serverPort); inet_aton(serverIp.c_str(), &server.sin_addr); // 2.2、发起请求,connect 会自动帮我们进行bind if (connect(sock, (const struct sockaddr *)&server, sizeof(server)) != 0) { cerr << "connect: " << strerror(errno) << endl; exit(CONN_ERR); } cout << "info: connect success: " << sock << endl; string message; while (!quit) { message.clear(); cout << "请输入表达式>>> "; // 1 + 1 getline(cin, message); if (strcasecmp(message.c_str(), "quit") == 0) { quit = true; continue; } // 把输入的字符串输出到Request请求结构体对象req中 Request req; if (!makeRequest(message, &req)) continue; string package; req.serialize(&package); // 序列化 cout << "debug->serialize-> " << package << endl; package = encode(package, package.size()); cout << "debug->encode-> \n" << package << endl; ssize_t s = write(sock, package.c_str(), package.size()); if (s > 0) { char buff[1024]; ssize_t s = read(sock, buff, sizeof(buff) - 1); if (s > 0) buff[s] = 0; string echoPackage = buff; Response resp; uint32_t len = 0; cout << "debug->get response-> \n" << echoPackage << endl; string tmp = decode(echoPackage, &len); // 解码 if (len > 0) { echoPackage = tmp; cout << "debug->decode-> " << echoPackage << endl; resp.deserialize(echoPackage); // 反序列化 printf("[exitcode: %d] %d\n", resp.exitCode_, resp.result_); } } else if (s <= 0) { break; } } close(sock); return 0; }
2.3、协议定制Protocol.hpp文件
- 要实现一个网络版的计算器,就必须保证通信双方能够遵守某种协议约定,因此我们需要设计一套简单的约定。数据可以分为请求数据和响应数据,因此我们分别需要对请求数据和响应数据进行约定。在实现时可以采用C++当中的类来实现,此时就需要一个请求Request类和一个响应Response类(计算最终结果)。
请求类和响应类的成员变量定义如下:
- 请求结构体中需要包括两个操作数,以及对应需要进行的操作。
- 响应结构体中需要包括一个计算结果,除此之外,响应结构体中还需要包括一个状态字段,表示本次计算的状态,因为客户端发来的计算请求可能是无意义的。
规定状态字段对应的含义:
- 状态字段为0,表示计算成功。
- 状态字段为1,表示出现除0错误。
- 状态字段为2,表示出现模0错误。
- 状态字段为3,表示非法计算。
Request请求类和Reponse响应类的主题框架很相似:都有如下的两个函数:
1、序列化serialisze函数:
- 因为我们客户端不能直接把此结构化的数据发送给服务端,所以得通过序列化的方式将结构化的数据转成字符串的格式
2、反序列化deserialisze函数:
- 当我们服务端收到客户端发来的数据(结构化的数据转为字符串),它也要通过反序列化的方式把字符串风格的信息转化成结构化的数据
注意:下面两个函数是全局函数,不独属于某个类内部成员函数。
1、给序列化后的字符串添加长度encode函数:
当客户端把结构化的数据发送给服务端时,作为服务端,我必须得知道此字符串的长度大小,就比如你去取朋友给你寄的快递,你的朋友不告诉你有多少快递要取,你就一直不知道快递有没有拿完。因此,我们需要在序列化后的字符串前面带上长度。不过此长度的设定有两种方案:
- strlenXXXXXXXX:定长4字节,将来对方发送数据,服务端先读取前4个字节(转换成有效字符串有多长)。此法可读性不好,中间出了问题不好调试。
- "strlen\r\n"XXXXXXXXXX\r\n:把长度定为字符串,中间用\r\n间隔序列化后的数据。先读长度,读完后另起一行再读后续的内容。推荐这种方法。
2、整个序列化之后的字符串进行提取长度decode函数:
- 此函数要确保序列化后的字符串必须具有完整的长度
- 必须具有和len相符合的有效载荷。如(9\r\n100 + 200\r\n)
- 我们才返回有效载荷和len。否则,decode就是一个检测函数
而针对于客户端和服务端,它们对数据处理的不同方式的需求导致了它们需要各自不同的协议,如下展开讨论。
服务端需要的协议
1、全局函数:整个序列化之后的字符串进行提取长度decode函数
- 服务端调用read函数读取数据是不一定能够一次读完的,所以调用decode函数检测客户端序列化后的数据是否是一个完整的报文
内部执行逻辑如下:
- 先确认是否是一个包含len的有效字符串
- 提取长度
- 确认有效载荷也是符合要求的
- 确认有完整的报文结构
- 将当前报文完整的从in中全部移除掉
- 正常返回
// encode,整个序列化之后的字符串进行添加长度 std::string encode(const string &in, uint32_t len) {} // decode,整个序列化之后的字符串进行提取长度 // 1、必须具有完整的长度 // 2、必须具有和len相符合的有效载荷 // 3、我们才返回有效载荷和len // 否则,decode就是一个检测函数 // 9\r\n100 + 200\r\n 9\r\n100 + 200\r\n std::string decode(string &in, uint32_t *len) { assert(len); // 1、确认是否是一个包含len的有效字符串 *len = 0; size_t pos = in.find(CRLF); if (pos == string::npos) return ""; // 1234\r\nYYYYYYY // 2、提取长度 string inLen = in.substr(0, pos); // 字符串长度 int intLen = atoi(inLen.c_str()); // 转为整型长度 // 3、确认有效载荷也是符合要求的 int surplus = in.size() - 2 * CRLF_LEN - pos; // 剩余的长度 if (surplus < intLen) // 没有完整的报文结构 return ""; // 4、确认有完整的报文结构 string package = in.substr(pos + CRLF_LEN, intLen); *len = intLen; // 5、将当前报文完整的从in中全部移除掉 int removeLen = inLen.size() + package.size() + 2 * CRLF_LEN; in.erase(0, removeLen); // 6、正常返回 return package; } // 定制的请求类 x_ op_ y_ class Request { public: Request() {} ~Request() {} // 序列化 —— 结构化的数据 -> 字符串 void serialize(string *out) {} // 反序列化 —— 字符串 -> 结构化的数据 bool deserialize(const string &in) {} // 调试 void debug() {} public: // 需要计算的数据 int x_; int y_; // 需要进行的计算种类 char op_; // + - * / % };
- 注意:因为此函数对于客户端到后面也需要,执行逻辑和这相差无几。所以我们将其设计为全局函数,方便后续调用。
2、Request请求类:反序列化deserialize函数:
- 当我服务端调用decode函数成功检测到读到的数据是一串完整的报文后,就要进行反序列化将字符串转为结构化的数据了。这里我们严格要求序列化后的字符串是类似于(100 + 200)的格式,便于我们后续进行反序列化。
内部执行逻辑如下:
- 找到第一和第二个空格的位置spaceOne,spaceTwo。若找不到直接返回false
- 复用substr函数截取从下标0到spaceOne的字符串为dataOne(第一个操作数)
- 复用substr函数截取从下标(spaceTwo + 空格的长度)到结尾的字符串为dataTwo(第二个操作数)
- 类似的,定义oper为计算的任务类型(+、-、*、/)
- 将上述提取的数据(spaceOne,spaceTwo,oper)转为内部成员(x_,y_,op_)即可
// encode,整个序列化之后的字符串进行添加长度 std::string encode(const string &in, uint32_t len) {} // decode,整个序列化之后的字符串进行提取长度 std::string decode(string &in, uint32_t *len) {} // 定制的请求 x_ op_ y_ class Request { public: Request() {} ~Request() {} // 序列化 —— 结构化的数据 -> 字符串 void serialize(string *out) {} // 反序列化 —— 字符串 -> 结构化的数据 bool deserialize(const string &in) { // 100 + 200 // 1、提取各个数据 size_t spaceOne = in.find(SPACE); if (string::npos == spaceOne) return false; size_t spaceTwo = in.rfind(SPACE); if (string::npos == spaceTwo) return false; string dataOne = in.substr(0, spaceOne); string dataTwo = in.substr(spaceTwo + SPACE_LEN); string oper = in.substr(spaceOne + SPACE_LEN, spaceTwo - (spaceOne + SPACE_LEN)); if (oper.size() != 1) return false; // 2、转成内部成员 x_ = atoi(dataOne.c_str()); y_ = atoi(dataTwo.c_str()); op_ = oper[0]; return true; } // 调试 void debug() {} public: // 需要计算的数据 int x_; int y_; // 需要进行的计算种类 char op_; // + - * / % };
3、Response响应类:序列化serialize函数
- 我上述执行的反序列化函数是为了提取计算数和计算符号(如10 + 20),提取好后服务端要进行calculator计算了,计算后的返回值是一个结构化的数据。我们需要对此返回值进行序列化将结构化的数据转为字符串,才能有助于后续服务端把结果写回客户端。
- 注意:序列化后的字符串格式要如同("exitCode_ result_")。
这个操作相对简单,直接看代码:
// encode,整个序列化之后的字符串进行添加长度 std::string encode(const string &in, uint32_t len) {} // decode,整个序列化之后的字符串进行提取长度 std::string decode(const string &in, uint32_t *len) {} // 定制的响应 class Response { public: Response() {} ~Response() {} // 序列化 —— 结构化的数据 -> 字符串 void serialize(string *out) { // "exitCode_ result_" string ec = to_string(exitCode_); string res = to_string(result_); *out = ec; *out += SPACE; *out += res; } // 反序列化 —— 字符串 -> 结构化的数据 void deserialize(string &in) {} public: // 退出状态,0表示运算结果合法,非0表示运算结果是非法的,!0是几就表示是什么原因错了 int exitCode_; // 运算结果 int result_; };
4、全局函数:添加报头长度encode函数
- 当把结果转为字符串后,还需要调用encode函数来帮助我们添加此字符串的长度报头。
我的核心宗旨就是把传过来的字符串如("exitCode_ result_")转为("len\r\nexitCode_ result_\r\n")的格式。代码如下:
// encode,整个序列化之后的字符串进行添加长度 std::string encode(const string &in, uint32_t len) { // "exitCode_ result_" // "len\r\nexitCode_ result_\r\n" string encodein = to_string(len); encodein += CRLF; encodein += in; encodein += CRLF; return encodein; } // decode,整个序列化之后的字符串进行提取长度 std::string decode(const string &in, uint32_t *len) {} // 定制的响应 class Response { public: Response() {} ~Response() {} // 序列化 —— 结构化的数据 -> 字符串 void serialize(string *out) {} // 反序列化 —— 字符串 -> 结构化的数据 void deserialize(string &in) {} public: // 退出状态,0表示运算结果合法,非0表示运算结果是非法的,!0是几就表示是什么原因错了 int exitCode_; // 运算结果 int result_; };
- 注意:因为此函数对于客户端到后面也需要,执行逻辑和这相差无几。所以我们将其设计为全局函数,方便后续调用。
客户端需要的协议
1、Request请求类:序列化serialize函数:
- 当客户端把输入的字符串填充到Request结构体后,首先要做的就是serialize序列化将结构化的数据转为字符串。
代码逻辑如下:
- 复用to_string函数把传入的Request结构体的两个成员变量(计算数)转为字符串,并分别保存起来
- 统一按照(10 + 20)的格式把这些字符串整合起来即可
// encode,整个序列化之后的字符串进行添加长度 std::string encode(const string &in, uint32_t len) {} // decode,整个序列化之后的字符串进行提取长度 std::string decode(string &in, uint32_t *len) {} // 定制的请求 x_ op_ y_ class Request { public: Request() {} ~Request() {} // 序列化 —— 结构化的数据 -> 字符串 // 认为结构化字段中的内容已经被填充 void serialize(string *out) { string xstr = to_string(x_); string ystr = to_string(y_); *out = xstr; *out += SPACE; *out += op_; *out += SPACE; *out += ystr; } // 反序列化 —— 字符串 -> 结构化的数据 bool deserialize(const string &in) {} // 调试 void debug() {} public: // 需要计算的数据 int x_; int y_; // 需要进行的计算种类 char op_; // + - * / % };
2、全局函数:添加报头长度encode函数
- 当序列化之后,就是要调用encode函数添加报头长度。encode函数上面已经实现过,这里把代码贴过来:
// encode,整个序列化之后的字符串进行添加长度 std::string encode(const string &in, uint32_t len) { string encodein = to_string(len); encodein += CRLF; encodein += in; encodein += CRLF; return encodein; } // decode,整个序列化之后的字符串进行提取长度 std::string decode(const string &in, uint32_t *len) {} // 定制的响应 class Response { public: Response() {} ~Response() {} // 序列化 —— 结构化的数据 -> 字符串 void serialize(string *out) {} // 反序列化 —— 字符串 -> 结构化的数据 void deserialize(string &in) {} public: // 退出状态,0表示运算结果合法,非0表示运算结果是非法的,!0是几就表示是什么原因错了 int exitCode_; // 运算结果 int result_; };
3、全局函数:整个序列化之后的字符串进行提取长度decode函数
- 后续客户端读取到由服务端返回的结果是带有长度报头的,这里我们需要调用decode函数进行解码,同时也是在检测读取到的结果是否是一个完整的报文。不过decode函数前面已经实现过,这里直接贴出代码:
// encode,整个序列化之后的字符串进行添加长度 std::string encode(const string &in, uint32_t len) {} // decode,整个序列化之后的字符串进行提取长度 // 1、必须具有完整的长度 // 2、必须具有和len相符合的有效载荷 // 3、我们才返回有效载荷和len // 否则,decode就是一个检测函数 // 9\r\n100 + 200\r\n 9\r\n100 + 200\r\n std::string decode(string &in, uint32_t *len) { assert(len); // 1、确认是否是一个包含len的有效字符串 *len = 0; size_t pos = in.find(CRLF); if (pos == string::npos) return ""; // 1234\r\nYYYYYYY // 2、提取长度 string inLen = in.substr(0, pos); // 字符串长度 int intLen = atoi(inLen.c_str()); // 转为整型长度 // 3、确认有效载荷也是符合要求的 int surplus = in.size() - 2 * CRLF_LEN - pos; // 剩余的长度 if (surplus < intLen) // 没有完整的报文结构 return ""; // 4、确认有完整的报文结构 string package = in.substr(pos + CRLF_LEN, intLen); *len = intLen; // 5、将当前报文完整的从in中全部移除掉 int removeLen = inLen.size() + package.size() + 2 * CRLF_LEN; in.erase(0, removeLen); // 6、正常返回 return package; } // 定制的响应 class Response { public: Response() {} ~Response() {} // 序列化 —— 结构化的数据 -> 字符串 void serialize(string *out) {} // 反序列化 —— 字符串 -> 结构化的数据 void deserialize(string &in) {} public: // 退出状态,0表示运算结果合法,非0表示运算结果是非法的,!0是几就表示是什么原因错了 int exitCode_; // 运算结果 int result_; };
4、Response响应类:反序列化deserialize函数
- 当服务端把结果返回到客户端,然后decode解码后,我们得到的结果是类似于("0 100")的字符串,下面就是调用deserialize反序列化函数将其转为结构化数据。
代码逻辑如下:
- 先找到空格的位置pos
- 复用substr函数分别保存空格两边的字符串,分别用codestr和reststr保存
- 最后将反序列化的结果写入到内部成员中,形成结构化数据
// encode,整个序列化之后的字符串进行添加长度 std::string encode(const string &in, uint32_t len) {} // decode,整个序列化之后的字符串进行提取长度 std::string decode(string &in, uint32_t *len) {} // 定制的响应 class Response { public: Response() {} ~Response() {} // 序列化 —— 结构化的数据 -> 字符串 void serialize(string *out) {} // 反序列化 —— 字符串 -> 结构化的数据 bool deserialize(string &in) { // "exitCode_ result_" "0 100" size_t pos = in.find(SPACE); // 找空格的位置 if (string::npos == pos) return false; string codestr = in.substr(0, pos); string reststr = in.substr(pos + SPACE_LEN); // 将反序列化的结果写入到内部成员中,形成结构化数据 exitCode_ = atoi(codestr.c_str()); result_ = atoi(reststr.c_str()); return true; } public: // 退出状态,0表示运算结果合法,非0表示运算结果是非法的,!0是几就表示是什么原因错了 int exitCode_; // 运算结果 int result_; };
5、makeRequest函数
- 当客户端输入字符串数据时,首先要把这些数据(类似于1+1)填充到请求类Request的对象中的成员变量里,此函数就是完成这个功能。
bool makeRequest(const string &str, Request *req) { // 1+1 1*1 1/1 char strtmp[BUFFER_SIZE]; snprintf(strtmp, sizeof (strtmp), "%s", str.c_str()); char *left = strtok(strtmp, OPS); if (!left) return false; char *right = strtok(nullptr, OPS); if (!right) return false; char mid = str[strlen(left)]; req->x_ = atoi(left); req->y_ = atoi(right); req->op_ = mid; return true; }
上述过程其实也是一个反序列化的过程,也证明了序列化、反序列化不仅仅在网络中应用,本地也是可以的。
协议定制Protocol.hpp总代码
#pragma once #include <iostream> #include <string> #include <cassert> #include "utli.hpp" using namespace std; // 协议定制 // 网络版本计算器 #define CRLF "\r\n" #define CRLF_LEN strlen(CRLF) #define SPACE " " #define SPACE_LEN strlen(SPACE) #define OPS "+-*/%" // encode,整个序列化之后的字符串进行添加长度 // strlen XXXXXXXXXX // "strlen\r\n"XXXXXXXXXX\r\n 采用这种方案 std::string encode(const string &in, uint32_t len) { // "exitCode_ result_" // "len\r\nexitCode_ result_\r\n" string encodein = to_string(len); encodein += CRLF; encodein += in; encodein += CRLF; return encodein; } // decode,整个序列化之后的字符串进行提取长度 // 1、必须具有完整的长度 // 2、必须具有和len相符合的有效载荷 // 3、我们才返回有效载荷和len // 否则,decode就是一个检测函数 // 9\r\n100 + 200\r\n 9\r\n100 + 200\r\n std::string decode(string &in, uint32_t *len) { assert(len); // 1、确认是否是一个包含len的有效字符串 *len = 0; size_t pos = in.find(CRLF); if (pos == string::npos) return ""; // 1234\r\nYYYYYYY // 2、提取长度 string inLen = in.substr(0, pos); // 字符串长度 int intLen = atoi(inLen.c_str()); // 转为整型长度 // 3、确认有效载荷也是符合要求的 int surplus = in.size() - 2 * CRLF_LEN - pos; // 剩余的长度 if (surplus < intLen) // 没有完整的报文结构 return ""; // 4、确认有完整的报文结构 string package = in.substr(pos + CRLF_LEN, intLen); *len = intLen; // 5、将当前报文完整的从in中全部移除掉 int removeLen = inLen.size() + package.size() + 2 * CRLF_LEN; in.erase(0, removeLen); // 6、正常返回 return package; } // 定制的请求 x_ op_ y_ class Request { public: Request() { } ~Request() { } // 序列化 —— 结构化的数据 -> 字符串 // 认为结构化字段中的内容已经被填充 void serialize(string *out) { string xstr = to_string(x_); string ystr = to_string(y_); *out = xstr; *out += SPACE; *out += op_; *out += SPACE; *out += ystr; } // 反序列化 —— 字符串 -> 结构化的数据 bool deserialize(const string &in) { // 100 + 200 // 1、提取各个数据 size_t spaceOne = in.find(SPACE); if (string::npos == spaceOne) return false; size_t spaceTwo = in.rfind(SPACE); if (string::npos == spaceTwo) return false; string dataOne = in.substr(0, spaceOne); string dataTwo = in.substr(spaceTwo + SPACE_LEN); string oper = in.substr(spaceOne + SPACE_LEN, spaceTwo - (spaceOne + SPACE_LEN)); if (oper.size() != 1) return false; // 2、转成内部成员 x_ = atoi(dataOne.c_str()); y_ = atoi(dataTwo.c_str()); op_ = oper[0]; return true; } // 调试 void debug() { cout << "#############################" << endl; cout << "x_: " << x_ << endl; cout << "op_: " << op_ << endl; cout << "y_ :" << y_ << endl; cout << "#############################" << endl; } public: // 需要计算的数据 int x_; int y_; // 需要进行的计算种类 char op_; // + - * / % }; // 定制的响应 class Response { public: Response() : exitCode_(0), result_(0) { } ~Response() { } // 序列化 —— 结构化的数据 -> 字符串 void serialize(string *out) { // "exitCode_ result_" string ec = to_string(exitCode_); string res = to_string(result_); *out = ec; *out += SPACE; *out += res; } // 反序列化 —— 字符串 -> 结构化的数据 bool deserialize(string &in) { // "exitCode_ result_" "0 100" size_t pos = in.find(SPACE); // 找空格的位置 if (string::npos == pos) return false; string codestr = in.substr(0, pos); string reststr = in.substr(pos + SPACE_LEN); // 将反序列化的结果写入到内部成员中,形成结构化数据 exitCode_ = atoi(codestr.c_str()); result_ = atoi(reststr.c_str()); return true; } // 调试 void debug() { cout << "#############################" << endl; cout << "exitCode_: " << exitCode_ << endl; cout << "result_: " << result_ << endl; cout << "#############################" << endl; } public: // 退出状态,0表示运算结果合法,非0表示运算结果是非法的,!0是几就表示是什么原因错了 int exitCode_; // 运算结果 int result_; }; bool makeRequest(const string &str, Request *req) { // 1+1 1*1 1/1 char strtmp[BUFFER_SIZE]; snprintf(strtmp, sizeof(strtmp), "%s", str.c_str()); char *left = strtok(strtmp, OPS); if (!left) return false; char *right = strtok(nullptr, OPS); if (!right) return false; char mid = str[strlen(left)]; req->x_ = atoi(left); req->y_ = atoi(right); req->op_ = mid; return true; }
2.4、测试结果
3、json序列化和反序列化
如上的网络版计算器我们是自己定制的协议,且全部都是手写的。encode和decode是我么必须要做的,不过针对于序列化和反序列化,我们可以采用别人的方案(xml、json、protobuf)。我们以json示例,如下进行演示。
3.1、安装json库
使用如下命令:
sudo yum install -y jsoncpp-devel
使用如下命令查看json的位置:
json的头文件:
#include <jsoncpp/json/json.h>
3.2、request请求类中的json
request的json序列化
// 序列化 —— 结构化的数据 -> 字符串 // 认为结构化字段中的内容已经被填充 void serialize(string *out) { #ifdef MY_SELF string xstr = to_string(x_); string ystr = to_string(y_); *out = xstr; *out += SPACE; *out += op_; *out += SPACE; *out += ystr; #else // json // 1、Value对象,万能对象 // 2、json是基于kv // 3、json有两套操作方法 // 4、序列化的时候,会将所有的数据内容,转换为字符串 Json::Value root; root["x"] = x_; root["y"] = y_; root["op"] = op_; Json::FastWriter fw; // 序列化 *out = fw.write(&root); // 将结构体写成字符串存入out中 #endif }
request的json反序列化
// 反序列化 —— 字符串 -> 结构化的数据 bool deserialize(string &in) { #ifdef MY_SELF // 100 + 200 // 1、提取各个数据 size_t spaceOne = in.find(SPACE); if (string::npos == spaceOne) return false; size_t spaceTwo = in.rfind(SPACE); if (string::npos == spaceTwo) return false; string dataOne = in.substr(0, spaceOne); string dataTwo = in.substr(spaceTwo + SPACE_LEN); string oper = in.substr(spaceOne + SPACE_LEN, spaceTwo - (spaceOne + SPACE_LEN)); if (oper.size() != 1) return false; // 2、转成内部成员 x_ = atoi(dataOne.c_str()); y_ = atoi(dataTwo.c_str()); op_ = oper[0]; return true; #else // json Json::Value root; Json::Reader rd; rd.parse(in, root); //把in反序列化进root中,这里的root是输出型参数 x_ = root["x"].asInt(); //asInt:把root["x"]对应的KV的V值转为整数存入x_ y_ = root["y"].asInt(); op_ = root["op"].asInt();//没有asChar一说,就是存入ASCII值,因为op_是char类型,所以读取时会以char类型读取 return true; #endif }
3.3、reponse响应类中的json
// 序列化 —— 结构化的数据 -> 字符串 void serialize(string *out) { #ifdef MY_SELF // "exitCode_ result_" string ec = to_string(exitCode_); string res = to_string(result_); *out = ec; *out += SPACE; *out += res; #else // json Json::Value root; root["exitcode"] = exitCode_; root["result"] = result_; Json::FastWriter fw; *out = fw.write(root); #endif } // 反序列化 —— 字符串 -> 结构化的数据 bool deserialize(string &in) { #ifdef MY_SELF // "exitCode_ result_" "0 100" size_t pos = in.find(SPACE); // 找空格的位置 if (string::npos == pos) return false; string codestr = in.substr(0, pos); string reststr = in.substr(pos + SPACE_LEN); // 将反序列化的结果写入到内部成员中,形成结构化数据 exitCode_ = atoi(codestr.c_str()); result_ = atoi(reststr.c_str()); return true; #else // json Json::Value root; Json::Reader rd; rd.parse(in, root); exitCode_ = root["exitcode"].asInt(); result_ = root["result"].asInt(); return true; #endif }
3.4、Makefile中的json操作
-D命令行定义宏
先前若我们想要让代码执行json版的序列化和反序列化操作需要用到宏定义:
- 如果我们定义了此宏,那么后续的序列化和反序列化操作就使用自己的,如果没定义,则用json的。现在我们可以在makefile中定义变量-D。
-D:命令行定义宏。目的:这样就不用把宏定义在源代码中(不用动源代码了),某种宏的定义会决定条件编译对相应代码进行裁剪。
使用效果如下:
如果利用#进行注释:Method=#-DMY_SELF —> 此时Method是无内容的,此时代码中的 #ifdef MY_SELF 条件编译不起作用而会执行#else,此时就会执行jason的序列化代码;
-ljsoncpp 包json对应的第三方库
-ljsoncpp :包第三方库,去掉
libjsoncpp.so前缀和后缀
3.5、代码测试
这里我让服务端和客户端均打印些提示信息。
- 服务端:
- 客户端:
- 测试如下:
3.6、Json::FastWriter与Json::StyledWriter两种显示风格
通常 Json::FastWriter 传输数据量较少,使用Json::StyledWriter较多。我们控制clientTcp的代码,先不向服务端发送,只是观察序列化后的结果:
Json::FastWriter和Json::StyledWriter对比如下:
4、源码gitee链接
总流程概述:
- 客户端:①客户输入1+1存入字符串。②字符串"1+1"通过makeReuquest函数—>req结构体。③req结构体 通过serialize—>字符串"1 + 1"。④字符串"1 + 1"encode加码后成为“5\r\n1 + 1\r\n”写字符串入套接字。(①②③是为了把不规范的字符串转成可以加码的规范字符串)
- 服务器:①服务端读取 “5\r\n1 + 1\r\n” 。②字符串 “5\r\n1 + 1\r\n” decode解码得到字符串"1 + 1"。③字符串"1 + 1" deserialize反序列化 存入req结构体。④Response resp = calculator(req); 计算rep后生成结果resp:exitCode_=0,result_=2。⑤resp serialize序列化后encode加码,返回给客户端。
源码gitee链接:
- 网络版计算器(自己定制协议 + json版序列化和反序列化)