网络协议定制

news2025/2/24 3:24:40

目录

一、协议定制

1.再谈协议

2.认识序列化与反序列化

二、网络计算器

1.序列化与反序列化头文件

2.服务端

3.客户端

4.makefile

5.运行结果

三、json序列化与反序列化

1.常用序列化与反序列化库

2.安装

3.使用

(1)Request的序列化

(2)Request的反序列化


一、协议定制

1.再谈协议

协议本质是一种“约定”,在前面的TCP和UDP网络通信中,读写数据的时候都是按照"字符串"的形式发送和接收的,那我们如果不发送字符串,而是要传送一些结构化的数据怎么办呢?

比如说,我们观察QQ或微信的聊天窗口,屏幕上的信息包括头像(url)、时间、昵称、消息等。

用户发送的消息虽然大部分是字符串,但是经过用户层(QQ或微信软件)处理后,还需要增加头像,时间,昵称等信息。如果我们将这几个信息看做多个字符串,那么就可以将这多个字符串形成一个结构化的数据,比如下面示例的struct message。

此时网络中发送的就不再只是一个字符串,而是像类变量这样的复杂结构。

struct message
{
    string url;
    string time;
    string nickname;
    string msg;
};

2.认识序列化与反序列化

序列化:将任意类型的数据或者数据结构转换成一个字符串。

由于系统调用接口的限制,在发送这样的结构体之前,必须将结构化的数据序列化,然后才能通过socket发送到网络中。

比如用户用微信A向用户B发送信息,微信就创建了一个上面的message结构体。序列化可将所有成员合并成一个字符串报文,网络再将序列化后的数据发送给用户B。

反序列化:将一个字符串中不同信息类型的字串提取出来,并且还原回结构化类型的数据。

用户B接收到一个字符串报文,然后用户B的应用层(微信软件)将接收到的报文反序列化,还原回原来结构化的message结构体,此时微信就能将对应数据显示在不同位置,用户B就可以看到这些信息。

对于面向字节流的TCP协议,业务结构数据在发送到网络中的时候,需要先序列化。接收方收到的是序列化后的字节流,要先进行反序列化才能使用。

由于UDP协议面向数据报,所以无需进行序列化以及反序列化。

在微信聊天的过程中,用户A发送的与用户B接受的都是同一种数据结构,所以用户A构建的message是按照特定的成员顺序组成的,用户B就必须按照这样的成员顺序去使用它接收到的message。

所以我们可以说,这样的通信“约定”就是一个用户层的协议,者个协议的载体就是message结构体。

二、网络计算器

我们使用之前写过的tcp通信客户端和服务端的多进程版本实现这个网络计算器。

数据的处理与流转过程大致如下:

用户输入计算式(比如:1+1)->构造一个Request变量->序列化为字符串"a op b"

->添加报头转化为报文"content_len"\r\n"a op b"\r\n->将报文发给服务端

服务端接收报文->去掉报头得到正文"a op b"->反序列化为Request变量->服务端使用计算函数处理Request变量得到Response变量->Response变量序列化为字符串"exitcode result"->添加报头转化为报文"content_len"\r\n"exitcode result"\r\n->将报文发给客户端

客户端接收报文->去掉报头得到正文"exitcode result"->反序列化为Response变量->打印Response变量的内容。

下面的代码中有详细的注释介绍

1.序列化与反序列化头文件

protocol.hpp

#pragma once
#define MYSELF
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>

#define SEP " "
#define SEP_LEN strlen(SEP) //使用sizeof()多了一个\0
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP) //使用sizeof()多了一个\0

enum
{
    OK = 0,
    DIV_ZERO,
    MOD_ZERO,
    OP_ERROR
};

//"content" 转化为 "content_len"\r\n"content"\r\n
//加上报头
std::string enlength(const std::string &text)
{
    //按"content_len"\r\n"content"\r\n拼接字符串
    std::string send_string = std::to_string(text.size());
    send_string += LINE_SEP;
    send_string += text;
    send_string += LINE_SEP;
    return send_string;
}

//"content_len"\r\n"content"\r\n 转化为 "content"
//去掉报头
bool delength(const std::string &package, std::string *text)
{
    //查找左侧的\r\n
    auto pos = package.find(LINE_SEP);
    if (pos == std::string::npos)
        return false;//没找到表明数据有问题
    //截取正文长度的字符串并转为整形
    std::string text_len_string = package.substr(0, pos);
    int text_len = std::stoi(text_len_string);
    //截取正文放入储存正文的string里
    *text = package.substr(pos + LINE_SEP_LEN, text_len);
    return true;
}

class Request
{
public:
    Request()
        :_a(0)
        ,_b(0)
        ,_op(0)
    {}

    Request(int a, int b, char op)
        :_a(a)
        ,_b(b)
        ,_op(op)
    {}
    
