计算机网络(五) —— 自定义协议简单网络程序
目录
一,关于“协议”
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转换使用的场合非常之多,尤其是电流型输出的传感器,比如光敏二极管、硅光电池等等,这些传感器输出的电流信号非常微弱,我们如果需要检测它们,首先得…...

华为云AI开发平台ModelArts
华为云ModelArts:重塑AI开发流程的“智能引擎”与“创新加速器”! 在人工智能浪潮席卷全球的2025年,企业拥抱AI的意愿空前高涨,但技术门槛高、流程复杂、资源投入巨大的现实,却让许多创新构想止步于实验室。数据科学家…...

iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘
美国西海岸的夏天,再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至,这不仅是开发者的盛宴,更是全球数亿苹果用户翘首以盼的科技春晚。今年,苹果依旧为我们带来了全家桶式的系统更新,包括 iOS 26、iPadOS 26…...
Linux链表操作全解析
Linux C语言链表深度解析与实战技巧 一、链表基础概念与内核链表优势1.1 为什么使用链表?1.2 Linux 内核链表与用户态链表的区别 二、内核链表结构与宏解析常用宏/函数 三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势5.1 插入效率5.2 安全…...

label-studio的使用教程(导入本地路径)
文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...

什么是库存周转?如何用进销存系统提高库存周转率?
你可能听说过这样一句话: “利润不是赚出来的,是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业,很多企业看着销售不错,账上却没钱、利润也不见了,一翻库存才发现: 一堆卖不动的旧货…...
vue3 字体颜色设置的多种方式
在Vue 3中设置字体颜色可以通过多种方式实现,这取决于你是想在组件内部直接设置,还是在CSS/SCSS/LESS等样式文件中定义。以下是几种常见的方法: 1. 内联样式 你可以直接在模板中使用style绑定来设置字体颜色。 <template><div :s…...
将对透视变换后的图像使用Otsu进行阈值化,来分离黑色和白色像素。这句话中的Otsu是什么意思?
Otsu 是一种自动阈值化方法,用于将图像分割为前景和背景。它通过最小化图像的类内方差或等价地最大化类间方差来选择最佳阈值。这种方法特别适用于图像的二值化处理,能够自动确定一个阈值,将图像中的像素分为黑色和白色两类。 Otsu 方法的原…...

【2025年】解决Burpsuite抓不到https包的问题
环境:windows11 burpsuite:2025.5 在抓取https网站时,burpsuite抓取不到https数据包,只显示: 解决该问题只需如下三个步骤: 1、浏览器中访问 http://burp 2、下载 CA certificate 证书 3、在设置--隐私与安全--…...
JVM暂停(Stop-The-World,STW)的原因分类及对应排查方案
JVM暂停(Stop-The-World,STW)的完整原因分类及对应排查方案,结合JVM运行机制和常见故障场景整理而成: 一、GC相关暂停 1. 安全点(Safepoint)阻塞 现象:JVM暂停但无GC日志,日志显示No GCs detected。原因:JVM等待所有线程进入安全点(如…...
C++八股 —— 单例模式
文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全(Thread Safety) 线程安全是指在多线程环境下,某个函数、类或代码片段能够被多个线程同时调用时,仍能保证数据的一致性和逻辑的正确性…...