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

这篇具有很好参考价值的文章主要介绍了【Linux】通过网络版计算器来认识协议。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

​🌠 作者:@阿亮joy.
🎆专栏:《学会Linux》
🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
【Linux】通过网络版计算器来认识协议

👉再谈协议👈

协议的概念

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

结构化数据的传输

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

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

序列化和反序列化

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

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

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

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

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

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

👉网络版计算器👈

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

协议定制

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

进程相关概念补充

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

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

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

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

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

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

【Linux】通过网络版计算器来认识协议
在 Linux 系统中,同一个父进程下的多个子进程称为兄弟进程。那么上面的 sleep 1000、sleep 2000 和 sleep 3000 就是兄弟进程,它们的父进程就是 bash。

什么是会话?

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

终端和 bash 的关系

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

什么是守护进程?

守护进程(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);
    }
}

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

守护进程通常不应该向显示器输出信息,因为守护进程运行在后台,没有终端(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"]
}

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

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

JSON的优点是:

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

JSON 库的安装

sudo yum install -y jsoncpp-devel

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

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;
}

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

注意:编译时需要加上 -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;
};

👉总结👈

本篇博客主要讲解了协议的概念、序列化和反序列化、守护进程以及网络版计算器的编写等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️文章来源地址https://www.toymoban.com/news/detail-417335.html

到了这里,关于【Linux】通过网络版计算器来认识协议的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 用javascript做一个计算器,用js做一个计算器代码

    大家好,给大家分享一下怎么用javascript做一个简单的计算器,很多人还不知道这一点。下面详细解释一下。现在让我们来看看! 页面布局设计(HTML+CSS)   由于在之前的博客中有对html和css进行详细的讲解,再次就不多叙述,直接上代码。因为js中用到了JQuery选择器所以在

    2024年02月04日
    浏览(69)
  • MFC基于对话框——仿照Windows计算器制作C++简易计算器

    目录 一、界面设计 二、设置成员变量 三、初始化成员变量  四、初始化对话框 ​五、添加控件代码 1.各个数字的代码(0~9) 2.清除功能的代码 3.退格功能的代码 4.加减乘除功能的代码 5.小数点功能的代码 6.正负号功能的代码 7.等于功能的代码 六、源码领取方式 制作好之后

    2024年02月05日
    浏览(70)
  • QT 简易计算器

    2024年02月09日
    浏览(44)
  • Java 实现计算器

    *使用Java语言编写一个能实现“加、减、乘、除”四则运算的计算器程序。* 要求: (1)在程序中要体现面向对象编程语言的三大特征:封装、继承和多态。 (2)该程序要易于扩展和复用。以后可以方便地增加新的运算;程序的业务逻辑与界面部分要实现分离,便于业务逻

    2024年02月12日
    浏览(40)
  • Qt制作计算器

    运用数据队列中的栈知识,使用Qt制作简易的计算器 ①支持中缀表达式的基本四则运算 ②支持软(软件自带键盘),硬(实体键盘)键盘均可操作,并且按键时触发小特效 ③支持查看历史记录 将用户输入的表达式分解为 运算符 和 操作数 两类 运算符:\\\"+\\\", \\\"-\\\", \\\"*\\\", \\\"/\\\", \\\"(\\\", \\\")\\\" 操

    2024年02月08日
    浏览(79)
  • Java编写计算器

    主要用JAVA编写一个计算器,能实现加减乘除、开平方、求阶乘等计算功能,计算器有图形化界面,能对异常进行处理,并且检验输入的合法性,对不合法的输入给出具体的提示信息 代码如下    

    2024年02月11日
    浏览(35)
  • JAVA简易计算器

    1.C是清除键,功能是将之前所输入的数字、计算结果等信息全部归零 2.CE,清除当前输入的数据或符号 3.单击MS存储当前显示值,可以理解为放到存储区 4.单击MC清除存储区数值 5.单击MR将存储区数据显示到屏幕上 6.M+:当前显示的数与存储区的数相加 7.M-:当前显示的数与存储

    2024年02月09日
    浏览(53)
  • 实现复数计算器

            本论文描述了一个复数计算器的设计和实现,旨在扩展传统计算器的功能,以支持复数的加法、减法、乘法和除法。通过使用Java编程语言和Swing图形用户界面库,我们创建了一个直观、易于使用的界面,允许用户输入复数,并执行基本的算术运算。         计

    2024年02月02日
    浏览(68)
  • html 计算器界面

    其他链接: https://www.freecodecamp.org/news/how-to-build-an-html-calculator-app-from-scratch-using-javascript-4454b8714b98/ https://codepen.io/pen/tour/welcome/start 下面展示一些 内联代码片 。

    2024年02月13日
    浏览(69)
  • 前端实现科学计算器

    这个作业属于哪个课程 https://bbs.csdn.net/forums/ssynkqtd-05 这个作业要求在哪里 https://bbs.csdn.net/topics/617294583 这个作业的目标 完成一个具有可视化界面的计算器 其他参考文献 无 源代码链接 PSP Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟) Planning 计划 60 40 • Est

    2024年02月07日
    浏览(65)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包