文章目录
- 一、序列化与反序列化概念
- 二、自定义协议设计网络计算机
- 2.1 服务端
- 2.1.1 服务端业务处理流程
- 2.1.2 TCP的发送与接收缓冲区
- 2.1.3 保证读取完整报文
- 2.1.4 自定义协议——序列化与反序列化
- 2.1.4.1 请求
- 2.4.1.2 响应
- 2.1.5 计算流程
- 2.1.6 在有效载荷前添加长度报头
- 2.1.7 发送响应send
- 2.1.8 读取一个完整的报文recv
- 2.2 客户端
- 2.3 结果
- 三、使用Json进行序列化和反序列化
一、序列化与反序列化概念
上一章讲解了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"
auto pos = in.find(SEP);
if(pos == std::string::npos) return false;
std::string se = in.substr(0, pos);
std::string sr = in.substr(pos + SEP_LEN);
if(se.empty() || sr.empty()) return false;
_exitcode = stoi(se);
_result = stoi(sr);
return true;
}
public:
int _exitcode = 0;// 退出码
int _result = 0;// 结果
};
2.1.5 计算流程
计算结果会形成一个resp响应,里面包含了退出码,我们可以自己设置退出码数值含义:
enum {
OK,
DIV_ZERO,
OP_ERROR
};
计算逻辑:
std::unordered_map<char, std::function<int(int, int)>> hash =
{
{'+', [](int x, int y)->int{return x + y;}},
{'-', [](int x, int y)->int{return x - y;}},
{'*', [](int x, int y)->int{return x * y;}},
{'/', [](int x, int y)->int{return x / y;}},
{'%', [](int x, int y)->int{return x % y;}},
};
bool calc(const Request& req, Response& resp)
{
// req已经反序列化好了
if(!hash.count(req._op))
{
resp._exitcode = OP_ERROR;
return false;
}
if(req._op == '/' || req._op == '%')
{
if(req._y == 0)
{
resp._exitcode = DIV_ZERO;
return false;
}
}
resp._result = hash[req._op](req._x, req._y);
return true;
}
2.1.6 在有效载荷前添加长度报头
"_x _op _y" -> "content_len\r\n_x _op _y\r\n"
"_exitcode _result" -> "content_len\r\n_exitcode _result\r\n"
// 给有效载荷添加报头信息
std::string enLength(const std::string& text)
{
std::string send_str = std::to_string(text.size());
send_str += SEP_LINE + text + SEP_LINE;
return send_str;
}
2.1.7 发送响应send
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
RETURN VALUE
On success, these calls return the number of characters sent.
On error, -1 is returned, and errno is set appropriately.
服务端收到请求到把响应发送出去的整个流程:
// 处理请求的入口
void handler(int sock, func_t func)
{
// 得到序列化好的请求对象
std::string req_str;
// 得到结构化请求对象
Request req;
if(!req.deserialize(req_str)) return;
// 计算,得到响应
Response resp;
func(req, resp);
// 序列化响应
std::string resp_str;
resp.serialize(&resp_str);
// 添加报头
std::string send_str = enLength(resp_str);
// 发送响应
send(sock, send_str.c_str(), send_str.size(), 0);
}
那么这里的第一步是怎么读取请求的呢?
这个请求必须是恰好一个完整的请求。
2.1.8 读取一个完整的报文recv
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
RETURN VALUE
These calls return the number of bytes received, or -1 if an error occurred. In the event of an error, errno is set to indicate the error. The
return value will be 0 when the peer has performed an orderly shutdown.
// "content_len\r\n_x _op _y\r\n"
// 读取一个完整报文
bool recvPackage(int sock, std::string& inbuf, std::string* out)
{
char buf[1024];
while(true)
{
ssize_t n = recv(sock, buf, sizeof buf - 1, 0);
if(n > 0)
{
buf[n] = '\0';
inbuf += buf;
auto pos = inbuf.find(SEP_LINE);
if(pos == std::string::npos) continue;// 还得继续读取
std::string text_len = inbuf.substr(0, pos);
int content_len = stoi(text_len);
int total_len = text_len.size() + 2 * SEP_LINE_LEN + content_len;// 一个完整报文长度
if(inbuf.size() < total_len) continue;// 还得继续读取
// 至少有一个完整报文
*out = inbuf.substr(0, total_len);
inbuf.erase(0, total_len);
return true;
}
else return false;
}
return true;
}
收到的请求还需要去掉报头
// 去掉有效载荷的报头信息
bool deLength(const std::string& pack, std::string *out)
{
auto pos = pack.find(SEP_LINE);
if(pos == std::string::npos) return false;
std::string text_len_string = pack.substr(0, pos);
int text_len = stoi(text_len_string);
*out = pack.substr(pos + SEP_LINE_LEN, text_len);
return true;
}
这样服务端的业务逻辑就完成了:
// 处理请求的入口
void handler(int sock, func_t func)
{
std::string inbuf;// 输入缓冲区
while(1)
{
// 得到序列化好的请求对象
std::string req_text;
if(!recvPackage(sock, inbuf, &req_text)) return;
std::string req_str;
if(!deLength(req_text, &req_str)) return;
// 得到结构化请求对象
Request req;
if(!req.deserialize(req_str)) return;
// 计算,得到响应
Response resp;
func(req, resp);
// 序列化响应
std::string resp_str;
resp.serialize(&resp_str);
// 添加报头
std::string send_str = enLength(resp_str);
// 发送响应
send(sock, send_str.c_str(), send_str.size(), 0);
}
}
2.2 客户端
大致流程跟服务端差不多:
void start()
{
struct sockaddr_in si;
bzero(&si, sizeof si);
si.sin_family = AF_INET;
si.sin_port = htons(_serverport);
si.sin_addr.s_addr = inet_addr(_serverip.c_str());
if(connect(_sock, (struct sockaddr*)&si, sizeof si) < 0)
{
std::cout << "connect socket error" << std::endl;
}
else
{
std::string msg;
std::string inbuf;// 输入缓冲区
while(1)
{
std::cout << "Please Enter#";
std::getline(std::cin, msg);// 1+2
// 解析字符串
Request req = PraseMsg(msg);
// 序列化
std::string content;
req.serialize(&content);
// 添加报头
std::string send_str = enLength(content);
// 发送
send(_sock, send_str.c_str(), send_str.size(), 0);
// 获取响应结果
std::string package;
// "content_len\r\n_x _op _y\r\n"
if(!recvPackage(_sock, inbuf, &package)) continue;// 还要继续读
// 去掉报头,提取正文
std::string text;
if(!deLength(package, &text)) continue;
// 反序列化获取退出码和结果
Response resp;
resp.deserialize(text);
std::cout << "exitcode: " << resp._exitcode << std::endl;
std::cout << "result: " << resp._result << std::endl;
}
}
}
// 解析字符串
Request PraseMsg(const std::string& msg)
{
// "123+456"
int idx_op = 0;
int idx = 0, n = msg.size();
// 找符号位置
while(idx < n)
{
if(hash.count(msg[idx]))
{
idx_op = idx;
break;
}
idx++;
}
Request req;
std::string sx = msg.substr(0, idx_op);
std::string sy = msg.substr(idx_op + 1);
req._x = stoi(sx);
req._y = stoi(sy);
req._op = msg[idx_op];
return req;
}
流程就是序列化请求,添加报头,发送,接收响应,去掉报头,反序列化,获取结果。
2.3 结果
客户端:
服务端:
三、使用Json进行序列化和反序列化
序列化与反序列化其实C++提供了Json的库。我们可以直接使用:
Json(JavaScript Object Notation)是一种轻量级的数据交换格式,常用于Web应用程序中的数据传输。它是一种基于文本的格式,易于读写和解析。Json格式的数据可以被多种编程语言支持,包括JavaScript、Python、Java、C#、C++等。Json数据由键值对组成,使用大括号表示对象,使用方括号表示数组。
首先先安装Json库。
sudo yum install -y jsoncpp-devel
头文件:#include <jsoncpp/json/json.h>
使用jsoncpp库记得在编译时加上-ljsoncpp
Makefile:
.PHONY:all
all:CalcServer CalcClient
CalcClient:CalcClient.cc
g++ -o $@ $^ -std=c++11 -ljsoncpp #-DMYSELF
CalcServer:CalcServer.cc
g++ -o $@ $^ -std=c++11 -ljsoncpp #-DMYSELF
.PHONY:clean
clean:
rm -f CalcClient CalcServer
// 请求
class Request
{
public:
Request(int x, int y, char op)
: _x(x)
, _y(y)
, _op(op)
{}
Request()
{}
// 序列化
bool serialize(std::string* out/*输出型参数*/)
{
#ifdef MYSELF
// "_x _op _y"
std::string sx = std::to_string(_x);
std::string sy = std::to_string(_y);
*out = sx + SEP + _op + SEP + sy;
#else
Json::Value root;// 万能对象,可接收任何对象
root["first"] = _x;// 自动将_x转换为字符串
root["second"] = _y;
root["oper"] = _op;
// 序列化
Json::FastWriter writer;
*out = writer.write(root);// 将root进行序列化
#endif
return true;
}
// 反序列化
bool deserialize(const std::string& in)
{
#ifdef MYSELF
// "_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];
#else
//Json反序列化
Json::Value root;// 万能对象,可接收任何对象
Json::Reader reader;
reader.parse(in,root);// 第一个参数:解析哪个流;第二个参数:将解析的数据存放到对象中
//反序列化
_x = root["first"].asInt();// 默认是字符串,转换为整型
_y = root["second"].asInt();
_op = root["oper"].asInt();// 转换为整型,整型可以给char类型
#endif
return true;
}
public:
int _x = 0;
int _y = 0;
char _op = 0;
};
// 响应
class Response
{
public:
Response(int exitcode, int result)
: _exitcode(exitcode)
, _result(result)
{}
Response()
{}
// 序列化
bool serialize(std::string* out/*输出型参数*/)
{
#ifdef MYSELF
std::string se = std::to_string(_exitcode);
std::string sr = std::to_string(_result);
*out = se + SEP + sr;
#else
Json::Value root;// 万能对象,可接收任何对象
root["exitcode"] = _exitcode;// 自动将_exit转换为字符串
root["result"] = _result;
// 序列化
Json::FastWriter writer;
*out = writer.write(root);// 将root进行序列化
#endif
return true;
}
// 反序列化
bool deserialize(const std::string& in)
{
// "_exitcode _result"
#ifdef MYSELF
auto pos = in.find(SEP);
if(pos == std::string::npos) return false;
std::string se = in.substr(0, pos);
std::string sr = in.substr(pos + SEP_LEN);
if(se.empty() || sr.empty()) return false;
_exitcode = stoi(se);
_result = stoi(sr);
#else
//Json反序列化
Json::Value root;// 万能对象,可接收任何对象
Json::Reader reader;
reader.parse(in,root);// 第一个参数:解析哪个流;第二个参数:将解析的数据存放到对象中
//反序列化
_exitcode = root["exitcode"].asInt();// 默认是字符串,转换为整型
_result = root["result"].asInt();
#endif
return true;
}
public:
int _exitcode = 0;// 退出码
int _result = 0;// 结果
};