    //Request序列化
    //Request结构体转化为字符串"a op b"
    bool serialize(std::string *out)
    {
        out->clear();//清空
        
        //将变量转为字符串
        std::string a_string = std::to_string(_a);
        std::string b_string = std::to_string(_b);

        *out = a_string;
        *out += SEP;
        *out += _op;
        *out += SEP;
        *out += b_string;
        return true;
    }

    //Request反序列化
    //字符串"a op b"转化为Request结构体
    bool deserialize(const std::string &in)
    {
        auto left = in.find(SEP);//查找左侧的SEP
        auto right = in.rfind(SEP);//查找右侧的SEP
        if (left == std::string::npos || right == std::string::npos)
            return false;//找不到,数据有问题
        if (left == right)
            return false;//只有一个SEP,数据有问题
        if ((right - 1) != (left + SEP_LEN))//在字符串"a op b"中,right - 1和left + SEP_LEN都指向op
            return false;//指向的不是一个位置,数据有问题

        //按左闭右开的方式构造两个数字
        std::string a_string = in.substr(0, left);
        std::string b_string = in.substr(right + SEP_LEN);

        //读取到的数字不能为空
        if (a_string.empty() || b_string.empty())
            return false;

        //填入数据
        _a = std::stoi(a_string);
        _b = std::stoi(b_string);
        _op = in[left + SEP_LEN];
        return true;
    }

public:
    int _a;
    int _b;
    char _op;
};

class Response
{
public:
    Response()
        :_exitcode(0)
        ,_result(0)
    {}
    
    Response(int exitcode, int result)
        :_exitcode(exitcode)
        ,_result(result)
    {}

    //Response序列化
    //Response结构体转化为字符串"exitcode result"
    bool serialize(std::string *out)
    {
        out->clear();//清空

        //将变量转为字符串
        std::string ec_string = std::to_string(_exitcode);
        std::string res_string = std::to_string(_result);

        //拼接字符串
        *out = ec_string;
        *out += SEP;
        *out += res_string;

        return true;
    }

    //Response反序列化
    //字符串"exitcode result"转化为Response结构体
    bool deserialize(const std::string &in)
    {
        auto mid = in.find(SEP);//查找中间的SEP
        if (mid == std::string::npos)
            return false;//找不到,出错

        //按左闭右开的方式构造两个数字
        std::string ec_string = in.substr(0, mid);
        std::string res_string = in.substr(mid + SEP_LEN);

        //读取到的退出码和计算结果不能为空
        if (ec_string.empty() || res_string.empty())
            return false;

        //填入数据
        _exitcode = std::stoi(ec_string);
        _result = std::stoi(res_string);

        return true;
    }

public:
    int _exitcode; // 0:计算成功,!0表示计算失败,具体是多少,定好标准
    int _result;   // 计算结果
};

//接收的数据:"content_len"\r\n"a op b"\r\n ......
bool recvpackage(int sock, std::string &inbuffer, std::string *text)
{
    char buffer[1024];//接收网络数据的字符串
    while (true)
    {
        ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);//接收数据
        if (n > 0)//读到了数据
        {
            buffer[n] = 0;
            inbuffer += buffer;//以追加的方式放入缓冲区ibuffer
            auto pos = inbuffer.find(LINE_SEP);//查找LINE_SEP,就是\r\n
            if (pos == std::string::npos)
                //如果没有找到LINE_SEP,就说明报文"content_len"的部分可能没有读全
                continue;//接着到最开始读数据直到读全
            
            //执行至此时,报文"content_len"\r\n这部分一定读全了
            //可以将"content_len"的部分取出来并转化为整形
            std::string text_len_string = inbuffer.substr(0, pos);
            int text_len = std::stoi(text_len_string);

            //我们现在知道了报文中正文部分的长度,所以就能确定我们读到的是否是一个完整的报文
            //一个报文的组成为:text_len_string + "\r\n" + text + "\r\n"
            //所以,只要缓冲区ibuffer的内容多于一个报文的字符个数,我们就必定保证ibuffer中已经存在了一个完整的报头;
            int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;
            std::cout << "处理前#inbuffer: \n" << inbuffer << std::endl;
            if (inbuffer.size() < total_len)
            {
                std::cout << "输入的消息没有遵守协议,正在等待后续内容" << std::endl;
                continue;//报文没读全继续到上面读取
            }

            //此时缓冲区中就至少有一个完整的报文
            *text = inbuffer.substr(0, total_len);//将一个完整报文构建子串放入输出参数中
            inbuffer.erase(0, total_len);//删除缓冲区中的完整报文
            std::cout << "处理后#inbuffer:\n " << inbuffer << std::endl;
            break;//准备处理报文,跳出循环
        }
        else//没读到数据
            return false;
    }
    return true;
}

2.服务端

server.hpp

#pragma once
#include<iostream>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<errno.h>
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<assert.h>
#include<pthread.h>
#include<functional>
#include"log.hpp"
#include"protocol.hpp"

