网络——协议定制

目录

再谈协议

结构化数据

计算器

封装socket

封装TcpServer服务器

协议定制

问题

解决方法

守护进程

setsid

守护进程函数

json库

序列化和反序列化


再谈协议

        原来不是说过什么是协议吗,协议不就是通信双方必须遵守的约定吗,那这就完了吗?那肯定是没有。

        网络协议就是双方必须遵守的约定,使数据在网络上能够传输,网络通信的双方必须遵循相同的规则,而协议就需要通过计算机语言的方式表示出来,只有通信计算机双方都遵守相同的协议才能互相通信交流。

        我们使用的TCP/UDP接口,都是传输层对TCP/UDP的包装,以文件的形式让我们可以进行应用层编程,所以我们原来写过的代码都是应用层开发,但是并没有涉及到协议的内容,只是用套接字进行收发工作。


结构化数据

        那如果我想要传输一些结构化的数据,这时就不能将数据一个个发送到网络中。可能光说要传入结构化的数据不好理解,我们接下来就通过一个例子来说一下。

        如果我想要实现一个网络版本的计算器,客户端发送数据,服务端处理数据,客户端需要发送两个操作数和一个操作符,而这就是一组结构化的数据,如果客户端一个一个地发送,那么服务端就不知道客户端需要做什么,所以我们通常是这样做的。

        有了这个序列化和反序列化的过程,就相当与加了一层软件层,实现业务逻辑和网络通信一定程度上的解耦。

        结构体中的字段就是协议的一部分,就可以规定a必须要在左边,b必须要做右边。


计算器

封装socket

        我们已经写过TCP服务器了,所以我们再来给他封装一下,主要的代码都没有改,把socket、bind、listen、accept、connect函数封装了一下,也为了以后更方便地使用。

// Sock.hpp
#include <iostream>
#include <string>

#include <cerrno>
#include <cstdio>
#include <cstring>
#include <cassert>

#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "log.hpp"

class Sock
{
public:
    const static int g_backlog = 20;
public:
    Sock(uint16_t port, std::string ip = "")
    {}

    int Socket()
    {
        // 创建socket
        int listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "sock: %d", listensock);
        return listensock;
    }

    void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
    {
        // bind
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = ip.empty() ? INADDR_ANY : inet_addr(ip.c_str());
        if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }
        logMessage(NORMAL, "bind success ...");
    }

    void Listen(int sock)
    {
        // tcp面向连接的,通信前要先建立连接,设置为监听状态,listen
        if (listen(sock, g_backlog) < 0)
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "listen success ...");
    }

    // const std::T& :输入型参数
    // std::T* :输出型参数
    // std::T& :输入输出型参数
    int Accept(int listensock, std::string* ip, uint16_t* port)
    {
        // 获取连接accept
        struct sockaddr_in src; // 客户端addr
        socklen_t len = sizeof(src);
        int servicesock = accept(listensock, (struct sockaddr *)&src, &len);
        if (servicesock < 0)
        {
            logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
            return -1; // 获取失败
        }
        if (port) 
            *port = ntohs(src.sin_port);
        if (ip)
            *ip = inet_ntoa(src.sin_addr);
        return servicesock;
    }

    bool Connect(int sock, const std::string& server_ip, const uint16_t& server_port)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(server_port);
        server.sin_addr.s_addr = inet_addr(server_ip.c_str());
        if (connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0) return true;
        else false;
    }

    ~Sock()
    {}
};

封装TcpServer服务器

        通过构造函数就可以完成创建套接字、bind、listen的任务,并且拿到了listensock,func_t就是函数对象,以后有任务就放到数组中。

// TcpServer.hpp
#include "Sock.hpp"

#include <functional>

namespace dsh_tcpserver
{
    typedef std::function<void(int)> func_t;

