网络计算器代码编写+注意点(序列化,反序列化,报头封装和解包,服务端和客户端,计算),客户端和服务端数据传递流程图,守护进程化+日志重定向到文件

目录

网络计算器代码

自定义协议

介绍

请求

响应 

序列化

思路

代码 

反序列化

思路

注意点 

代码 

报头封装和解包

思路

注意点 

代码

网络通信接口

服务端

将序列化和网络通信关联在一起

处理收到的数据

多进程

处理多份数据的能力

代码 

客户端

读的处理

写的处理

处理多份数据 

代码

help.hpp 

计算

思路

代码

 守护进程化

客户端和服务端数据传递流程图


网络计算器代码

思路什么的之前我们就已经大概介绍过了 -- 网络通信中字节流存在的问题,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 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值