Linux网络序列化与反序列化(6)
文章目录
- 前言
- 一、什么是协议?
- 协议的概念
- 结构化数据的传输
- 序列化与反序列化
- 二、Request & Response
- Request类
- Response类
- 综合
- 三、网络版计算器
- 定义请求和响应协议
- Tcp服务端设计
- 业务端处理逻辑
- Tcp客户端代码
- 总结
前言
Hello,我又回来了,也是该静下心来好好继续沉淀了
不过话说真是离谱,之前的很多知识都快忘记了,幸好有博客的帮助,虽然费时间也是真的费时间,不过总体来说还是利大于弊的
一、什么是协议?
协议的概念
协议就是指的网络协议,网络协议是通信计算机双方必须共同遵从的一组约定,比如怎么建立连接、怎么互相识别等。
本质上就是一种约定
结构化数据的传输
在网络通信中,数据通常以字节流的形式发送和接收,但是举个例子,在QQ群聊中,除了文字消息外,还包含头像、时间和昵称,这些信息都需要以某种方式发送给对方,如果逐个发送就很麻烦,接收方也很难处理,所以我们考虑将这些数据进行打包
这样的话,未来接收方只需要通过 对象.url、对象.time 就可以了拿到数据了
而这里的结构体如 message 就是传说中的业务协议!是不是很惊讶!所以其实没那么玄乎,放开心就行
序列化与反序列化
所以我们接着来探讨这两个概念,为了简化结构化数据的传输,我们通常将多个独立的信息合并为一个报文。这就是序列化的过程,将数据打包成一个字符串或字节流,再通过网络发送。接收方收到数据后,需要通过反序列化,将收到的数据解析回原来的结构化数据
二、Request & Response
接下来我们就通过实例来感受什么是序列化,什么是反序列化
序列化的作用是将类中的成员变量转换成字符串格式,方便在网络中传输;反序列化的作用是将字符串解析回类的成员变量,恢复为结构化数据
知道这个后,我们来实现一个比较简易的计算器
Request类
class Request
{
public:// 定义常量字符串分隔符和其长度static const char SPACE = ' ';static const int SPACE_LEN = 1;
这个类表示客户端发送给服务器的计算请求,包含两个操作数(_x 和 _y)以及一个操作符(_op)。它提供了序列化和反序列化的能力
构造函数:
Request()
{}
这个是默认构造函数,不进行任何初始化操作
Request(int x, int y, int op): _x(x), _y(y), _op(op)
{}
这是一个带参数的构造函数,它接受两个整数操作数 x、y 和一个字符操作符 op,并将它们赋值给类中的成员变量 _x、_y、_op
序列化函数:Serialize()
std::string Serialize()
{std::string str;str = std::to_string(_x);str += SPACE;str += _op;str += SPACE;str += std::to_string(_y);return str;
}
功能:将 Request 对象中的数据成员 _x、_op 和 _y 组合成一个字符串。返回组合好的字符串。最终结果类似 “1 + 2” 的格式
反序列化函数:DeSerialize()
bool DeSerialize(const std::string& str)
{size_t left = str.find(SPACE);if(left == std::string::npos){return false;}size_t right = str.rfind(SPACE);if (right == std::string::npos){return false;}_x = atoi(str.substr(0, left).c_str());_y = atoi(str.substr(right + SPACE_LEN).c_str());if(left + SPACE_LEN < str.size()){_op = str[left + SPACE_LEN];return true;}else{return false;}
}
功能:从输入的字符串中提取出操作数 _x 和 _y 以及操作符 _op,并将它们存储到 Request 对象的成员变量中
步骤如下:
- str.find(SPACE):在字符串 str 中查找第一个空格的位置,用作分隔符。如果找不到,返回 false。
- str.rfind(SPACE):查找最后一个空格的位置,表示第二个操作数的开头。如果找不到,返回 false。
- 使用 atoi 函数从字符串中提取整数操作数 _x 和 _y。substr(0, left) 获取左侧字符串,即第一个操作数,substr(right + SPACE_LEN) 获取右侧字符串,即第二个操作数。
- 从字符串 str 中获取操作符 _op,位于第一个空格后的位置。
- 如果解析成功,返回 true;否则返回 false。
至于atoi函数,大家可以自行翻阅头文件cstdlib
具体我们来重点关注一下如何从字符串中获得想要的操作符_op
if(left + SPACE_LEN < str.size())
{_op = str[left + SPACE_LEN];return true;
}
else
{return false;
}
这里的 left 是 str 中第一个空格字符的位置,SPACE_LEN 是空格字符的长度,通常为 1。所以 left + SPACE_LEN 是第一个空格字符之后的位置,即操作符 _op 应该出现的位置。str[left + SPACE_LEN] 获取该位置的字符并将其赋值给 _op,逻辑清楚透彻
成员变量:
public:int _x;int _y;char _op;
};
Response类
Response 类表示服务器的响应,包含两个数据成员:计算结果 _ret 和 状态码 _code
构造函数:
Response()
{}
这是默认构造函数,不初始化任何成员。
Response(int ret, int code): _code(code), _ret(ret)
{}
这是一个带参数的构造函数,用来初始化响应结果 ret 和状态码 code
序列化函数:Serialize()
std::string Serialize()
{std::string str = std::to_string(_code);str += SPACE;str += std::to_string(_ret);return str;
}
功能:将 Response 对象的两个成员变量 _code 和 _ret 组合成字符串。
反序列化函数:DeSerialize()
bool DeSerialize(std::string& str)
{size_t pos = str.find(SPACE);if(pos == std::string::npos){return false;}_code = atoi(str.substr(0, pos).c_str());_ret = atoi(str.substr(pos + SPACE_LEN).c_str());return true;
}
功能:从输入的字符串中解析出状态码 _code 和计算结果 _ret,并存入 Response 对象
成员变量:
int _ret; // 计算结果
int _code; // 状态码
Response 类包含两个成员变量:
- _ret:存储服务器的计算结果(例如:加法、减法的结果)。
- _code:存储状态码,0 表示成功,非 0 表示错误(例如:除以 0 错误时 _code 可能为 1)。
综合
class Request
{
public:Request(){}Request(int x, int y, int op): _x(x), _y(y), _op(op){}~Request(){}// _x _op _ystd::string Serialize(){std::string str;str = std::to_string(_x);str += SPACE;str += _op;str += SPACE;str += std::to_string(_y);return str;}bool DeSerialize(const std::string& str){size_t left = str.find(SPACE);if(left == std::string::npos){return false;}size_t right = str.rfind(SPACE);if (right == std::string::npos){return false;}_x = atoi(str.substr(0, left).c_str());_y = atoi(str.substr(right + SPACE_LEN).c_str());if(left + SPACE_LEN < str.size()){_op = str[left + SPACE_LEN];return true;}else{return false;}}
public:int _x;int _y;char _op;
};class Response
{
public:Response(){}Response(int ret, int code): _code(code), _ret(ret){}~Response(){}// _code _retstd::string Serialize(){std::string str = std::to_string(_code);str += SPACE;str += std::to_string(_ret);return str;}bool DeSerialize(std::string& str){size_t pos = str.find(SPACE);if(pos == std::string::npos){return false;}_code = atoi(str.substr(0, pos).c_str());_ret = atoi(str.substr(pos + SPACE_LEN).c_str());return true;}
public:int _ret; // 计算结果int _code; // 计算结果的状态码
};
三、网络版计算器
我们通过实现一个简单的TCP服务端来展示协议、序列化和反序列化的运作过程。这个服务端会处理简单的数学运算请求,客户端发送请求,服务端进行计算并返回结果
总体框架图如下:
定义请求和响应协议
写到这里其实你可以再次回顾一下OSI七层模型,看看协议对应那一层,相信我,你会有更深的感悟的
class Request
{
public:Request(int x, int y, char op): _x(x),_y(y), _op(op) {}// 序列化:将请求转换为字符串格式bool serialize(string* out) {*out = to_string(_x) + " " + _op + " " + to_string(_y);return true;}// 反序列化:从字符串解析出请求内容bool deserialize(const string& in) {auto left = in.find(' ');auto right = in.rfind(' ');if (left == string::npos || right == string::npos || left == right)return false;_x = stoi(in.substr(0, left));_op = in[left + 1];_y = stoi(in.substr(right + 1));return true;}
public:int _x; // 操作数1int _y; // 操作数2char _op; // 操作符
};class Response
{
public:Response(int exitcode, int result) : _exitcode(exitcode), _result(result) {}// 序列化:将响应转换为字符串格式bool serialize(string* out) {*out = to_string(_exitcode) + " " + to_string(_result);return true;}// 反序列化:从字符串解析出响应内容bool deserialize(const string& in) {auto pos = in.find(' ');if (pos == string::npos) return false;_exitcode = stoi(in.substr(0, pos));_result = stoi(in.substr(pos + 1));return true;}
public:int _exitcode; // 状态码int _result; // 计算结果
};
Tcp服务端设计
这里设计了一个简单的TCP服务器 CalServer,用于处理客户端的请求并返回响应
- handlerEntry 函数:负责处理单个客户端连接。接收请求、反序列化、执行计算、序列化响应并发送回客户端。
- CalServer 类:负责监听端口并处理多个客户端连接。
class CalServer
{
public:CalServer(const uint16_t port) : _port(port), _listensock(-1) {}void initServer() {_listensock = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;bind(_listensock, (struct sockaddr*)&local, sizeof(local));listen(_listensock, 5);}void start(func_t func) {signal(SIGCHLD, SIG_IGN);while (true) {int sock = accept(_listensock, nullptr, nullptr);if (fork() == 0) {handlerEntry(sock, func);close(sock);exit(0);}close(sock);}}
};
业务端处理逻辑
处理请求的逻辑被封装在 Cal 函数中。它根据请求中的操作符计算结果并填充响应
void Cal(const Request& req, Response& resp)
{switch (req._op) {case '+': resp._result = req._x + req._y; break;case '-': resp._result = req._x - req._y; break;case '*': resp._result = req._x * req._y; break;case '/': if (req._y == 0) resp._exitcode = 1; else resp._result = req._x / req._y; break;default: resp._exitcode = 2; break;}
}
Tcp客户端代码
class CalClient
{
public:CalClient(const string& ip, const uint16_t& port) : _serverip(ip), _serverport(port), _sockfd(-1) {}void initClient() {_sockfd = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(_serverport);server.sin_addr.s_addr = inet_addr(_serverip.c_str());connect(_sockfd, (struct sockaddr*)&server, sizeof(server));}void run() {while (true) {string msg;cout << "Enter calculation: ";getline(cin, msg);Request req = parseInput(msg);string req_str;req.serialize(&req_str);send(_sockfd, req_str.c_str(), req_str.size(), 0);char buffer[1024];recv(_sockfd, buffer, sizeof(buffer), 0);Response resp;resp.deserialize(buffer);cout << "Result: " << resp._result << endl;}}
private:string _serverip;uint16_t _serverport;int _sockfd;
};
总结
通过序列化与反序列化,应用程序与网络通信得到了有效解耦,事实上,序列化与反序列化这种工作也轮不到我们来做,因为有更好更强的库,比如 Json、XML、Protobuf 等,这个我们后面再说