【Linux网络】再谈 “协议“

目录

再谈 "协议"

结构化数据的传输

序列化和反序列化

网络版计算器 

封装套接字操作 

服务端代码

服务进程执行例程

启动网络版服务端

协议定制

客户端代码

代码测试

使用JSON进行序列化与反序列化


我们程序员写的一个个解决我们实际问题,满足我们日常需求的网络程序,都是在应用层。

再谈 "协议"

我们之前讲过:协议是一种 "约定"。网络协议是通信计算机双方必须共同遵从的一组约定,只有通信计算机双方都遵守相同的协议,计算机之间才能互相通信交流。

结构化数据的传输

socket api的接口,在读写数据时,都是按 "字符串" 的方式来发送接收的。

  • 如果需要传输的数据是一个字符串,那么直接将这一个字符串发送到网络当中,此时对端也能从网络当中获取到这个字符串。
  • 但如果需要传输的是一些结构化的数据,此时就不能将这些数据一个个发送到网络当中。 

那么如果我们要传输一些"结构化的数据" 怎么办呢?

例如,我们需要实现一个服务器版的加法器。我们需要客户端把要计算的两个加数发过去,然后由服务器进行计算,最后再把结果返回给客户端。那么客户端每次给服务端发送的请求数据当中,就需要包括左操作数、右操作数以及对应需要进行的操作,此时客户端要发送的就不是一个简单的字符串,而是一组结构化的数据。

当客户端选择将结构化的数据逐一发送到网络中,服务端接收数据的过程也会相应地碎片化。每次从网络中接收到一部分数据,服务端都需要对这些离散的信息进行整理,尝试将它们重新组合成原始的结构化数据。这个过程既复杂又容易出错,因为数据可能在传输过程中出现丢失或顺序混乱的情况。

因此,为了简化数据传输和处理的流程,客户端通常会采取“打包”策略。打包意味着将多个相关的数据元素组合成一个整体,然后再进行传输。这样,服务端每次从网络中接收到的都是一个完整的数据包,其中包含了所有必要的信息。

客户端常见的“打包”方式主要有两种:

约定方案一:

  • 客户端发送一个形如"1+1"的字符串;
  • 这个字符串中有两个操作数, 都是整形;
  • 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
  • 数字和运算符之间没有空格;

客户端能够将结构化的数据编排成一个字符串格式,并通过网络将其发送出去。当服务端从网络接收到这个字符串时,它会采用与客户端相同的解析方法,从而从这个字符串中提取出原始的结构化数据。这样的通信方式确保了数据的完整性和准确性在客户端和服务端之间的传输。

约定方案二:

  • 定义结构体来表示我们需要交互的信息;
  • 发送数据时将这个结构体按照一个规则转换成字符串,接收到数据的时候再按照相同的规则把字符串转化回结构体;
  • 这个过程叫做 "序列化" 和 “反序列化”

客户端可以设计一个特定的结构体,将需要交互的信息定义到这个结构体当中。在发送数据前,客户端会利用序列化技术将这个数据结构转换成一种统一的、可传输的字符串或字节流格式。当服务端接收到这些数据后,它会利用反序列化技术将这个字符串或字节流还原成原始的数据结构。通过这种方式,服务端可以轻松地提取出客户端发送的信息,并进行相应的处理。这种序列化和反序列化的过程确保了数据在不同系统间的兼容性和可交换性。 

序列化和反序列化

  • 序列化是将对象的状态信息转换为可以存储或传输的形式(字节序列)的过程。
  • 反序列化是把字节序列恢复为对象的过程。

OSI七层模型中的表示层的主要任务是将设备内部特有的数据格式,即应用层上的数据格式,转换为符合网络传输标准的格式。这种网络标准数据格式通常是通过序列化过程得到的,使得数据能够以一致和可理解的方式在网络中进行传输。

网络版计算器 

封装套接字操作 

由于服务端和客户端都需要创建套接字,以及使用套接字完成一些固定的操作,因此我们实现一个简单的TCP套接字(socket)类的实现,它封装了套接字的基本操作:包括创建、绑定、监听、接受连接和连接。这样服务端和客户端都可以直接调用这些函数。封装套接字操作可以使服务端和客户端代码更整洁、可重用,并减少重复代码。

Socket.hpp

#pragma once

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

enum{
    SocketErr = 2,
    BindErr,
    ListenErr,
    AcceptErr
};

#define backlog 10

class Sock
{
public:
    Sock()
    {
    }
    ~Sock()
    {
    }
public:
    void Socket()
    {
        sockfd_ = socket(AF_INET,SOCK_STREAM,0);
        if(sockfd_ < 0)
        {
            lg(Fatal,"socker error, %s:%d",strerror(errno),errno);
            exit(SocketErr);
        }   
    }

