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

news2024/12/23 14:04:01

​🌠 作者:@阿亮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;
};

👉总结👈

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

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

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

相关文章

动力节点Springsecurity视频笔记06-13基于数据库的方法授权

6 密码处理 6.1 为什么要加密&#xff1f; csdn 密码泄露事件 泄露事件经过&#xff1a;https://www.williamlong.info/archives/2933.html 泄露数据分析&#xff1a;https://blog.csdn.net/crazyhacking/article/details/10443849 6.2加密方案 密码加密一般使用散列函数&a…

二极管基础知识

1、二极管是常用的半导体器件、由P/N结组成。 有插件类和贴片类两大封装&#xff0c;常用的插件封装有DO-15、D0-27、DO-35、DO-41、TO-220等&#xff0c;常用的贴片封装有SMA、SMB、SMc、s0D-123、SOD-323、TO-277等。 按反向恢复时间分类有&#xff0c;普通〈慢恢复&#xff…

java的validation框架(参数校验)

一.bean validation和hibernate validator参数校验常用约束注解&#xff1a; 空值校验类&#xff1a;Null&#xff0c;NotNull&#xff0c;NotEmpty&#xff0c;NotBlank等 范围校验类&#xff1a;Min&#xff0c;Size&#xff0c;Digits&#xff0c;Future&#xff0c;Negati…

Web Components 技术分析

简括&#xff1a; Web Components 基于四个主要的规范&#xff1a; Custom Elements&#xff0c;Shadow DOM&#xff0c;HTML Templates 和 HTML Imports。 Custom Elements 可以让开发人员创建自定义的 HTML 标签。 Shadow DOM 可以让开发人员将样式和行为封装到自定义元素内…

ASM字节码处理工具原理及实践(二)

0. 相关分享 ASM字节码处理工具原理及实践&#xff08;一&#xff09; 上一篇讲了ASM的简介、导入&#xff0c;以及字节码文件结构&#xff0c;并给出了ASM通过ClassVisitor对class进行访问的基础实战。本篇将进入MethodVisitor&#xff0c;尝试对方法进行访问、生成、转换。…

29.Linux网络编程

把昨天的 第二天的内容说一下&#xff0c;复习一下&#xff0c;第二天 讲的东西不算多&#xff0c;但是有两个作业题来写一写&#xff0c; 大致浏览一下&#xff0c;三次握手 四次挥手的过程&#xff0c;大家有没有画一下&#xff1f; 能画出来吗&#xff1f;同学们&#xff0…

前后端分离开发、Yapi、Swagger、项目部署

一、前后端分离开发 1.1、介绍 前后端分离开发&#xff0c;就是在项目开发过程中&#xff0c;对于前端代码的开发由专门的前端开发人员负责&#xff0c;后端代码则由后端开发人员负责&#xff0c;这样可以做到分工明确、各司其职&#xff0c;提高开发效率&#xff0c;前后端代…

RocketMQ单机环境搭建测试+springboot整合

1.资源下载 官网&#xff1a;下载 | RocketMQ 这里选择使用编译后可以直接用的 下载后解压&#xff1a;略 2.更改配置 主要是更改 conf/broker.conf 的配置&#xff0c;记得添加上下面这几行&#xff0c;否则消息发送失败 autoCreateTopicEnabletrue # 支持自动创建topic…

浅谈日出日落的计算方法以及替代工具 - 日出日落 API

引言 如果你想知道精确的日落日出时间&#xff0c;又或者你想设计一个日出日落时间查询的应用&#xff0c;又或者你只是好奇点进来了&#xff0c;还是可以过来围观一下涨涨知识&#xff0c;今天想跟大家聊一聊的是日出日落的计算方法以及替代工具 - 日出日落 API 。 日出日落…

大数据=SQL Boy,SQL Debug打破SQL Boy 的僵局

网上经常盛传 大数据sql boy&#xff0c;后端开发crud boy&#xff0c;算法工程师调参boy 在大数据领域也工作了好几年了&#xff0c;确实大数据开发&#xff0c;很多工作就是写sql&#xff0c;hive sql、spark sql、flink sql等等sql 一、背景&#xff1a; 但是经常有这样一…

