【Linux】通过网络版计算器来认识协议

​🌠 作者:@阿亮joy.
🎆专栏:《学会Linux》
🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
在这里插入图片描述

目录

    • 👉再谈协议👈
      • 协议的概念
      • 结构化数据的传输
      • 序列化和反序列化
    • 👉网络版计算器👈
      • 协议定制
      • 日志功能
      • 套接字的封装
      • 服务端编写
      • 客户端编写
      • 进程相关概念补充
      • 进程守护进程化
      • 现成的协议方案JSON
    • 👉总结👈

👉再谈协议👈

协议的概念

网络通信中的协议是指在网络中进行数据传输时遵循的一些规则和标准,用于确保不同设备之间的通信能够顺利进行。协议的本质是软件,它最终是需要通过计算机语言(编码)的方式来表现出来,协议如何编写取决于我们的应用场景。

结构化数据的传输

通行双方在进行网络通信时:

  • 如果想要传输的数据是一个字符串,那么可以直接将这个字符串发送到网络中,此时对端从网络中获取到这个字符串就实现实现通信了。
  • 而如果想要传输的数据是一些结构化的数据,那么就无法直接将这些数据发送到网络中,需要先进行序列化再发送到网络中;然后对端从网络中获取到序列化的数据,并对这些数据进行反序列化就可以得到结构化的数据了。

序列化和反序列化

序列化和反序列化是计算机中常用的概念,用于在不同系统或网络之间传输数据或存储数据时进行格式转换。

序列化是指将对象或数据结构转换成字节流的过程,以便于在网络或存储设备上进行传输或存储。在序列化的过程中,会将对象或数据结构的属性或元素逐个转换成二进制格式,并将这些二进制数据组成一个连续的字节流,以便于传输或存储。

反序列化是指将序列化后的字节流转换成对象或数据结构的过程,以便于在程序中进行操作。在反序列化的过程中,会将字节流逐个读取,并将其转换成相应的对象属性或数据结构元素,以便于程序对其进行操作。

在网络通信中,客户端向服务器发送请求时,需要将请求对象序列化成字节流进行传输;服务器收到请求后,需要将接收到的字节流反序列化成请求对象进行处理。

注:序列化和反序列化可以让上层业务和网络传输进行一定程度的解耦。

在这里插入图片描述

👉网络版计算器👈

网络版计算器要实现的功能:我们需要客户端把数据和操作符发给服务器,然后由服务器进行计算,最后再把结果返回给客户端。为了实现这样的网络版计算器,我们就需要进行协议定制。

协议定制

  • 定义结构体来表示我们需要交互的信息,如客服端的请求中需要需要包含两个操作数和一个操作码,服务端的应答中需要包含表示计算结果的状态码和计算结果。
  • 发送数据时将这个结构体按照一个规则序列化成字符串,接收到数据的时候再按照相同的规则把字符串反序列化成结构体。
  • 对要发送的数据进行序列化后,还需要进行添加报头的操作。为什么要添加报头呢?因为添加报头可以解决黏包问题,以确保每次读取数据时读到的都是一个完整的报文。
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>

#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define SEP "\r\n"
#define SEP_LEN strlen(SEP)
#define SIZE 1024
#define MYSELF

class Request
{
public:
    Request() {}

    Request(int x, int y, char op)
        : _x(x)
        , _y(y)
        , _op(op)
    {}

    ~Request() {}

    std::string Serialize()
    {
    #ifdef MYSELF        
        std::string str = std::to_string(_x);
        str += SPACE;
        str += _op;
        str += SPACE;
        str += std::to_string(_y);
        return str;
    #else

    #endif
    }

    bool Deserialize(std::string& str)
    {
    #ifdef MYSELF
        size_t left = str.find(SPACE);
        if(left == std::string::npos)
            return false;
        size_t right = str.rfind(SPACE);
        if(right == std::string::npos)
            return false;

        if(left + SPACE_LEN >= str.size())
            return false;

        _x = atoi(str.substr(0, left).c_str());
        _y = atoi(str.substr(right + SPACE_LEN).c_str());
        _op = str[left + SPACE_LEN];
    #else

    #endif
    }

public:
    int _x;
    int _y;
    char _op;
};

class Response
{
public:
    Response() {}

    Response(int code, int ret, int x, int y, char op)
        : _code(code)
        , _ret(ret)
        , _x(x)
        , _y(y)
        , _op(op)
    {}

    ~Response() {}