    class TcpServer
    {
    public:
        TcpServer(const uint16_t &port, const std::string &ip = "0.0.0.0")
            :sock_(port)
        {
            listensock_ = sock_.Socket();
            sock_.Bind(listensock_, port, ip);
            sock_.Listen(listensock_);
        }
        ~TcpServer()
        {
            if (listensock_ >= 0)
                close(listensock_);
        }
    private:
        int listensock_;
        Sock sock_;
        std::vector<func_t> func_;
    };

}

         服务器的初始化工作已经做好后就是启动了,通过Accept拿到了对端IP和端口号,之后还是创建线程,让线程去做这些任务。

// TcpServer.hpp
class ThreadData
{
public:
    ThreadData(int sock, TcpServer* server)
        :sock_(sock)
        ,server_(server)
    {}
    ~ThreadData(){}
public:
    int sock_;
    TcpServer* server_;
};

class TcpServer
{
private:
    static void *ThreadRoutine(void *args)
    {
        pthread_detach(pthread_self()); // 线程分离
        ThreadData* td = static_cast<ThreadData*>(args); // 强制转化
        td->server_->Excute(td->sock_); // 线程调用类内成员函数
        close(td->sock_); // 调用完后关闭sock并释放td
        delete td;
        return nullptr;
    }

public:
    void BindService(func_t func) // 绑定执行函数
    {
        func_.push_back(func);
    }

    // 执行对应的函数
    void Excute(int sock) 
    {
        for (auto& f : func_)
        {
            f(sock);
        }
    }

    void Start()
    {
        while (true)
        {
            std::string clientip;
            uint16_t clientport;
            int sock = sock_.Accept(listensock_, &clientip, &clientport); // 接收的sock
            if (sock == -1)
                continue;
            logMessage(NORMAL, "accept new link success, sock: %d", sock);
            pthread_t tid;
            ThreadData* td = new ThreadData(sock, this);
            pthread_create(&tid, nullptr, ThreadRoutine, (void*)td);
        }
    }
};

        当我们运行server.cc的时候,拿到TcpServer就可以把要执行的函数绑定到函数对象数组中,从对端获取连接成功后,创建线程,把需要的数据放在ThreadData中,执行对应的ThreadRoutine函数,不想对线程join,直接设置线程分离,通过传入的this指针指向的TcpServer执行函数数组中的函数,执行完后,关闭此次连接的sock,释放td。

// CalServer.cc
static void Usage(const std::string& process)
{
    std::cout << "\nUsage: " << process << " port\n" << std::endl;
}

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }
    std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1])));
    server->BindService(/*要放入函数数组中的函数*/); 
    server->Start();

    return 0;
}

这就把网络功能和服务进行了解耦,在server.cc中就可以这样写。

协议定制

        因为我们要实现的是一个网络计算器,一定要有我们自己定制的协议。

        想要从客户端发送一个Request,将这个Request序列化为字节流式的,通过网络发送到服务端,服务端把字节流式的数据反序列化为一个Request,处理这个Request。

        Request处理完后就需要把结果Response返还给客户端,将Response序列化为字节流式的数据,通过网络返还给客户端,客户端拿到字节流式的数据反序列化为Response。

// Protocol.hpp

#define SPACE " "
#define SPACE_LEN strlen(SPACE)

class Request
{
public:
    std::string Serialize() // 序列化 -> "x_ op_ y_"
    {
        std::string str;
        str = std::to_string(x_);
        str += SPACE;
        str += op_;
        str += SPACE;
        str += std::to_string(y_);
        return str;
    }

    bool Deserialize(const std::string& str) // 反序列化 "x_ op_ y_" -> 结构化数据
    {
        std::size_t left = str.find(SPACE);
        if (left == std::string::npos) return false;
        std::size_t right = str.rfind(SPACE);
        x_ = stoi(str.substr(0, left));
        y_ = stoi(str.substr(right + SPACE_LEN));
        op_ = str[left + SPACE_LEN];
        return true;
    }

public:
    Request(){}
    Request(int x, int y, char op)
        :x_(x)
        ,y_(y)
        ,op_(op)
    {}
    ~Request(){}
public:
    int x_;
    int y_;
    char op_;
};

class Response
{
public:
    std::string Serialize() // 序列化 -> "code_ result_"
    {
        std::string s;
        s = std::to_string(code_);
        s += SPACE;
        s += std::to_string(result_);
        return s;
    }

