文章目录
- 一、序列化与反序列化概念
- 二、自定义协议设计网络计算机
- 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 CalcClientCalcClient:CalcClient.ccg++ -o $@ $^ -std=c++11 -ljsoncpp #-DMYSELF
CalcServer:CalcServer.ccg++ -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;
#elseJson::Value root;// 万能对象,可接收任何对象root["first"] = _x;// 自动将_x转换为字符串root["second"] = _y;root["oper"] = _op;// 序列化Json::FastWriter writer;*out = writer.write(root);// 将root进行序列化
#endifreturn 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类型
#endifreturn 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 MYSELFstd::string se = std::to_string(_exitcode);std::string sr = std::to_string(_result);*out = se + SEP + sr;
#elseJson::Value root;// 万能对象,可接收任何对象root["exitcode"] = _exitcode;// 自动将_exit转换为字符串root["result"] = _result;// 序列化Json::FastWriter writer;*out = writer.write(root);// 将root进行序列化
#endifreturn true;}// 反序列化bool deserialize(const std::string& in){// "_exitcode _result"
#ifdef MYSELFauto 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();
#endifreturn true;}
public:int _exitcode = 0;// 退出码int _result = 0;// 结果
};