尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

【Linux网络】定制协议

【Linux网络】定制协议 1. 定制协议1.1 基本结构要实现一个网络版的计算器客户端首先需要将两个操作数和一个操作符这种结构化的数据序列化为一条请求报文发送给服务端然后服务端将请求报文反序列化为原来的结构化数据根据两个操作数和操作符计算结果服务端将运算结果序列化为一条应答报文发送给客户端最后客户端将应答报文反序列化为结构化数据从而拿到运算结果框架如下代码语言javascriptAI代码解释#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字符串代码如下代码语言javascriptAI代码解释// 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字符串的长度加上分隔符报尾则是分隔符这样就能封装一个完整的报文代码如下代码语言javascriptAI代码解释class Protocol { public: Protocol() {} ~Protocol() {} // 封装报文 std::string Encode(const std::string jsonstr) { // len\r\n{x: 10, y : 20, oper : }\r\n std::string len std::to_string(jsonstr.size()); return len sep jsonstr sep; } private: };解析报文服务端拿到一条完整报文之后肯定需要对报文进行解析因为我们需要得到JSON字符串然后对JSON字符串进行反序列化。代码如下代码语言javascriptAI代码解释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\n std::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: };获取请求报文我们知道服务端在接受连接之后需要一边继续监听一边执行任务也就是把客户端发送的数据进行处理这个时候服务端就需要回调出去处理。那在哪处理呢要知道我们服务端收到的数据是封装之后的那我们就需要进行解析报文然后反序列化而这些就是我们自定义协议需要做的所以我们可以在协议中进行处理。当我们处理完之后拿到结构化数据时就需要交给应用层封装的计算器来执行这个时候同样也需要回调去应用层处理应用层处理完回调返回结果然后将结果进行序列化封装报文再发送给客户端。代码语言javascriptAI代码解释void GetRequest(std::shared_ptrSocket 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; } } }获取应答报文客户端同样需要对服务端发送的应答报文进行处理代码语言javascriptAI代码解释bool GetResponse(std::shared_ptrSocket 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; } } }构建请求报文服务端要想获取请求报文需要客户端先构建请求报文代码语言javascriptAI代码解释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. 应用层封装计算器因为实现的计算器比较简单就不多说了代码如下代码语言javascriptAI代码解释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; } };下面补充一下获取结构数据的方法代码语言javascriptAI代码解释// 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. 服务端主程序代码如下代码语言javascriptAI代码解释#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_ptrCal cal std::make_uniqueCal(); // 协议层 std::unique_ptrProtocol protocol std::make_uniqueProtocol([cal](Request req)-Response{ return cal-Execute(req); }); // 服务器层 std::unique_ptrTcpServer tsvr std::make_uniqueTcpServer(std::stoi(argv[1]), [protocol](std::shared_ptrSocket sock, InetAddr client){ protocol-GetRequest(sock, client); }); tsvr-Start(); return 0; }在这里插入图片描述所以这三层没有内置在OS中就是因为这三层由用户决定用户需要实现什么样的功能——应用层用户要选择哪种数据格式转换——表示层用户需要建立通信连接等——会话层。这三层我们已经自己定义实现了。4. 客户端主程序代码如下代码语言javascriptAI代码解释#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_ptrSocket client std::make_sharedTcpSocket(); client-BuildTcpClientSocketMethod(); if (client-Connect(server_ip, server_port) ! 0) { // 失败 std::cerr connect error std::endl; exit(CONNECT_ERR); } std::unique_ptrProtocol protocol std::make_uniqueProtocol(); 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表示结果异常。至此我们实现了一个简易的网络计算器
返回列表