    void Bind(uint16_t& port)
    {
        struct sockaddr_in local;
        memset(&local,0,sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;

        if(bind(sockfd_,(struct sockaddr *)&local,sizeof(local)) < 0)
        {
            lg(Fatal,"bind error, %s: %d", strerror(errno), errno);
            exit(BindErr);
        }
    }

    void Listen()
    {
        if(listen(sockfd_ , backlog) < 0)
        {
            lg(Fatal,"listen error, %s: %d", strerror(errno), errno);
            exit(ListenErr);
        }
    }

    int Accept(std::string* clientip, std::uint16_t* clientport)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int newfd = accept(sockfd_,(struct sockaddr*)&peer,&len);
        if(newfd < 0)
        {
            lg(Warning,"accept error, %s: %d", strerror(errno), errno);
            exit(AcceptErr);
        }
        char ipstr[64];
        inet_ntop(AF_INET,&(peer.sin_addr.s_addr),ipstr,sizeof(ipstr));
        *clientip = ipstr;
        *clientport = ntohs(peer.sin_port);

        return newfd;
    }

    bool Connect(const std::string &ip, const uint16_t &port)
    {
        struct sockaddr_in peer;
        memset(&peer,0,sizeof(peer));
        peer.sin_family = AF_INET;
        peer.sin_port = htons(port);
        inet_pton(AF_INET,ip.c_str(),&(peer.sin_addr.s_addr));

        int n = connect(sockfd_,(struct sockaddr*)&peer,sizeof(peer));
        if(n == -1)
        {
            std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;
            return false;
        }

        return true;
    }

    void Close()
    {
        close(sockfd_);
    }

    int FD()
    {
        return sockfd_;
    }

private:
    int sockfd_;
};

服务端代码

首先,我们需要初始化服务器,这包括三个关键步骤:

  1. 使用socket函数来创建一个新的套接字。
  2. 接着,通过bind函数,我们将这个套接字绑定到一个特定的端口号上,这样客户端就可以通过这个端口与服务器建立连接。
  3. 然后,通过调用listen函数,我们将套接字设置为监听状态,等待客户端的连接请求。

服务器初始化完成后,就可以启动它了。启动后,服务器的主要任务是不断地调用accept函数,从监听套接字中接收新的连接请求。每当成功接受到一个新连接时,服务器会创建一个新的进程。这个新进程将负责为该客户端提供计算服务,确保每个客户端都能得到及时且独立的响应。 

TcpServer.hpp 

#pragma once

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <functional>
#include "Socket.hpp"

//这允许我们为 TCP 服务器提供一个自定义的回调函数,该函数处理从客户端接收到的数据。
using func_t = std::function<std::string(std::string &package)>; //std::function对象,该对象接受一个 std::string 引用作为参数并返回一个 std::string

class TcpServer
{
public:
    TcpServer(uint16_t port,func_t callback)
    :port_(port),callback_(callback)
    {
    }

    //初始化tcp服务器
    bool InitServer()
    {
        listensock_.Socket();
        listensock_.Bind(port_);
        listensock_.Listen();
        lg(Info,"init server .... done");

        return true;
    }

    //启动服务器
    void Start()
    {   
        signal(SIGCHLD, SIG_IGN);//忽略了 SIGCHLD 和 SIGPIPE 信号,当子进程终止或管道写入失败时,服务器不会接收到这些信号。
        signal(SIGPIPE, SIG_IGN);
        
        //无限循环,在该循环中,它尝试接受新的客户端连接。对于每个新的连接,它创建一个子进程来处理该连接。
        //并使用之前提供的回调函数来处理这些数据。如果回调函数返回一个非空字符串,那么该字符串将被发送回客户端。
        while (true)
        {
            std::string clientip;
            uint16_t clientport;
            int sockfd = listensock_.Accept(&clientip,&clientport);
            if(sockfd < 0)
                continue;// 返回继续监听       
            lg(Info,"accept a new link, sockfd: %d, clientip: %s, clientport: %d",sockfd, clientip, clientport);
            // 提供服务
            if(fork() == 0)//
            {
                listensock_.Close();//在子进程中,服务器不再需要监听套接字,调用 listensock_.Close(); 关闭监听套接字,释放相关的系统资源。  
                std::string inbuffer_stream;
                //数据计算
                while (true)
                {
                    char buffer[1280];
                    ssize_t n = read(sockfd,buffer,sizeof(buffer));
                    if(n > 0)   
                    {
                        buffer[n] = 0;
                        inbuffer_stream += buffer;
                    
                        while(true)
                        {
                            std::string info = callback_(inbuffer_stream);
                            if(info.empty())
                                break;

                            write(sockfd, info.c_str(), info.size());
                        }

                    }
                    else if(n == 0)
                        break;
                    else
                        break;
                }   

                exit(0);//
            }
            close(sockfd);
        }   
    }

    ~TcpServer()
    {}
private:
    uint16_t port_;
    Sock listensock_;
    func_t callback_;
};