    bool Deserialize(const std::string& str) // 反序列化 "code_ result_" -> 结构化数据
    {
        std::size_t pos = str.find(SPACE);
        if (pos == std::string::npos) return false;
        code_ = stoi(str.substr(0, pos));
        result_ = stoi(str.substr(pos + SPACE_LEN));
        return true;
    }
public:
    Response(){}
    Response(int result, int code)
        :result_(result)
        ,code_(code)
    {}
    ~Response(){}
public:
    int result_; // 结果
    int code_;   // 结果状态码
};

// 封装一下recv和send
std::string Recv(int sock)
{
    char inbuffer[1024];
    ssize_t s = recv(sock, inbuffer, sizeof(inbuffer), 0);
    if (s > 0)
    {
        inbuffer[s] = 0;
        return inbuffer;
    }
    else 
        return "";
}

void Send(int sock, const std::string str)
{
    send(sock, str.c_str(), str.size(), 0);
}
// CalServer.cc

static Response calculatorHelp(const Request &req) // 拿到结构化的请求,返回结构化的数据
{
    Response resp(0, 0);
    switch (req.op_)
    {
    case '+':
        resp.result_ = req.x_ + req.y_;
        break;
    case '-':
        resp.result_ = req.x_ - req.y_;
        break;
    case '*':
        resp.result_ = req.x_ * req.y_;
        break;
    case '/':
        if (0 == req.y_) resp.code_ = 1;
        else resp.result_ = req.x_ / req.y_;
        break;
    case '%':
        if (0 == req.y_) resp.code_ = 2;
        else resp.result_ = req.x_ % req.y_;
        break;
    default:
        resp.code_ = 3;
        break;
    }
    return resp;
}

void calculator(int sock)
{
    while (true)
    {
        std::string str = Recv(sock); // 读到了一个请求
        if (str.empty()) break;
        Request req;
        req.Deserialize(str);                      // 反序列化
        Response resp = calculatorHelp(req);       // 得到一个结构化的结果
        std::string respString = resp.Serialize(); // 结果序列化

        // 如果正在写入的时候对端关闭了,那就不能再写入了,此时OS回向进程发送SIGPIPE信号,所以我们设置忽略即可
        Send(sock, respString);
    }
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }

    // 通常我们的服务器会忽略这个信号,防止出现非法写入的问题
    signal(SIGPIPE, SIG_IGN);

    std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1])));
    server->BindService(calculator);
    server->Start();

    return 0;
}

        那什么是协议定制呢,这就类似于一个约定,我们规定发过来的数据必须是左操作数 操作符 右操作数。返回的数据必须有code来检测此次返还的数据是否是有效的:

  • 0表示成功
  • 1表示除0错误
  • 2表示模0错误
  • 3表示非法操作

最后我们在把客户端写完就可以了。

// CalClient.cc

static void Usage(const std::string &process)
{
    std::cout << "\nUsage: " << process << " ip port\n"
              << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    std::string server_ip = argv[1];
    uint16_t server_port = atoi(argv[2]);
    Sock sock;
    int sockfd = sock.Socket(); // 创建套接字
    if (!sock.Connect(sockfd, server_ip, server_port)) // 连接
    {
        std::cerr << "Connect error" << std::endl;
    }

    // 通信
    std::string s = "";
    while (true)
    {

        std::cout << "请输入(格式: x op y): ";
        std::getline(std::cin, s);
        if (s == "quit")
        {
            close(sockfd);
            break;
        }
        Request req;
        req.Deserialize(s);
        s = req.Serialize();
        Send(sockfd, s);

        s = Recv(sockfd);
        Response resp;
        resp.Deserialize(s);
        std::cout << "code: " << resp.code_ << std::endl;
        std::cout << "result: " << resp.result_ << std::endl;
    }

    return 0;
}

