【Linux网络】定制协议
这篇文章我们就来实现一个自定义协议,应用层我们来写一个计算器,将计算的操作数和操作符等结构化的数据使用我们自己定义的协议序列化为字符串流,发送给服务端,然后服务端将字符串流反序列化为原来的结构化数据,进行运算,接着再将结果序列化为字符串流发给对应的客户端,最后客户端再反序列化拿到计算结果。这其实就是一个网络版的计算器
文章目录
- 1. 定制协议
- 1.1 基本结构
- 1.2 序列化和反序列化
- 1.3 自定义协议
- 封装报文
- 解析报文
- 获取请求报文
- 获取应答报文
- 构建请求报文
- 2. 应用层封装计算器
- 3. 服务端主程序
- 4. 客户端主程序
1. 定制协议
1.1 基本结构
要实现一个网络版的计算器,客户端首先需要将两个操作数和一个操作符这种结构化的数据序列化为一条请求报文发送给服务端,然后服务端将请求报文反序列化为原来的结构化数据,根据两个操作数和操作符计算结果,服务端将运算结果序列化为一条应答报文发送给客户端,最后客户端将应答报文反序列化为结构化数据,从而拿到运算结果
框架如下:
#pragma once#include "Socket.hpp"
#include <jsoncpp/json/json.h>using namespace SocketModule;// client -> server
class Request
{
public:Request(){}Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper){}~Request(){}
private:int _x;int _y;char _oper; // + - * /
};// server -> client
class Response
{
public:Response(){}Response(int result, int code) :_result(result), _code(code){}~Response(){}
private:int _result; // 运算结果异常,无法区分_result是正常的结果,还是异常值int _code; // 0:sucess, 1,2,3,4->不同的运算异常的情况, 这就是一种约定!!!
};
我们运算结果可能是运算错误的结果,比如除0错误,那么该结果是错误的,所以一个_result不够,还需要再引入一个变量_code,表示运算结果是正常结果还是异常值
1.2 序列化和反序列化
对于序列化和反序列化,我们已经在上篇文章中介绍了JSONCPP的方案,这里就不多介绍了,不过需要提一点,就是我们网络通信肯定选择紧凑格式的JSON字符串
代码如下:
// client -> server
class Request
{
public:Request(){}Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper){}std::string Serialize(){Json::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper; Json::FastWriter writer;std::string s = writer.write(root);return s;}// {"x": 10, "y" : 20, "oper" : '+'}bool Deserialize(std::string &in){// "10" "20" '+' -> 以空格作为分隔符 -> 10 20 '+'Json::Value root;Json::Reader reader;bool ok = reader.parse(in, root);if (ok){_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();}return ok;}~Request(){}
private:int _x;int _y;char _oper; // + - * /
};// server -> client
class Response
{
public:Response(){}Response(int result, int code) :_result(result), _code(code){}std::string Serialize(){Json::Value root;root["result"] = _result;root["code"] = _code;Json::FastWriter writer;return writer.write(root);}bool Deserialize(std::string &in){Json::Value root;Json::Reader reader;bool ok = reader.parse(in, root);if (ok){_result = root["result"].asInt();_code = root["code"].asInt();}return ok;}~Response(){}
private:int _result; // 运算结果异常,无法区分_result是正常的结果,还是异常值int _code; // 0:sucess, 1,2,3,4->不同的运算异常的情况, 这就是一种约定!!!
};
注意 :我们将操作符反序列化时,使用了asInt方法,其实就是操作符的ASCII码值,所以根据ASCII码值也能拿到操作符
1.3 自定义协议
现在我们结构化的数据已经具备了序列化和反序列化的能力,但是对于自定义协议来说还是不够,为什么这么说呢?
在TCP通信中,由于TCP是流式协议,没有消息边界,因此当我们通过TCP发送多个JSON报文时,接收方可能一次读取到多个报文粘在一起,或者一个报文被拆分成多次接收。
也就是:
-
粘包:多个 JSON 报文被合并到一个接收缓冲区中
-
拆包:一个 JSON 报文被拆分成多次接收
那有什么解决办法码?
下面我们分别介绍两种方法在JSON传输中的应用:
方法一:使用长度前缀
发送方先计算JSON字符串的长度,然后将长度转换为固定格式(例如4字节的整数)放在JSON字符串的前面,接收方先读取4字节得到长度,再读取指定长度的JSON字符串。
方法二:使用分隔符
在每个JSON字符串的末尾加上一个特殊的分隔符,例如换行符’\n’。这样,接收方可以一直读取直到遇到换行符,然后解析一个JSON对象。
如果使用长度前缀的话,你读取报文都不能确定是否完整,那你怎么知道你读取的长度前缀就是完整的呢?所以我们可以结合长度前缀和分隔符一起使用,只要读到分隔符,我就能确定读到了一个完整的长度前缀,同时为了保证可读性,我们在报文结尾也加上一个分隔符
封装报文
那么序列化之后的JSON字符串就需要进行封装,报头为JSON字符串的长度加上分隔符,报尾则是分隔符,这样就能封装一个完整的报文
代码如下:
class Protocol
{
public:Protocol() {}~Protocol() {}// 封装报文std::string Encode(const std::string &jsonstr){// len\r\n{"x": 10, "y" : 20, "oper" : '+'}\r\nstd::string len = std::to_string(jsonstr.size());return len + sep + jsonstr + sep;}
private:
};
解析报文
服务端拿到一条完整报文之后,肯定需要对报文进行解析,因为我们需要得到JSON字符串,然后对JSON字符串进行反序列化。
代码如下:
const std::string sep = "\r\n";class Protocol
{
public:Protocol() {}~Protocol() {}// 封装报文std::string Encode(const std::string &jsonstr){// len\r\n{"x": 10, "y" : 20, "oper" : '+'}\r\nstd::string len = std::to_string(jsonstr.size());return len + sep + jsonstr + sep;}// 解析报文// 一条完整报文如: 50\r\n{"x": 10, "y" : 20, "oper" : '+'}\r\n// read读取到的内容可能如下:// 5// 50// 50\r// 50\r\n// 50\r\n{"x": 10, "// 50\r\n{"x": 10, "y" : 20, "oper" : '+'}\r\n// 50\r\n{"x": 10, "y" : 20, "oper" : '+'}\r\n50\r\n{"x": 10, "y" : 20, "ope//.....// // 1. 判断报文完整性// 2. 如果包含至少一个完整请求,提取他,并移除它,方便处理下一个bool Decode(std::string &buffer, std::string *package){size_t pos = buffer.find(sep);if(pos == std::string::npos)return false; // 长度不完整,让调用方继续从内核中读取数据// 此时得到json字符串的长度std::string package_len_str = buffer.substr(0, pos);int package_len = std::stoi(package_len_str);// 判断buffer是否有一条完整的报文int target_len = package_len_str.size() + package_len + sep.size() * 2;if(buffer.size() < target_len)return false; // buffer中没有一条完整的报文// 这时一定拿到了一条完整报文*package = buffer.substr(pos + sep.size(), package_len);buffer.erase(0, target_len);return true;}
private:
};
获取请求报文
我们知道服务端在接受连接之后,需要一边继续监听,一边执行任务(也就是把客户端发送的数据进行处理),这个时候服务端就需要回调出去处理。那在哪处理呢?
要知道我们服务端收到的数据是封装之后的,那我们就需要进行解析报文,然后反序列化,而这些就是我们自定义协议需要做的,所以我们可以在协议中进行处理。当我们处理完之后拿到结构化数据时,就需要交给应用层封装的计算器来执行,这个时候同样也需要回调去应用层处理,应用层处理完回调返回结果,然后将结果进行序列化,封装报文,再发送给客户端。
void GetRequest(std::shared_ptr<Socket>& socket, InetAddr& client){// 从接收缓冲区读取数据std::string buffer;while(true){int n = socket->Recv(&buffer);if(n > 0){// 解析报文, 提取完整的json字符串,如果不完整,就让服务器继续读取std::string json_package;while(Decode(buffer, &json_package)){LOG(LogLevel::DEBUG) << client.StringAddr() << " 请求: " << json_package;// 将json字符串反序列化Request req;bool ok = req.Deserialize(json_package);if(!ok)continue;// 回调到应用层进行处理Response res = _func(req);// 将结果序列化std::string jsonstr = res.Serialize();// 封装报文std::string message = Encode(jsonstr);socket->Send(message);}}else if (n == 0){LOG(LogLevel::INFO) << "client:" << client.StringAddr() << "Quit!";break;}else{LOG(LogLevel::WARNING) << "client:" << client.StringAddr() << ", recv error";break;}}}
获取应答报文
客户端同样需要对服务端发送的应答报文进行处理
bool GetResponse(std::shared_ptr<Socket> &client, std::string &resp_buff, Response *resp){while (true){int n = client->Recv(&resp_buff);if (n > 0){// 成功std::string json_package;// 1. 解析报文,提取完整的json请求,如果不完整,就让服务器继续读取while (Decode(resp_buff, &json_package)){// 2. 反序列化resp->Deserialize(json_package);}return true;}else if (n == 0){std::cout << "server quit " << std::endl;return false;}else{std::cout << "recv error" << std::endl;return false;}}}
构建请求报文
服务端要想获取请求报文,需要客户端先构建请求报文
std::string BuildRequestString(int x, int y, char oper){// 1. 构建一个完整的请求Request req(x, y, oper);// 2. 序列化std::string json_req = req.Serialize();// 3. 添加长度报头return Encode(json_req);}
2. 应用层封装计算器
因为实现的计算器比较简单,就不多说了
代码如下:
class Cal
{
public:Response Execute(Request &req){Response resp(0, 0); // code: 0表示成功switch (req.Oper()){case '+':resp.SetResult(req.X() + req.Y());break;case '-':resp.SetResult(req.X() - req.Y());break;case '*':resp.SetResult(req.X() * req.Y());break;case '/':{if (req.Y() == 0){resp.SetCode(1); // 1除零错误}else{resp.SetResult(req.X() / req.Y());}}break;case '%':{if (req.Y() == 0){resp.SetCode(2); // 2 mod 0 错误}else{resp.SetResult(req.X() % req.Y());}}break;default:resp.SetCode(3); // 非法操作break;}return resp;}
};
下面补充一下获取结构数据的方法
// client -> server
class Request
{
public:Request(){}Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper){}std::string Serialize(){Json::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper; Json::FastWriter writer;std::string s = writer.write(root);return s;}// {"x": 10, "y" : 20, "oper" : '+'}bool Deserialize(std::string &in){// "10" "20" '+' -> 以空格作为分隔符 -> 10 20 '+'Json::Value root;Json::Reader reader;bool ok = reader.parse(in, root);if (ok){_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();}return ok;}int X(){return _x;}int Y(){return _y;}char Oper(){return _oper;}~Request(){}
private:int _x;int _y;char _oper; // + - * /
};// server -> client
class Response
{
public:Response(){}Response(int result, int code) :_result(result), _code(code){}std::string Serialize(){Json::Value root;root["result"] = _result;root["code"] = _code;Json::FastWriter writer;return writer.write(root);}bool Deserialize(std::string &in){Json::Value root;Json::Reader reader;bool ok = reader.parse(in, root);if (ok){_result = root["result"].asInt();_code = root["code"].asInt();}return ok;}void SetResult(int res){_result = res;}void SetCode(int code){_code = code;}void ShowResult(){std::cout << "计算结果是: " << _result << "[" << _code << "]" << std::endl;}~Response(){}
private:int _result; // 运算结果异常,无法区分_result是正常的结果,还是异常值int _code; // 0:sucess, 1,2,3,4->不同的运算异常的情况, 这就是一种约定!!!
};
3. 服务端主程序
代码如下:
#include "NetCal.hpp"
#include "Protocol.hpp"
#include "TcpServer.hpp"void Usage(std::string proc)
{std::cerr << "Usage: " << proc << " port" << std::endl;
}// ./tcpserver port
int main(int argc, char* argv[])
{if (argc != 2){Usage(argv[0]);exit(USAGE_ERR);}// 应用层std::unique_ptr<Cal> cal = std::make_unique<Cal>();// 协议层std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>([&cal](Request &req)->Response{return cal->Execute(req);});// 服务器层std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::stoi(argv[1]),[&protocol](std::shared_ptr<Socket> &sock, InetAddr &client){protocol->GetRequest(sock, client);});tsvr->Start();return 0;
}

所以这三层没有内置在OS中,就是因为这三层由用户决定,用户需要实现什么样的功能——应用层,用户要选择哪种数据格式转换——表示层,用户需要建立通信连接等——会话层。这三层我们已经自己定义实现了。
4. 客户端主程序
代码如下:
#include "Socket.hpp"
#include "Common.hpp"
#include "Protocol.hpp"using namespace SocketModule;void Usage(std::string proc)
{std::cerr << "Usage: " << proc << " server_ip server_port" << std::endl;
}void GetDataFromStdin(int *x, int *y, char *oper)
{std::cout << "Please Enter x: ";std::cin >> *x;std::cout << "Please Enter y: ";std::cin >> *y;std::cout << "Please Enter oper: ";std::cin >> *oper;
}// ./tcpclient server_ip server_port
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(USAGE_ERR);}std::string server_ip = argv[1];uint16_t server_port = std::stoi(argv[2]);std::shared_ptr<Socket> client = std::make_shared<TcpSocket>();client->BuildTcpClientSocketMethod();if (client->Connect(server_ip, server_port) != 0){// 失败std::cerr << "connect error" << std::endl;exit(CONNECT_ERR);}std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>();std::string resp_buffer;// 连接服务器成功while (true){// 1. 从标准输入当中获取数据int x, y;char oper;GetDataFromStdin(&x, &y, &oper);// 2. 构建一个请求-> 可以直接发送的字符串std::string req_str = protocol->BuildRequestString(x, y, oper);// 3. 发送请求client->Send(req_str);// 4. 获取应答Response resp;bool res = protocol->GetResponse(client, resp_buffer, &resp);if(res == false)break;// 5. 显示结果resp.ShowResult();}client->Close();return 0;
}
运行结果:

可以看到code值为0时表示结果正确,非0表示结果异常。
至此,我们实现了一个简易的网络计算器