using namespace std;

enum errorcode
{
    USAGE_ERROR = 1,
    SOCKET_ERROR,
    BIND_ERROR,
    LISTEN_ERROR
};

static const uint16_t given_port = 8080;
static const int given_backlog = 5;
typedef function<bool(const Request&, Response&)> func_t;

void handler_enter(func_t func, int sock)
{
    std::string inbuffer;//缓冲区
    while (true)
    {
       //第一步,读取一个完整的"content_len"\r\n"a op b"\r\n字符串请求
        std::string req_text;//原数据
        std::string req_str;//正文
        //使用recvPackage接收数据并将一个完整的请求放入req_text
        if (!recvpackage(sock, inbuffer, &req_text))
            return;//函数执行错误,退出
        //打印该请求,让我们看到执行流程
        std::cout << "未去掉报头的请求:\n" << req_text << std::endl;
        //将字符串转化为格式化数据保存在req_text内
        if (!delength(req_text, &req_str))
            return;//函数执行错误,退出
        //打印,让我们看到执行流程
        std::cout << "已去掉报头的正文:\n" << req_str << std::endl;

        //第二步,对Request进行反序列化
        Request req;
        if (!req.deserialize(req_str))//将正文信息填入req内
            return;//函数执行错误,退出

        //第三步,利用func函数处理请求结构体
        Response resp;
        func(req, resp);//将req的处理结果放入resp

        //第四步,对Response进行序列化
        std::string resp_str;
        resp.serialize(&resp_str);//将序列化后的结构题放入resp_str
        std::cout << "计算完成, 序列化后的信息为:\n" <<  resp_str << std::endl;

        //将序列化的Response加上报头并发回客户端
        std::string send_string = enlength(resp_str);//加报头
        std::cout << "构建完成完整的响应\n" <<  send_string << std::endl;//打印加上报头后的信息
        send(sock, send_string.c_str(), send_string.size(), 0);//发回
    }
}

class tcpserver
{
public:
    //构造函数
    tcpserver(const uint16_t& port = given_port)
        :_port(port)
        ,_listensock(-1)
    {}

    //初始化服务端进程
    void initserver()
    {
        _listensock = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
        if(_listensock < 0)//创建套接字失败打印错误原因
        {
            logmessage(FATAL, "create socket error");//socket失败属于最严重的错误
            exit(SOCKET_ERROR);//退出
        }
        logmessage(NORMAL, "create socket success:%d", _listensock);//创建套接字成功,打印让用户观察到

        struct sockaddr_in local;//储存本地网络信息
        local.sin_family = AF_INET;//通信方式为网络通信
        local.sin_port = htons(_port);//将网络字节序的端口号填入
        local.sin_addr.s_addr = INADDR_ANY;//INADDR_ANY就是ip地址0.0.0.0的宏
        if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)//绑定IP,不成功打印信息
        {
            logmessage(FATAL, "bind socket error");//bind失败也属于最严重的错误
            exit(BIND_ERROR);//退出
        }
        logmessage(NORMAL, "bind socket success");//绑定IP成功,打印让用户观察到

        //_listensock用于监听,不是用于通信的端口号
        //listen函数可设置socket为监听模式
        if(listen(_listensock, given_backlog) < 0) // 第二个参数backlog后面在填这个坑
        {
            logmessage(FATAL, "listen socket error");
            exit(LISTEN_ERROR);
        }
        logmessage(NORMAL, "listen socket success");
    }

    //启动服务端进程,多进程版本
    void start(func_t func)
    {
        while(1)
        {
            struct sockaddr_in peer;//储存本地网络信息
            socklen_t len = sizeof(peer);
            int sock = accept(_listensock, (struct sockaddr*)&peer, &len);
            //如果没有客户端连接服务端,则accept会阻塞等待新连接
            //如果有客户端需要连接服务端,则返回一个用于网络通信的描述符sock
            if(sock < 0)
            {
                logmessage(ERROR, "accept fail");//接收新文件描述符失败
                continue;//重新回到头接收
            }
            logmessage(NORMAL, "accept a new link");//接收新文件描述符成功
            
            //创建子进程
            pid_t id = fork();
            if(id == 0)
            {
                //由于子进程也继承了父进程的监听套接字,而监听套接字只需要一个,所以需要关闭
                close(_listensock);
                if(fork() > 0)
                {
                    //子进程再次创建子进程,fork对父进程(当前是服务器的子进程)返回子进程pid,对子进程(当前是服务器的孙子进程)返回0
                    exit(0);//服务器子进程退出
                }
                //服务器的孙子进程执行IO处理
                //不断处理IO任务
                handler_enter(func, sock);
                //退出该函数时,客户端已经退出,需要将进行网络传输的文件描述符释放,否则会引起资源泄露
                close(sock);
                //退出孙子进程,由于它是孤儿进程,所以会被操作系统自行回收
                exit(0);
            }
        }
    }

    ~tcpserver()
    {}