说明一下: 

  • 当前服务器采用的是多进程的方案,对于每个新的连接,创建一个子进程来处理该连接。
  • 提供的回调函数来处理客户端发送过来的数据。如果回调函数返回一个非空字符串,那么该字符串将被发送回客户端。

服务进程执行例程

当服务端调用accept函数获取到新连接并创建新进程后,该线程就需要为该客户端提供计算服务,此时该进程需要先读取客户端发来的计算请求,然后进行对应的计算操作,如果客户端发来的计算请求存在除0、模0、非法运算等问题,就将响应结构体当中的状态字段对应设置为1、2、3即可。

ServerCal.hpp 

#pragma once
#include <iostream>
#include "Protocol.hpp"

enum
{
    DivZero = 1,
    ModZero,
    Other_Oper
};

class ServerCal
{
public:
    ServerCal()
    {
    }

    Response CalculatorHelper(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 (req.y != 0)
                resp.result = req.x / req.y;
            else
                resp.code = DivZero;
        }
        break;
        case '%':
        {
            if (req.y != 0)
                resp.result = req.x % req.y;
            else
                resp.code = ModZero;
        }
        break;
        default:
            resp.code = Other_Oper;
            break;
        }
        return resp;
    }

    // "len"\n"10 + 20"\n
    std::string Calculator(std::string &package)
    {
        std::string content;
        bool r = Decode(package, &content); // "len"\n"10 + 20"\n
        if (!r)
            return "";
        // "10 + 20"
        Request req;
        r = req.Deserialize(content); // 反序列化完req中的变量就拿到值了
        if (!r)
            return "";

        content = "";                          //清空
        Response resp = CalculatorHelper(req); // result=30 code=0;

        // 计算完进行序列化
        resp.Serialize(&content);
        content = Encode(content);

        return content;
    }

    ~ServerCal()
    {
    }
};

启动网络版服务端

ServerCal.cpp

前面我们在TcpServer.hpp封装了服务器初始化和启动服务器函数的类,以及ServerCal类实现网络版计算器的类执行例程。下面我们实现一个ServerCal.cpp来启动网络版服务器,只有要调用前面两个类实现的接口即可。

  • 从命令行参数获取端口号
  • 创建ServerCal实例
  • 绑定ServerCal的Calculator方法
  • 创建TcpServer实例,并将绑定的Calculator方法和端口号作为参数传递给它。
  • 调用InitServer方法初始化服务器
  • 最后调用Start方法启动服务器。
#include "TcpServer.hpp"
#include "ServerCal.hpp"

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

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);
    ServerCal cal;
    //std::bind是C++标准库中的一个函数模板,它可以将一个可调用对象(如函数、lambda函数或成员函数指针)与其参数绑定,生成一个新的可调用对象。
    //&ServerCal::Calculator是ServerCal类中的Calculator成员函数的指针。
    //&cal是ServerCal类的一个实例的地址,该实例用于调用Calculator成员函数。
    //std::placeholders::_1是一个占位符,它表示bind生成的新可调用对象接受的第一个参数将传递给Calculator成员函数作为它的第一个参数。
    TcpServer *tsvp = new TcpServer(port,std::bind(&ServerCal::Calculator,&cal,std::placeholders::_1));
    
    tsvp->InitServer();
    tsvp->Start();

    return 0;
}

协议定制

为了实现一个网络版的计算器,确保通信双方遵循共同的规则和约定是至关重要的。这就需要我们制定一套简明的协议。数据的交互通常涉及请求数据和响应数据,因此需要分别定义两者的结构。在实现层面,C++允许通过类来组织代码和数据,但同样也可以使用更简单的结构体来定义数据结构。考虑到简洁性和直接性,这里我们选择使用结构体来定义请求和响应的数据格式。

因此我们需要设计一个请求结构体,用于封装从客户端发送到服务器的计算请求信息,以及一个响应结构体,用于封装服务器处理完请求后返回给客户端的结果。通过这种方式,我们可以确保通信双方按照预定的格式发送和接收数据,从而实现网络计算器的功能。

  • 请求结构体中需要包括两个操作数,以及对应需要进行的操作。
  • 响应结构体中需要包括一个计算结果,除此之外,响应结构体中还需要包括一个状态字段,表示本次计算的状态,因为客户端发来的计算请求可能是无意义的。
  • 请求结构体和响应结构体当中都封装了序列化函数和反序列化函数。
  • 我们在类外设置了编码函数和解码函数
#pragma once

#include <iostream>
#include <string>

const std::string blank_space_sep = " ";
const std::string protocol_sep  = "\n";

std::string Encode(std::string &content)
{
    std::string package = std::to_string(content.size());
    package += protocol_sep;
    package += content;
    package += protocol_sep;

    return package;
}

