目录
网络计算器代码
思路什么的之前我们就已经大概介绍过了 -- 网络通信中字节流存在的问题,tcp协议特点,自定义协议(引入+介绍,序列化反序列化介绍,实现思路)-CSDN博客
下面是更详细的在代码方面的介绍
代码的难度其实就在于[计算逻辑代码]和[反序列化+解包报头]
自定义协议
介绍
根据计算器的功能,可以把它拆分成三个部分 -- 用户输入,计算,返回结果
- 那么,我们就需要定义两个结构体,来帮助输入/输出数据的结构化,以便我们提取数据
请求
我们这里选择让用户传入字符串("1+1="的形式),所以不需要为请求定义结构体
- 因为我是直接拿了直接写过的本地计算器的计算代码,当时是老师要求将中序表达式转换为后序表达式,然后计算
响应
而响应则是按照上图里的形式,结构体里有两个成员变量 -- 结果,错误码
之前我们已经探讨过,结构体/字符串单一形式不足以满足我们的需求,所以将他们二者结合起来:
- 所以,在协议里要定义出序列化和反序列化的方法,以便我们拿到数据后方便处理
序列化
思路
我们需要将[结构体化的数据]转换成[某种特定格式]的形式
- 具体什么格式由场景决定 -- 它可以是字符串格式,json格式protocol格式等等
- 比如:如果我们定义一个简单的请求结构体,里面有2个操作数和操作符,如果是字符串形式,就可以是"x op b"
这里我选择在网络中传递的是字符串,并且用户输入也是字符串
- 所以用户输入不需要经历序列化
但计算结果返回时,需要序列化,也就是 -- result=x,err_code=y -> "result err_code"
- 直接字符串拼接即可
代码
void serialize(std::string &content) { //-> "result_ err_code_" content = std::to_string(result_); content += " "; content += std::to_string(err_code_); }
反序列化
思路
和序列化同理,输入也不需要反序列化,因为本身就已经是我们规定的字符串格式了
而响应的反序列化是需要的,也就是 -- "result err_code" -> result=x,err_code=y
- 可以考虑在传入的数据里寻找空格,这样空格左侧就是result,右侧就是err_code
注意点
当然,要注意"字符串里会出现的一些问题"--不一定拥有一份完整的数据
- 所以需要判断
- 如果不完整,直接返回即可,也许下次就会将剩余的数据传进来了
因为我们的客户端和服务端都是自己编写的,所以肯定满足我们的协议
- 所以不需要判断result,err_code里是否存的是数字,其他也是同理
- 当然,以防万一,如果担心考虑不周而导致出错,加上判断也很好
代码
#define space_sep ' ' bool deserialize(const std::string &data) { //"result_ err_code_" -> result_,err_code_ size_t pos = data.find(space_sep); if (pos == std::string::npos) { return false; } result_ = std::stoi(data.substr(0, pos)); err_code_ = std::stoi(data.substr(pos + 1)); return true; }
报头封装和解包
思路
除此之外,我们之前就已经介绍过了,报文里其实不止有有效数据,还有一堆的报头
- 它可以在报文中添加很多信息 -- 比如:报文大小,源地址,目标地址,数据类型,编码方式等等
- 接收方可以根据不同的数据类型,选择使用不同的协议来处理,这样就实现了动态更换协议的效果
- 详细说明一下就是 -- 可以先定义好协议的基类,然后通过继承,根据要操作变量的类型,定义不同的方法 ; 在报文里添加协议序列号,可以帮助我们动态使用对应的协议
这里我们将报文大小编入报文里
- 以便在提取时,可以检测该报文里是否包含一条完整的有效数据
我们还可以在报头和有效载荷之间添加分隔符,方便我们在提取时区分开两者
- 这个分隔符一定是有效载荷里不会出现的字符 -- 比如这里的计算器里就不会出现\n,可以让它当分隔符,并且它在打印上更加清晰
- 并且为了更好的打印效果,可以在有效载荷后也添加该分隔符
报头封装很简单,就是拼接字符串,注意要把分隔符封进去
- "result err_code" -> "size"\n"result err_code"\n
报头解包的话,就是在报文里寻找两个分隔符,分隔符中间是有效载荷,第一个分隔符之前是数据大小
- 但是有很多注意点
注意点
还是要注意我们可能收到的是不完整的报文
- 如果没有成功找到两个分隔符,则说明该报文不完整
- 如果报文不完整,不需要处理,保留并返回即可(因为不完整可能是因为发送的原因,当后面的数据发送过来后,就可以拼成完整的报文)
- (注意:不能直接从报文尾部找第二个分隔符,因为可能该报文里包含多份数据)
还可能在找到后,实际size和理论size不匹配 / 本应该存的是size,但不是数字
- 虽然想想应该不可能,但还是要保证一下,防止我们读取错误
- 并且要把这样的错误报文给删除掉,留着毫无意义,因为它不是不完整,而是错误
总之,经过一系列的排查后,就可以成功读取出正确数据了
- 如果成功解包出一条完整数据,就将该条报文从源数据中删除
代码
#define protocol_sep '\n' bool encode(std::string &content) { // 封装报文大小 int size = content.size(); std::string tmp; tmp = std::to_string(size); tmp += protocol_sep; tmp += content; tmp += protocol_sep; content = tmp; return true; } bool decode(std::string &content, std::string &data) // 把非法的/处理完成的报文删除 { size_t left = content.find(protocol_sep); if (left == std::string::npos) // 不完整的报文 { return false; } size_t right = content.find(protocol_sep, left + 1); if (right == std::string::npos) // 不完整的报文 { return false; } // 拆出size std::string size_arr = content.substr(0, left); if (size_arr[0] < '0' || size_arr[0] > '9') // 注意size_arr里存放的不一定是数字 { content.erase(0, size_arr.size() + 1); // 包括分隔符 return false; } int size = std::stoi(size_arr); if (right - left != size + 1) // 错误的报文 -- right-left-1是实际有效长度,而size是理论有效长度,如果二者不匹配,说明封装上就有问题/传数据有问题 { content.erase(0, size_arr.size() + 1 + right - left); // 两个分隔符+数字长度+实际数据长度 return false; } data = content.substr(left + 1, size); // 截断size长度的数据(这个就是完整的有效数据) content.erase(0, size + size_arr.size() + 2); // 两个分隔符+数字的长度 return true; }
网络通信接口
用的是我们自己封装出来的接口 -- 网络通信接口封装+日志打印对象-CSDN博客
服务端
将序列化和网络通信关联在一起
我的想法是:
- 服务端只管读写
- 单独会有一个函数,来处理收到的报文 -- 去掉报头,计算,将计算结果序列化,封装报头
- 这样就实现了网络通信与处理报文这两个功能的解耦
而要将这个方法放入网络通信的服务端里,有两种方法:
- 静态类 -- 直接在服务端实例化这个计算类,然后调用函数
- 回调函数 -- 使用bind(返回值是function对象),将计算类的函数作为参数传给服务端类 (因为需要this指针,所以直接传函数指针是不可以的)
using cal_t = std::function<std::string(std::string &arr)>; calculate Cal; std::bind(&calculate::cal, &Cal, std::placeholders::_1)
处理收到的数据
每次从客户端读取时,将传过来的数据看作是连续的
- 所以用+=
并且每次在decode中,会自动将已经处理完成的报文从数据中删除,所以不会出现重复的问题
多进程
它将处理数据的任务分配给子进程,并且让它自生自灭(忽略SIGCHLD信号)
自己则会一直处于等待接受连接的状态
- 如果客户端关闭,子进程退出
处理多份数据的能力
当服务端同时收到多份数据时,可能in_buffer里会存放多份报文
- 所以我们可以设置一个while循环来处理,直到无法处理为止
代码
#include <signal.h> #include <cstring> #include <functional> #include "cal.hpp" #include "socket.hpp" static MY_SOCKET my_socket; // static calculate Cal; bool exit_ = false; using cal_t = std::function<std::string(std::string &arr)>; // 网络服务端 class server { public: server(const uint16_t port, const std::string &ip, cal_t callback) : port_(port), ip_(ip), callback_(callback) { } ~server() { } void run() { init(); while (true) { uint16_t client_port; std::string client_ip; lg(DEBUG, "accepting ..."); int sockfd = my_socket.Accept(client_ip, client_port); if (sockfd == -1) { continue; } lg(INFO, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, client_ip.c_str(), client_port); int ret = fork(); if (ret == 0) { my_socket.Close(); char buffer[buff_size]; std::string in_buffer; while (!exit_) { memset(buffer, 0, sizeof(buffer)); int n = read(sockfd, buffer, sizeof(buffer)); //"size"\n"a op b"\n if (n > 0) { buffer[n] = 0; lg(INFO, "get request : %s", buffer); in_buffer += buffer; // 连续读取 while (true) // 处理多份数据 { std::string content = callback_(in_buffer); //->"size"\n"result code"\n if (content.empty()) { break; } write(sockfd, content.c_str(), content.size()); } } else if (n == 0) { lg(INFO, "%s quit", client_ip.c_str()); break; } else // 读出错误 { break; } } // lg(INFO, "fork quit"); exit(0); close(sockfd); } } } private: void init() { signal(SIGPIPE, SIG_IGN); signal(SIGCHLD, SIG_IGN); my_socket.Socket(); my_socket.Bind(port_); my_socket.Listen(); lg(INFO, "server init done"); } public: uint16_t port_; std::string ip_; cal_t callback_; };
客户端
读的处理
和服务端类似,它在读取响应消息时,也不能一定可以保证一次就读取出一份完整的报文
- 所以也要定义一个输入缓冲区,不断地往里加数据
因为收到的一定是经过封装的响应报文,所以需要先解包,再反序列化,拿到结构化数据后,返回给用户
- 如果处理完成一份报文/报文格式错误,就从缓冲区中删除
写的处理
写数据其实也可能会只写了一部分
- 比如向内核缓冲区写了一部分/向tcp协议定义的缓冲区写了一部分
- 但这部分属于内核与tcp协议的工作范围,我们不做处理
我们要处理的是写失败,写失败了就要让他们重写
处理多份数据
和服务端类似,也是需要while循环不断处理
- 通过decode的返回值来判断是否还能处理
代码
#include <signal.h> #include "socket.hpp" #include "Serialization.hpp" #include "help.hpp" static MY_SOCKET my_socket; // 网络客户端 class client { public: client(const uint16_t port, const std::string &ip) : port_(port), ip_(ip) { } ~client() { my_socket.Close(); } void run() { init(); while (true) { bool ret = my_socket.Connect(ip_, port_); if (!ret) { continue; } std::string tmp, in_buffer; char buffer[buff_size]; while (true) { std::cout << "please enter:"; getline(std::cin, tmp); // 读取用户输入 //"a op b" encode(tmp); //"size\nbuffer\n" int n = write(my_socket.get_fd(), tmp.c_str(), tmp.size()); if (n < 0) { continue; } memset(buffer, 0, sizeof(buffer)); int ret = read(my_socket.get_fd(), buffer, sizeof(buffer)); //->"size"\n"result code"\n if (ret > 0) { in_buffer += buffer; while (true) // 处理多份数据 { std::string data; bool ret = decode(in_buffer, data); //"result code" if (!ret) // 不是完整的报文 { break; } response res; res.deserialize(data); if (res.err_code_ == 0) { std::cout << "result : " << res.result_ << std::endl; } else { std::map<int, std::string> err; init_err(err); std::cout << "error : " << err[res.err_code_] << std::endl; } } } else { break; } } } } private: void init() { signal(SIGPIPE, SIG_IGN); signal(SIGCHLD, SIG_IGN); my_socket.Socket(); } public: uint16_t port_; std::string ip_; };
help.hpp
帮助我们翻译错误码
#pragma once #include<map> #include<string> #define buff_size 256 enum { ERROR_DIVIDE_BY_ZERO = 1, // 除0/模0错误 ERROR_INVALID_EXPRESSION, // 无效表达式错误(符号识别不了) ERROR_MEMORY_OVERFLOW, // 内存溢出错误 ERROR_SYNTAX_ERROR, // 语法错误(比如没有=) }; void init_err(std::map<int,std::string>& m){ m[ERROR_DIVIDE_BY_ZERO]="除0/模0错误"; m[ERROR_INVALID_EXPRESSION]="无效表达式"; m[ERROR_MEMORY_OVERFLOW]="内存溢出"; m[ERROR_SYNTAX_ERROR]="语法错误"; }
计算
思路
计算逻辑在之前有介绍 -- 计算器(有qt界面)-CSDN博客
- 这里在原来的基础上增加了错误码的设置
- 以及写了一个计算的总逻辑 -- 拿到报文->解包->转换为后序表达式->用这个表达式计算->将结果返回->序列化响应->封装响应报头->返回
代码
#pragma once #include <map> #include <utility> #include <vector> #include <stack> #include <iostream> #include "Serialization.hpp" #include "help.hpp" // 提供计算和解析封装报文的功能 class calculate { public: calculate() : err_code_(0) {} ~calculate() {} std::string cal(std::string &content) // 封装后的报文,//"size"\n"a op b"\n { err_code_ = 0; // 解析出表达式 std::string data; bool ret = decode(content, data); // data:"a op b" if (!ret) // 不是完整的报文 { return ""; } // 因为这里我们直接用表达式计算,所以不需要反序列化 // 计算 std::map<std::string, int> comp; confirm_priority(comp); std::vector<std::string> tmp = to_suffix(data, comp); if (tmp.empty()) { return ""; } response res; res.result_ = work(tmp, comp); res.err_code_ = err_code_; std::string res_tmp; res.serialize(res_tmp); //"result code" encode(res_tmp); // size\n"result code"\n return res_tmp; } private: void confirm_priority(std::map<std::string, int> &comp) { comp.insert(std::make_pair("=", -1)); comp.insert(std::make_pair("||", 1)); comp.insert(std::make_pair("&&", 2)); comp.insert(std::make_pair("==", 3)); comp.insert(std::make_pair("!=", 3)); comp.insert(std::make_pair(">", 4)); comp.insert(std::make_pair("<", 4)); comp.insert(std::make_pair("<=", 4)); comp.insert(std::make_pair(">=", 4)); comp.insert(std::make_pair("+", 5)); comp.insert(std::make_pair("-", 5)); comp.insert(std::make_pair("*", 6)); comp.insert(std::make_pair("/", 6)); comp.insert(std::make_pair("%", 6)); comp.insert(std::make_pair("!", 7)); } std::vector<std::string> to_suffix(const std::string arr, std::map<std::string, int> &comp) { bool prev_was_operator = true; // std::cout << "arr:" << arr << std::endl; std::string numbers("0123456789"); std::vector<std::string> ans, err; std::stack<std::string> cal; for (auto i = 0; i < arr.size(); ++i) { // std::cout << "i:" << i << std::endl; if (arr[i] >= '0' && arr[i] <= '9') { auto pos = arr.find_first_not_of(numbers, i); if (pos == std::string::npos) { // 没有= err_code_ = ERROR_SYNTAX_ERROR; return err; } std::string tmp_arr(arr.begin() + i, arr.begin() + pos); ans.push_back(tmp_arr); i = pos - 1; prev_was_operator = false; } else { // 符号 if (arr[i] == '-' && (prev_was_operator || i == 0)) // Check if it's a negative number or subtraction operator { std::string tmp = "-"; // Check if next character is a digit if (i + 1 < arr.size() && arr[i + 1] >= '0' && arr[i + 1] <= '9') { auto pos = arr.find_first_not_of(numbers, i + 1); std::string tmp_arr(arr.begin() + i + 1, arr.begin() + pos); ans.push_back(tmp + tmp_arr); i = pos - 1; prev_was_operator = false; // Since we found a negative number, set the flag to false } else // It's a subtraction operator { cal.push(tmp); prev_was_operator = true; // Set the flag to true since it's an operator } continue; } std::string sym; sym += arr[i]; // std::cout << "sym:" << sym << std::endl; if (((arr[i] == '>' || arr[i] == '<' || arr[i] == '=' || arr[i] == '!') && arr[i + 1] == '=') || (arr[i] == '|' && arr[i + 1] == '|') || (arr[i] == '&' && arr[i + 1] == '&')) { ++i; sym += arr[i]; } if (sym == "(") { cal.push(sym); continue; } else if (sym == ")") { while (cal.top() != "(") { ans.push_back(cal.top()); cal.pop(); } cal.pop(); } else { if (comp[sym] == 0) { err_code_ = ERROR_INVALID_EXPRESSION; return err; } while (!cal.empty()) { std::string top = cal.top(); if (comp[sym] > comp[top]) { break; } else { ans.push_back(top); cal.pop(); } } cal.push(sym); } } // for (auto c : ans) // { // std::cout << c << " "; // } // std::cout << std::endl; // if (!cal.empty()) // { // std::cout << cal.top() << std::endl; // } } while (!cal.empty()) { ans.push_back(cal.top()); cal.pop(); } // std::cout << "ans: "; // for (auto c : ans) // { // std::cout << c << " "; // } // std::cout << std::endl; return ans; } int work(std::vector<std::string> &tokens, std::map<std::string, int> &comp) { std::stack<int> s; int num = 0; for (auto c : tokens) { if (c == "=") { break; } if (comp[c] != 0) // 是符号 { int ans = 0; int ret = comp[c]; if (comp[c] != 7) { // 拿到运算数据 int a = s.top(); s.pop(); // 先拿到的是右 int b = s.top(); s.pop(); // 然后是左 if (ret == 1) { ans = b || a; } else if (ret == 2) { ans = b && a; } else if (ret == 3) { if (c == "==") { ans = b == a; } else { ans = b != a; } } else if (ret == 4) { if (c == ">") { ans = b > a; } else if (c == ">=") { ans = b >= a; } else if (c == "<") { ans = b < a; } else { ans = b <= a; } } else if (ret == 5) { if (c == "+") { ans = b + a; } else { ans = b - a; } } else if (ret == 6) { if (c == "*") { ans = b * a; } else if (c == "/") { if (a == 0) { err_code_ = ERROR_DIVIDE_BY_ZERO; return 0; } ans = b / a; } else { if (a == 0) { err_code_ = ERROR_DIVIDE_BY_ZERO; return 0; } ans = b % a; } } } else { // 拿到运算数据 int a = s.top(); s.pop(); // 单目 ans = !a; } s.push(ans); // 结果入栈 } else // 数字或其他符号 { // 是数字 -- 要string转int,然后数字入栈 if (c[0] == '-') { c.erase(0, 1); num = stoi(c); num = -num; } else { num = stoi(c); } s.push(num); } } if (s.empty()) { err_code_ = ERROR_SYNTAX_ERROR; return 0; } return s.top(); // 最后一个元素就是结果 } public: int err_code_; };
守护进程化
可以直接调用daemon函数,也可以自己实现,在之前的博客里有介绍 -- 基于tcp协议的网络通信(将服务端守护进程化)-CSDN博客
而日志重定向,使用的是日志对象里的enable方法,也在那篇博客里有介绍