    std::string Serialize()
    {
    #ifdef MYSELF
        std::string str = std::to_string(_code);
        str += SPACE;
        str += std::to_string(_ret);
        return str;
    #else

    #endif
    }

    bool Deserialize(const std::string& str)
    {
    #ifdef MYSELF
        size_t pos = str.find(SPACE);
        if(pos == std::string::npos)
            return false;
        _code = atoi(str.substr(0, pos).c_str());
        _ret = atoi(str.substr(pos + SPACE_LEN).c_str());
        return true;
    #else

    #endif
    }

public:
    int _code;
    int _ret;
    int _x;
    int _y;
    char _op;
};

bool Recv(int sock, std::string* out)
{
    char buffer[SIZE];
    ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
    if(s > 0)
    {
        buffer[s] = '\0';
        *out += buffer;
        return true;
    }
    else if(s == 0)
    {
        std::cout << "Client Quit!" << std::endl;
        return false;
    }
    else
    {
        std::cout << "Recv Error!" << std::endl;
        return false;
    }
}

void Send(int sock, const std::string& str)
{
    int n = send(sock, str.c_str(), str.size(), 0);
    if(n < 0)
        std::cout << "Send Error!" << std::endl;
}

// 去除报头
std::string Decode(std::string& buffer)
{
    size_t pos = buffer.find(SEP);
    if(pos == std::string::npos)
        return "";
    
    int size = atoi(buffer.substr(0, pos).c_str());
    int leftSize = buffer.size() - pos - 2 * SEP_LEN;
    if(leftSize >= size)
    {
        // 至少有一个完整的报文
        buffer.erase(0, pos + SEP_LEN);
        std::string s = buffer.substr(0, size);
        buffer.erase(0, size + SEP_LEN);
        return s;
    }
    else // 没有完整的报文,不进行解析
        return "";
}

// 添加报头
// 有效载荷长度\r\n有效载荷\r\n
std::string Encode(std::string& s)
{
    std::string newPackage = std::to_string(s.size());
    newPackage += SEP;
    newPackage += s;
    newPackage += SEP;
    return newPackage;
}

功能说明:

  • Request 和 Response 中都使用了条件编译,主要的目的是可以进行快速地进行协议方案的切花,其中 MYSELF 是我们自己定制的协议,else 是采用现成的协议方案。
  • Request 是客户端向服务端发起的请求,其中包含两个操作数 _x 和 _y,其中 _x 是左操作数,_y 是右操作数,_op 是操作符。
  • Response 是服务端给客户端的应答,其中包含 _code、_ret、_x、_y 和 _op,_code 表示计算结果的状态码,_ret 表示计算结果,_x、_y 和 _op 是客户端的请求字段。
  • _ret 只有在 _code 等于 0 的时候,才有意义。_code 等于 1,表示除零错误;_code 等于 2 时,表示模零错误;_code 等于 3 时,表示非法的操作符。
  • Request 和 Response 的序列化都是字段、空格再加字段,如 Request 的序列化,_x 转化成字符串,加上 SPACE,加上操作符 _op,再加上 SPACE,最后将 _y 转化成字符串并拼接上去,即可完成序列化。而 Request 和 Response 的反序列化就是字符串进行分析提取相应的字段。
  • 进行序列化后,还要进行报头的添加 Encode 形成一个完整的报文,然后将该报文发送给对端。一个完整的报文:有效载荷长度\r\n有效载荷\r\n。对端收到报文后,需要先去除报头 Decode,然后才能进行反序列化。
  • Recv 是将接收的数据直接添加到输出型参数 out 中,然后将该数据交给服务端。Decode 是对 Recv 接收到的数据进行分析,如果数据至少包含一个完整的报文,则返回一个完整的报文;否则返回空字符串。

日志功能

日志是计算机系统中的一种记录信息的机制,可以用来追踪系统运行的情况和出现问题时进行分析和调试。所以我们编写的网络版计算器也引入了之前写的日志组件。

#pragma once

#include <cstdio>
#include <cstdarg>
#include <string>
#include <iostream>
#include <ctime>

// 日志等级
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

#define LOGFILE "./Calculate.log"

const char* levelMap[] = 
{
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"
};