// "len"\n"x op y"\nXXXXXX
// "protocolnumber"\n"len"\n"x op y"\nXXXXXX
bool Decode(std::string &package, std::string *content)
{
    size_t pos = package.find(protocol_sep);
    if(pos == std::string::npos) return false;

    std::string len_str = package.substr(0,pos);
    std::size_t len = std::stoi(len_str);

    std::size_t total_len = len_str.size() + len + 2;
    if(package.size() < total_len)  return false;//传入的序列化字符串没有达到报头提供的字符串长度

    *content = package.substr(pos+1,len);
    package.erase(0,total_len);//

    return true;
}

class Request
{
public:
    Request(int data1,int data2,char oper)
    :x(data1),y(data2),op(oper)
    {
    }
    Request()//
    {}
public:
    bool Serialize(std::string *out)
    {
        // 构建报文的有效载荷
        // struct => string, "x op y"
        std::string s = std::to_string(x);
        s += blank_space_sep;
        s += op;
        s += blank_space_sep;
        s += std::to_string(y);
        *out = s;
        return true;
    }

    bool Deserialize(const std::string &in)// "x op y"
    {
        std::size_t left = in.find(blank_space_sep);
        if(left == std::string::npos)
            return false;
        std::string part_x = in.substr(0,left);

        std::size_t right = in.rfind(blank_space_sep);
        if(right == std::string::npos)
            return false;
        std::string part_y = in.substr(right);

        if(left+2 != right)
            return false;

        op = in[left + 1];
        x = std::stoi(part_x);
        x = std::stoi(part_y);

        return true;
    }

     void DebugPrint()
     {
         std::cout << "新请求构建完成:  " << x << op << y << "=?" << std::endl;
     }


public:
    //x op y
    int x;
    int y;
    char op;//+ - * / %
};

class Response
{
public:
    Response(int res,int c)
    :result(res),code(c)
    {}

    Response()
    {}
public:
    bool Serialize(std::string *out)
    {
        // "result code"
        // 构建报文的有效载荷
        std::string s = std::to_string(result);
        s += blank_space_sep;
        s += std::to_string(code);
        *out = s;

        return true;
    }

    bool Deserialize(const std::string &in)
    {
        std::size_t pos = in.find(blank_space_sep);
        if(pos == std::string::npos) return false;

        std::string part_left = in.substr(0,pos);
        std::string part_right = in.substr(pos+1);

        result = std::stoi(part_left);
        code = std::stoi(part_right);

        return true;
    }

    void DebugPrint()
    {
        std::cout << "结果响应完成, result: " << result << ", code: "<< code << std::endl;
    }

public:
    int result;
    int code;//0表示结果是可信的;否则!0具体是几,表明对应的错误原因
};

请求结构体

  • 序列化函数用于构建报文的有效载荷,将 `Request` 对象转换为一个字符串。它首先将 `x` 和 `y` 转换为字符串,并使用空格和操作符 `op` 将它们连接在一起。例如,如果 `x` 是 5,`y` 是 3,并且 `op` 是 `+`,则生成的字符串将是 `"5 + 3"`
  • 反序列化函数尝试从一个字符串中恢复一个 `Request` 对象。它首先查找空格和操作符来分隔 `x`、`op` 和 `y`。然后,它将这些部分转换回它们的原始类型,并检查字符串的格式是否正确。如果一切正常,它将更新 `Request` 对象的 `x`、`y` 和 `op`。

响应结构体

  • Serialize 函数接受一个指向 std::string 的指针 out,并将 result 和 code 成员变量的值转换为字符串,然后用空格(blank_space_sep)分隔它们,并将结果字符串存储在 out 所指向的位置。函数总是返回 true,表示序列化操作总是成功的。
  • Deserialize 函数接受一个常量字符串引用 in,并尝试从中解析出 result 和 code 的值。它首先查找空格的位置,然后提取空格前后的两个子字符串,并将它们分别转换为整数来更新 result 和 code 的值。如果字符串中没有找到空格,函数返回 false,否则返回 true。

编码函数 Encode:

  • 函数接受一个字符串 content,并返回一个编码后的字符串 package。
  • 首先,将 `content` 的大小(长度)转换为字符串并添加到 `package`。  
  • 然后,添加一个换行符。  
  • 接着,添加原始的 `content`。  
  • 最后,再添加一个换行符。这样,编码后的字符串格式是:`"length\ncontent\n"`。

解码函数 Decode:

  • 这个函数尝试从给定的 package 字符串中解码出 content。它首先查找换行符来确定 content 的长度,并检查 package 是否包含足够的数据。如果成功,它会提取 content 并从 package 中删除已解码的部分。

注意:

  • 编码函数和解码函数是多个结构体或类都可能需要的共同操作,因此将它们放在类外作为独立的函数。这种做法不仅增强了代码的可重用性,还方便了协议的编码和解码逻辑的更换。通过将编码和解码逻辑与具体的数据结构分离,我们可以在不修改数据结构定义的情况下更换编码和解码的实现,从而实现了更好的模块化和可扩展性。

