计算机网络(五) —— 自定义协议简单网络程序

目录

一,关于“协议”

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头文件了,这个文件具体包含下面几个内容:

  1. "100","+","200" --> "100 + 200"
  2. "100 + 200" --> "9"\n"100 + 200"
  3.  "9"\n"100 + 200" --> "100 + 200"
  4. "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;
}

 效果演示:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/421290.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

分享一个基于微信小程序的医院挂号就诊一体化平台uniapp医院辅助挂号应用小程序设计(源码、调试、LW、开题、PPT)

&#x1f495;&#x1f495;作者&#xff1a;计算机源码社 &#x1f495;&#x1f495;个人简介&#xff1a;本人 八年开发经验&#xff0c;擅长Java、Python、PHP、.NET、Node.js、Android、微信小程序、爬虫、大数据、机器学习等&#xff0c;大家有这一块的问题可以一起交流&…

NVD系列语音芯片在报警器中通常应用在哪些场景中

语音芯片在各类场景中应用的最大作用就是进行语音提示和警报提示。本文将对NVD系列语音芯片应用在报警器中的场景。 1.提升电动车的安全性 当电动车遭受震动或异常移动时&#xff0c;报警器会感应到并触发报警装置&#xff0c;通常是通过发出高分贝的声音警报来吸引人们的注意…

苹果iOS/ iPadOS18 RC 版、17.7 RC版更新发布

iPhone 16 / Pro 系列新机发布后&#xff0c;苹果一同推出了 iOS 18 和 iPadOS 18 的 RC 版本&#xff0c;iOS 18 RC 的内部版本号为22A3354&#xff0c;本次更新距离上次发布 Beta/RC 间隔 12 天。 在 iOS 18 中&#xff0c;苹果给我们带来了 Apple Intelligence&#xff0c;这…

如何在内网中与阿里云服务器进行文件传输?[2024详细版]

随着云计算发展&#xff0c;企业和个人选择将数据存储在云端&#xff0c;以提高数据的安全性和可访问性。阿里云作为国内领先的云服务提供商之一&#xff0c;提供了多种云产品和服务。其中&#xff0c;云服务器ECS&#xff08;Elastic Compute Service&#xff09;因其灵活性和…

VBA CSV数据拆分

1. Range.TextToColumns函数 Option ExplicitSub txt2Col()ActiveSheet.Range("A2").CopyActiveSheet.PasteSelection.TextToColumns DataType:xlDelimited, _ConsecutiveDelimiter:True, Comma:True End Sub 2. 效果 执行前 cccccc 执行后效果​ cccc 3. 参照 更…

Selenium自动化测试面试题合集!

1、什么是自动化测试、自动化测试的优势是什么&#xff1f; 通过工具或脚本代替手工测试执行过程的测试都叫自动化测试。 自动化测试的优势&#xff1a; 1、减少回归测试成本 2、减少兼容性测试成本 3、提高测试反馈速度 4、提高测试覆盖率 5、让测试工程师做更有意义的…

【docker】基于docker-compose 安装elasticsearch + kibana + ik分词器(8.10.4版本)

记录下&#xff0c;使用 docker-compose 安装 Elasticsearch 和 Kibana&#xff0c;并配置 IK 分词器&#xff0c;你可以按照以下步骤进行。此过程适用于 Elasticsearch 和 Kibana 8.10.4 版本。 安装 首先&#xff0c;在你的工作目录下创建一个 docker-compose.yml 文件&…

智能语音交互:人工智能如何改变我们的沟通方式?

在科技飞速发展的今天&#xff0c;人工智能&#xff08;AI&#xff09;已经渗透到我们生活的方方面面。其中&#xff0c;智能语音交互作为AI技术的一个重要分支&#xff0c;正以前所未有的速度改变着我们的沟通方式。从智能家居的控制到办公自动化的应用&#xff0c;再到日常交…

SonicWall SSL VPN曝出高危漏洞,可能导致防火墙崩溃

近日&#xff0c;有黑客利用 SonicWall SonicOS 防火墙设备中的一个关键安全漏洞入侵受害者的网络。 这个不当访问控制漏洞被追踪为 CVE-2024-40766&#xff0c;影响到第 5 代、第 6 代和第 7 代防火墙。SonicWall于8月22日对其进行了修补&#xff0c;并警告称其只影响防火墙的…