问题

        虽然我们的代码写完了,但是还是存在一定的问题的,虽然我规定了数据的格式,返还的格式,但是我们要明确的一点就是,TCP是面向字节流的,我如何确定一个发送过来的数据就是完整的,如果此时缺失了几个字符,那我们上面写的代码就有问题了。

        TCP不像UDP,UDP是面向数据报的,就是对端发一个我就接收一个,这一定是一个完整的报文,但是如果使用TCP,这一次我可能发送了一长串的请求,格式也是正确的,那我们该怎么做呢?

        那我们就要再说一下什么是TCP了,TCP是传输控制协议

        我们只是把我们自己定义的缓冲区中的数据放到了传输层给我们提供的发送缓冲区中,说白了我们曾经说过的IO接口本质上都是拷贝函数,只是你以为调用了send或者write就发送到了对方的主机中,这是不对的。

        所以调用send就一直往缓冲区中拷贝,什么时候发不是应用层考虑的事情,当服务端拿到了数据时就从接收缓冲区中向我们定义的缓冲区中拷贝,拷了多少个也不知道,所以根本不能确定读到的是一个完整的报文。所以TCP发送的次数和TCP接收的次数没有关系,这就是面向字节流

解决方法

        要解决这个文件我就要再来看看我们定制的协议。

// 根据我们定制的协议和TCP发送的数据可能是这样的
x_ op_ y_x_ op_ y_x_ op_ y_x_ op_ y_

        所以我们要重新定制协议,我们规定:

// 每个完整报文的格式必须是这样的:
len\r\nx_ op_ y_\r\n

        len就是我们读取到一个完整报文的长度,为了更好的处理,我们给报文中加入\r\n这种特殊字符对内容进行区分。

        有了这个约定我们还要注意的是,这一次我们可能读到的数据是半个,这时候我们就得等一等,等到下次再读取到完整的报文时在做处理,所以我们需要一个缓冲区来记录这一次读到的数据。

bool Recv(int sock, std::string* out) // 修改一下Recv
{
    char buffer[1024];
    ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
    if (s > 0)
    {
        buffer[s] = 0;
        *out += buffer;
    }
    else if (s == 0)
    {
        std::cout << "client quit" << std::endl;
        return false;
    }
    else
    {
        std::cout << "recv error" << std::endl;
        return false;
    }
    return true;
}

#define SEP "\r\n"
#define SEP_LEN strlen(SEP)
// len\r\nx_ op_ y_\r\n
std::string Decode(std::string &buffer)
{
    std::size_t pos = buffer.find(SEP);
    if (pos == std::string::npos) return "";
    int size = atoi(buffer.substr(0, pos).c_str()); // 拿到len
    int surplus = buffer.size() - pos - 2 * SEP_LEN; // 去掉len和两个标记符的长度
    if (surplus >= size)
    {
        // 至少有一个完整的报文
        buffer.erase(0, pos + SEP_LEN); // 去掉len\r\n
        std::string s = buffer.substr(0, size); // 提取x_ op_ y_
        buffer.erase(0, size + SEP_LEN); // 去掉x_ op_ y_\r\n
        return s;
        // 后面就是新的报文了  
    }
    else return ""; // 没有一个完整的报文
}

void calculator(int sock)
{
    // 1.读取数据
    std::string inbuffer;
    while (true)
    {
        bool res = Recv(sock, &inbuffer); // 读到了一个请求
        if (!res)
            break;
        // 读取成功

        // 获得一个完整的报文
        std::string package = Decode(inbuffer);
        if (package.empty())
            continue;
        else
        {
            Request req;
            req.Deserialize(package);                  // 反序列化
            Response resp = calculatorHelp(req);       // 得到一个结构化的结果
            std::string respString = resp.Serialize(); // 结果序列化

            // 如果正在写入的时候对端关闭了,那就不能再写入了,此时OS回向进程发送SIGPIPE信号,所以我们设置忽略即可
            Send(sock, respString);
        }
    }
}

        现在的流程就是这样的,我自己定义一个缓冲区,作为输出型参数传入Recv,这样就不需要Recv返回一个string了,只需要判断是否读取正确,而且调用recv接口的时候也是往我们自己定义的缓冲区中+=,这样就可以把传输层的缓冲区中的数据添加到后面,然后我们通过Decode函数拿到一个完整的报文,也要判断一下是否是一个完整的报文,之后的操作就是反序列化,处理数据返回Response结果,这一步也是需要重新定制协议的。