规定状态字段对应的含义:

  • 状态字段为0,表示计算成功。
  • 状态字段为1,表示出现除0错误。
  • 状态字段为2,表示出现模0错误。
  • 状态字段为3,表示非法计算。

此时我们就完成了协议的设计,但需要注意,只有当响应结构体当中的状态字段为0时,计算结果才是有意义的,否则计算结果无意义。 

客户端代码

客户端首先也需要进行初始化:

  • 调用socket函数,创建套接字。

客户端初始化完毕后需要调用connect函数连接服务端,当连接服务端成功后,客户端就可以向服务端发起计算请求了。这里可以让用户输入两个操作数和一个操作符构建一个计算请求,然后将该请求发送给服务端。而当服务端处理完该计算请求后,会对客户端进行响应,因此客户端发送完请求后还需要读取服务端发来的响应数据。

客户端在向服务端发送或接收数据时,可以使用write或read函数进行发送或接收(也可以使用send或recv函数对应进行发送或接收。)

  • 连接服务器

由于我们前面封装了TCP套接字(socket)类的实现,这里我们之间调用我们封装的接口即可,下面是客户端代码:

客户端代码:

#include <iostream>
#include <string>
#include <time.h>
#include <assert.h>
#include "Protocol.hpp"
#include"Socket.hpp"

void Usage(const std::string &proc)
{
    std::cout << "\nUsage: " << proc << " serverip serverport\n" << std::endl;
}

// ./clientcal ip port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    //创建套接字
    Sock sockfd;
    sockfd.Socket();
    //链接服务器
    bool r = sockfd.Connect(serverip,serverport);
    if(!r) return 1;

    srand(time(nullptr)^getpid());
    int cnt = 1;
    const std::string opers = "+-*/%=-=&^";

    std::string inbuffer_stream;
    while(cnt <= 10)
    {
        //准备数据
        std::cout << "===============第" << cnt << "次测试....., " << "===============" << std::endl;
        int x = rand() % 100 + 1;
        usleep(1000);
        int y = rand() % 100 + 1;
        usleep(1000);
        char op = opers[rand() % opers.size()];
        Request req(x,y,op);
        req.DebugPrint();

        //客户端发送请求
        std::string packge;
        req.Serialize(&packge);

        packge = Encode(packge);
        write(sockfd.FD(),packge.c_str(),packge.size());

        //接受请求响应
        char buffer[128];
        ssize_t n = read(sockfd.FD(),buffer,sizeof(buffer));// 我们也无法保证我们能读到一个完整的报文
        if(n > 0)
        {
            buffer[n] = 0;
            inbuffer_stream += buffer;// "len"\n"result code"\n
            std::cout << inbuffer_stream << std::endl;
            
            std::string content;
            bool r = Decode(inbuffer_stream,&content);// "result code"
            assert(r);

            Response resp;
            r = resp.Deserialize(content);
            assert(r);

            resp.DebugPrint();
        }

        std::cout << "=================================================" << std::endl;
        sleep(1);

        cnt++;
    }

    sockfd.Close();
    return 0;
}

代码测试

运行服务端后再让客户端连接服务端,此时服务端就会对客户端发来的计算请求进行处理,并会将计算后的结果响应给客户端。

我们看到如果客户端要进行除0、模0、非法运算,在服务端识别后就会按照约定对应将响应数据的状态码设置为1、2、3,此时响应状态码为非零,因此在客户端打印出来的计算结果就是没有意义的。

此时我们就以这样一种方式约定出了一套应用层的简单的网络计算器,这就叫做协议。 

使用JSON进行序列化与反序列化

上面我们进行序列化和反序列化是自己进行协议定制,其实我们也可以用JSON或者Protobuf进行数据的序列化和反序列化操作。

JSON (JavaScript Object Notation) 和 Protobuf (Protocol Buffers) 都是数据序列化格式,但它们在设计目标、性能、使用场景等方面有所不同。

下面我们主要来介绍一下使用JSON进行序列化和反序列化操作:

JSON

  • 设计目标:JSON 主要用于人类可读性和易于编写。它是基于 JavaScript 的子集,但不仅限于 JavaScript 使用。
  • 性能:JSON 的解析和序列化速度相对较慢,尤其是对于大型数据结构。
  • 使用场景:JSON 广泛用于 API 通信、配置文件、Web 存储等场景,因为它易于阅读和编写,并且跨语言、跨平台。
  • 在使用 JsonCpp 之前,你需要确保已经安装了这个库。
sudo yum install -y jsoncpp-devel
  • 安装完成后,项目中加入头文件#include <jsoncpp/json/json.h>
  • 编译命令后面加上-ljsoncpp

