目录
网络计算器代码
思路什么的之前我们就已经大概介绍过了 -- 网络通信中字节流存在的问题,tcp协议特点,自定义协议(引入+介绍,序列化反序列化介绍,实现思路)-优快云博客
下面是更详细的在代码方面的介绍
代码的难度其实就在于[计算逻辑代码]和[反序列化+解包报头]
自定义协议
介绍
根据计算器的功能,可以把它拆分成三个部分 -- 用户输入,计算,返回结果
- 那么,我们就需要定义两个结构体,来帮助输入/输出数据的结构化,以便我们提取数据
请求
我们这里选择让用户传入字符串("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