private:
    uint16_t _port;//服务端进程的端口号
    int _listensock;//监听文件描述符
};

server.cc

#include"server.hpp"
#include<memory>
#include<unistd.h>
#include<fcntl.h>

//计算器函数
const string ops = "+-*/%";
bool calculate(const Request &req, Response &resp)
{
    // req已经有结构化完成的数据啦,你可以直接使用
    resp._exitcode = OK;
    resp._result = 0;
    switch (req._op)
    {
    case '+':
        resp._result = req._a + req._b;
        break;
    case '-':
        resp._result = req._a - req._b;
        break;
    case '*':
        resp._result = req._a * req._b;
        break;
    case '/':
    {
        if (req._b == 0)
            resp._exitcode = DIV_ZERO;
        else
            resp._result = req._a / req._b;
    }
    break;
    case '%':
    {
        if (req._b == 0)
            resp._exitcode = MOD_ZERO;
        else
            resp._result = req._a % req._b;
    }
    break;
    default:
        resp._exitcode = OP_ERROR;
        break;
    }
    return true;
}

static void Usage(string proc)
{
    printf("\nUsage:\n\t%s local_port\n\n",proc.c_str());
}
int main(int argc, char* argv[])
{
    if(argc != 2)//如果没输入端口号,argc保存的命令参数只有一个,进程出错
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }

    uint16_t port = atoi(argv[1]);
    unique_ptr<tcpserver> p(new tcpserver(port));

    p->initserver();

    p->start(calculate);

    return 0;
}

3.客户端

client.hpp

#pragma once
#include<iostream>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<errno.h>
#include<string.h>
#include<strings.h>
#include<istream>
#include<stdlib.h>
#include<stdio.h>
#include<memory>
#include<ctype.h>
#include"log.hpp"
#include"protocol.hpp"

#define NUM 1024
enum errorcode
{
    USAGE_ERROR = 1,
    SOCKET_ERROR,
    BIND_ERROR,
    CONNECT_ERROR
};

Request ParseLine(const std::string &line);

class tcpclient
{
public:
    //构造函数
    tcpclient(const std::string& ip, const uint16_t& port)
        :_ip(ip)
        ,_port(port)
        ,_sock(-1)
    {}

    void initclient()
    {
        //创建套接字,创建失败打印错误原因
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if(_sock == -1)
        {
            logmessage(FATAL, "create socket error");//socket失败属于最严重的错误
            exit(SOCKET_ERROR);//退出
        }
        logmessage(NORMAL, "create socket success:%d", _sock);//创建套接字成功,打印让用户观察到
        //客户端不需要显式绑定,该工作交给操作系统完成
    }

    //启动客户端进程
    void run()
    {
        struct sockaddr_in local;
        local.sin_family = AF_INET;//通信方式为网络通信
        local.sin_port = htons(_port);//将网络字节序的端口号填入
        local.sin_addr.s_addr = inet_addr(_ip.c_str());//填充结构体

        //客户端连接服务器
        if(connect(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)//客户端与服务器进行连接
        {
            logmessage(FATAL, "connect error");//connect失败属于最严重的错误
        }
        else
        {
            std::string line;
            std::string inbuffer;
            while (true)
            {
                std::cout << "mycal>>> ";
                std::getline(std::cin, line);  // 1+1
                Request req = ParseLine(line); // "1+1"
                std::string content;
                req.serialize(&content);
                std::string send_string = enlength(content);
                std::cout << "sendstring:\n" << send_string << std::endl;
                send(_sock, send_string.c_str(), send_string.size(), 0); // bug?? 不管

                std::string package, text;
                //  "content_len"\r\n"exitcode result"\r\n
                if (!recvpackage(_sock, inbuffer, &package))
                    continue;
                if (!delength(package, &text))
                    continue;
                // "exitcode result"
                Response resp;
                resp.deserialize(text);
                std::cout << "exitCode: " << resp._exitcode << std::endl;
                std::cout << "result: " << resp._result << std::endl;
            }
        }
    }

    Request ParseLine(const std::string &line)
    {
        // 建议版本的状态机!
        //"1+1" "123*456" "12/0"
        int status = 0; // 0:操作符之前,1:碰到了操作符 2:操作符之后
        int i = 0;
        int cnt = line.size();
        std::string left, right;
        char op;
        while (i < cnt)
        {
            switch (status)
            {
            case 0:
            {
                if(!isdigit(line[i]))
                {
                    op = line[i];
                    status = 1;
                }
                else left.push_back(line[i++]);
            }
            break;
            case 1:
                i++;
                status = 2;
                break;
            case 2:
                right.push_back(line[i++]);
                break;
            }
        }
        std::cout << std::stoi(left)<<" " << std::stoi(right) << " " << op << std::endl;
        return Request(std::stoi(left), std::stoi(right), op);
    }
    
    //析构函数要释放不使用的文件描述符
    ~tcpclient()
    {
        if( _sock >= 0)
            close(_sock);
    }
private:
    int _sock;//套接字文件描述符
    std::string _ip;//服务器IP地址
    uint16_t _port;//服务器的端口号
};

client.cc

#include"client.hpp"

using namespace std;

static void Usage(string proc)
{
    printf("\nUsage:\n\t%s server_ip server_port\n\n", proc.c_str());
}
int main(int argc, char* argv[])
{
    if(argc != 3)//如果没输入端口号和目的IP,argc保存的命令参数就不是三个,进程出错
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }

