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

目录

网络计算器代码

自定义协议

介绍

请求

响应 

序列化

思路

代码 

反序列化

思路

注意点 

代码 

报头封装和解包

思路

注意点 

代码

网络通信接口

服务端

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

处理收到的数据

多进程

处理多份数据的能力

代码 

客户端

读的处理

写的处理

处理多份数据 

代码

help.hpp 

计算

思路

代码

 守护进程化

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


网络计算器代码

思路什么的之前我们就已经大概介绍过了 -- 网络通信中字节流存在的问题,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方法,也在那篇博客里有介绍

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值