void logMessage(int level, const char* format, ...)
{
    // 只有定义了DEBUG_SHOW,才会打印debug信息
    // 利用命令行来定义即可,如-D DEBUG_SHOW
#ifndef DEBUG_SHOW
    if(level == DEBUG) return;
#endif

    char stdBuffer[1024];   // 标准部分
    time_t timestamp = time(nullptr);
    // struct tm *localtime = localtime(&timestamp);
    snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", levelMap[level], timestamp);

    char logBuffer[1024];   // 自定义部分
    va_list args;   // va_list就是char*的别名
    va_start(args, format); // va_start是宏函数,让args指向参数列表的第一个位置
    // vprintf(format, args); // 以format形式向显示器上打印参数列表
    vsnprintf(logBuffer, sizeof logBuffer, format, args);

    va_end(args);   // va_end将args弄成nullptr

    FILE *fp = fopen(LOGFILE, "a");
    // printf("%s%s\n", stdBuffer, logBuffer);
    fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
    fclose(fp);
}

套接字的封装

#pragma once

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>
#include <string>
#include <cstring>
#include "Log.hpp"

class Sock
{
private:
    const static int backlog = 20;

public:
    Sock() {}

    // 返回值是创建的套接字
    int Socket()
    {
        int sock = socket(AF_INET, SOCK_STREAM, 0);
        if (sock < 0)
        {
            logMessage(FATAL, "Create Socket Error! Errno:%d Strerror:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "Create Socket Success! Socket:%d", sock);
        return sock;
    }

    // 绑定端口号
    void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = inet_addr(ip.c_str());
        if (bind(sock, (struct sockaddr *)&local, sizeof local) < 0)
        {
            logMessage(FATAL, "Bind Error! Errno:%d Strerror:%s", errno, strerror(errno));
            exit(3);
        }
    }

    // 将套接字设置为监听套接字
    void Listen(int listenSock)
    {
        if (listen(listenSock, backlog) < 0)
        {
            logMessage(FATAL, "Listen Error! Errno:%d Strerror:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "Init Server Success!");
    }

    // 接收链接,返回值是为该连接服务的套接字
    // ip和port是输出型参数,返回客户端的ip和port
    int Accept(int listenSock, std::string *ip, uint16_t *port)
    {
        struct sockaddr_in src;
        socklen_t len = sizeof(src);
        int serviceSock = accept(listenSock, (struct sockaddr *)&src, &len);
        if (serviceSock < 0)
        {
            logMessage(FATAL, "Accept Error! Errno:%d Strerror:%s", errno, strerror(errno));
            return -1;
        }
        if (ip)
            *ip = inet_ntoa(src.sin_addr);
        if (port)
            *port = ntohs(src.sin_port);
        return serviceSock;
    }

    // 发起连接
    bool Connet(int sock, const std::string &serverIP, const int16_t &serverPort)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof server);
        server.sin_family = AF_INET;
        server.sin_port = htons(serverPort);
        inet_pton(AF_INET, serverIP.c_str(), &server.sin_addr);

        if (connect(sock, (struct sockaddr *)&server, sizeof server) == 0)
            return true;
        else
            return false;
    }

    ~Sock() {}
};

服务端编写

TcpServer.hpp

#pragma once

#include "Sock.hpp"
#include <functional>
#include <vector>
#include <unistd.h>
#include <pthread.h>

using func_t = std::function<void(int)>;

class TcpServer; // 类型声明

class ThreadData
{
public:
    ThreadData(int sock, TcpServer* ptr)
        : _sock(sock)
        , _ptr(ptr)
    {}

    ~ThreadData() {}

public:
    int _sock;
    TcpServer* _ptr;
};

class TcpServer
{
public:
    TcpServer(const uint16_t& port, const std::string& ip = "0.0.0.0")
    {
        _listenSock = _sock.Socket();
        _sock.Bind(_listenSock, port, ip);
        _sock.Listen(_listenSock);
    }

    ~TcpServer()
    {
        if(_listenSock >= 0) close(_listenSock);
    }

    void BindService(func_t func)
    {
        _func.push_back(func);
    }

    void Start()
    {
        while(true)
        {
            std::string clientIP;
            uint16_t clientPort;
            int sock = _sock.Accept(_listenSock, &clientIP, &clientPort);
            if(sock == -1) continue; // 获取连接失败
            logMessage(NORMAL, "Create A New Link! Socket:%d", sock);
            pthread_t tid;
            ThreadData* td = new ThreadData(sock, this);
            // 创建线程完成用户的请求
            pthread_create(&tid, nullptr, ThreadRoutine, (void*)td);
        }
    }