    uint16_t port = atoi(argv[2]);
    string ip = argv[1];
    
    unique_ptr<tcpclient> p(new tcpclient(ip, port));

    p->initclient();
    p->run();

    return 0;
}

4.makefile

.PHONY:
all:server client
server:server.cc
    g++ server.cc -o server -std=c++11 -ljsoncpp 
client:client.cc
    g++ client.cc -o client -std=c++11 -ljsoncpp 
.PHONY:
clean:
    rm -f server client

5.运行结果

对于正确的计算式,也能返回0作为退出码,指示结果有效。

对于不正确的计算式,也能返回非0退出码,指示结果无效。

三、json序列化与反序列化

1.常用序列化与反序列化库

上面的序列化和反序列化都是我们自己通过处理字符串实现的。其实这部分的工作早就有人写好了现成的库,我们直接使用它们对数据进行序列化和反序列化即可。我们以后代码中的序列化和反序列化,绝对不要自己写。

常见的有以下三个:

json——使用简单。

protobuf——比较复杂,局域网或者本地网络通信使用较多。

xml——适合在其他编程语言中使用(如Java等)。

这里只介绍使用最简单也最广泛的json的使用,学有余力可以去了解下protobuf。

所以,都说到这里了,序列化反序列化与协议又有和关系呢?

序列化与反序列化可以理解为,为了传输结构体这样的结构将原数据转化为字符串方便传输。

而协议是网络通信双方约定以某种数据结构或者机制进行数据的解读和包装。

二者并不相同,协议可以自己定制,序列化一般使用别人写好的库。二者也都是组成程序的重要部分。

2.安装

在使用json之前,需要先在Linux机器上使用yum安装json工具

语句:sudo yum install -y jsoncpp-devel

如上图所示就是安装成功了。(我直接以root身份安装的,不用sudo)

json安装后,它的头文件json.h所在路径为/usr/include/jsoncpp/json/。而由于编译器查找头文件的目录仅限usr/include,所以在包含头文件需要加路径jsoncpp/json/json.h。

json是一个动态库,它所在路径为/lib64/,完整的名字为libjsoncpp.sp。在使用的时候,编译器会自动到/lib64路径下查找所用的库,但编译需要指定库名,也需要加上-ljonscpp。

还可以在makefile中增加一个宏-DMYSELF,实现条件编译,做到对序列化与反序列化的自我实现函数和json库实现的切换。

3.使用

Json数据的格式是采用键值对的形式,key为字符串,value为需要存储的变量如:

"first" : a、"second" : b、"oper" : op、"exitcode" : exitcode、"result" : result

序列化就是将这些键值对拼接在一起形成一个字符串。

反序列化就是将多个字符串拆开,用户根据键值对的对应关系可以找到绑定的变量。

当然这仅仅是一个很粗糙的理解,它内部的实现很复杂,我们只需要知道如何使用Json这个工具即可。

我们使用Json对Request和Response进行序列化和反序列化写出一个新的版本,并利用条件编译实现其切换。

我以Request的序列化和反序列化为例:

(1)Request的序列化

Json::Value root创建的Value类型对象root可以看作一个万能对象。

使用root["字符串"] = 变量;的形式将不同类型的变量和字符串绑定形成键值对。

然后创建一个Json::FastWriter类型的对象writer,使用write函数对root进行序列化,返回的结果为string类型,赋值给*out。

(2)Request的反序列化

首先,依旧要创建一个万能对象root,然后再创建一个Json::Reader类型的对象reader。

调用对象reader的成员函数parse()将字符串in反序列化并将得到的键值对再放入到万能对象root中。

根据键值对中的key值("first"等字符串)使用[]就能找到value。由于反序列化后的value依旧为字符串,所以需要使用asInt()转化成int类型,此时才算彻底完成了反序列化。

再使用Json对Response进行序列化和反序列化的过程除了成员变量的数目和属性不同之外,其他并没有什么不同,代码就都放在下面了。

#pragma once
#define MYSELF
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>

#define SEP " "
#define SEP_LEN strlen(SEP) //使用sizeof()多了一个\0
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP) //使用sizeof()多了一个\0

enum
{
    OK = 0,
    DIV_ZERO,
    MOD_ZERO,
    OP_ERROR
};

