计算机网络(五) —— 自定义协议简单网络程序
目录
一,关于“协议”
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;
}
效果演示:
相关文章:

计算机网络(五) —— 自定义协议简单网络程序
目录 一,关于“协议” 1.1 结构化数据 1.2 序列化和反序列化 二,网络版计算器实现准备 2.1 套用旧头文件 2.2 封装sock API 三,自定义协议 3.1 关于自定义协议 3.2 实现序列化和反序列化 3.3 测试 三,服务器实现 3.1…...

开源模型应用落地-qwen2-7b-instruct-LoRA微调-unsloth(让微调起飞)-单机单卡-V100(十七)
一、前言 本篇文章将在v100单卡服务器上,使用unsloth去高效微调QWen2系列模型,通过阅读本文,您将能够更好地掌握这些关键技术,理解其中的关键技术要点,并应用于自己的项目中。 使用unsloth能够使模型的微调速度提高 2 - 5 倍。在处理大规模数据或对时间要求较高的场景下,…...

[数据集][目标检测]车油口挡板开关闭合检测数据集VOC+YOLO格式138张2类别
数据集格式:Pascal VOC格式YOLO格式(不包含分割路径的txt文件,仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数):138 标注数量(xml文件个数):138 标注数量(txt文件个数):138 标注类别…...

Delphi 的 RSA 库 LockBox
LockBox 是用于 Delphi 的一套加密/解密控件 最早是一套商业控件,后来开源了。再后来,又有一个新版本的 LockBox,和旧版本完全不同。 旧版本的 LockBox 叫 LockBox 2;新版本的叫 LockBox 3。 这两个控件,都可以通过…...

element UI学习使用(1)
https://element.eleme.cn/2.6/#/zh-CN/component/container vue模块库,可复制直接使用 1、搜索框、下拉搜索框 <el-form :inline"true" class"demo-form-inline"><el-form-item label"结果搜索"><el-inputplaceho…...

如何搞定日语翻译?试试这四款工具
写一篇字数800-1000字的软文,用翻译新手的角度分享福昕翻译在线、福昕翻译客户端、海鲸AI翻译以及彩云翻译在翻译日语时候的表现,要求口语化表达。 最近对于一些轻小说突然感兴趣了,所以我开始尝试各种翻译工具来帮助我搞定日语翻译。今天&am…...

【STM32】独立看门狗(IWDG)原理详解及编程实践(上)
本篇文章是对STM32单片机“独立看门狗(IWDG)”的原理进行讲解。希望我的分享对你有所帮助! 目录 一、什么是独立看门狗 (一)简介 (二)、独立看门狗的原理 (三)、具体操…...

前端框架大观:探索现代Web开发的基石
目录 引言 一、前端框架概述 二、主流前端框架介绍 2.1 React 2.1.1 简介 2.1.2 特点 2.1.3 代码示例 2.2 Vue.js 2.2.1 简介 2.2.2 特点 2.2.3 代码示例 2.3 Angular 2.3.1 简介 2.3.2 特点 2.3.3 代码示例 三、其他前端框架与库 四、前端框架的选择 五、结…...