通过卷积神经网络(CNN)识别和预测手写数字

一&#xff1a;卷积神经网络&#xff08;CNN&#xff09;和手写数字识别MNIST数据集的介绍 卷积神经网络&#xff08;Convolutional Neural Networks&#xff0c;简称CNN&#xff09;是一种深度学习模型&#xff0c;它在图像和视频识别、分类和分割任务中表现出色。CNN通过模仿…

005:VTK世界坐标系中的相机和物体

VTK医学图像处理---世界坐标系中的相机和物体 左侧是成像结果 右侧是世界坐标系中的相机与被观察物体 目录 VTK医学图像处理---世界坐标系中的相机和物体 简介 1 在三维空间中添加坐标系 2 世界坐标系中的相机 3 世界…

价值流的实践应用:驱动企业运营效率与数字化转型的全面指南

价值流如何在实践中变革企业运营 在当今复杂的商业环境下&#xff0c;企业正在快速迈向数字化和自动化。为了在日益竞争激烈的市场中保持竞争力&#xff0c;企业需要优化其业务架构、提高运营效率并增强客户体验。《价值流指南》由The Open Group发布的企业数字化转型专业参考…

xlsx插件实现excel表格数据导入并解析成table——js技能提升

之前写后台管理系统的时候&#xff0c;遇到一个需求&#xff0c;就是要上传文件&#xff0c;并解析成table预览到页面上&#xff0c;效果如下&#xff1a; 这样做的目的也是为了帮助用户确认导入的内容是否正确&#xff0c;方便核实。 下面介绍实现步骤&#xff1a; 解决步骤…

Nginx.conf没有server和location模块的解决方法

网上有些说法说自己在配置文件里面添加server和location模块&#xff0c;但是我发现好像可以不用&#xff0c;其实nginx的配置文件还是给了我们提示的&#xff0c;如图&#xff1a; 在最后一行其实引入了另一个配置文件&#xff0c;我们cd进去看一下有什么内容。输入ls命令发现…

vue的学习之路(Vue中组件(component )

注意&#xff1a;其中添加div的意义就是让template标签有一个根标签 &#xff0c;否则只展示“欢迎进入登录程序” 不加div效果图 &#xff08;2&#xff09;两种开发方式 第一种开发方式 //局部组件登录模板声明 let login { //具体局部组件名称 template:‘ 用户登录 ’…

新专利:作物生长期预测方法及装置

近日,国家知识产权局正式授权了一项由北京市农林科学院智能装备技术研究中心、江苏省农业科学院联合申请的发明专利"作物生长期预测方法及装置"(专利号:ZL 2024 1 0185298.1)。该专利由 于景鑫 、任妮、吕志远、李友丽、吴茜等发明人耗时多年潜心研发&#xff0c;犹如…

EasyPlayer.js网页H5 Web js播放器能力合集

最近遇到一个需求&#xff0c;要求做一款播放器&#xff0c;发现能力上跟EasyPlayer.js基本一致&#xff0c;满足要求&#xff1a; 需求 功性能 分类 需求描述 功能 预览 分屏模式 单分屏&#xff08;单屏/全屏&#xff09; 多分屏&#xff08;2*2&#xff09; 多分屏…

[数据集][目标检测]抽烟检测数据集VOC+YOLO格式22559张2类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;22559 标注数量(xml文件个数)&#xff1a;22559 标注数量(txt文件个数)&#xff1a;22559 标…

Oracle数据恢复—Oracle数据库误删除表数据如何恢复数据?

删除Oracle数据库数据一般有以下2种方式&#xff1a;delete、drop或truncate。下面针对这2种删除oracle数据库数据的方式探讨一下oracle数据库数据恢复方法&#xff08;不考虑全库备份和利用归档日志&#xff09;。 1、delete误删除的数据恢复方法。 利用oracle提供的闪回方法…

Golang | Leetcode Golang题解之第397题整数替换

题目&#xff1a; 题解&#xff1a; func integerReplacement(n int) (ans int) {for n ! 1 {switch {case n%2 0:ansn / 2case n%4 1:ans 2n / 2case n 3:ans 2n 1default:ans 2n n/2 1}}return }