    void ExcuteService(int sock)
    {
        for(auto& f : _func)
        {
            f(sock); // 执行服务端绑定的每一个服务
        }
    }

private:
    static void* ThreadRoutine(void* args)
    {
        pthread_detach(pthread_self()); // 线程分离
        ThreadData* td = static_cast<ThreadData*>(args);
        td->_ptr->ExcuteService(td->_sock);
        close(td->_sock); // 服务完成后关闭文件描述符
        delete td;
        return nullptr;
    }   

private:
    Sock _sock;
    int _listenSock;
    std::vector<func_t> _func;
    // std::unordered_map<std::string, func_t> _func; 
};

CalServer.cc

#include "TcpServer.hpp"
#include "Protocol.hpp"
#include <memory>
#include <signal.h>

static void Usage(const std::string& proc)
{
    std::cout << "\nUsage: " << proc << " Port" << std::endl;
}

static Response CalculatorHelper(const Request &req)
{
    Response resp(0, 0, req._x, req._y, req._op);
    switch (req._op)
    {
    case '+':
        resp._ret = req._x + req._y;
        break;
    case '-':
        resp._ret = req._x - req._y;
        break;
    case '*':
        resp._ret = req._x * req._y;
        break;
    case '/':
        if (req._y == 0)
            resp._code = 1;
        else
            resp._ret = req._x / req._y;
        break;
    case '%':
        if (req._y == 0)
            resp._code = 2;
        else
            resp._ret = req._x % req._y;
        break;
    default:
        resp._code = 3;
        break;
    }
    return resp;
}

void Calculator(int sock)
{
    std::string inbuffer;
    while(true)
    {
        bool ret = Recv(sock, &inbuffer);
        if(!ret) break;
        // 读取成功
        std::string package = Decode(inbuffer);
        if(package.empty()) continue;
        // 保证该报文是一个完整的报文
        logMessage(NORMAL, "%s", package.c_str());
        Request req;
        // 反序列化:字节流 -> 结构化
        req.Deserialize(package);
        // 业务逻辑
        Response resp = CalculatorHelper(req);
        // 序列化
        std::string respStr = resp.Serialize();
        // 添加报头,形成一个完整的报文
        respStr = Encode(respStr);
        Send(sock, respStr); // 将处理结果返回给客户端
    }
}

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

    signal(SIGPIPE, SIG_IGN); 

    std::unique_ptr<TcpServer> ptr(new TcpServer(atoi(argv[1])));
    ptr->BindService(Calculator); // 绑定服务
    ptr->Start();	// 开始服务

    return 0;
}

为什么服务端通常需要忽略 SIGPIPE 和 SIGCHLD 信号呢?

  1. 忽略 SIGPIPE 信号:在 TCP 通信中,当客户端已经关闭了连接,但服务端仍然向该连接发送数据时,就会触发 SIGPIPE 信号,如果不忽略该信号,进程会因为该信号的默认处理方式(终止进程)而异常退出。因此,服务端需要设置忽略 SIGPIPE 信号,以免因此而导致进程异常退出。
  2. 忽略 SIGCHLD 信号:在服务端处理客户端连接时,使用 fork 创建子进程来处理每个连接,当子进程退出时,内核会向父进程发送 SIGCHLD 信号,父进程默认会忽略该信号。如果父进程没有调用 wait 或 waitpid 来等待子进程,那么子进程退出时将会造成僵尸进程问题,从而导致系统资源的泄漏。而如果父进程调用 sigaction 或 signal 将 SIGCHLD 的处理动作置为 SIG_IGN,这样 fork 出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。因此,服务端需要设置忽略 SIGCHLD 信号,这样就不需要处理僵尸进程问题了。

客户端编写

#include "Protocol.hpp"
#include "Sock.hpp"
#include <ctime>
#include <unistd.h>