len\r\ncode_ result_\r\n
// len\r\nXXXXXXX\r\n
std::string Encode(std::string& s)
{
    std::string len = std::to_string(s.size());
    std::string new_package = len;
    new_package += SEP;
    new_package += s;
    new_package += SEP;
    return new_package;
}

void calculator(int sock)
{
    // 1.读取数据
    std::string inbuffer;
    while (true)
    {
        // 1.读取数据
        bool res = Recv(sock, &inbuffer); // 读到了一个请求
        if (!res)
            break;
        std::cout << "inbuffer: " << inbuffer << std::endl;

        // 2.协议解析,获得一个完整的报文
        std::string package = Decode(inbuffer);
        if (package.empty())
            continue;
        std::cout << "package: " << package << std::endl;

        Request req;
        // 3.反序列化
        req.Deserialize(package);         
        // 4.业务逻辑,得到一个结构化的结果         
        Response resp = calculatorHelp(req);       
        // 5.结果序列化
        std::string respString = resp.Serialize(); 
        std::cout << "respString: " << respString << std::endl;
        // 如果正在写入的时候对端关闭了,那就不能再写入了,此时OS回向进程发送SIGPIPE信号,所以我们设置忽略即可
        
        // 6.在发送之前也要对结果加上len\r\ncode_ result_\r\n
        respString = Encode(respString);
        std::cout << "encode: " << respString << std::endl; 

        // 7.发送也是有问题的,这个到后面再说
        Send(sock, respString);
    }
}

客户端当然也是要改的。

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    std::string server_ip = argv[1];
    uint16_t server_port = atoi(argv[2]);
    Sock sock;
    int sockfd = sock.Socket();                        // 创建套接字
    if (!sock.Connect(sockfd, server_ip, server_port)) // 连接
    {
        std::cerr << "Connect error" << std::endl;
        exit(2);
    }

    // 通信
    bool quit = false;
    std::string s = "";
    while (true)
    {
        // 1.获取
        std::cout << "请输入(格式: x op y): ";
        std::getline(std::cin, s);
        if (s == "quit")
        {
            close(sockfd);
            break;
        }
        Request req;
        req.Deserialize(s);
        // 以上只是为了方便解析字符串才这样写的

        // 2.序列化
        s = req.Serialize();
        // 3.添加报头
        s = Encode(s);
        // 4.发送给服务端
        Send(sockfd, s);

        // 5.读取
        std::string buffer;
        while (true)
        {
            bool ret = Recv(sockfd, &buffer);
            if (!ret)
            {
                quit = true;
                break;
            }
            // 6.去掉报头
            std::string package = Decode(buffer);
            if (package.empty()) continue;
            Response resp;
            resp.Deserialize(package);
            std::cout << "code: " << resp.code_ << std::endl;
            std::cout << "result: " << resp.result_ << std::endl;
            break;
        }
    }
    close(sockfd);
    return 0;
}

这样我们的网络版本的计算器就已经完成了,输入输出的格式也可以自己修改。