下面是一个简单的示例,展示了如何使用 JsonCpp 来解析和生成 JSON 数据:

#include <iostream>  
#include <jsoncpp/json/json.h>  
#include <unistd.h>
  
int main() {  
    // 创建一个 JSON 对象  
    Json::Value root; // 将用于存储 JSON 数据的根对象  
    root["x"] = 40;  
    root["y"] = 30;  
    root["op"] = '+';  
    root["desc"] = "this is a + oper";  
  
    // 序列化:将 JSON 对象转换为字符串  
    Json::FastWriter writer;  
    //Json::StyledWriter writer;  //StyledWriter比Fastwriter多加了\n,可读性比较好
    std::string jsonString = writer.write(root);  
  
    // 输出 JSON 字符串  
    std::cout << "JSON string: " << jsonString << std::endl;  
    
    sleep(3);
    // 反序列化:从字符串解析 JSON  
    Json::Value v;  
    Json::Reader Reader;  
    Reader.parse(jsonString,v);
   
    // 访问 JSON 对象中的值  
    int x = v["x"].asInt();  
    int y = v["y"].asInt();  
    char op = v["op"].asInt();  
    std::string desc = v["desc"].asString();  
  
    // 输出解析后的值  
    std::cout << x <<std::endl;  
    std::cout << y <<std::endl;      
    std::cout << op <<std::endl;      
    std::cout << desc <<std::endl;     
  
    return 0;  
}

运行结果:

有了上面对JSON基本使用的理解后,下面我们在网络版计算器的协议定制的代码中增加JSON方式的序列化与反序列化:

我们根据是否定义了MySelf宏,来选择使用两种序列化方式:

#pragma once

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

// #define MySelf 1

const std::string blank_space_sep = " ";
const std::string protocol_sep  = "\n";

std::string Encode(std::string &content)
{
    std::string package = std::to_string(content.size());
    package += protocol_sep;
    package += content;
    package += protocol_sep;

    return package;
}

// "len"\n"x op y"\nXXXXXX
// "protocolnumber"\n"len"\n"x op y"\nXXXXXX
bool Decode(std::string &package, std::string *content)
{
    size_t pos = package.find(protocol_sep);
    if(pos == std::string::npos) return false;

    std::string len_str = package.substr(0,pos);
    std::size_t len = std::stoi(len_str);

    std::size_t total_len = len_str.size() + len + 2;
    if(package.size() < total_len)  return false;//传入的序列化字符串没有达到报头提供的字符串长度

    *content = package.substr(pos+1,len);
    package.erase(0,total_len);//

    return true;
}

// json, protobuf
class Request
{
public:
    Request(int data1,int data2,char oper)
    :x(data1),y(data2),op(oper)
    {
    }
    Request()//
    {}
public:
    bool Serialize(std::string *out)
    {
#ifdef MySelf
        // 构建报文的有效载荷
        // struct => string, "x op y"
        std::string s = std::to_string(x);
        s += blank_space_sep;
        s += op;
        s += blank_space_sep;
        s += std::to_string(y);
        *out = s;
        return true;
#else
        Json::Value root;
        root["x"] = x;
        root["y"] = y;
        root["op"] = op;
        // Json::FastWriter w;
        Json::StyledWriter w;
        *out = w.write(root);

        return true;
#endif
    }

    bool Deserialize(const std::string &in)// "x op y"
    {
#ifdef MySelf
        std::size_t left = in.find(blank_space_sep);
        if(left == std::string::npos)
            return false;
        std::string part_x = in.substr(0,left);

        std::size_t right = in.rfind(blank_space_sep);
        if(right == std::string::npos)
            return false;
        std::string part_y = in.substr(right);

        if(left+2 != right)
            return false;

        op = in[left + 1];
        x = std::stoi(part_x);
        y = std::stoi(part_y);

        return true;
#else
        Json::Value root;
        Json::Reader r;
        r.parse(in,root);

        x = root["x"].asInt();
        y = root["y"].asInt();
        char op = root["op"].asInt();
        return true;
#endif
    }

    void DebugPrint()
    {
        std::cout << "新请求构建完成:  " << x << op << y << "=?" << std::endl;
    }

public:
    //x op y
    int x;
    int y;
    char op;//+ - * / %
};

class Response
{
public:
    Response(int res,int c)
    :result(res),code(c)
    {}

    Response()
    {}
public:
    bool Serialize(std::string *out)
    {
#ifdef MySelf
        // "result code"
        // 构建报文的有效载荷
        std::string s = std::to_string(result);
        s += blank_space_sep;
        s += std::to_string(code);
        *out = s;

        return true;
#else
        Json::Value root;
        root["result"] = result;
        root["code"] = code;
        // Json::FastWriter w;
        Json::StyledWriter w;
        *out = w.write(root);

        return true;
#endif
    }

    bool Deserialize(const std::string &in)
    {
#ifdef MySelf
        std::size_t pos = in.find(blank_space_sep);
        if(pos == std::string::npos) return false;

        std::string part_left = in.substr(0,pos);
        std::string part_right = in.substr(pos+1);

        result = std::stoi(part_left);
        code = std::stoi(part_right);

        return true;
#else
        Json::Value root;
        Json::Reader r;
        r.parse(in,root);

        int result = root["result"].asInt();
        int code = root["code"].asInt();
#endif
    }

    void DebugPrint()
    {
        std::cout << "结果响应完成, result: " << result << ", code: "<< code << std::endl;
    }

public:
    int result;
    int code;//0表示结果是可信的;否则!0具体是几,表明对应的错误原因
};

makefile文件:

编译时要加上-ljsoncpp选项,我们也可以在makefile文件中进行宏定义Myself

.PHONY:all
all:servercal clientcal

# Flag= -DMySelf=1
Flag= #-DMySelf=1
Lib=-ljsoncpp

servercal:ServerCal.cpp
	g++ -o $@ $^ -std=c++11 $(Lib) $(Flag)
clientcal:ClientCal.cpp
	g++ -o $@ $^ -std=c++11 $(Lib) $(Flag)

.PHONY:clean
clean:
	rm -f servercal clientcal

代码测试:

以上我们就成功用Json实现了数据序列化和反序列化。

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

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

相关文章

最强照片AI无损放大工具

使用人工智能的能力来放大图像&#xff0c;同时为惊人的结果添加自然的细节。 使用深度学习技术&#xff0c;A.I.GigaPixEL可以放大图像并填满其他调整大小的产品所遗漏的细节。 下载地址&#xff1a;最强照片AI无损放大工具.zip

LeetCode-第201题-数字范围按位与

1.题目描述 给你两个整数 left 和 right &#xff0c;表示区间 [left, right] &#xff0c;返回此区间内所有数字 按位与 的结果&#xff08;包含 left 、right 端点&#xff09;。 2.样例描述 3.思路描述 方法一&#xff1a;按位与&#xff0c;求两端数字二进制的公共前缀&…

数据库系列之:什么是 SAP HANA?

数据库系列之&#xff1a;什么是 SAP HANA&#xff1f; 一、什么是 SAP HANA&#xff1f;二、什么是内存数据库&#xff1f;三、SAP HANA 有多快&#xff1f;四、SAP HANA 的十大优势五、SAP HANA 架构六、数据库设计七、数据库管理八、应用开发九、高级分析十、数据虚拟化 一、…

18.网络游戏逆向分析与漏洞攻防-网络通信数据包分析工具-数据分析工具数据与消息配置的实现

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 上一个内容&#xff1a;17.数据分析工具配置功能的实现 码云地址&#xff08;master 分支&#xff09;&#xff1a;https://gitee.com/dye_your_fingers/titan…

中医舌苔笔记

舌诊时按照舌尖-舌中-舌根-舌侧的顺序进行观察。 先看舌体再看舌苔&#xff0c;30秒左右。 如果一次望舌判断不清&#xff0c;可令病人休息3~5分钟后&#xff0c;重新观察一次 舌诊脏腑部位分属图 舌体 胖嫩而边有齿痕为气虚、阳虚。 薄白而润为风寒&#xff1b; 薄白而燥…

CVE-2020-27194:eBPF verifier 整数截断导致的越界读写

前言 影响版本&#xff1a;5.8.x 内核分支&#xff0c;v5.8.15 以及更低的版本 编译选项&#xff1a;CONFIG_BPF_SYSCALL&#xff0c;config 所有带 BPF 字样的编译选项 漏洞概述&#xff1a;eBPF 验证程序中进行 or 操作时&#xff0c;scalar32_min_max_or 函数将 64 位的值赋…

Android开发社招面试总结,Android程序员面试必备的知识点

导语 学历永远是横在我们进人大厂的一道门槛&#xff0c;好像无论怎么努力&#xff0c;总能被那些985,211 按在地上摩擦&#xff01; 不仅要被“他们”看不起&#xff0c;在HR挑选简历&#xff0c;学历这块就直接被刷下去了&#xff0c;连证明自己的机会也没有&#xff0c;学…

社区分享|中华保险基于MeterSphere开展接口自动化测试

中华联合保险集团股份有限公司&#xff08;以下简称为“中华保险”&#xff09;始创于1986年&#xff0c;是全国唯一一家以“中华”冠名的国有控股保险公司。截至2022年12月底&#xff0c;中华保险总资产为1006.06亿元&#xff0c;在全国拥有超过2900个营业网点&#xff0c;员工…

2024 年广西职业院校技能大赛高职组《云计算应用》赛项赛题第 3 套