NODEJS安装和vue安装及运行方法以及出现Cannot find module ‘node-sass‘ Require stack问题解决方法

安装nodejs 官网下载&#xff1a; https://registry.npmmirror.com/binary.html?pathnode/选择要下载的版本 一般建议下载msi 选择自己的安装位置一直下一步即可完成 检查一下是否安装成功 打开cmd&#xff0c;输入如下指令 node -vnpm -v输出了版本号就说明安装成功了 …

GCM与CCM的动作过程

CCM CCM&#xff08;Counter with CBC-MAC&#xff09;是一种基于对称加密算法的认证加密&#xff08;Authenticated Encryption&#xff09;模式&#xff0c;结合了CBC-MAC&#xff08;Cipher Block Chaining Message Authentication Code&#xff09;用于消息认证和CTR&…

[java聊天室]服务器发送消息给客户端守护线程同步锁(三)

守护线程 守护线程也称为:后台线程 守护线程是通过普通线程调用setDaemon(boolean on)方法设置而来的,因此创建上与普通线程无异.守护线程的结束时机上有一点与普通线程不同,即:进程的结束.进程结束:当一个进程中的所有普通线程都结束时,进程就会结束,此时会杀掉所有正在运行…

嵌入式Linux(2):将Helloworld驱动编译到内核

文章目录 分析一个例子仿写一个例子&#xff08;需要编译成.ko的&#xff09;写三个文件编辑上一级目录的Makefile文件编辑上一级目录的Kconfig文件make menuconfig进行配置 烧录到开发板上 分析一个例子 例子&#xff1a; source "drivers/redled/Kconfig" config…

简单分享微信怎么添加报名链接的步骤

最近看到很多小伙伴都在问有没有简单的报名链接制作办法&#xff0c;因为最近是暑期活动开展的前期&#xff0c;需要用到很多报名链接&#xff0c;希望可以直接通过微信小程序进行报名&#xff0c;扫一扫微信小程序的二维码就可以进入报名页面&#xff0c;然后制作步骤也是简单…

残差 Gabor 卷积网络和 FV-Mix 指数级数据增强策略用于手指静脉识别

论文背景 手指静脉识别系统的性能受到手指静脉训练样本不足的限制&#xff0c;导致特征学习不足和模型泛化能力弱&#xff1a;DCNN 需要大量的数据来学习更抽象的语义信息进行分类。对于指静脉识别&#xff0c;由于每个类别只包含少量样本&#xff0c;极易出现过拟合。原因之一…

MinIO快速入门

一、MinIO概述 官网地址&#xff1a;http://www.minio.org.cn/ 文档地址&#xff1a;http://docs.minio.org.cn/docs/ MinIO是一款基于Apache License v2.0开源协议的分布式文件系统&#xff08;或者叫对象存储服务&#xff09;&#xff0c;可以做为云存储的解决方案用来保存海…

如何借助测控终端实现设备远程运维?

随着物联网技术的发展&#xff0c;数字化越来越重要。数据是新的生产要素&#xff0c;是基础性资源和战略性资源&#xff0c;也是重要生产力。因此许多企业纷纷转型智慧工厂&#xff0c;但老旧的设备无法获取相应的数据&#xff0c;更换老旧设备的成本又太高&#xff0c;就无法…

【计算机架构】如何计算 CPU 时间

目录 0x00 响应时间和吞吐量&#xff08;Response Time and Throughput&#xff09; 0x01 相对性能&#xff08;Relative Performance&#xff09; 0x02 执行时间测量&#xff08;Measuring Execution Time&#xff09; 0x03 CPU 时钟&#xff08;Clocking&#xff09; 0x…

用docker承载mysql

这两天部署系统到生产服务器&#xff0c;前端后端部署docker是毫无疑义的&#xff0c;但mysql呢&#xff1f; 答案是mysql可以部署到docker。 1、数据文件挂载到宿主机 将mysql部署于docker&#xff0c;会有一个担心&#xff0c;就是docker容器的删除非常的容易&#xff0c;…