Linux网络编程:应用层自定义协议与序列化

📅 发布时间:2026/7/16 21:09:12 👁️ 浏览次数:
Linux网络编程:应用层自定义协议与序列化
我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端。这个时候我们就要进行约定方案了。因为你需要传递信息过去。约定方案一:客户端发送一个形如11的字符串;这个字符串中有两个操作数, 都是整形;两个数字之间会有一个字符是运算符, 运算符只能是 ;数字和运算符之间没有空格;…约定方案二:定义结构体来表示我们需要交互的信息;发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;第一种方案明显有很强的局限性适合短期、简单需求追求极简实现。在我们现代编程中很明显大多都是选择的第二种方案适合长期维护、需扩展的系统尤其是需要跨语言或高性能的场景。而方案二的第二步这个互相转化的步骤就叫做序列化与反序列化序列化与反序列化序列化Serialization和反序列化Deserialization是计算机中用于将数据结构或对象转换为可存储或传输的格式如字节流、字符串以及从该格式还原回原始数据的过程。我们上面说协议就是双方约定好的结构化的数据。是因为我们要传输的这个数据必须要用数据结构来管理起来这是因为我们的操作系统内核和网络协议栈的实现本质上就是通过数据结构 算法来管理数据所以你自己本身也要使用数据结构管理起来。就以我们这里的计算器为例我们想传递的信息为1 3那么我们就要把这个数据用一个类管理起来比如代码语言javascriptAI代码解释class Data { private: int x; int y; char ch; };我们就需要用这样一个类型来管理起来。再以这张图为例在这里插入图片描述我们所谓的序列化就是在传递信息之前将我们的所有信息按照一定规则排列为一行完整的字符串信息进行传输。所谓反序列化就是将这个字符串进行相同规则的拆分转化为原本的字符信息。但是无论我们采用方案一, 还是方案二, 还是其他的方案, 只要保证, 一端发送时构造的数据,在另一端能够正确的进行解析, 就是 ok 的. 这种约定, 就是 应用层协议。为了让我们深刻理解协议我们打算自定义实现一下协议的过程。这里我们需要采用方案2.序列化有两种方法第一种就是我们自己定义规则去序列化但是这个太麻烦我们这里选择第二个方法使用现成的一些方法比如xmljsonprotobuf。我们这里选择json。为什么我们需要序列化呢这就要涉及的到我们的read、write、recv、send 和 TCP 为什么支持全双工这个特性了。全双工的支持原理为什么我们的read、write、recv、send 和 TCP都支持全双工在这里插入图片描述大家请看上面这一张图。以TCP为例服务端与客户端建立后两端客户端和服务器各自维护独立的发送和接收缓冲区可以同时发送和接收数据。所以为什么TCP支持全双工因为他有两对缓冲区。所以我们的send这些调用本质上也还是拷贝。这些缓冲区的刷新原理是一样的什么时候刷新缓冲区由我们的操作系统进行决定。所以OS的内部是不是可能存在大量的报文那么这些报文是不是就需要被管理起来如何管理呢先描述在组织在 Linux 内核中网络报文Packet并不是以原始字节流直接传递而是通过一个高度优化的数据结构struct sk_buffSocket Buffer简称 SKB来管理。这个结构体是内核网络栈的核心负责存储、操作和传递网络数据。以下是 Linux 内核中sk_buff的部分定义不同内核版本可能有差异代码语言javascriptAI代码解释struct sk_buff { // 双向链表用于管理 SKB 队列 struct sk_buff *next; struct sk_buff *prev; // 网络设备网卡和协议栈状态 struct net_device *dev; // 发送/接收数据的网卡 int protocol; // 协议类型ETH_P_IP, ETH_P_ARP 等 // 数据缓冲区指针 unsigned char *head; // 缓冲区的起始地址 unsigned char *data; // 当前协议层数据的起始地址 unsigned char *tail; // 当前协议层数据的结束地址 unsigned char *end; // 缓冲区的结束地址 // 长度信息 unsigned int len; // 当前协议层的数据长度 unsigned int data_len; // 分片数据的长度 // 协议头指针支持快速访问 union { struct tcphdr *th; // TCP 头 struct udphdr *uh; // UDP 头 struct icmphdr *icmph; // ICMP 头 struct iphdr *iph; // IP 头 struct ethhdr *eth; // Ethernet 头 } h; // 控制信息如校验和、时间戳 __u32 csum; // 校验和 __u32 tstamp; // 时间戳 // 引用计数避免重复拷贝 atomic_t users; // 引用计数 };通过对prevnext结构体指针我们把对报文的增删查改管理转化成为了对链表的增删查改管理。至此操作系统就管理上了缓冲区而我们的TCP要进行流量控制如果对方接受缓冲区不足那么可能只发部分报文。所以read可能只读取原始报文的一部分剩下的部分无法发给对方的缓冲区这就可能造成数据的缺失等问题所以我们会对我们的信息串进行处理。后面会讲TCP 不保证应用层消息边界只保证字节的有序性和可靠性这个特性就是面向字节流。所以我们需要程序员写应用层代码来保证报文的完整性。实际数据什么时候发发多少出错了怎么办由 TCP 控制所以 TCP 叫做传输控制协议。一个网络计算器的实现Request与Response结构体我们接下来就以网络计算器为例实现数据的序列化与反序列化过程带领大家梳理一遍。依旧是以我们之前的TCP的远程命令执行器的代码为前提。我今天将其改造为我们的网络计算器。首先我们需要创建一个新的头文件Protocol.hpp这个头文件如同他的名字一样是用来实现协议的。我们最主要的就是先创建两个类Request 和 Response为什么要创建这个两个类呢大家之后就能理解这个行为主要体现了结构化网络通信协议的关键设计思想目的是在客户端和服务端之间标准化数据交换格式确保通信的可靠性、可扩展性和跨平台兼容性。Request结构体的作用主要是表示客户端发送给服务端的计算请求包含_x,_y两个整数操作数_oper运算符如,-,*,/实现与 JSON 字符串的互相转换而Response结构体主要为我们实现表示服务端返回给客户端的计算结果包含_result计算结果如3 5 8_code错误码如0表示成功1表示除零错误同样支持 JSON 序列化/反序列化代码语言javascriptAI代码解释#pragma once class Request//用来表示表示客户端发送给服务端的计算请求 { public: Request(int x,int y ,char oper): _x(x), _y(y), _oper(oper) {} private: int _x; int _y;//规定x与y都是int类型的整数 char _oper;//表示运算符号 }; class Response//表示服务端返回给客户端的计算结果 { public: Response(int result,int code): _result(result), _code(code) {} private: int _result;//计算的结果返回 int _code;//约定好的返回码0为计算成功1表示一些计算错误 };接下来我们要实现数据的序列化与反序列化功能注意我们这里使用的是json标准的字符风格所以就使用json提供的接口来完成序列化与反序列化。jsoncpp的使用要使用我们的json接口就需要使用到jsoncpp库。Jsoncpp 是一个用于处理 JSON 数据的 C 库。它提供了将 JSON 数据序列化为字符串以及从字符串反序列化为 C 数据结构的功能。Jsoncpp 是开源的广泛用于各种需要处理 JSON 数据的 C 项目中。特性简单易用Jsoncpp 提供了直观的 API使得处理 JSON 数据变得简单。高性能Jsoncpp 的性能经过优化能够高效地处理大量 JSON 数据。全面支持支持 JSON 标准中的所有数据类型包括对象、数组、字符串、数字、布尔值和 null。错误处理在解析 JSON 数据时Jsoncpp 提供了详细的错误信息和位置方便开发者调试。当使用 Jsoncpp 库进行 JSON 的序列化和反序列化时存在不同的做法和工具类可供选择。以我们的ubuntu系统为例我们需要先用指令安装此库sudo apt-get install libjsoncpp-dev之后就可以在代码中使用了由于不是系统的库所以我们在编译时需要带上库的名字。而使用json进行序列化的方式主要有以下三种使用 Json::Value 的 toStyledString 方法将 Json::Value 对象直接转换为格式化的 JSON 字符串。代码语言javascriptAI代码解释#include iostream #include string #include jsoncpp/json/json.h int main() { Json::Value root; root[name] joe; root[sex] 男; std::string s root.toStyledString(); std::cout s std::endl; return 0; } $ ./test.exe { name : joe, sex : 男 }注意这些使用方法是死板的所以大家只需要记忆就行了。使用 Json::StreamWriter这个提供了更多的定制选项如缩进、换行符等。代码语言javascriptAI代码解释#include iostream #include string #include sstream #include memory #include jsoncpp/json/json.h int main() { Json::Value root; root[name] joe; root[sex] 男; Json::StreamWriterBuilder wbuilder; // StreamWriter 的工厂 std::unique_ptrJson::StreamWriter writer(wbuilder.newStreamWriter()); std::stringstream ss; writer-write(root, ss); std::cout ss.str() std::endl; return 0; } $ ./test.exe { name : joe, sex : 男 }使用 Json::FastWriter比 StyledWriter 更快因为它不添加额外的空格和换行符:代码语言javascriptAI代码解释#include iostream #include string #include sstream #include memory #include jsoncpp/json/json.h int main() { Json::Value root; root[name] joe; root[sex] 男; Json::FastWriter writer; std::string s writer.write(root); std::cout s std::endl; return 0; } $ ./test.exe { name : joe, sex : 男 }而反序列化主要有两种实现方式使用 Json::Reader提供详细的错误信息和位置方便调试代码语言javascriptAI代码解释#include iostream #include string #include jsoncpp/json/json.h int main() { // JSON 字符串 std::string json_string {\name\:\张三\,\age\:30, \city\:\北京\}; // 解析 JSON 字符串 Json::Reader reader; Json::Value root; // 从字符串中读取 JSON 数据 bool parsingSuccessful reader.parse(json_string,root); if (!parsingSuccessful) { // 解析失败输出错误信息 std::cout Failed to parse JSON: reader.getFormattedErrorMessages() std::endl; return 1; } // 访问 JSON 数据 std::string name root[name].asString(); int age root[age].asInt(); std::string city root[city].asString(); // 输出结果 std::cout Name: name std::endl; std::cout Age: age std::endl; std::cout City: city std::endl; return 0; } $ ./test.exe Name: 张三 Age: 30 City: 北京使用 Json::CharReader 的派生类在某些情况下你可能需要更精细地控制解析过程可以直接使用Json::CharReader 的派生类。但是在绝大多数情况下使用 Json::parseFromStream 或 Json::Reader 的 parse方法就足够了。所以我们这里不再赘述这个方法。序列化与反序列化的实现知道了上面的json使用方法后我们这里就照着尝试写一下我们的类的序列化与反序列化函数。这里的序列化方式我们就选择最麻烦的第二种方式但一开始的步骤都是先创建一个Json::Value类。代码语言javascriptAI代码解释#pragma once #include jsoncpp/json/json.h #include memory #include sstream #include iostream class Request // 用来表示表示客户端发送给服务端的计算请求 { public: Request(int x, int y, char oper): _x(x), _y(y), _oper(oper) {} // 序列化函数将当前对象转换为 JSON 格式的字符串 // 参数 // - out_string: 输出参数用于存储生成的 JSON 字符串 // 返回值 // - bool: 理论上可以返回是否序列化成功但当前实现总是返回 true bool Serialize(std::string out_string) { // 1. 创建 JSON 根节点对象 Json::Value root; // 2. 将成员变量填充到 JSON 对象中 // 注意这里使用与成员变量相同的名字作为 JSON 字段名 root[_x] _x; // 序列化第一个操作数 root[_y] _y; // 序列化第二个操作数 root[_oper] _oper; // 序列化操作符 // 3. 创建 JSON 写入器 Json::StreamWriterBuilder wbuilder;// 写入器构建器 std::unique_ptrJson::StreamWriterwriter(wbuilder.newStreamWriter()); // 创建写入器 // 4. 将 JSON 数据写入字符串流 std::stringstream ss; // 创建字符串流 writer-write(root, ss); // 将 JSON 数据写入流 // 5. 获取最终的 JSON 字符串 out_string ss.str(); // 将字符串流内容赋给输出参数 // 6. 返回成功 return true; } // 反序列化函数将 JSON 格式的字符串解析并填充到当前对象 // 参数 // - in_string: 输入参数包含要解析的 JSON 字符串 // 返回值 // - bool: 返回反序列化是否成功 bool Deserialize(std::string in_string) { // 1. 创建 JSON 根节点对象和解析器 Json::Value root; // 用于存储解析后的 JSON 数据 Json::Reader reader; // JSON 解析器 // 2. 尝试解析输入的 JSON 字符串 bool parsingSuccessful reader.parse(in_string, root); // 3. 检查解析是否成功 if (!parsingSuccessful) { // 输出解析错误信息用于调试 std::cout Failed to parse JSON: reader.getFormattedErrorMessages() std::endl; return false; // 返回失败 } // 4. 从 JSON 对象中提取数据并赋值给成员变量 // 注意这里假设 JSON 字段名与代码中的不同没有下划线前缀 _x root[x].asInt(); // 提取第一个操作数并转为 int _y root[y].asInt(); // 提取第二个操作数并转为 int _oper root[oper].asInt(); // 提取操作符并转为 int实际应为 char,但char可以当做ASCII码存储 // 5. 返回成功 return true; } private: int _x; int _y; // 规定x与y都是int类型的整数 char _oper; // 表示运算符号 }; class Response // 表示服务端返回给客户端的计算结果 { public: Response(int result, int code) : _result(result), _code(code) { } // 序列化函数将当前对象转换为 JSON 格式的字符串 // 参数 // - out_string: 输出参数用于存储生成的 JSON 字符串 // 返回值 // - bool: 理论上可以返回是否序列化成功但当前实现总是返回 true bool Serialize(std::string out_string) { // 1. 创建 JSON 根节点对象 Json::Value root; // 2. 将成员变量填充到 JSON 对象中 // 注意这里使用与成员变量相同的名字作为 JSON 字段名 root[_result] _result; // 序列化我们的返回值 root[_code] _code; // 序列化错误码 // 3. 创建 JSON 写入器 Json::StreamWriterBuilder wbuilder; // 写入器构建器 std::unique_ptrJson::StreamWriter writer(wbuilder.newStreamWriter()); // 创建写入器 // 4. 将 JSON 数据写入字符串流 std::stringstream ss; // 创建字符串流 writer-write(root, ss); // 将 JSON 数据写入流 // 5. 获取最终的 JSON 字符串 out_string ss.str(); // 将字符串流内容赋给输出参数 // 6. 返回成功 return true; } // 反序列化函数将 JSON 格式的字符串解析并填充到当前对象 // 参数 // - in_string: 输入参数包含要解析的 JSON 字符串 // 返回值 // - bool: 返回反序列化是否成功 bool Deserialize(std::string in_string) { // 1. 创建 JSON 根节点对象和解析器 Json::Value root; // 用于存储解析后的 JSON 数据 Json::Reader reader; // JSON 解析器 // 2. 尝试解析输入的 JSON 字符串 bool parsingSuccessful reader.parse(in_string, root); // 3. 检查解析是否成功 if (!parsingSuccessful) { // 输出解析错误信息用于调试 std::cout Failed to parse JSON: reader.getFormattedErrorMessages() std::endl; return false; // 返回失败 } // 4. 从 JSON 对象中提取数据并赋值给成员变量 // 注意这里假设 JSON 字段名与代码中的不同没有下划线前缀 _code root[_code].asInt(); _result root[_result].asInt(); // 5. 返回成功 return true; } private: int _result; // 计算的结果返回 int _code; // 约定好的返回码0为计算成功1表示一些计算错误 };我们这里使用的是之前介绍的序列化的第二种方法通过一个stringstream来进行序列化。这些都是固定的写法对底层感兴趣的同学可以去了解一下Value类这些的实现。添加报头与解析报头光有序列化与反序列化可不够这个序列化的操作只是统一了我们传输的形式。但是并没有保障我们传输信息时的完整与安全性。在对方接收缓冲区不足时仍然可能接收部分内容所以我们需要程序员自己手动在应用层实现这个保障。那么我们怎么保障传输信息的完整性与安全性呢添加报头我们用一个报头来表示后面跟着信息的长度严格要求接收信息你空间判断后不够你就不要接收你空间够才能来接收消息。那么我们就需要保证我们的代码能够分辨清楚报头与报文所以我们需要用一个分割符号隔开我们这里就设定我们的分割符为\r\n。这样我们传进来的信息的完整格式就应该严格遵守len\r\n{json}\r\n这个形式其中len是后面json串的长度。比如123\r\n{json}\r\n我们需要严格以这个为传输信息的单位其余格式一律都是错误的我们不予处理。所以我们就需要两个接口一个负责添加报头一个负责解析报头。具体实现如下大家可以看我注释代码语言javascriptAI代码解释const std::string sep \r\n; // 123\r\n{json}\r\n bool Encode(std::string message) { if (message.size() 0) { return false; // 字符串长度为0就不予处理 } std::string tmp std::to_string(message.size()) sep message sep; message tmp; return true; } //进行解析报头 //我们的解析报头的工作要麻烦一点 bool Decode(std::string pakage,std::string *cotent) { //开局我们先找我们规定好的分割符,分隔符存在那么报头就一定存在。 auto pos pakage.find(sep); if(posstd::string::npos) { //说明没找到 return false;//不用继续处理 } //提取报头知晓内容长度 std::string code_len_strpakage.substr(0,pos); int code_lenstd::stoi(code_len_str);//转化为整形 //计算完整消息长度 int code_len_totalcode_len_str.size()code_len2*sep.size();//这里要加上两个分隔符的长度 if(code_len_totalpakage.size()) { //说明传过来的数据不完整长度不符合预期不予处理 return false; } //处理提取消息字符串 *cotentpakage.substr(possep.size(),code_len); //从消息字符串中处理掉已经接收的部分把传输失败的数据不予处理 pakage.erase(0,code_len_total);//其实这里还有处理但是等我们后续网络代码时再来研究 return true; }这样一来我们的准备工作就完成了后面就是我们计算部分与服务端和客户端的代码的修改。客户端代码修改HandlerRequest修改我们需要对客户端的代码进行一定程度的修改我这里拿的是之前TCP远程执行命令的服务端代码。在这个代码中我们通过main函数传递了一个回调函数给我们的服务器类通过多线程来执行这些命令请求。这里我们同样使用这个思维只不过要修改一下服务端的HandlerRequest调用接口。因为我们的回调函数类型依旧是using handler_t std::functionstd::string(std::string);所以这些类型都不用变化我们现在HandlerRequest中新增一个string类型的packge变量这个变量主要是用来形成我们的传输信息的而之前的buffer是用来接受客户端发来的信息。同时我们在传递回调函数的参数时也应该传递packge而不再是buffer在这个新的回调函数中我们约定好会先进行我们的反序列化解析报头的操作随后再进行计算最后再把计算结果序列化添加报头返回我们的客户端。代码语言javascriptAI代码解释void HandlerRequest(int sockfd) { LOG(LogLevel::INFO) HandlerRequest, sockfd is : sockfd; char buffer[4096]; std::string packge; while (true) { // int n ::read(sockfd, buffer, sizeof(buffer) - 1); int n ::recv(sockfd, buffer, sizeof(buffer) - 1, 0); if (n 0) { buffer[n] 0; // 手动置入一个结束标记 LOG(LogLevel::INFO) \n buffer; packge buffer; std::string cmd_result _handler(buffer); ::send(sockfd, cmd_result.c_str(), cmd_result.size(), 0); } else if (n 0) { // n0的情况我们说过就是指的是服务端那里的write挂掉了也就是说服务端退出了那么我们此时约定服务端也不再继续循环去读客户端的消息 LOG(LogLevel::INFO) client quit: sockfd; break; } else { break; // 表示读取失败 } } // 循环结束不再使用fd ::close(sockfd); }所以我们接下来最主要任务就是对我们的服务端的main进行修改改变他原本传进来的之前的执行命令的函数。我们之前是定义了一个命名执行的类利用lambda传递去接口函数。这里我们也是通过同样的思想但是之前写的调用与参数类都不太合适因为网络传输是流式的数据可能被拆分成多个包到达单个TCP包可能包含不完整的消息或多个消息的片段。所以我们还需要新建一个Parse类跟踪不完整的数据负责进行维护了package缓冲区可以累积数据直到形成完整消息。Parse类的实现所以我们需要一个Parse类这个类不需要什么其他参数他唯一的参数只会是我们之后实现的计算方法的调用传参。他的成员方法也只会有一个在这个方法里我们要实现判断报文的完整性反序列化计算序列化添加长度报头字段拼接应答并返回给客户端把这个类定义好后我们就只需要进lambda回调传参让服务端的代码获取消息后进行以上六个逻辑。在计算时进入我们的计算逻辑我们还没实现从而实现一个分层的效果。我们现在就来实现一下具体代码如下可以看我注释代码语言javascriptAI代码解释class Parse { public: std::string Entry(std::string packge) { // 1判断报文的完整性 std::string message; std::string respstr; while (Decode(packge, message)) // 由于我们传过来的报文可能会有多个len\r\n{json}\r\n所以需要一直用while循环来判断 { LOG(LogLevel::DEBUG) Content: \n message; // 此时我们的message一定是成功的因为如果解析报头出错会直接退出while循环 if (message.empty()) // 但是我们要判断获取的message信息是否为空为空就没必要做以下运算了 break; // 2进行反序列化 Request ret; if (!ret.Deserialize(message)) { // 反序列化失败 LOG(LogLevel::INFO) Deserialize failed \n; break; } // 3计算此时的我们的ret在反序列化的时候已经初始化完成了可以传递这个ret用来计算 // 虽然不知道我们的计算具体实现但是可以肯定的是我们要用response类获取计算结果信息 Response res cal(ret);//cal就是我们的回调函数的名字 // 4序列化此时我们的返回信息都在res中我们需要对其进行序列化 std::string res_str; res.Serialize(res_str); // res_str是一个输出型参数此时我们就拿到了序列化后的字符串 // 5添加报头 Encode(res_str); // 6 拼接应答 // 还记得我们在外部定义的respstr吗这是是负责返回给服务端的字符串信息所以我们把需要返回的信息加给他 respstr res_str; } LOG(LogLevel::DEBUG) respstr: \n respstr; return respstr; } private: //这里需要一个计算的方法回调函数但是我们还没实现 };计算类的实现接下来我们就来实现我们的计算器的执行方法。首先我们要明确的是我们的计算过程中传递进来的是一个Request对象所以我们可以通过这个对象来进行计算。代码语言javascriptAI代码解释#pragma once #include Protocol.hpp class Calculator // 创建这个类的目的只是为了构造对象方便回调函数 { public: Calculator() { } Response Execute(const Request rq) { Response resp; switch (rq.Get_oper()) // 这里为了拿到类的成员变量的值我们还需要增加接口返回值 { case : // 这里为了给resp的成员变量赋值我们同样需要补充几个接口 resp.SetResult(rq.Get_x() rq.Get_y()); break; case -: resp.SetResult(rq.Get_x() - rq.Get_y()); break; case *: resp.SetResult(rq.Get_x() * rq.Get_y()); break; case /: { if (rq.Get_y() 0) { resp.SetCode(1); // 表示除0错误 } else { resp.SetResult(rq.Get_x() rq.Get_y()); } } break; case %: { if (rq.Get_y() 0) { resp.SetCode(2); // 2 就是mod 0 } else { resp.SetResult(rq.Get_x() % rq.Get_y()); } } break; default: resp.SetCode(3); // 3 用户发来的计算类型无法识别 break; } return resp; } ~Calculator() { } };值得注意的是我们这里为了获取Response与Request的内部成员变量顺手完善了几个接口来获取代码语言javascriptAI代码解释#pragma once #include jsoncpp/json/json.h #include memory #include sstream #include string #include iostream const std::string sep \r\n; // 123\r\n{json}\r\n bool Encode(std::string message) { if (message.size() 0) { return false; // 字符串长度为0就不予处理 } std::string tmp std::to_string(message.size()) sep message sep; message tmp; return true; } // 进行解析报头 // 我们的解析报头的工作要麻烦一点 bool Decode(std::string pakage, std::string *cotent) { // 开局我们先找我们规定好的分割符,分隔符存在那么报头就一定存在。 auto pos pakage.find(sep); if (pos std::string::npos) { // 说明没找到 return false; // 不用继续处理 } // 提取报头知晓内容长度 std::string code_len_str pakage.substr(0, pos); int code_len std::stoi(code_len_str); // 转化为整形 // 计算完整消息长度 int code_len_total code_len_str.size() code_len 2 * sep.size(); // 这里要加上两个分隔符的长度 if (code_len_total pakage.size()) { // 说明传过来的数据不完整长度不符合预期不予处理 return false; } // 处理提取消息字符串 *cotent pakage.substr(pos sep.size(), code_len); // 从消息字符串中处理掉已经接收的部分把传输失败的数据不予处理 pakage.erase(0, code_len_total); // 其实这里还有处理但是等我们后续网络代码时再来研究 return true; } class Request // 用来表示表示客户端发送给服务端的计算请求 { public: Request() { } Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper) { } // 序列化函数将当前对象转换为 JSON 格式的字符串 // 参数 // - out_string: 输出参数用于存储生成的 JSON 字符串 // 返回值 // - bool: 理论上可以返回是否序列化成功但当前实现总是返回 true bool Serialize(std::string out_string) { // 1. 创建 JSON 根节点对象 Json::Value root; // 2. 将成员变量填充到 JSON 对象中 // 注意这里使用与成员变量相同的名字作为 JSON 字段名 root[_x] _x; // 序列化第一个操作数 root[_y] _y; // 序列化第二个操作数 root[_oper] _oper; // 序列化操作符 // 3. 创建 JSON 写入器 Json::StreamWriterBuilder wbuilder; // 写入器构建器 std::unique_ptrJson::StreamWriter writer(wbuilder.newStreamWriter()); // 创建写入器 // 4. 将 JSON 数据写入字符串流 std::stringstream ss; // 创建字符串流 writer-write(root, ss); // 将 JSON 数据写入流 // 5. 获取最终的 JSON 字符串 out_string ss.str(); // 将字符串流内容赋给输出参数 // 6. 返回成功 return true; } int Get_x() const { return _x; } int Get_y() const { return _y; } char Get_oper() const { return _oper; } // 反序列化函数将 JSON 格式的字符串解析并填充到当前对象 // 参数 // - in_string: 输入参数包含要解析的 JSON 字符串 // 返回值 // - bool: 返回反序列化是否成功 bool Deserialize(std::string in_string) { // 1. 创建 JSON 根节点对象和解析器 Json::Value root; // 用于存储解析后的 JSON 数据 Json::Reader reader; // JSON 解析器 // 2. 尝试解析输入的 JSON 字符串 bool parsingSuccessful reader.parse(in_string, root); // 3. 检查解析是否成功 if (!parsingSuccessful) { // 输出解析错误信息用于调试 std::cout Failed to parse JSON: reader.getFormattedErrorMessages() std::endl; return false; // 返回失败 } // 4. 从 JSON 对象中提取数据并赋值给成员变量 // 注意这里假设 JSON 字段名与代码中的不同没有下划线前缀 _x root[x].asInt(); // 提取第一个操作数并转为 int _y root[y].asInt(); // 提取第二个操作数并转为 int _oper root[oper].asInt(); // 提取操作符并转为 int实际应为 char,但char可以当做ASCII码存储 // 5. 返回成功 return true; } private: int _x; int _y; // 规定x与y都是int类型的整数 char _oper; // 表示运算符号 }; class Response // 表示服务端返回给客户端的计算结果 { public: Response() { } Response(int result, int code) : _result(result), _code(code) { } // 序列化函数将当前对象转换为 JSON 格式的字符串 // 参数 // - out_string: 输出参数用于存储生成的 JSON 字符串 // 返回值 // - bool: 理论上可以返回是否序列化成功但当前实现总是返回 true bool Serialize(std::string out_string) { // 1. 创建 JSON 根节点对象 Json::Value root; // 2. 将成员变量填充到 JSON 对象中 // 注意这里使用与成员变量相同的名字作为 JSON 字段名 root[_result] _result; // 序列化我们的返回值 root[_code] _code; // 序列化错误码 // 3. 创建 JSON 写入器 Json::StreamWriterBuilder wbuilder; // 写入器构建器 std::unique_ptrJson::StreamWriter writer(wbuilder.newStreamWriter()); // 创建写入器 // 4. 将 JSON 数据写入字符串流 std::stringstream ss; // 创建字符串流 writer-write(root, ss); // 将 JSON 数据写入流 // 5. 获取最终的 JSON 字符串 out_string ss.str(); // 将字符串流内容赋给输出参数 // 6. 返回成功 return true; } // 反序列化函数将 JSON 格式的字符串解析并填充到当前对象 // 参数 // - in_string: 输入参数包含要解析的 JSON 字符串 // 返回值 // - bool: 返回反序列化是否成功 bool Deserialize(std::string in_string) { // 1. 创建 JSON 根节点对象和解析器 Json::Value root; // 用于存储解析后的 JSON 数据 Json::Reader reader; // JSON 解析器 // 2. 尝试解析输入的 JSON 字符串 bool parsingSuccessful reader.parse(in_string, root); // 3. 检查解析是否成功 if (!parsingSuccessful) { // 输出解析错误信息用于调试 std::cout Failed to parse JSON: reader.getFormattedErrorMessages() std::endl; return false; // 返回失败 } // 4. 从 JSON 对象中提取数据并赋值给成员变量 // 注意这里假设 JSON 字段名与代码中的不同没有下划线前缀 _code root[_code].asInt(); _result root[_result].asInt(); // 5. 返回成功 return true; } int Result() const { return _result; } int Code() const { return _code; } void SetResult(int res) { _result res; } void SetCode(int c) { _code c; } private: int _result; // 计算的结果返回 int _code; // 约定好的返回码0为计算成功1表示一些计算错误 };服务端最后完善当我们把计算的功能写好后就需要完善我们的Parse类的Entry方法以及增加他的回调函数成员。我们的回调函数的类型不是之前的stringstring所以我们要重新声明一下using cal_fun std::functionResponse(const Request req);并按照老方法,进行Parse的初始化功能。并且我们需要在main函数中创建一个Calculator类对象以及Parse对象并使用lambda传递回调函数总体代码如下代码语言javascriptAI代码解释#include TcpServer.hpp #include Protocol.hpp #include memory #include Calculator.hpp using cal_fun std::functionResponse(const Request req);//定义类型 class Parse { public: Parse(cal_fun gcal) : cal(gcal) { } std::string Entry(std::string packge) { // 1判断报文的完整性 std::string message; std::string respstr; while (Decode(packge, message)) // 由于我们传过来的报文可能会有多个len\r\n{json}\r\n所以需要一直用while循环来判断 { LOG(LogLevel::DEBUG) Content: \n message; // 此时我们的message一定是成功的因为如果解析报头出错会直接退出while循环 if (message.empty()) // 但是我们要判断获取的message信息是否为空为空就没必要做以下运算了 break; // 2进行反序列化 Request ret; if (!ret.Deserialize(message)) { // 反序列化失败 LOG(LogLevel::INFO) Deserialize failed \n; break; } // 3计算此时的我们的ret在反序列化的时候已经初始化完成了可以传递这个ret用来计算 // 虽然不知道我们的计算具体实现但是可以肯定的是我们要用response类获取计算结果信息 Response res cal(ret); // cal就是我们的回调函数的名字 // 4序列化此时我们的返回信息都在res中我们需要对其进行序列化 std::string res_str; res.Serialize(res_str); // res_str是一个输出型参数此时我们就拿到了序列化后的字符串 // 5添加报头 Encode(res_str); // 6 拼接应答 // 还记得我们在外部定义的respstr吗这是是负责返回给服务端的字符串信息所以我们把需要返回的信息加给他 respstr res_str; } LOG(LogLevel::DEBUG) respstr: \n respstr; return respstr; } private: // 这里需要一个计算的方法回调函数但是我们还没实现 cal_fun cal; }; int main() { // 1. 计算模块 Calculator mycal; // 2. 解析对象 Parse myparse([mycal](const Request req) { return mycal.Execute(req); }); // 3. 通信模块 // 只负责进行IO std::unique_ptrTcpServer tcp_ptr std::make_uniqueTcpServer([myparse](std::string package) { return myparse.Entry(package); }); tcp_ptr-InitServer(); tcp_ptr-Start(); return 0; }至此我们服务端的实现就告一段落。为了运行我们的代码我们还要对我们的客户端进行更改主要是完成客户端发送信息的序列化与添加报头的操作。客户端的修改首先就是我们的读取方式变化了不像以前直接通过getline获取整行数据而是一个一个的输入x,y,oper并借此初始化一个Request对象。所以我们的Start函数应该修改为代码语言javascriptAI代码解释void Start() { is_running true; std::string message; while (is_running) { // 设置一个缓冲区方便我们写入读取数据 char buffer[1024]; int x, y; char oper; std::cout input x: ; std::cin x; std::cout input y: ; std::cin y; std::cout input oper: ; std::cin oper; Request req(x, y, oper); // 进行序列化与添加报头 // 1. 序列化 req.Serialize(message); // 2. Encode Encode(message); // 3. 之后我们就可以进行写入数据给对方了 // int n::write(_sockfd,message.c_str(),message.size()); int n ::send(_sockfd, message.c_str(), message.size(), 0); if (n 0) { // 写入成功就要开始读了 int m ::read(_sockfd, buffer, sizeof(buffer)); // 如果读取返回的消息也成功了 if (m 0) { buffer[m] 0; std::string package buffer; std::string content; // 4. 读到应答完整 Decode(package, content); // 5. 反序列化 Response resp; resp.Deserialize(content); // 6. 得到结构化数据 std::cout resp.Result() [ resp.Code() ] std::endl; } else { break; } } else { break; } } }这样我们的代码就初步改造完成了运行代码在这里插入图片描述可以看见已经能够初步运行了。但是我们这里还是有一点小bug因为我们的result是int类型而除法很可能结果是浮点数所以我们这里遇见除法可能会出现截断的错误。重看OSI 七层模型我们现在已经写完了一个简单的网络版计算器了大家是不是又有了更深层次的体会了呢现在请大家重新来看一下我们的OSI七层模型在这里插入图片描述首先下面的四层我们都不用考虑因为这是被硬件决定好的。我想请大家着重关注上面三层并结合这个我们写好的代码结构你会发现这个计算模块的所作所为不就对应着我们的应用层吗解析对象不就是我们表示层的效果吗通信模块不就是我们的会话层负责管理连接的建立与中断吗