#需要资源或有问题的&#xff0c;可私博主&#xff01;&#xff01;&#xff01; #需要资源或有问题的&#xff0c;可私博主&#xff01;&#xff01;&#xff01; #需要资源或有问题的&#xff0c;可私博主&#xff01;&#xff01;&#xff01; 某企业根据自身业务需求&…

模仿Gitee实现站外链接跳转时进行确认

概述 如Gitee等网站&#xff0c;在有外部链接的时候如果不是同域则会出现一个确认页面。本文就带你看看这个功能应该如何实现。 效果 实现 1. 实现思路 将打开链接作为参数传递给一个中间页面&#xff0c;在页面加载的时候判断链接的域名和当前网站是否同域&#xff0c;同域…

web学习笔记(二十六)

目录 1.JS执行队列 1.1JS是单线程 1.2Web Worker 1.3同步和异步 1.4JS执行机制 2.location对象 2.1什么是location对象 2.2url包含的信息 2.3location对象属性 2.4location对象的方法 3.navigator对象和history对象 3.1navigator对象 3.2history对象 1.JS执行队…

基于深度学习的苹果叶片病害检测系统(含UI界面、yolov8、Python代码、数据集)

项目介绍 项目中所用到的算法模型和数据集等信息如下&#xff1a; 算法模型&#xff1a;     yolov8 yolov8主要包含以下几种创新&#xff1a;         1. 可以任意更换主干结构&#xff0c;支持几百种网络主干。 数据集&#xff1a;     网上下载的数据集&#x…

Jmeter吞吐量控制器使用

场景&#xff1a;在同一个线程组里&#xff0c;有10个并发&#xff0c;7个做A业务&#xff0c;3个做B业务&#xff0c;要模拟这种场景&#xff0c;可以通过吞吐量模拟器来实现。 添加吞吐量控制器 用法1&#xff1a;Percent Executions 在一个线程组内分别建立两个吞吐量控制…

rtt的io设备框架面向对象学习-电阻屏LCD设备

目录 1.8080电阻屏LCD设备1.1 构造流程1.2 使用 2.spi电阻屏LCD3.i2c电阻屏LCD4.总结 电阻屏LCD通信接口有支持I2c、SPI和8080通信接口的&#xff0c;根据通信接口分立章节。 另外&#xff0c;lcd这块不像其他设备类&#xff0c;rtt没有实现lcd设备类的设备驱动框架层&#xf…

我们发布了一款类Wox和Alfred,全新的桌面端效率工具RunFlow,欢迎大家来体验

RunFlow是一款跨平台的生产力工具&#xff0c;可以启动应用程序和搜索文件等&#xff0c;类似于Windows平台的Wox和PowerToys&#xff0c;同样也类似于Mac平台的Alfred和Raycast。但我们并不与这些工具相同&#xff0c;我们有自己独特的新特性。下面&#xff0c;我们将向您详细…

仙宫云:细节控ComfyUI AI写实摄影+视频镜像

在使用comfyui工作流时经常遇到插件安装&#xff0c;模型下载的问题&#xff0c;为了方便大家使用和体验comfyui&#xff0c;我在仙宫云上部署了一个云端comfyui镜像包&#xff0c;开放给大家使用。 细节控ComfyUI AI写实摄影视频工作流&#xff1a; 镜像主页&#xff1a;仙宫…

Stable Diffusion V3测评

1.引言 3月5号&#xff0c;Stability AI发布了介绍Stable Diffusion V3的研究论文&#xff0c;链接地址&#xff1a;戳我 这是目前他们发布的最先进、功能最强大的图像生成器&#xff0c;与一年多前发布的令人印象深刻的 Stable Diffusion V2.1 相比有了大幅升级。SD3所带来的…

机器视觉 /从bottle.hdev示例程序开启HalconHDevelop征程

文章目录 概述示例程序bottle.hdev源码Step 0: PreparationsStep 1: Segmentation - 读取并显示图片Step 1: Segmentation - 创建并设置OCR模型Step 1: Segmentation - 文本分割与识别计算结果显示内存释放 导出为C代码导出为C代码配置 VS Halcon 环境VS程序执行结果HTuple hv…

代码随想录day13(1)栈与队列:用栈实现队列(leetcode232)

题目要求&#xff1a;使用栈实现push、pop、empty、peek&#xff08;返回队列首部元素&#xff09;。 思路&#xff1a;本题思路比较容易&#xff0c;即用两个栈模拟即可&#xff0c;pop时只需要先判断stackout栈是否为空&#xff0c;如果不空直接弹出&#xff0c;如果空就将s…

数据库-ER图教程

一.什么是E-R图 E-R图全称&#xff1a;“Entity-Relationship Approach”&#xff0c;是一种“实体-联系”方法。 E-R图的优点&#xff1a; 1.自然地描述现实世界。 2.图形结构简单。 3.设计者和用户易理解。 4.是数据库设计的中间步骤&#xff0c;易于向数据模型转换。 …