目录
一,关于“协议”
1.1 结构化数据
1.2 序列化和反序列化
二,网络版计算器实现准备
2.1 套用旧头文件
2.2 封装sock API
三,自定义协议
3.1 关于自定义协议
3.2 实现序列化和反序列化
3.3 测试
三,服务器实现
3.1 逻辑梳理
3.2 各头文件实现
四,客户端实现
一,关于“协议”
1.1 结构化数据
两个主机通过网络和协议进行通信时,发送的数据有两种形式:
- 如果传输的数据直接就是一个字符串,那么把这个字符串发出去,对方也能得到这个字符串
- 如果需要传输的是一个struct结构体,那么不能将结构体数据一个个发送到网络中
比如我要实现一个网络版的计算器,那么客户端给服务器发送的数据,就要包含左操作数,运算符和右操作数,那么这就不仅仅是一个字符串了,而是一组数据
所以客户端不能把这些数据一个个发送过去,需要把这些数据“打个包”,统一发到网络中,此时服务器就能获取到一个完整的数据请求,“打包”方式有两种:
方案一:将结构化的数据结合成一个大的字符串
- 比如我要发送“1+1”,用户输入的是“整型”,“字符”,“整型”
- 我们先用to_string函数把整型转为字符串,然后用strcat或者C++/string的 "+="运算符重载将这三个字符拼接成一个长字符串,然后就可以直接发送
- 最后服务器收到了长字符串,再以相同的方式进行拆分,用stoi函数将字符串转整型,就可以提取这些结构化的数据
方案二:定制结构化数据,实现序列化和反序列化
- 客户端可以定制一个结构体,将需要交互的信息放到结构体种
- 客户端发送前,将结构体的数据进行序列化,服务器收到数据后进行反序列化,此时服务器就能得到客户端发送过来的结构体,下面我们来详细讲讲序列化和反序列化
1.2 序列化和反序列化
- 序列化是将对象的状态信息转换为可以存储或传输的形式(字节序列)的过程
- 反序列化就是把序列化的字节序列恢复为对象的过程
OSI七层模型中表示层的作用,就是“实现数据格式和网络标准数据格式的转换”。前者数据格式就是指数据再应用层上的格式,后者就是指序列化之后可以进行网络传输的数据格式
- 序列化的目的,是为了方便网络数据的发送和接收,序列化后数据就全变成了二进制数据,此时底层在进行数据传输时看到的统一都是二进制序列
- 我发的是二进制数据,所以对方收到的也是二进制数据,所以需要进行反序列化,将二进制数据转化为上层能够识别的比如字符串,整型数据
二,网络版计算器实现准备
前置博客:计算机网络(三) —— 简单Udp网络程序-CSDN博客
计算机网络(四) —— 简单Tcp网络程序-CSDN博客
下面我们来全程手搓一个网络版计算器服务,并且我们自己实现一个自定义协议,主要是为了感受一下协议的实现,后面我们就不会再自定义协议了,直接用现成的
2.1 套用旧头文件
源代码下载:计算机网络/自定义协议——网络版计算器 · 小堃学编程/Linux学习 - 码云 - 开源中国 (gitee.com)
网络版计算器我们要用到的头文件有以下几个:
我们先把前面写的头文件套用一下:
makefile:
.PHONY:all
all:servercal clientcalFlag=#-DMySelf=1
Lib=-ljsoncpp #这个是后面使用json头文件时要用的servercal:ServerCal.ccg++ -o $@ $^ -std=c++11 $(Lib) $(Flag)
clientcal:ClientCal.ccg++ -o $@ $^ -std=c++11 -g $(Lib) $(Flag).PHONY:clean
clean:rm -f clientcal servercal
Log.hpp:
#pragma once#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>#define SIZE 1024#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4#define Screen 1
#define Onefile 2
#define Classfile 3#define LogFile "log.txt"class Log
{
public:Log(){printMethod = Screen;path = "./log/";}void Enable(int method){printMethod = method;}std::string levelToString(int level){switch (level){case Info:return "Info";case Debug:return "Debug";case Warning:return "Warning";case Error:return "Error";case Fatal:return "Fatal";default:return "None";}}void printLog(int level, const std::string &logtxt){switch (printMethod){case Screen:std::cout << logtxt << std::endl;break;case Onefile:printOneFile(LogFile, logtxt);break;case Classfile:printClassFile(level, logtxt);break;default:break;}}void printOneFile(const std::string &logname, const std::string &logtxt){std::string _logname = path + logname;int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"if (fd < 0)return;write(fd, logtxt.c_str(), logtxt.size());close(fd);}void printClassFile(int level, const std::string &logtxt){std::string filename = LogFile;filename += ".";filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"printOneFile(filename, logtxt);}~Log(){}void operator()(int level, const char *format, ...){time_t t = time(nullptr);struct tm *ctime = localtime(&t);char leftbuffer[SIZE];snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,ctime->tm_hour, ctime->tm_min, ctime->tm_sec);va_list s;va_start(s, format);char rightbuffer[SIZE];vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);va_end(s);// 格式:默认部分+自定义部分char logtxt[SIZE * 2];snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);// printf("%s", logtxt); // 暂时打印printLog(level, logtxt);}private:int printMethod;std::string path;
};Log log;
Deamon.hpp:
#pragma once#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>const std::string nullfile = "/dev/null";void Daemon(const std::string &cwd = "")
{// 1. 忽略其他异常信号signal(SIGCLD, SIG_IGN);signal(SIGPIPE, SIG_IGN);signal(SIGSTOP, SIG_IGN);// 2. 将自己变成独立的会话if (fork() > 0)exit(0);setsid();// 3. 更改当前调用进程的工作目录if (!cwd.empty())chdir(cwd.c_str());// 4. 标准输入,标准输出,标准错误重定向至/dev/nullint fd = open(nullfile.c_str(), O_RDWR);if (fd > 0){dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);}
}
2.2 封装sock API
在Udp和Tcp服务器编写时,可以发现在使用sock API以及填装sockaddr结构体时,步骤都非常相似,所以我们可以把这些相似的步骤都封装起来,下面是Socket.hpp的代码:
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"#include <cstring>enum{SocketErr = 2,BindErr,ListenErr,
};const int backlog = 10;class Sock
{
public:Sock(){}~Sock(){}public:void Socket() // 创建套接字{_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){log(Fatal, "socket error, %s: %d", strerror(errno), errno);exit(SocketErr);}}void Bind(uint16_t port) // 绑定套接字{struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;if (bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0) // 如果小于0就绑定失败{log(Fatal, "bind error, %s: %d", strerror(errno), errno);exit(BindErr);}}void Listen() // 监听套接字{if (listen(_sockfd, backlog) < 0) // 如果小于0就代表监听失败{log(Fatal, "listen error, %s: %d", strerror(errno), errno);exit(ListenErr);}}int Accept(std::string *clientip, uint16_t *clientport) // 获取连接,参数做输出型参数{struct sockaddr_in peer;socklen_t len = sizeof(peer);int newfd = accept(_sockfd, (struct sockaddr *)(&peer), &len);if (newfd < 0) // 获取失败{log(Warning, "accept error, %s: %d", strerror(errno), errno);return -1;}char ipstr[64];inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr)); // 把网络字节序列转化为字符串保存在ipstr数组里供用户读取*clientip = ipstr;*clientport = ntohs(peer.sin_port);return newfd;}bool Connect(const std::string &ip, const uint16_t port){struct sockaddr_in peer;memset(&peer, 0, sizeof(peer));peer.sin_family = AF_INET;peer.sin_port = htons(port);inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));int n = connect(_sockfd, (struct sockaddr *)&peer, sizeof(peer));if (n == -1){std::cerr << "connect to " << ip << ":" << port << "error" << std::endl;return false;}return true;}void Close(){close(_sockfd);}int Fd(){return _sockfd;}private:int _sockfd;
};
三,自定义协议
3.1 关于自定义协议
在之前的文章中介绍过,任何的网络协议,都要提供两种功能,下面是博客的截图:计算机网络(一) —— 网络基础入门_计算机网络基础教程-CSDN博客
网络版计算器,用户会在命令行输入三个字符:"1","+","1",然后我们可以拼接成一个长字符串:"1 + 1",数字与运算符通过一个空格隔开,
但是,如果客户端连续发了两个字符串,那么最终服务器收到的报文就是“1 + 12 + 1”,可以发现,两个字符串粘在了一起,所以我们的自定义协议,不仅仅要提供将报文和有效载荷分离的能力,也要提供将报文与报文分开的能力,有下面两种方法:
- 方案一,用特殊字符隔开报文与报文 --> "1 + 1" \n "2 + 2"
- 方案二,在报文前面加上报文的长度,也就是报头 --> "9"\n"100 + 200"\n,这样就为一个完整的报文(其实只要有长度就可以了,这里增加\n是为了可读性,也是为了方便后面打印)
所以下面来梳理一下我们自定义协议的序列化和反序列化全流程:
3.2 实现序列化和反序列化
这个部分就是具体实现Protocol.hpp头文件了,这个文件具体包含下面几个内容:
- "100","+","200" --> "100 + 200"
- "100 + 200" --> "9"\n"100 + 200"
- "9"\n"100 + 200" --> "100 + 200"
- "100 + 200" --> "100","+","200"
该文件包含两个类,一个类是请求类,是客户端发给服务器用到的类;另一个类是响应类,是服务器处理完后,返回给客户端的类;此外还包括两个方法,分别是封装报头和将报头和有效载荷分离
Request类:
#pragma once
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>#define MySelf 0 // 去掉注释就是用我们自己的序列化和反序列化,加上注释就是用json库提供的const std::string blank_space = " "; // 分隔符
const std::string protocol_sep = "\n";class Request // 计算的请求
{
public:Request(int data1, int data2, char oper): x(data1), y(data2), op(oper){}Request(){}~Request(){}public:bool Serialize(std::string *out) // 序列化{
#ifdef MySelf// 1,构建报文的有效载荷// 需要把结构化的数据转化为字符串 struct --> string, "x op y"std::string s = std::to_string(x);s += blank_space;s += op;s += blank_space;s += std::to_string(y);// 走到这里的时候就是字符串 "x op y"// 但是在传输的时候可能发过来的不是完整的一个报文:"10 + 20",而是只有半个报文:"10 + "// 解决方案一:用特殊字符隔开报文与报文 --> "10 + 20" \n "20 + 40"// 解决方案二:再在报文前面加一个字符串的长度也就是报头,例如s.size()// 结合起来就是"9"\n"100 + 200"\n,为一个完整的报文,其实只要有长度就可以了,这里增加\n是为了可读性,也是为了方便后面// 2,封装报头*out = s;return true;
#elseJson::Value root;root["x"] = x;root["y"] = y;root["op"] = op;Json::FastWriter w;*out = w.write(root);return true;#endif}bool DeSerialize(const std::string &in) // 反序列化 "9"\n"10 + 20"{
#ifdef MySelfstd::size_t left = in.find(blank_space); // 找空格的左边,"10 + 20",也就是找10的右边位置if (left == std::string::npos) // 没找到空格,说明当前解析错误{return false;}std::string part_x = in.substr(0, left); // 截取第一个数字,也就是10std::size_t right = in.rfind(blank_space); // 逆向再次找空格,"10 + 20",找20左边的位置if (right == std::string::npos) // 没找到空格,说明当前解析错误{return false;}std::string part_y = in.substr(right + 1); // 截取后面的数字,也就是20,+1是因为找到的是空格的右边,+1跳过空格才是数字if (left + 2 != right)return false; // 数字中间还有运算符,所以left+2就应该是right的左边那个空格的左边位置,如果不是那么就是解析错误op = in[left + 1]; // 拿到运算符// op = in[right - 1]; //一样的x = std::stoi(part_x); // 拿到数字y = std::stoi(part_y);return true;
#elseJson::Value root;Json::Reader r;r.parse(in, root);x = root["x"].asInt();y = root["y"].asInt();op = root["op"].asInt();return true;
#endif}void DebugPrint(){std::cout << "新请求构建完成: " << x << " " << op << " " << y << "=?" << std::endl;}public:int x;int y;char op; // 运算符
};class Response // 计算的应答
{
public:Response(int res, int c): result(res), code(c){}Response(){}~Response(){}public:bool Serialize(std::string *out) // 序列化{
#ifdef MySelf// 1,构建报文的有效载荷//"len"\n"result code"std::string s = std::to_string(result);s += blank_space;s += std::to_string(code);*out = s;return true;
#elseJson::Value root;root["result"] = result;root["code"] = code;// Json::FastWriter w;Json::StyledWriter w;*out = w.write(root);return true;
#endif}bool DeSerialize(const std::string &in) // 反序列化{
#ifdef MySelf// 对服务器发过来的结果报文做解析: "result code"std::size_t pos = in.find(blank_space); // 找空格的左边if (pos == std::string::npos) // 没找到空格,说明当前解析错误{return false;}std::string part_left = in.substr(0, pos); // 截取第一个数字,也就是resultstd::string part_right = in.substr(pos + 1); // 截取后面第二个数字,也就是coderesult = std::stoi(part_left);code = std::stoi(part_right);return true;
#elseJson::Value root;Json::Reader r;r.parse(in, root);result = root["result"].asInt();code = root["code"].asInt();return true;
#endif}void DebugPrint(){std::cout << "结果响应完成, result: " << result << ", code: " << code << std::endl;}public:int result; // x op yint code; // 错误码,为0时结果正确,为其它数时对应的数表示对应的原因
};
Response类:
class Response // 计算的应答
{
public:Response(int res, int c): result(res), code(c){}Response(){}~Response(){}public:bool Serialize(std::string *out) // 序列化{
#ifdef MySelf// 1,构建报文的有效载荷//"len"\n"result code"std::string s = std::to_string(result);s += blank_space;s += std::to_string(code);*out = s;return true;
#elseJson::Value root;root["result"] = result;root["code"] = code;// Json::FastWriter w;Json::StyledWriter w;*out = w.write(root);return true;
#endif}bool DeSerialize(const std::string &in) // 反序列化{
#ifdef MySelf// 对服务器发过来的结果报文做解析: "result code"std::size_t pos = in.find(blank_space); // 找空格的左边if (pos == std::string::npos) // 没找到空格,说明当前解析错误{return false;}std::string part_left = in.substr(0, pos); // 截取第一个数字,也就是resultstd::string part_right = in.substr(pos + 1); // 截取后面第二个数字,也就是coderesult = std::stoi(part_left);code = std::stoi(part_right);return true;
#elseJson::Value root;Json::Reader r;r.parse(in, root);result = root["result"].asInt();code = root["code"].asInt();return true;
#endif}void DebugPrint(){std::cout << "结果响应完成, result: " << result << ", code: " << code << std::endl;}public:int result; // x op yint code; // 错误码,为0时结果正确,为其它数时对应的数表示对应的原因
};
添加和去掉报头函数:
std::string Encode(const std::string &content) // 添加报头
{std::string packge = std::to_string(content.size()); // 加报头packge += protocol_sep; // 加\npackge += content; // 加正文packge += protocol_sep; // 再加\nreturn packge;
}bool Decode(std::string &package, std::string *content) // 解析并去掉报头 "9"\n"10 + 20"\n -->"10 + 20" 俗称解包,但是只是去掉了报头,没有做报文的具体解析
{std::size_t pos = package.find(protocol_sep); // 找到\n的左边if (pos == std::string::npos)return false; // 解析失败std::string len_str = package.substr(0, pos); // 从开始截到我找到的\n处,把前面的9给截出来std::size_t len = std::stoi(len_str); // 把截出来的报头转化为size_t,也就是把字符串9转化成数字9// packge的长度 = 报头长度len_str + 有效载荷长度content_str + 两个\n 2std::size_t total_len = len_str.size() + len + 2;// ①找到了第一个\n说明一定有长度,如果没找到\n就说明连报头都没有// ②有了长度报头,你也还得保证后面的内容也是完整的,如果不完整也就是长度不一样,那我也就不玩了if (package.size() < total_len)return false;// 走到这一步说明我们能保证报文是完整的,开始拿有效载荷*content = package.substr(pos + 1, len); // pos现在是第一个\n左边的位置,+1后面的就是正文内容,正文内容长度为len// 移除一个报文,该功能需要和网络相结合package.erase(0, total_len);return true;
}
3.3 测试
我们可以在ServerCal.cc文件里测试上面我们的序列化和反序列化操作
先测试Request:
ServerCal.cc:
#include "Log.hpp"
#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "ServerCal.hpp"
#include "Deamon.hpp"int main()
{// Request测试--------------------Request req(10, 20, '+');std::string s;req.Serialize(&s);std::cout << "有效载荷为: " << s << std::endl;s = Encode(s);std::cout << "报文为:" << s;std::string content;bool r = Decode(s, &content); //分离报头和有效载荷std::cout << "分离报头后的有效载荷为: "<< content << std::endl;Request temp;temp.DeSerialize(content); //解析有效载荷std::cout<< "有效载荷分离后, x为: " << temp.x << " 运算符为:\"" << temp.op << "\" y为: " << temp.y << std::endl;return 0;
}
然后是Response的测试:
ServerCal.cc:
#include "Log.hpp"
#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "ServerCal.hpp"
#include "Deamon.hpp"int main()
{// Response测试--------------------Response resp(10, 20);std::string s;resp.Serialize(&s);std::cout << "有效载荷为: " << s << std::endl;std::string package = Encode(s); //分离报头和有效载荷std::cout << "报文为: " << package;s = "";bool r = Decode(package, &s);std::cout << "分离报头后的有效载荷为: " << s << std::endl;Response temp;temp.DeSerialize(s); // 解析有效载荷std::cout << "解析有效载荷: " << std::endl;std::cout << "结果为: " << temp.result << std::endl;std::cout << "错误码为: " << temp.code << std::endl;return 0;
}
三,服务器实现
3.1 逻辑梳理
服务器涉及两个个头文件和一个源文件,有点绕,下面先梳理一下:
有三个文件:
- 首先,TcpServer.hpp是服务器主函数,ServerCal.cc包含服务器初始化和启动的main函数,ServerCal.hpp是进行计算器运算的头文件
- 首先构建服务器对象,并在构造函数里将ServerCal.cc里面的运算函数带进去,然后是初始化服务器,执行创建套接字等操作,然后启动服务器
- 当服务器收到客户端发来的报文后,直接将报文传给运算函数,由运算函数做去掉报头,解析有效载荷等过程,并执行运算,最后把运算结果再次构建成响应报文,以返回值形式返回给服务器运行函数
- 然后服务器再把响应报文发给客户端,完成一次计算请求处理
3.2 各头文件实现
Server.hpp实现:
#pragma once
#include <iostream>
#include <string>
#include "Protocol.hpp"enum
{Div_Zero = 1,Mod_Zero,Other_Oper
};class ServerCal
{
public:ServerCal(){}~ServerCal(){}Response CalculatorHelper(const Request &req){Response resp(0, 0);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 '/':{if (req.y == 0){resp.code = Div_Zero;}else{resp.result = req.x / req.y;}}break;case '%':{if (req.y == 0){resp.code = Mod_Zero;}else{resp.result = req.x % req.y;}}break;default:resp.code = Other_Oper;break;}return resp;}std::string Calculator(std::string &package){std::string content;if (!Decode(package, &content)) // 分离报头和有效载荷:"len"\n"10 + 20"\nreturn "";// 走到这里就是完整的报文Request req;if (!req.DeSerialize(content)) // 反序列化,解析有效载荷 "10 + 20" --> x=10 op="+" y=20return "";content = "";Response resp = CalculatorHelper(req); // 执行计算逻辑resp.Serialize(&content); // 序列化计算结果的有效载荷 result=10, code=0content = Encode(content); // 将有效载荷和报头封装成响应报文 "len"\n"30 0"return content;}
};
TcpServer.hpp实现:
#pragma once
#include "Log.hpp"
#include "Socket.hpp"
#include <signal.h>
#include <string>
#include <functional>using func_t = std::function<std::string(std::string &package)>;class TcpServer
{
public:TcpServer(uint16_t port, func_t callback): _port(port), _callback(callback){}bool InitServer(){// 创建,绑定,监听套接字_listensockfd.Socket();_listensockfd.Bind(_port);_listensockfd.Listen();log(Info, "Init server... done");return true;}void Start(){signal(SIGCHLD, SIG_IGN); // 忽略signal(SIGPIPE, SIG_IGN);while (true){std::string clientip;uint16_t clientport;int sockfd = _listensockfd.Accept(&clientip, &clientport);if (sockfd < 0)continue;log(Info, "accept a new link, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);// 走到了这里就是成功获取发起连接方IP与port,后面就是开始提供服务if (fork() == 0){_listensockfd.Close();// 进行数据运算服务std::string inbuffer_stream;while (true){char buffer[1280];ssize_t n = read(sockfd, buffer, sizeof(buffer));if (n > 0){buffer[n] = 0;inbuffer_stream += buffer; // 这里用+=log(Debug, "debug:\n%s", inbuffer_stream.c_str());while (true){std::string info = _callback(inbuffer_stream);// if (info.size() == 0) //ServerCal.hpp,解析报文失败的话会返回空串if (info.empty()) // 空的话代表inbuffstream解析时出问题,表示不遵守协议,发不合法的报文给我,我直接丢掉不玩了break; // 不能用continuelog(Debug, "debug, response:\n%s", info.c_str());log(Debug, "debug:\n%s", inbuffer_stream.c_str());write(sockfd, info.c_str(), info.size());}}else if (n == 0) // 读取出错break;else // 读取出错break;}exit(0);}close(sockfd);}}~TcpServer(){}private:uint16_t _port;Sock _listensockfd;func_t _callback;
};
ServerCal.cc实现:
#include "Log.hpp"
#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "ServerCal.hpp"
#include "Deamon.hpp"static void Usage(const std::string &proc)
{std::cout << "\nUsage: " << proc << "port\n\n"<< std::endl;
}int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]);ServerCal cal;TcpServer *tsvp = new TcpServer(port, std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1));tsvp->InitServer();//Daemon();//daemon(0, 0);tsvp->Start();// Request测试--------------------// Request req(10, 20, '+');// std::string s;// req.Serialize(&s);// std::cout << "有效载荷为: " << s << std::endl;// s = Encode(s);// std::cout << "报文为:" << s;// std::string content;// bool r = Decode(s, &content); //分离报头和有效载荷// std::cout << "分离报头后的有效载荷为: "<< content << std::endl;// Request temp;// temp.DeSerialize(content); //解析有效载荷// std::cout<< "有效载荷分离后, x为: " << temp.x << " 运算符为:\"" << temp.op << "\" y为: " << temp.y << std::endl;// Response测试--------------------// Response resp(10, 20);// std::string s;// resp.Serialize(&s);// std::cout << "有效载荷为: " << s << std::endl;// std::string package = Encode(s); //分离报头和有效载荷// std::cout << "报文为: " << package;// s = "";// bool r = Decode(package, &s);// std::cout << "分离报头后的有效载荷为: " << s << std::endl;// Response temp;// temp.DeSerialize(s); // 解析有效载荷// std::cout << "解析有效载荷: " << std::endl;// std::cout << "结果为: " << temp.result << std::endl;// std::cout << "错误码为: " << temp.code << std::endl;return 0;
}
四,客户端实现
客户端的话,为了方便发送计算请求,会采用随机数的方式获取运算数和运算符,如下代码:
#include <iostream>
#include <string>
#include <ctime>
#include <cassert>
#include <unistd.h>
#include "Socket.hpp"
#include "Protocol.hpp"static void Usage(const std::string &proc)
{std::cout << "\nUsage: " << proc << " serverip serverport\n"<< std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]); //获取IP和端口Sock sockfd;sockfd.Socket();if (!sockfd.Connect(serverip, serverport))return 1;srand(time(nullptr) ^ getpid()); // 种随机数种子int cnt = 1;const std::string opers = "+-*/%-=&^";std::string inbuffer_stream;while (cnt <= 5){std::cout << "===============第" << cnt << "次测试....., " << "===============" << std::endl;int x = rand() % 100 + 1;usleep(1234);int y = rand() % 100;usleep(4321);char oper = opers[rand() % opers.size()];Request req(x, y, oper);req.DebugPrint();// 下面是根据协议发送给对方std::string package;req.Serialize(&package); // 序列化package = Encode(package); // 形成报文int fd = sockfd.Fd(); // 获取套接字write(fd, package.c_str(), package.size()); // 将请求从客户端往服务端写过去// 下面是读取服务器发来的结果并解析char buffer[128];ssize_t n = read(sockfd.Fd(), buffer, sizeof(buffer)); // 读取服务器发回来的结果,但是这里也无法保证能读取到一个完整的报文if (n > 0) // 读成功了{buffer[n] = 0;inbuffer_stream += buffer; // "len"\n"result code"\nstd::cout << inbuffer_stream << std::endl;std::string content;bool r = Decode(inbuffer_stream, &content); // 去掉报头"result code"\nassert(r); // r为真说明报头成功去掉Response resp;r = resp.DeSerialize(content); // 对有效荷载进行反序列化assert(r);resp.DebugPrint(); // 打印结果}std::cout << "=================================================" << std::endl;sleep(1);cnt++;}sockfd.Close();return 0;
}
效果演示: