【Linux网络】再谈 “协议“

news2024/11/15 11:55:16

目录

再谈 "协议"

结构化数据的传输

序列化和反序列化

网络版计算器 

封装套接字操作 

服务端代码

服务进程执行例程

启动网络版服务端

协议定制

客户端代码

代码测试

使用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实现了数据序列化和反序列化。

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

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

相关文章

一手实测【Claude3】 - GPT4啊,你的时代终于要过去了

通过虚拟卡 WildCard 的方式来升级Claude3最快了&#xff0c;大概2分钟就可以开通完成, 而且升级 GPT 4.0 价钱也不贵&#xff0c;虚拟卡一年10美元&#xff0c;Claude3 每个月也才 20美元。如果你觉得Claude3对你可能有帮助&#xff0c;那就赶快来升级吧&#xff01; Claude3…

这本书太好了!150页就能让你上手大模型应用开发

如果问个问题&#xff1a;有哪些产品曾经创造了伟大的奇迹&#xff1f;ChatGPT 应该会当之无愧入选。仅仅发布 5 天&#xff0c;ChatGPT 就吸引了 100 万用户——当然&#xff0c;数据不是关键&#xff0c;关键是其背后的技术开启了新的 AI 狂潮&#xff0c;成为技术变革的点火…

多功能声学综合馆:革新解决气膜场馆噪音难题

近年来&#xff0c;气膜场馆在各类活动中的广泛应用带来了许多便利&#xff0c;但其内部噪音问题也一直困扰着人们。为了有效解决这一挑战&#xff0c;多功能声学综合馆崭露头角&#xff0c;通过创新的声学技术成为解决气膜场馆噪音问题的独特方案。 在这个嘈杂的世界中&#x…

第三篇【传奇开心果系列】Python的自动化办公库技术点案例示例:深度解读Pandas股票市场数据分析

传奇开心果博文系列 系列博文目录Python的自动化办公库技术点案例示例系列 博文目录前言一、Pandas进行股票市场数据分析常见步骤和示例代码1. 加载数据2. 数据清洗和准备3. 分析股票价格和交易量4. 财务数据分析 二、扩展思路介绍1. 技术指标分析2. 波动性分析3. 相关性分析4.…

MYSQL07高级_Hash结构、平衡二叉树、B树、B+树介绍

文章目录 ①. 全表遍历②. Hash结构③. 平衡二叉搜索树(AVL)④. B树⑤. B树⑥. 时间复杂度 选择的合理性 磁盘的I/O操作次数对索引的使用效率至关重要查找都是索引操作,一般来说索引非常大,尤其是关系型数据库,当数据量比较大的时候,索引的大小有可能几个G甚至更多,为了减少索引…

头像剪切上传

头像剪切上传 文章说明核心Api示例源码效果展示源码下载 文章说明 本文主要为了学习头像裁剪功能&#xff0c;以及熟悉canvas绘图和转文件的相关操作&#xff0c;参考教程&#xff08;Web渡一前端–图片裁剪上传原理&#xff09; 核心Api 主要就一个在canvas绘图的操作 context…

【触想智能】工业一体机刷卡应用知识分享

工业一体机刷卡技术是一种高效、稳定、安全的身份认证方式&#xff0c;具有广泛的应用场景和优势。在工业自动化控制、生产过程监测等领域&#xff0c;它已成为必不可少的设备之一。 一、工业一体机刷卡的原理:工业一体机刷卡的原理和普通的刷卡设备类似&#xff0c;都是通过读…

VS2022如何添加行号?(VS2022不显示行号解决方法)

VS2022不显示行号解决方法 VS2022是非常好用的工具&#xff0c;很多同学在初学C/C的时候&#xff0c;都会安装&#xff0c;默认安装好VS2022后&#xff0c;写代码时&#xff0c;在编辑框的窗口左边就有显示行号&#xff0c;如下图所示&#xff1a; 但是有些同学安装好后&#…

阿里云中小企业扶持权益

为企业提供云资源和技术服务&#xff0c;助力企业开启智能时代创业新范式。阿里云推出中小企业扶持权益 上云必备&#xff0c;助力企业长期低成本用云 一、ECS-经济型e实例、ECS u1实例活动规则 活动时间 2023年10月31日0点0分0秒至2026年3月31日23点59分59秒 活动对象 同时满…

GEE:使用Sigmoid激活函数对单波段图像进行变换(以NDVI为例)

作者:CSDN @ _养乐多_ 本文将介绍在 Google Earth Engine (GEE)平台上,对任意单波段影像进行 Sigmoid 变换的代码。并以对 NDVI 影像像素值的变换为例。 文章目录 一、Sigmoid激活函数1.1 什么是 Sigmoid 激活函数1.2 用到遥感图像上有什么用?二、代码链接三、完整代码一…

Tomcat概念、安装及相关文件介绍

目录 一、web技术 1、C/S架构与B/S架构 1.1 http协议与C/S架构 1.2 http协议与B/S架构 2、前端三大核心技术 2.1 HTML&#xff08;Hypertext Markup Language&#xff09; 2.2 css&#xff08;Cascading Style Sheets&#xff09; 2.3 JavaScript 3、同步和异步 4、…

吴恩达机器学习全课程笔记第七篇

目录 前言 P114-P120 推荐系统 协同过滤 均值归一化 协同过滤的tensorflow实现 查找相关项目 P121-P130 基于内容的过滤 强化学习 P131-P142 状态动作值函数定义 Bellman方程 随机环境 连续状态空间应用实例 前言 这是吴恩达机器学习笔记的第七篇&#xff0c;…

linux kernel物理内存概述(二)

目录 物理内存数据结构 设备数物理内存描述 物理内存映射 map_kernel map_mem zone数据结构 zone类型 物理内存数据结构 站在处理器角度&#xff0c;管理物理内存的最小单位是页面。使用page数据结构描述&#xff0c;通常默认大小4kB&#xff0c;采用mem_map[]数组来存…

(每日持续更新)jdk api之PipedWriter基础、应用、实战

博主18年的互联网软件开发经验&#xff0c;从一名程序员小白逐步成为了一名架构师&#xff0c;我想通过平台将经验分享给大家&#xff0c;因此博主每天会在各个大牛网站点赞量超高的博客等寻找该技术栈的资料结合自己的经验&#xff0c;晚上进行用心精简、整理、总结、定稿&…

合作的终极策略,竟如此有数学规律?《多Agent系统引论》第6章 多Agent交互 原文注释

6.0 前言 本文介绍一下多Agent交互过程中的一些概念&#xff0c;并且我保证能给你在人类社会中的工作生活学习带来启发。 6.1 效用和偏好 6.1.1 不知道什么是效用&#xff1f;那我告诉你什么是边际效应递减&#xff01; 想象一个人&#xff0c;他总资产只有1块钱&#xff0c;…

Vue前端+快速入门【详解】

目录 1.Vue概述 2. 快速入门 3. Vue指令 4.表格信息案例 5. 生命周期 1.Vue概述 1.MVVM思想 原始HTMLCSSJavaScript开发存在的问题&#xff1a;操作麻烦&#xff0c;耦合性强 为了实现html标签与数据的解耦&#xff0c;前端开发中提供了MVVM思想&#xff1a;即Model-Vi…

什么是 End-to-End 测试?

在使用 vue 的模板创建新项目的时候&#xff0c;有一个选项是问&#xff0c;是否添加“端到端”测试&#xff1f;说实在我不知道&#xff0c;而且三个选项一个都不认识。 ? Add an End-to-End Testing Solution? › - Use arrow-keys. Return to submit. ❯ NoCypressNigh…

QChart柱状图

//柱状图// 创建柱状图数据QBarSet *set0 new QBarSet("");*set0 << 1601 << 974 << 655 << 362;QBarSeries *series new QBarSeries();series->append(set0);set0->setColor(QColor("#F5834B"));// 创建柱状图QChart *ch…

基于springboot+vue的美食烹饪互动平台

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

Anthropic

文章目录 关于 Anthropic公司产品anthropic-sdk-python 基本使用 关于 Anthropic 官网&#xff1a;https://www.anthropic.comhuggingface : http://huggingface.co/Anthropicgithub : https://github.com/anthropics https://github.com/anthropics/anthropic-sdk-python官方…