static void Usage(const std::string& proc)
{
    std::cout << "\nUsage: " << proc << " ServerIP ServerPort" << std::endl;
}

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

    std::string ServerIP = argv[1];
    uint16_t ServerPort = atoi(argv[2]);
    Sock sock;
    int sockfd = sock.Socket(); // 创建套接字
    // 发起连接请求
    if(!sock.Connet(sockfd, ServerIP, ServerPort))
    {
        std::cerr << "Connet Error!" << std::endl;
        exit(2);
    }

    bool quit = false; // false表示不退出循环
    std::string buffer;
    srand((unsigned int)time(nullptr));
    const char* op = "+-*/%";

    while(!quit)
    {
        // 获取需求(此部分可以设置为手动输入)
        Request req;
        req._x = rand() % 100;
        req._y = rand() % 100;
        req._op = op[rand() % 5];

        // 序列化
        std::string str = req.Serialize();
        std::string tmp = str;
        // 添加报头
        str = Encode(str);
        // 向服务器发起请求
        Send(sockfd, str);
        // 接收服务器的应答
        while(true)
        {
            bool ret = Recv(sockfd, &buffer);
            // 服务器关闭连接或Recv异常
            if(!ret)
            {
                quit = true;
                break;
            }

            std::string package = Decode(buffer);
            if(package.empty()) continue;
            // 接收到一个完整的报文
            Response resp;
            resp.Deserialize(package);
            std::string err;
            switch(resp._code)
            {
            case 1:
                err = "除0错误";
                break;
            case 2:
                err = "模0错误";
                break;
            case 3:
                err = "非法操作";
                break;
            default:
                std::cout << "[Calculate Success] " << tmp <<  " = " << resp._ret << std::endl;
                // std::cout << "[Calculate Success] " << resp._ret << std::endl;
                // std::cout << "[Calculate Success] " << resp._x << " " << resp._op << " " << resp._y << " = " << resp._ret << std::endl;
                break;
            }
            if(!err.empty()) std::cerr << err << std::endl;
            sleep(1);
            break;
        }
    }
	close(sockfd);
	
    return 0;
}

在这里插入图片描述
在这里插入图片描述

进程相关概念补充

什么是前台进程?什么是后台进程?

在 Linux 系统中,一个进程可以在前台运行或者在后台运行。

前台进程是指用户当前正在与之交互的进程,通常在终端(Terminal)上显示进程的输出信息,同时接收用户输入的命令。当一个进程在前台运行时,它会阻塞终端(bash)的输入,直到该进程退出或者被暂停。前台进程可以使用 Ctrl+Z 将前台进程暂停,可以使用 Ctrl + C 将前台进程终止。

后台进程是指在后台运行的进程,不与终端交互,通常不会在终端上输出信息。后台进程可以继续运行,即使用户退出了终端。可以通过在命令行末尾添加“&”符号将进程放到后台运行。

任何一个 Xshell 登录,只允许一个前台进程和多个后台进程。

一个进程除了有 PID、PPID(父进程 ID),还有一个组 ID(PGID)。每个进程都属于一个进程组。进程组是一组具有相同进程组 ID(PGID)的进程的集合,同时被创建的多个进程可以成为一个进程组,第一个进程的进程 ID 成为进程组 ID。一个进程可以将它的子进程加入到同一个进程组中,从而使得这些进程可以共享同一个终端。

在这里插入图片描述
在 Linux 系统中,同一个父进程下的多个子进程称为兄弟进程。那么上面的 sleep 1000、sleep 2000 和 sleep 3000 就是兄弟进程,它们的父进程就是 bash。

什么是会话?

在 Linux 中,会话(session)是指从用户登录开始,到用户退出结束这段时间内的整个过程。通常情况下,一个会话包含多个进程,这些进程可以是由当前会话的 bash 启动的,也可以是由其他进程启动的。当用户退出登录时,会话会被终止。具体来说,这意味着所有与该会话相关的进程都将被终止,包括终端和 bash 进程。当用户使用 exit 命令或输入 Ctrl+D 组合键退出 bash 时,会话会被终止。在会话终止之前,系统会执行一些清理工作,例如向所有已连接的进程发送 SIGHUP 信号,以通知它们会话已经终止。注:bash 是自成一个进程组的!

终端和 bash 的关系

在这里插入图片描述

什么是守护进程?

守护进程(Daemon)是一种在后台运行的进程,通常在启动系统时自动启动,一直运行直到系统关闭。守护进程通常不会与用户交互,也不会直接响应用户请求,而是通过监听网络端口或定期执行任务等方式,提供某种服务或功能。守护进程常常用于网络服务、系统监控、定时任务等方面。

在 Linux 系统中,守护进程通常通过 fork 函数创建子进程的方式启动,并且需要调用 setsid 函数创建新会话(Session)和进程组(Process Group),以便于与终端(Terminal)分离,避免受到用户登录或注销的影响。此外,守护进程还需要关闭不需要的文件描述符(File Descriptor)、改变工作目录(Working Directory)等操作,以提高系统的安全性和稳定性。

注:setsid 要调用成功,必须保证当前进程不是进程组的组长。守护进程是自成一个会话的!

进程守护进程化

#pragma once

#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_RDONLY);
    if(devnull > 0)
    {
        dup2(0, devnull);
        dup2(1, devnull);
        dup2(2, devnull);
        close(devnull);
    }
}

在这里插入图片描述

守护进程通常不应该向显示器输出信息,因为守护进程运行在后台,没有终端(Terminal)或标准输入输出(stdin/stdout/stderr)设备,也没有交互界面。如果守护进程尝试向显示器输出信息,可能会导致进程暂停或终止。

