在网络编程中,协议是一个关键概念。协议本质上是一种“约定”,规定了两方在通信时如何格式化和处理数据。本文将深入探讨如何通过协议进行结构化数据的传输,并且通过一个具体的网络版计算器( TCP服务器-客户端)示例,展示序列化与反序列化的实现。
学习导图:
1. 理解协议
协议,简单来说,就是通信双方都遵守的规则。在前面的例子中,我们使用了父亲和儿子通过电话沟通的场景。父亲告诉儿子会在特定时间打电话,这就是一种约定——协议。
1.1 结构化数据的传输
在网络通信中,数据通常以字节流的形式发送和接收。当我们需要传输的是结构化数据时,例如在QQ群聊中,除了文字消息外,还包含头像、时间和昵称。这些信息都需要以某种方式发送给对方。如果我们逐个发送这些数据,不仅麻烦,接收方也难以处理,因此需要对这些数据进行打包。
为什么要把字符串转成结构化数据呢?未来这个结构化的数据一定是一个对象,然后使用它的时候,直接对象.url 、对象.time 拿到。
而这里的结构体如message就是传说中的业务协议。
因为它规定了我们聊天时网络通信的数据。
序列化与反序列化
为了简化结构化数据的传输,我们通常将多个独立的信息合并为一个报文。这就是序列化的过程:将数据打包成一个字符串或字节流,再通过网络发送。接收方收到数据后,需要通过反序列化,将收到的数据解析回原来的结构化数据。
代码感知:
这段代码的核心功能是实现请求(
Request
)和响应(
Response
)的序列化与反序列化。序列化的作用是将类中的成员变量转换成字符串格式,方便在网络中传输;反序列化的作用是将字符串解析回类的成员变量,恢复为结构化数据。以下是对该代码的详细解释:
Request
类
class Request
{
public:
// 定义常量字符串分隔符和其长度
static const char SPACE = ' ';
static const int SPACE_LEN = 1;
这个类表示客户端发送给服务器的计算请求,包含两个操作数(
_x
和
_y
)以及一个操作符(
_op
)。它提供了序列化和反序列化的能力。
1. 构造函数
Request()
{}
这个是默认构造函数,不进行任何初始化操作,只是声明了
Request
对象。
Request(int x, int y, int op)
: _x(x), _y(y), _op(op)
{}
这是一个带参数的构造函数,它接受两个整数操作数
x
、
y
和一个字符操作符
op
,并将它们赋值给类中的成员变量
_x
、
_y
、
_op
。
2. 序列化函数:
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"
的格式。
3. 反序列化函数:
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
。2.str.rfind(SPACE)
:查找最后一个空格的位置,表示第二个操作数的开头。如果找不到,返回false
。3. 使用atoi
函数从字符串中提取整数操作数_x
和_y
。substr(0, left)
获取左侧字符串,即第一个操作数,substr(right + SPACE_LEN)
获取右侧字符串,即第二个操作数。4. 从字符串str
中获取操作符_op
,位于第一个空格后的位置。5. 如果解析成功,返回true
;否则返回false
。
设计思路:
补充
- 上面代码中的atoi是怎么使用的,介绍一下atoi接口
- 是如何从字符串 str 中获取操作符 _op
atoi
函数的使用:atoi
是 C++ 标准库函数之一,它位于<cstdlib>
头文件中。该函数的作用是将一个字符串(以空字符结尾的字符数组)转换为int
类型的整数。其原型如下:
int atoi(const char *str);
参数
str
是指向要转换的以空字符结尾的字符串的指针。
atoi
会从字符串的开头开始转换,直到遇到第一个非数字字符或到达字符串的结尾。如果字符串以数字开头,
atoi
会返回这些数字对应的整数值。如果字符串不是以数字开头,或者字符串为空,
atoi
会返回 0。
以下是一些使用
atoi
的例子:
#include <cstdlib>
#include <iostream>
int main() {
const char *str1 = "123";
const char *str2 = "12abc34";
const char *str3 = "abc123";
int num1 = atoi(str1); // num1 will be 123
int num2 = atoi(str2); // num2 will be 12
int num3 = atoi(str3); // num3 will be 0 (no digits at the start)
std::cout << "num1: " << num1 << std::endl;
std::cout << "num2: " << num2 << std::endl;
std::cout << "num3: " << num3 << std::endl;
return 0;
}
需要注意的是,
atoi
不进行错误检查,如果字符串不能完全转换为数字,那么未转换的部分将被忽略。此外,
atoi
无法处理整数溢出,如果转换的数字超出了
int
的表示范围,结果是不确定的。
🎢2. 从字符串
str
中获取操作符
_op
:
在
Request
类的
DeSerialize
方法中,以下是获取操作符
_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
。
4. 成员变量
public:
int _x;
int _y;
char _op;
};
Response
类
Response
类表示服务器的响应,包含两个数据成员:计算结果
_ret
和状态码
_code
。
1. 构造函数
Response()
{}
这是默认构造函数,不初始化任何成员。
Response(int ret, int code)
: _code(code), _ret(ret)
{}
这是一个带参数的构造函数,用来初始化响应结果
ret
和状态码
code
。
2. 序列化函数:
Serialize()
std::string Serialize()
{
std::string str = std::to_string(_code);
str += SPACE;
str += std::to_string(_ret);
return str;
}
- 功能:将
Response
对象的两个成员变量_code
和_ret
组合成字符串。
3. 反序列化函数:
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
对象。
4. 成员变量
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 _y
std::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 _ret
std::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; // 计算结果的状态码
};
代码展示了如何将
Request
和
Response
对象进行序列化和反序列化,以便在网络中传输结构化数据。序列化将对象转换为字符串进行网络传输,反序列化将接收到的字符串重新解析为对象,方便在应用程序中处理。
Request
主要表示操作请求,包括两个操作数和一个操作符。Response
表示服务器的响应,包括计算结果和状态码。- 序列化与反序列化是网络编程中常用的技术,能有效将复杂的结构化数据转化为适合网络传输的简单格式。
2. 实验:网络版计算器
为了加深理解,我们通过实现一个简单的TCP服务端来展示协议、序列化和反序列化的运作过程。这个服务端会处理简单的数学运算请求,客户端发送请求,服务端进行计算并返回结果。
2.1 定义请求和响应协议
我们定义了一个
Request
类来表示计算请求,包含两个操作数和一个操作符。同时,我们定义了
Response
类来表示响应结果,包含计算结果和状态码。
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; // 操作数1
int _y; // 操作数2
char _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; // 计算结果
};
2.2 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);
}
}
};
2.3 业务处理逻辑
处理请求的逻辑被封装在
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;
}
}
3. 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;
};
4. 序列化与反序列化的重要性
- 序列化:将结构化的数据(如对象)转换为字节流或字符串,以便传输。
- 反序列化:将字节流或字符串重新解析为结构化数据,供应用程序使用。
通过序列化与反序列化,应用程序与网络通信得到了有效解耦。这种设计使得复杂的数据可以轻松地在网络中传输,并且为上层应用提供了灵活的操作方式。
对于网络版计算器所有部分的完整代码,之后将上传 gitee,下篇文章将对于设计思路和一些坑点继续讲解~
版权归原作者 lvy- 所有, 如有侵权,请联系我们删除。