守护进程

        在这之前,我们运行起来的服务器都是前台进程

  • 前台进程就是和终端相关联的进程

        意思就是这个进程如果可以处理我输入的命令,那他就是一个前台进程,所以bash就是一个前台进程,我们可以在命令行中输入命令,并显示处理的结果。

        如果我们把服务器启动,这是在输入ls这样的命令就不会再生效了,所以此时的bash就不再是前台进程了,而服务端就变成了前台进程。

  • 我们使用shell登录的时候,只允许一个前台进程和多个后台进程
  • 进程除了有pid和ppid,还要有组id
  • 在命令行中通过管道同时启动多个进程,这些进程就是兄弟关系,父进程都是bash

        在这之中有一行PGID,这就是组id,而且组id就是第一个进程的id。

  • 同时被创建的多个进程可以成为一个进程组,组长就是第一个进程

        在这之中还有一列就是SID,这叫做会话id。

  • 任何一次登录的用户需要多个进程或进程组,来为用户提供服务(bash),用户也可以启动进程和进程组,那么这些进程或者服务都属于这一次会话

        当我们登录后,系统会帮我们创建一次会话,创建bash和终端,之后就可以启动自己的进程或者进程组。退出登录后,系统要释放此次会话的资源,如果此时会话中还有正在运行的前台进程,那这个进程就有可能也要被释放,所以我们想要一个服务器一直运行,它就不能随着会话退出而终止。

        我们使用Windows操作系统的时候,关机的位置会有一个注销的选项,注销就相当于退出登录,终止此次启动的服务和进程。

        所以如果想要让服务器不随会话退出,那就需要把它拉出来,让他自成一个会话,原来的会话退出就不会影响它了,所以这就叫做守护进程。

setsid

返回值:成功返回这个进程的sid,失败返回-1

【注意】:setsid要成功调用,必须保证当前进程不是进程组的组长,进程组的组长就是启动的第一个进程,那让它不是第一个不就可以了吗。

守护进程函数

        我们可以在服务器启动之前,调用我们自己写的函数,通过这个函数执行一系列操作。

  1. 忽略SIGPIPE、SIGCHLD信号
  2. fork创建子进程,让父进程直接退出
  3. 调用setsid
  4. 标准输入、输出、错误的重定向,它已经是一个独立的进程了,所以不能再向显示器打印

        我们还要知道的就是在/dev目录中有一个null文件。

        如果输出重定向到这个文件中,数据就会自动丢弃,输入重定向也读不到任何数据。

// Daemon.hpp

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

void MyDaemon()
{
    // 1. 忽略信号,SIGPIPE,SIGCHLD
    signal(SIGPIPE, SIG_IGN);
    signal(SIGCHLD, SIG_IGN);

    // 2. 不要让自己成为进程组组长
    if (fork() > 0) exit(0); // 父进程直接终止
    // 那子进程就一定不是组长

    // 3. 调用setsid
    setsid();

    // 4. 标准输入、输出、错误的重定向,它已经是一个独立的进程了,所以不能再向显示器打印
    int devnull = open("/dev/null", O_RDONLY | O_WRONLY);
    if (devnull > 0)
    {
        dup2(devnull, 0);
        dup2(devnull, 1);
        dup2(devnull, 2);
        close(devnull);
    }
}

        所以这就是我们为什么要写日志文件的原因,不能向显示器中打印,那就要向文件中打印。

        当我们把服务端以守护进程的方式启动就会变成这样,pid、pgid、sid都是一样的,证明它自成一个会话,而它的ppid也变成了bash,所以守护进程也是孤儿进程的一种。


json库

        我们上面自己写的字符串解析的方式一定会有问题的,所以我们也可以使用别人的库,使用sudo yum -y install jsoncpp-devel下载这个库,下面我们就来看看怎么使用吧,这个库中的数据是key-value结构的。

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>

using namespace std;

int main()
{
    int x = 10;
    int y = 20;
    char z = '+';

    Json::Value root; // 万能对象
    root["x"] = x;    // key-value
    root["y"] = y;
    root["op"] = z;

    Json::StyledWriter writer; // 以某种风格写入
    string s = writer.write(root);
    cout << s << endl;

    return 0;
}

使用StyledWriter输出的形式就是上面这样的,如果使用FastWriter就会是下面这样的。

序列化和反序列化

        有了这个库,我们就可以重新编写一下序列化和反序列化的代码,虽然引入了库,但是需要改的地方只有序列化和反序列化,其他地方都不会变,添加报头和标记符都没有变。

#include <jsoncpp/json/json.h>

class Request
{
public:
    std::string Serialize() // 序列化
    {
        Json::Value root; // 万能对象
        root["x"] = x_;
        root["y"] = y_;
        root["op"] = op_;
        Json::FastWriter writer;
        return writer.write(root);
    }