/dev/null 文件的介绍

/dev/null 是 Linux 操作系统中的一种特殊文件,它通常用于丢弃不需要的输出或输入数据。在 Linux 操作系统中,一切皆文件,/dev/null 也被看做是一个文件,但是它并不会存储数据,而是会将一切写入它的操作视为成功,并不做任何操作。

守护进程和孤儿进程的区别

守护进程是孤儿进程的一种,它们的父进程都是 1 号进程,最主要的区别就是守护进程自成一个会话,而孤儿进程是属于某个会话的。

现成的协议方案JSON

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,常用于数据的序列化和跨语言数据交换。JSON 采用键值对的方式来组织数据,数据格式为键值对之间用逗号分隔,键和值之间用冒号分隔,整个数据由一对大括号包含。

例如,以下是一个 JSON 格式的数据:

{
    "name": "Alice",
    "age": 20,
    "gender": "female",
    "interests": ["reading", "music", "travel"]
}

这个数据包含了一个人的姓名、年龄、性别和兴趣爱好。其中,键名是字符串类型,键值可以是字符串、数字、布尔值、数组或对象。

在这里插入图片描述

JSON的优点是:

  • 易于阅读和编写:JSON 的格式简洁明了,易于理解和编写;
  • 跨语言支持:由于 JSON 格式是纯文本格式,因此可以轻松地跨语言进行数据交换;
  • 支持复杂数据类型:JSON 可以支持包含对象和数组等复杂数据类型的数据。

JSON 库的安装

sudo yum install -y jsoncpp-devel

在这里插入图片描述

JSON 库的简单使用

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

int main()
{
    int a = 10;
    int b = 20;
    char op = '+';

    Json::Value root;
    root["aa"] = a;
    root["bb"] = b;
    root["op"] = op;

    Json::Value sub;
    sub["other"] = 200;
    sub["other1"] = "hello";
    root["sub"] = sub;

    Json::StyledWriter writer;
    // Json::FastWriter writer;
    std::string s = writer.write(root);
    std::cout << s << std::endl;

    return 0;
}

在这里插入图片描述

注意:编译时需要加上 -ljosncpp,否则无法找到库。

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>

class Request
{
public:
    Request() {}

    Request(int x, int y, char op)
        : _x(x)
        , _y(y)
        , _op(op)
    {}

    ~Request() {}

    std::string Serialize()
    {
    #ifndef MYSELF        
        std::string str = std::to_string(_x);
        str += SPACE;
        str += _op;
        str += SPACE;
        str += std::to_string(_y);
        return str;
    #else
        Json::Value root;
        root["x"] = _x;
        root["op"] = _op;
        root["y"] = _y;
        Json::FastWriter writer;
        return writer.write(root);
    #endif
    }

    bool Deserialize(std::string& str)
    {
    #ifndef MYSELF
        size_t left = str.find(SPACE);
        if(left == std::string::npos)
            return false;
        size_t right = str.rfind(SPACE);
        if(right == std::string::npos)
            return false;

        if(left + SPACE_LEN >= str.size())
            return false;

        _x = atoi(str.substr(0, left).c_str());
        _y = atoi(str.substr(right + SPACE_LEN).c_str());
        _op = str[left + SPACE_LEN];
    #else
        Json::Value root;
        Json::Reader reader;
        reader.parse(str, root);
        _x = root["x"].asInt();
        _op = root["op"].asInt();
        _y = root["y"].asInt();
        return true;
    #endif
    }

public:
    int _x;
    int _y;
    char _op;
};

class Response
{
public:
    Response() {}

    Response(int code, int ret, int x, int y, char op)
        : _code(code)
        , _ret(ret)
        , _x(x)
        , _y(y)
        , _op(op)
    {}

    ~Response() {}

    std::string Serialize()
    {
    #ifndef MYSELF
        std::string str = std::to_string(_code);
        str += SPACE;
        str += std::to_string(_ret);
        return str;
    #else
        Json::Value root;
        root["code"] = _code;
        root["ret"] = _ret;
        root["x"] = _x;
        root["op"] = _op;
        root["y"] = _y;
        Json::FastWriter writer;
        return writer.write(root);
    #endif
    }