//"content" 转化为 "content_len"\r\n"content"\r\n
//加上报头
std::string enlength(const std::string &text)
{
    //按"content_len"\r\n"content"\r\n拼接字符串
    std::string send_string = std::to_string(text.size());
    send_string += LINE_SEP;
    send_string += text;
    send_string += LINE_SEP;
    return send_string;
}

//"content_len"\r\n"content"\r\n 转化为 "content"
//去掉报头
bool delength(const std::string &package, std::string *text)
{
    //查找左侧的\r\n
    auto pos = package.find(LINE_SEP);
    if (pos == std::string::npos)
        return false;//没找到表明数据有问题
    //截取正文长度的字符串并转为整形
    std::string text_len_string = package.substr(0, pos);
    int text_len = std::stoi(text_len_string);
    //截取正文放入储存正文的string里
    *text = package.substr(pos + LINE_SEP_LEN, text_len);
    return true;
}

class Request
{
public:
    Request()
        :_a(0)
        ,_b(0)
        ,_op(0)
    {}

    Request(int a, int b, char op)
        :_a(a)
        ,_b(b)
        ,_op(op)
    {}
    
    //Request序列化
    //Request结构体转化为字符串"a op b"
    bool serialize(std::string *out)
    {
#ifdef MYSELF
        out->clear();//清空
        
        //将变量转为字符串
        std::string a_string = std::to_string(_a);
        std::string b_string = std::to_string(_b);

        *out = a_string;
        *out += SEP;
        *out += _op;
        *out += SEP;
        *out += b_string;
#else
        Json::Value root;
        root["first"] = _a;
        root["second"] = _b;
        root["oper"] = _op;

        Json::FastWriter writer;
        // Json::StyledWriter writer;
        *out = writer.write(root);
#endif
        return true;
    }

    //Request反序列化
    //字符串"a op b"转化为Request结构体
    bool deserialize(const std::string &in)
    {
#ifdef MYSELF
        auto left = in.find(SEP);//查找左侧的SEP
        auto right = in.rfind(SEP);//查找右侧的SEP
        if (left == std::string::npos || right == std::string::npos)
            return false;//找不到,数据有问题
        if (left == right)
            return false;//只有一个SEP,数据有问题
        if ((right - 1) != (left + SEP_LEN))//在字符串"a op b"中,right - 1和left + SEP_LEN都指向op
            return false;//指向的不是一个位置,数据有问题

        //按左闭右开的方式构造两个数字
        std::string a_string = in.substr(0, left);
        std::string b_string = in.substr(right + SEP_LEN);

        //读取到的数字不能为空
        if (a_string.empty() || b_string.empty())
            return false;

        //填入数据
        _a = std::stoi(a_string);
        _b = std::stoi(b_string);
        _op = in[left + SEP_LEN];
#else
        Json::Value root;
        Json::Reader reader;
        reader.parse(in, root);

        _a = root["first"].asInt();
        _b = root["second"].asInt();
        _op = root["oper"].asInt();
#endif
        return true;
    }

public:
    int _a;
    int _b;
    char _op;
};

class Response
{
public:
    Response()
        :_exitcode(0)
        ,_result(0)
    {}
    
    Response(int exitcode, int result)
        :_exitcode(exitcode)
        ,_result(result)
    {}

    //Response序列化
    //Response结构体转化为字符串"exitcode result"
    bool serialize(std::string *out)
    {
#ifdef MYSELF
        out->clear();//清空

        //将变量转为字符串
        std::string ec_string = std::to_string(_exitcode);
        std::string res_string = std::to_string(_result);

        //拼接字符串
        *out = ec_string;
        *out += SEP;
        *out += res_string;
#else
        Json::Value root;
        root["exitcode"] = _exitcode;
        root["result"] = _result;

        Json::FastWriter writer;
        *out = writer.write(root);
#endif
        return true;
    }

    //Response反序列化
    //字符串"exitcode result"转化为Response结构体
    bool deserialize(const std::string &in)
    {
#ifdef MYSELF
        auto mid = in.find(SEP);//查找中间的SEP
        if (mid == std::string::npos)
            return false;//找不到,出错

        //按左闭右开的方式构造两个数字
        std::string ec_string = in.substr(0, mid);
        std::string res_string = in.substr(mid + SEP_LEN);

        //读取到的退出码和计算结果不能为空
        if (ec_string.empty() || res_string.empty())
            return false;

        //填入数据
        _exitcode = std::stoi(ec_string);
        _result = std::stoi(res_string);
#else
        Json::Value root;
        Json::Reader reader;
        reader.parse(in, root);
        
        _exitcode = root["exitcode"].asInt();
        _result = root["result"].asInt();
#endif
        return true;
    }

public:
    int _exitcode; // 0:计算成功,!0表示计算失败,具体是多少,定好标准
    int _result;   // 计算结果
};