    bool Deserialize(const std::string& str) // 反序列化 
    {
        Json::Value root;
        Json::Reader reader;
        reader.parse(str, root);
        x_ = root["x"].asInt();
        y_ = root["y"].asInt();
        op_ = root["op"].asInt();
        return true;
    }

public:
    Request(){}
    Request(int x, int y, char op)
        :x_(x)
        ,y_(y)
        ,op_(op)
    {}
    ~Request(){}
public:
    int x_;
    int y_;
    char op_;
};

class Response
{
public:
    std::string Serialize() // 序列化
    {
        Json::Value root; // 万能对象
        root["code"] = code_;
        root["result"] = result_;
        Json::FastWriter writer;
        return writer.write(root);
    }

    bool Deserialize(const std::string& str) // 反序列化
    {
        Json::Value root;
        Json::Reader reader;
        reader.parse(str, root);
        code_ = root["code"].asInt();
        result_ = root["result"].asInt();
        return true;
    }
public:
    Response(){}
    Response(int result, int code)
        :result_(result)
        ,code_(code)
    {}
    ~Response(){}
public:
    int result_; // 结果
    int code_;   // 结果状态码
};

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

大语言模型---强化学习

本文章参考&#xff0c;原文链接&#xff1a;https://blog.csdn.net/qq_35812205/article/details/133563158 SFT使用交叉熵损失函数&#xff0c;目标是调整参数使模型输出与标准答案一致&#xff0c;不能从整体把控output质量 RLHF&#xff08;分为奖励模型训练、近端策略优化…

Vue系列——数据对象

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>el:挂载点</title> </head> <body&g…

在国内如何给Chrome浏览器安装Vue Devtools插件

Vue Devtools是一个强大的Chrome浏览器插件&#xff0c;为Vue.js开发者提供了丰富的调试和性能优化功能。然而&#xff0c;在国内由于无法访问Google Chrome Web Store&#xff0c;安装Vue Devtools需要使用其他方法。以下是安装步骤&#xff1a; 步骤一&#xff1a;下载Vue D…

逆向分析之antibot

现在太卷了&#xff0c;没资源&#xff0c;很难接到好活&#xff0c;今天群里看到个单子&#xff0c;分析了下能做&#xff0c;结果忙活了一小会&#xff0c;幸好问了下&#xff0c;人家同时有多个人再做&#xff0c;直接就拒绝再继续了。就这次忘了收定金了&#xff0c;所以原…

输出1到10的阶乘--C语言

#include<stdio.h> int fac(int n){if(n<1){return 1;}elsereturn fac(n-1)*n; } int main(){int i, result;for(i1;i<10;i){resultfac(i);printf("%d!%d\n",i,result);}} 输出结果&#xff1a;

AI跟踪报道第35期-新加坡内哥谈技术-本周AI新闻:终于打败ChatGPT的机器人

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

二维码门楼牌管理应用平台:创新管理与服务的全新方式

文章目录 前言一、二维码门楼牌管理应用平台的建设背景二、二维码门楼牌管理应用平台的功能特点三、切换功能在城市管理中的应用四、二维码门楼牌管理应用平台的未来展望 前言 在数字化时代的浪潮中&#xff0c;二维码技术以其便捷、高效的特点&#xff0c;正逐渐渗透到我们生…

C语言每日一题

1.题目 二.分析 本题有两点需要注意的&#xff1a; do-while循环 &#xff1a;在判断while条件前先执行一次do循环static变量 &#xff1a;程序再次调用时static变量的值不会重新初始化&#xff0c;而是在上一次退出时的基础上继续执行。for( i 1; i < 3; i )将调用两次…

容器四(Map 接口)

目录 HashMap 和 HashTable Map 接口中的常用方法 HashMap 底层实现 Hashmap 基本结构 存储数据过程 put(key,value) 取数据过程 get(key) 扩容问题 JDK8 将链表在大于 8 情况下变为红黑二叉树 Map 就是用来存储“键(key)&#xff0d;值(value) 对”的。 Map 类中存储的…

Vue.js前端开发零基础教学(四)