    bool Deserialize(const std::string& str)
    {
    #ifndef MYSELF
        size_t pos = str.find(SPACE);
        if(pos == std::string::npos)
            return false;
        _code = atoi(str.substr(0, pos).c_str());
        _ret = atoi(str.substr(pos + SPACE_LEN).c_str());
        return true;
    #else
        Json::Value root;
        Json::Reader reader;
        reader.parse(str, root);
        _code = root["code"].asInt();
        _ret = root["ret"].asInt();
        _x = root["x"].asInt();
        _op = root["op"].asInt();
        _y = root["y"].asInt();
        return true;
    #endif
    }

public:
    int _code;
    int _ret;
    int _x;
    int _y;
    char _op;
};

👉总结👈

本篇博客主要讲解了协议的概念、序列化和反序列化、守护进程以及网络版计算器的编写等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️

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

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

相关文章

腾讯云4核8G12M轻量服务器配置性能评测

腾讯云轻量4核8G12M服务器&#xff0c;之前是4核8G10M配置&#xff0c;现在公网带宽和月流量包整体升级&#xff0c;12M公网带宽下载速度可达1536KB/秒&#xff0c;系统盘为180GB SSD盘&#xff0c;每月2000GB免费流量&#xff0c;腾讯云百科来详细说下4核8G12M轻量应用服务器配…

AJAX | 拦截器、文件上传和下载

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; AJAX Ajax即Asynchronous Javascript And XML&#xff08;异步JavaScript和XML&#xff09;&#xff1b;Ajax技术网页应用能够快速地将增量更新呈现在用户界面上&…

锁子甲 bulid+sim

链接: youtube 分析&#xff1a;洒一堆点——copy 模型——点和模型符合一定规律 点和点的距离符合上述图中的关系 &#xff08;横纵&#xff09; 横向 但是我们要横向10个点够了&#xff1a; 用modulo 除余 纵向 这里用除法向上取整 /10 eg &#xff1a; 0-9 得0 10-19 得1…

redis哨兵模式配置(配置文件等)

Redis-Sentinel机制主要用三个功能&#xff1a; (1)监控&#xff1a;不停监控Redis主从节点是否安装预期运行 (2)提醒&#xff1a;如果Redis运行出现问题可以 按照配置文件中的配置项 通知客户端或者集群管理员 (3)自动故障转移&#xff1a;当主节点下线之后&#xff0c;哨兵…

【版本控制】Github同步Gitee镜像仓库自动化脚本

文章目录Github同步Gitee镜像仓库自动化脚本前言什么是Hub Mirror Action&#xff1f;1.介绍2.用法配置步骤1.生成密钥对2.GitHub私钥配置3.Gitee公钥配置4.Gitee生成私人令牌5.Github绑定Gitee令牌6.编写CI脚本7.多仓库同步推送8.定时运行脚本总结Github同步Gitee镜像仓库自动…

【MyBatis Plus】002 -- 通用CRUD(插入、更新、删除、查询)

目录 3、通用CRUD 3.1 插入操作 3.1.1 方法定义 3.1.2 测试用例 3.1.3 测试 3.1.4 TableField 3.2 更新操作 3.2.1 根据id更新 3.2.2 根据条件更新 3.3 删除操作 3.3.1 根据id删除&#xff08;deleteById&#xff09; 3.3.2 根据Map删除数据&#xff08;deleteByMap&#xff09…

Level_2(2)题目整理

文章目录L2-022 重排链表&#xff08;模拟❗&#xff09;L2-023 图着色问题L2-024 部落(并查集)L2-025 分而治之&#xff08;与 L2-023差不多&#xff0c;邻接表遍历&#xff09;L2-026 小字辈&#xff08;求树的深度&#xff09;L2-027 名人堂与代金券(&#x1f4a1;处理&…

得物 API一站式协作平台的一些思考

1.背景 Mooncake是得物API一站式协作平台。从2022年3月份开始负责Mooncake&#xff0c;到现在已经一年了&#xff0c;回顾这一年&#xff0c;Mooncake大的阶段上&#xff0c;总共经历过两个版本: 1、Mooncake 1.0: 面向前端和客户端的mock平台&#xff0c;主要解决接口调用者…

C++实现前缀树

文章目录1. 什么是前缀树2. 前缀树的实现2.1 前缀树的基本结构2.2 插入2.3 word出现了几次2.3 word作为前缀出现几次2.4 删除1. 什么是前缀树 假设这里有一个字符串数组&#xff0c;和一个树的根结点&#xff1a; 这个结点的pass意思是&#xff1a;有几个字符通过了这个结点。…

ubuntu下Thrift安装