//接收的数据:"content_len"\r\n"a op b"\r\n ......
bool recvpackage(int sock, std::string &inbuffer, std::string *text)
{
    char buffer[1024];//接收网络数据的字符串
    while (true)
    {
        ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);//接收数据
        if (n > 0)//读到了数据
        {
            buffer[n] = 0;
            inbuffer += buffer;//以追加的方式放入缓冲区ibuffer
            auto pos = inbuffer.find(LINE_SEP);//查找LINE_SEP,就是\r\n
            if (pos == std::string::npos)
                //如果没有找到LINE_SEP,就说明报文"content_len"的部分可能没有读全
                continue;//接着到最开始读数据直到读全
            
            //执行至此时,报文"content_len"\r\n这部分一定读全了
            //可以将"content_len"的部分取出来并转化为整形
            std::string text_len_string = inbuffer.substr(0, pos);
            int text_len = std::stoi(text_len_string);

            //我们现在知道了报文中正文部分的长度,所以就能确定我们读到的是否是一个完整的报文
            //一个报文的组成为:text_len_string + "\r\n" + text + "\r\n"
            //所以,只要缓冲区ibuffer的内容多于一个报文的字符个数,我们就必定保证ibuffer中已经存在了一个完整的报头;
            int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;
            std::cout << "处理前#inbuffer: \n" << inbuffer << std::endl;
            if (inbuffer.size() < total_len)
            {
                std::cout << "输入的消息没有遵守协议,正在等待后续内容" << std::endl;
                continue;//报文没读全继续到上面读取
            }

            //此时缓冲区中就至少有一个完整的报文
            *text = inbuffer.substr(0, total_len);//将一个完整报文构建子串放入输出参数中
            inbuffer.erase(0, total_len);//删除缓冲区中的完整报文
            std::cout << "处理后#inbuffer:\n " << inbuffer << std::endl;
            break;//准备处理报文,跳出循环
        }
        else//没读到数据
            return false;
    }
    return true;
}

上面我们使用了条件编译,如果MYSELF被定义,就是用我们自己写的序列化和反序列化。

如果MYSELF没被定义,就使用json库的定义。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1037713.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

39.动画动作菜单

特效 源码 index.html <!DOCTYPE html> <html> <head> <title>Animated Action Menu</title> <link rel="stylesheet" type="text/css" href="style.css"> </head> <body><div class=&quo…

功能测试自动化测试流程

1概述 本流程是描述软件功能自动化测试过程中的步骤、内容与方法&#xff0c;明确各阶段的职责、活动与产出物。 2流程活动图 3活动说明 3.1测试计划&#xff08;可选&#xff09; 与以前的测试计划过程一致&#xff0c;只是在原来的测试计划中&#xff0c;添加对项目实施自动…

4、SpringBoot_Mybatis、Druid、Juint整合

五、SSM整合 1.整合Mybatis 1.1springmvc 整合回顾 导入坐标 <dependency><groupId>org.springframework</groupId><artifactId>spring-jdbc</artifactId><version>5.2.17.RELEASE</version></dependency><dependency>…

力扣刷题-链表-两两交换链表中的节点

24.两两交换链表中的节点 给定一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后的链表。你不能只是单纯的改变节点内部的值&#xff0c;而是需要实际的进行节点交换。 解题思路 采用正常模拟的方法。 建议使用虚拟头结点&#xff0c;这样会方便很多&am…

大数据从入门到精通(超详细版)之BI工具的安装

前言 嗨&#xff0c;各位小伙伴&#xff0c;恭喜大家学习到这里&#xff0c;不知道关于大数据前面的知识遗忘程度怎么样了&#xff0c;又或者是对大数据后面的知识是否感兴趣&#xff0c;本文是《大数据从入门到精通&#xff08;超详细版&#xff09;》的一部分&#xff0c;小…

iMAP——论文解析

iMAP: Implicit Mapping and Positioning in Real-Time iMAP 是第一个提出用 MLP 作为场景表征的实时 RGB-D SLAM。iMAP 采用关键帧结构和多进程&#xff0c;通过动态信息引导的像素采样来提高速度&#xff0c;跟踪频率为 10 Hz&#xff0c;全局地图更新频率为 2 Hz。隐式 MLP…

Vite打包时使用plugin解决浏览器兼容问题

一、安装Vite插件 在终端输入如下命令&#xff1a; npm add -D vitejs/plugin-legacy 二、配置config文件 在项目目录下创建vite.config.js文件夹&#xff0c;配置如下代码&#xff1a; import { defineConfig } from "vite"; import legacy from "vitejs/pl…

VM虚拟机克隆

VMware 克隆虚拟机具有以下优点&#xff1a; 快速部署&#xff1a;通过克隆虚拟机&#xff0c;可以快速创建新的虚拟机副本&#xff0c;而无需从头开始进行操作系统和应用程序的安装。这节省了大量的时间和工作量。一致性和稳定性&#xff1a;克隆虚拟机是通过复制现有虚拟机来…