16 训练自己语言模型
在很多场景下下,可能微调模型并不能带来一个较好的效果。因为特定领域场景下,通用话模型过于通用,出现多而不精。样样通样样松;本章主要介绍如何在特定的数据上对模型进行预训练; 训练自己的语言模型(从头开…...

udp网络通信 socket
套接字是实现进程间通信的编程。IP可以标定主机在全网的唯一性,端口可以标定进程在主机的唯一性,那么socket通过IP端口号就可以让两个在全网唯一标定的进程进行通信。 套接字有三种: 域间套接字:实现主机内部的进程通信的编程 …...

LG AI研究开源EXAONE 3.0:一个7.8B双语语言模型,擅长英语和韩语,在实际应用和复杂推理中表现出色
EXAONE 3.0介绍:愿景与目标 EXAONE 3.0是LG AI研究所在语言模型发展中的一个重要里程碑,特别是在专家级AI领域。 “EXAONE”这个名称源自于“ EX pert A I for Every ONE”,反映了LG AI研究所致力于将专家级别的人工智能能力普及化的承诺。这…...

【mysql】mysql之主从部署以及介绍
本站以分享各种运维经验和运维所需要的技能为主 《python零基础入门》:python零基础入门学习 《python运维脚本》: python运维脚本实践 《shell》:shell学习 《terraform》持续更新中:terraform_Aws学习零基础入门到最佳实战 《k8…...

Invoke-Maldaptive:一款针对LDAP SearchFilter的安全分析工具
关于Invoke-Maldaptive MaLDAPtive 是一款针对LDAP SearchFilter的安全分析工具,旨在用于对LDAP SearchFilter 执行安全解析、混淆、反混淆和安全检测。 其基础是 100% 定制的 C# LDAP 解析器,该解析器处理标记化和语法树解析以及众多自定义属性&#x…...

QT 读取Excel表
一、QAxObject 读取excel表的内容,其仅在windows下生效,当然还有其他跨平台的方案。 config qaxcontainer #include <QAxObject>QStringList GetSheets(const QString& strPath) {QAxObject* excel new QAxObject("Excel.Application&…...

深入理解 Vue 组件样式管理:Scoped、Deep 和 !important 的使用20240909
深入理解 Vue 组件样式管理:Scoped、Deep 和 !important 的使用 在前端开发中,样式的管理与组件化开发之间的平衡一直是一个难题。Vue.js 提供了一些强大的工具来帮助开发者在开发复杂的应用时管理样式。这篇文章将详细介绍 Vue 中的 scoped、:deep() 和…...

C语言内存函数(21)
文章目录 前言一、memcpy的使用和模拟实现二、memmove的使用和模拟实现三、memset函数的使用四、memcmp函数的使用总结 前言 正文开始,发车! 一、memcpy的使用和模拟实现 函数模型:void* memcpy(void* destination, const void* source, size…...

三高基本概念之-并发和并行
并行和并发是计算机科学中两个重要但容易混淆的概念,它们之间的主要区别可以从以下几个方面进行阐述: 一、定义与含义 并行(Parallel):并行是指两个或多个事件在同一时刻发生,即这些事件在微观和宏观上都…...

宝塔面板FTP连接时“服务器发回了不可路由的地址。使用服务器地址代替。”
参考 https://blog.csdn.net/neizhiwang/article/details/106628899 错误描述 我得服务器是腾讯,然后使用宝塔建了个HTML网站,寻思用ftp上传,结果报错: 状态: 连接建立,等待欢迎消息... 状态: 初始化 TLS 中... 状…...

面试的一些小小经验
无论何时,找到合适的满意的工作(距离住处的地理位置,薪资,工作氛围)并不是一件容易的事情。个人能力与职位的适配性永远是有误差的客观存在。 十全十美难得,满足个人的个体化优先级才是客观的存在。 1.投简…...

IV转换放大器原理图及PCB设计分析
【前言】 今天给大家分享一下关于IV转换放大器的相关电路设计心得。IV转换使用的场合非常之多,尤其是电流型输出的传感器,比如光敏二极管、硅光电池等等,这些传感器输出的电流信号非常微弱,我们如果需要检测它们,首先得…...

【数学建模经验贴】一个研赛数模老手的经验
我(非C君,是一个朋友)参加了3次“深圳杯”数模,1次全国大学生数模,以及1次全国研究生数模,2016年参加了全国研究生数模的交流会,但没有参加过美赛,应该算是一个江湖老手了吧。下面内…...

vivo手机已删除的短信还能恢复吗?
虽然现在我们很少使用vivo手机的短信功能,但是我们偶尔还会通过vivo手机短信功能接收一些重要的信息。如果我们在清理垃圾短信的时候误删了vivo手机重要短信,该怎么恢复呢? 方法一:通过vivo云服务恢复 1、确保您已开启vivo云服务…...

[网络][CISCO]CISCO IOS升级
CISCO IOS升级-(转)2008-06-27 15:35IOS 升级 在介绍CISCO路由器IOS升级方法前,有必要对Cisco路由器的存储器的相关知识作以简单介绍。路由器与计算机相似,它也有内存和操作系统。在Cisco路由器中,其操作系统叫做互连…...

通过python提取PDF文件指定页的图片
整体思路 要从 PDF 文件中提取指定页和指定位置的图片,可以分几个步骤来实现: 1.1 准备所需工具与库 在 Python 中处理 PDF 和图像时,需要使用几个库: PyMuPDF (fitz):用于读取和处理 PDF 文件,可以精确…...

Leetcode Hot 100刷题记录 -Day12(轮转数组)
轮转数组 问题描述: 给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。 示例 1: 输入: nums [1,2,3,4,5,6,7], k 3 输出: [5,6,7,1,2,3,4]解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,2,3,4,5] 向…...

GitHub每日最火火火项目(9.13)
以下是对这些项目的详细介绍: fishaudio 的 fish-speech: 基本信息:这是一种全新的语音技术解决方案,属于文本到语音(Text-to-Speech,TTS)技术范畴。技术特点: 多语言支持ÿ…...

力扣--649.Dota2参议院
Dota2 的世界里有两个阵营:Radiant(天辉)和 Dire(夜魇) Dota2 参议院由来自两派的参议员组成。现在参议院希望对一个 Dota2 游戏里的改变作出决定。他们以一个基于轮为过程的投票进行。在每一轮中,每一位参…...

vim 安装与配置教程(详细教程)
vim就是一个功能非常强大的文本编辑器,可以自己DIY的那种 ,不但可以写代码 ,还可编译 ,可以让你手不离键盘的完成鼠标的所有操作。 如果想要了解vim的的发展历史和详细解说,可以自行上网搜索,我主要是记录一…...

【WPF】Popup的使用
WPF(Windows Presentation Foundation)中的Popup控件用于创建弹出窗口,如工具提示、上下文菜单等。Popup控件本身并不直接显示任何内容,它需要一个子元素来显示实际的内容。 以下是一个简单的XAML示例,展示如何创建一…...

力扣刷题之2576.求出最多标记下标
题干描述 给你一个下标从 0 开始的整数数组 nums 。 一开始,所有下标都没有被标记。你可以执行以下操作任意次: 选择两个 互不相同且未标记 的下标 i 和 j ,满足 2 * nums[i] < nums[j] ,标记下标 i 和 j 。 请你执行上述操…...