学习目标&#xff1a; 熟悉选项式API和组合式API&#xff0c;能够说出选项式API和组合式API的区别 掌握注册组件的方法&#xff0c;能够运用全局注册或者局部注册的方式完成组件的注册 掌握父组件向子组件传递数据的方法&#xff0c;能够使用props实现数据传递等等 前言 在学习…

【Leetcode每日一题】 递归 - 求根节点到叶节点数字之和(难度⭐⭐)(47)

1. 题目解析 题目链接&#xff1a;129. 求根节点到叶节点数字之和 这个问题的理解其实相当简单&#xff0c;只需看一下示例&#xff0c;基本就能明白其含义了。 2.算法原理 递归函数设计&#xff1a; 我们设计了一个递归函数 int dfs(TreeNode* root, int num)&#xff0c;其…

【42 可视化大屏 | 某瓣电影Top250数据分析可视化大屏】

文章目录 &#x1f3f3;️‍&#x1f308; 1 普版大屏&#x1f3f3;️‍&#x1f308;2 Flask版大屏&#x1f3f3;️‍&#x1f308;3 FlaskMysql版大屏&#x1f3f3;️‍&#x1f308; 4. 可视化项目源码数据 大家好&#xff0c;我是 &#x1f449;【Python当打之年(点击跳转)…

PonyAi Planning-横纵向轨迹规划

PonyAi Planning-横纵向轨迹规划 轨迹规划的探索和挑战 轨迹规划的概念安全舒适两不误&#xff1a;探讨优化算法在规划控制中的应用 轨迹规划的概念 决策 横向规划 纵向规划 优化算法在规划&#xff08;Planning&#xff09;中的应用 附赠自动驾驶学习资料和量产经验…

七、函数的使用方法

函数的调用 nameinput&#xff08;&#xff09;#输入参数并赋值name print&#xff08;name&#xff09;#d打印name 格式&#xff1a;返回值函数名&#xff08;参数&#xff09; def get_sum(n):#形式参数计算累加和:param n::return: sumsum0for i in range(1,n1):sumiprint…

Linux编译器-gcc/g++/gdb使用

Linux编译器-gcc/g/gdb使用 一、背景知识二、 gcc如何完成2.1 预处理(进行宏替换)2.2 编译&#xff08;生成汇编&#xff09;2.3 汇编&#xff08;生成机器可识别代码&#xff09;2.4 连接&#xff08;生成可执行文件或库文件&#xff09; 三、函数库四、gcc选项五、gdb5.1 背景…

Node.js的Event Loop:六个阶段详解

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

Vue2(十二):Vuex环境搭建、Vuex工作原理、几个配置项

一、Vuex 1.概念 专门在Vue中实现集中式状态&#xff08;数据&#xff09;管理的一个Vue插件&#xff08;use引入&#xff09;&#xff0c;对vue应用中多个组件的共享状态进行集中式的管理&#xff08;读&#xff0f;写&#xff09;&#xff0c;也是一种组件间通信的方式&…

JavaSE:继承和多态(下篇)

目录 一、前言 二、多态 &#xff08;一&#xff09;多态的概念 &#xff08;二&#xff09;多态实现条件 &#xff08;三&#xff09;多态的优缺点 三、重写 &#xff08;一&#xff09;重写的概念 &#xff08;二&#xff09;重写的规则 &#xff08;三&#xff09;重…

Vue-vue3

一、Vue3简介二、Vue3有那些优化性能的提升源码升级拥抱TypeScript新的特性 三、创建Vue3.0工程四、Vue3工程结构&#xff08;使用cli创建的vue3&#xff09;五、常用的Composition API&#xff08;组合式API&#xff09;setupsetup的两个注意点 ref函数reactive函数Vue3.0中的…

Java实验报告2

一、实验目的 本实验为Java课程的第二次实验&#xff0c;其主要目的如下&#xff1a; 理解继承和多态的概念&#xff1b; 掌握域和方法在继承中的特点&#xff1b; 掌握构造函数的继承和重载&#xff1b; 掌握this和super的用法&#xff1b; 二、实验原理 ​ 继承性是面…