DeepMind: 用ReLU取代Softmax可以让Transformer更快

注意力是人类认知功能的重要组成部分&#xff0c;当面对海量的信息时&#xff0c;人类可以在关注一些信息的同时&#xff0c;忽略另一些信息。当计算机使用神经网络来处理大量的输入信息时&#xff0c;也可以借鉴人脑的注意力机制&#xff0c;只选择一些关键的信息输入进行处理…

常见限流算法学习

文章目录 常见限流算法学习前言限流算法基本介绍固定窗口计数器限流算法计数器限流算法相关介绍计数器限流算法的实现&#xff08;基于共享变量&#xff09;计数器限流算法的实现&#xff08;基于Redis&#xff09; 滑动窗口计数器算法滑动时间窗口算法相关介绍介绍滑动时间窗口…

【软件设计师-从小白到大牛】上午题基础篇:第五章 结构化开发方法

文章目录 前言结构化设计1、基本原则真题链接2、内聚与耦合真题链接3、系统结构/模块结构真题链接用户界面设计的黄金原则&#xff08;补充&#xff09;真题链接数据流图&#xff08;补充&#xff09;真题链接系统文档&#xff08;补充&#xff09;真题链接 前言 ​ 本系列文章…

如何使用 Git 进行多人协作开发(全流程图解)

文章目录 分支管理策略1.什么是Feature Branching&#xff1f;2.Feature Branching如何工作&#xff1f; 多人协作一&#xff1a;单分支1.准备工作2.创建分支3.在分支上开发4.分支合并5.清理 多人协作二&#xff1a;多分支1.创建分支2.在分支上开发3. pull request4.清理 在软件…

/usr/bin/ld: cannot find -lmysqlcllient

文章目录 1. question: /usr/bin/ld: cannot find -lmysqlcllient2. solution 1. question: /usr/bin/ld: cannot find -lmysqlcllient 2. solution 在 使用编译命令 -lmysqlclient时&#xff0c;如果提示这个信息。 先确认一下 有没有安装mysql-devel 执行如下命令 yum inst…

js对象属性

在面向对象的语言中有一个标志&#xff0c;那就是都有类&#xff0c;通过类可以创建任意多个相同属性、方法的对象。在js中没有类的存在&#xff0c;所以js中的对象&#xff0c;相对于类语言中对象有所不同。 js中定义对象为&#xff1a;“无序属性的集合&#xff0c;其属性可…

新版绿豆视频APP视频免授权源码 V6.6插件版

新版绿豆视频APP视频免授权源码 V6.6插件版 简介&#xff1a; 新版绿豆视频APP视频免授权源码 插件版 后端插件开源&#xff0c;可直接反编译修改方便 对接苹果cms,自定义DIY页面布局&#xff01; 绿豆影视APP对接苹果cms 所有页面皆可通过后端自由定制 此版本后端源码 前…

二叉树创建、前序遍历、中序遍历、后序遍历、层序遍历

#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<malloc.h> #define N 100 typedef char data_t;typedef struct tree {data_t data;//存放本节点数据struct tree* l_child;//存放左孩子节点地址struct tree* r_child;//存放右孩子节点地址 }Tree;Tre…

Zig实现Hello World

1. 什么是zig 先列出一段官方的介绍: Zig is a general-purpose programming language and toolchain for maintaining robust, optimal, and reusable software. 大概意思就是说&#xff1a; Zig是一种通用编程语言和工具链&#xff0c;用于维护健壮、最佳和可重用的软件。 官…

电脑计算机xinput1_3.dll丢失的解决方法分享,四种修复手段解决问题

日常生活中可能会遇到的问题——xinput1_3.dll丢失的解决方法。我相信&#xff0c;在座的很多朋友都曾遇到过这个问题&#xff0c;那么接下来&#xff0c;我将分享如何解决这个问题的解决方法。 首先&#xff0c;让我们来了解一下xinput1_3.dll文件。xinput1_3.dll是一个动态链…

服务注册发现_高可用Eureka注册中心搭建

在微服务架构这样的分布式环境中&#xff0c;我们需要充分考虑发生故障的情况&#xff0c;所以在生产环境中必须对各个组件进行高可用部署&#xff0c;对于微服务如此&#xff0c;对于服务注册中心也一样。 问题&#xff1a; Spring-Cloud为基础的微服务架构&#xff0c;所有的…

vulhub venom

文章目录 靶场环境信息收集ftp服务二、信息利用三、任意文件上传三 sudo提权靶场环境 `vmware 靶场信息:https://www.vulnhub.com/entry/venom-1,701/ 下载地址:https://download.vulnhub.com/venom/venom.zip 新建虚拟机打开下载后的ovf文件 遇见导入失败合规性检查时,重试…