thrift是一种常用rpc框架&#xff0c;工作中经常会用到&#xff0c;本文记录一下其安装过程。 目录 1.下载软件包 1.1thrift下载 1.2libevent下载 1.3boost下载 2.安装&#xff08;注意步骤&#xff09; 2.1安装libevent 2.2安装boost 2.3安装与Python2.7版本对应的py…

【工作感悟】老程序员总结的四条工作经验教训

文章目录前言1. 不要做小需求2. 要做大需求3. 定期同步工作进度4. 项目结束&#xff0c;主动复盘总结前言 想来从事互联网工作已经很多年了&#xff0c;已经从当初的懵懂少年逐渐退化成老油条。刚毕业的时候&#xff0c;真是个愣头青&#xff0c;什么都不懂&#xff0c;也什么…

UE4 回放系统升级到UE5之后的代码报错问题解决

关键词&#xff1a; UE4 回放系统 升级 UE5 报错 DemoNetDriver GetDemoCurrentTime GetDemoTotalTime 背景 照着网上教的UE4的回放系统&#xff0c;也叫重播系统&#xff0c;英文Replay。做完了&#xff0c;测试运行正常&#xff0c;可升级到UE5却报了一堆 WorldSetting 和 …

计算机组成原理——第五章中央处理器

半生风雨半生伤&#xff0c;半醉半醒半心凉 文章目录前言5.1 CPU的功能和基本结构5.2 指令周期的数据流5.3.1 单总线结构5.3.2 专用通路结构前言 之前我们就说过CPU主要包括两个部分&#xff0c;运算器和控制器&#xff0c;运算器主要是实现算数运算.逻辑运算&#xff0c; 运算…

亲测:腾讯云轻量应用服务器性能如何?

腾讯云轻量应用服务器性能评测&#xff0c;轻量服务器CPU主频、处理器型号、公网带宽、月流量、Ping值测速、磁盘IO读写及使用限制&#xff0c;轻量应用服务器CPU内存性能和标准型云服务器CVM处于同一水准&#xff0c;所以大家不要担心轻量应用服务器的性能&#xff0c;腾讯云百…

springboot项目中的mysql用国产数据库达梦替换的相关说明

一、 用“DM管理工具”的“管理用户”创建你需要用户&#xff0c;也是达梦的模式。 用户的权限问题可以直接角色授权&#xff0c;方便一些。 二、借用达梦的“DM数据迁移工具”做数据库的表内容转移。 1. 新建工程、新建迁移 编辑mysql的数据库源 编辑达梦的目的端数据库 选择之…

应届生通过Java培训班转行IT有前途吗?

借用邓小平同志曾说过的一句话&#xff1a;科学技术是第一生产力。IT行业作为科技行业中的一员&#xff0c;不管是在自身的发展&#xff0c;还是支持其他行业的发展中都扮演了不可或缺的角色&#xff0c;“互联网”是社会发展的趋势&#xff0c;前途是无限的。而计算机语言是目…

春季儿童吃什么有助于长高,3款适合孩子长高的食谱做法,学起来

儿童身高一直以来都比较受到父母的关注&#xff0c;虽然身高不能说明一个人的能力有多强&#xff0c;但是会影响到人的外表。身高影响成败&#xff0c;一些专业对身高要求非常严格&#xff0c;因此大部分家长都希望孩子在身高方面能有一定的优势。 春季是孩子分泌生长激素增加时…

你了解C语言中的数组指针和函数指针吗?

如题&#xff0c;本篇文章重点讲解C语言中的数组指针和函数指针。这2种指针其实都不是很常用&#xff0c;个人感觉使用起来代码的可读性不是很高&#xff0c;但是还是需要了解一下。 数组指针 数组指针&#xff0c;即指向数组的指针&#xff0c;是用来存放数组的地址的。那如…

Redis Lua沙盒绕过命令执行(CVE-2022-0543)

一、描述 影响范围&#xff1a;Debian系得linux发行版本Ubuntu Debian系得linux发行版本 其并非Redis本身漏洞&#xff0c;形成原因在于系统补丁加载了一些redis源码注释了的代码 揭露时间&#xff1a;2022.3.8 二、原理 redis在用户连接后可以通过eval命令执行Lua脚本&#x…

Flutter成不了“顶流明星”的7大理由

Flutter是一款由Google推出的跨平台移动应用开发框架&#xff0c;近年来备受关注。尽管Flutter在某些方面表现出色&#xff0c;但仍然有一些人对它的发展前景表示怀疑。近期一些文章针对Flutter的发展提出了不少质疑和批评&#xff0c;称其难以成为移动应用开发的“顶流明星”&…