文章目录
一、序列化与反序列化概念
上一章讲解了TCP通信【网络编程】demo版TCP网络服务器实现,我们知道TCP是面向字节流的方式进行通信。
但是这里就会引发一个问题:怎么保证正好就读到一个完整的数据呢?
举个例子:我们使用QQ发送消息的时候别人接收到的不仅仅只有消息,而是包含了头像信息,昵称,消息。这就叫做结构化的数据。这些结构化的数据可以打包成一个报文(变成一个整体),这个过程就叫做序列化。而把这个整体报文解开的过程就叫做反序列化。
结构化数据要先序列化再发送到网络中,收到序列字节流后,要先反序列化再使用。
而这里序列化和反序列化的过程用的就是业务协议
二、自定义协议设计网络计算机
2.1 服务端
自定义协议里要包含两各类,一个是请求,一个是响应
服务端会收到请求,客户端收到响应。
// 请求
class Request
{
public:
public:
int _x = 0;
int _y = 0;
char _op = 0;
};
// 响应
class Response
{
public:
int _exitcode = 0;// 退出码
int _result = 0;// 结果
};
请求就是左操作符、右操作符和符号
响应包含了退出码和结果,如果正常结束退出码为0,如果有错误,我们可以自定义不同的退出码表示不同的错误。
2.1.1 服务端业务处理流程
先来看一下服务端处理数据流程:
客户端发过来的数据已经序列化成了一个序列字节流数据(报文),所以服务端首先要先把报文反序列化,构成一个结构化请求对象Request。然后就可以进行计算处理形成一个Response对象,再序列化后发送给客户端。
可以看到计算处理这一步其实跟接收发送消息、序列化与反序列化没什么关系,所以可以把计算处理任务在服务端启动的时候传递进去。
计算处理函数:
typedef std::function<bool(const Request& req, Response& resp)> func_t;
这里的req是输入型参数(已经反序列化好的对象),resp是输出型参数,为了获取计算结果。
2.1.2 TCP的发送与接收缓冲区
我们前面使用的write和read接口并不是直接往网络里发送数据或者从网络里读取数据,write其实是把数据拷贝到传输层的缓冲区,由TCP协议决定什么时候把缓冲区的数据发送到网络中。所以TCP协议也叫传输控制协议。
发送数据的本质就是将数据从发送缓冲区拷贝到接收缓冲区。
所以客户端/服务端发送数据不会影响接受数据。
所以TCP是全双工的。
而这就会导致一个问题:可能数据堆积在缓冲区来不及度,一次会读取多个报文挨在一起。那么怎么保证读取完整报文呢?
2.1.3 保证读取完整报文
因为TCP是面向字节流的,所以要明确报文与报文的分界。
为什么要这样呢?举个例子:
现在要把两个数字合并成字符串发送,1、12,如果不处理的话就是"112"
,这样我们反序列化的时候就不知道到底怎么组合了。
而如果我们在分割的地方加一个符号比如,
,序列化后:"1,12"
,这样就很容易拆分。
保证报文读取完整性的方法:
1️⃣ 定长: 规定长度,每次就读取这么多。
2️⃣ 特殊字符: 就是上面的方法。
3️⃣ 自描述方式: 比如在报文前面带上四个字节的字段,标识报文长度。
2.1.4 自定义协议——序列化与反序列化
先来看请求的序列化与反序列化
2.1.4.1 请求
int _x = 0;
int _y = 0;
char _op = 0;
我们希望序列化成这样:"_x _op _y"
#define SEP " "
#define SEP_LEN strlen(SEP)
#define SEP_LINE "\r\n"
#define SEP_LINE_LEN strlen(SEP_LINE)
// 请求
class Request
{
public:
Request(int x, int y, char op)
: _x(x)
, _y(y)
, _op(op)
{
}
Request()
{
}
// 序列化
bool serialize(std::string* out/*输出型参数*/)
{
// "_x _op _y"
std::string sx = std::to_string(_x);
std::string sy = std::to_string(_y);
*out = sx + SEP + _op + SEP + sy;
return true;
}
// 反序列化
bool deserialize(const std::string& in)
{
// "_x _op _y"
auto lsep = in.find(SEP);
auto rsep = in.rfind(SEP);
if(lsep == std::string::npos || rsep == std::string::npos
|| lsep == rsep) return false;
std::string sx = in.substr(0, lsep);
std::string sy = in.substr(rsep + SEP_LEN);
if(sx.empty() || sy.empty()) return false;
_x = stoi(sx);
_y = stoi(sy);
_op = in[lsep + SEP_LEN];
return true;
}
public:
int _x = 0;
int _y = 0;
char _op = 0;
};
这里的反序列化我们传进去的字符串已经把"\r\n"
去掉了。
先来看响应的序列化与反序列化
2.4.1.2 响应
我们希望序列化成这样:"_exitcode _result"
// 响应
class Response
{
public:
Response(int exitcode, int result)
: _exitcode(exitcode)
, _result(result)
{
}
Response()
{
}
// 序列化
bool serialize(std::string* out/*输出型参数*/)
{
std::string se = std::to_string(_exitcode);
std::string sr = std::to_string(_result);
*out = se + SEP + sr;
return true;
}
// 反序列化
bool deserialize(const std::string& in)
{
// "_exitcode _result"