前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >【Linux】:应用层自定义协议 & 序列化 & 网络版计算器

【Linux】:应用层自定义协议 & 序列化 & 网络版计算器

作者头像
IsLand1314
发布2025-01-24 09:09:31
发布2025-01-24 09:09:31
4300
代码可运行
举报
文章被收录于专栏:学习之路学习之路
运行总次数:0
代码可运行

1. 前言 🚀

🔥 在网络编程中,协议是一个关键概念。协议本质上是一种“约定”,规定了两方在通信时如何格式化和处理数据。本文将深入探讨如何通过协议进行结构化数据的传输,并且通过一个具体的网络版计算器( TCP服务器-客户端)示例,展示序列化与反序列化的实现。

2. 应用层

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

2.1 再谈协议 -- 结构化数据的传输

🔥 协议,简单来说,就是通信双方都遵守的规则。比如:我们每天上学,为了避免不迟到,就会定一个闹钟来提醒自己在一个特定的时间起床,这就是一种约定——协议。

🧀 在网络通信中,数据通常以字节流的形式发送和接收。

  • 当我们需要传输的是结构化数据时,例如在QQ群聊中,除了文字消息外,还包含头像、时间和昵称。这些信息都需要以某种方式发送给对方。
  • 如果我们逐个发送这些数据,不仅麻烦,接收方也难以处理,因此需要对这些数据进行 打包 成一个然后发给对方。
  • 为什么要把字符串转成结构化数据呢?未来这个结构化的数据一定是一个对象,然后使用它的时候,直接对象.url 、对象.time 拿到。

而这里的结构体如message就是传说中的业务协议。 因为它规定了我们聊天时网络通信的数据。

结论:协议就是双方约定好的结构化的数据

2.2 序列化反序列化

🔥 为了简化结构化数据的传输,我们通常将多个独立的信息合并为一个报文。这就是序列化的过程:将数据打包成一个字符串或字节流,再通过网络发送。接收方收到数据后,需要通过反序列化,将收到的数据解析回原来的结构化数据。

为了方便理解,我们下面举一个网络版计算器的例子

2.3 网络版计算器

例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端.

约定方案一:

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

约定方案二:

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

无论我们采用方案一, 还是方案二,还是其他的方案, 只要保证一端发送时构造的数据,在另一端能够正确的进行解析就是可以的。这种约定就是 应用层协议

但是,为了深刻理解协议,下面将会自定义实现一下协议的过程。

  • 我们采用方案 2,我们也要体现协议定制的细节
  • 我们要引入序列化和反序列化,只不过我们这里直接采用现成的方案 -- jsoncpp库
  • 我们要对 socket 进行字节流的读取处理
2.4 基本实现 -- 知识补充

如果要实现它,我们需要用到我们之前所学的知识

  1. 网络功能 --- tcp 来做(之前的博客有说过,这里就不过多说了)
  2. 协议定好 + 序列化和反序列化的方案
  3. tcp 全双工 + 面向字节流
协议定好 + 序列化和反序列化的方案

上面要实现序列化和反序列化,有两种方案

  1. 自己做:x + oper(+ - * /) + y,做空格的字符串分割就行
  2. xml && json && protobuf

这里我们为了增加可读性,建议将结构化数据转化为 json(jsoncpp) 的字符串,这篇文章主要是关于第二种方案

Tcp 全双工 + 面向字节流

在主机 A 中,我们创建一个 tcp sockfd 的时候,在 OS 内部会给 Tcp 套接字创建两个缓冲区(发送、接收缓冲区),同样主机 B 也是如此

  • 我们把用户层的字符串通过文件描述符传递给OS,其实本质是拷贝给操作系统,然后再由操作系统进行封装向下发送
  • 因此 我们之前用到的 read/send/write/recv 其实并不是发送到网络里面,而是发送到缓冲区里面,而数据 什么时候刷新、写到网络里面 其实是由操作系统自主决定的,对 Tcp 也同样

主机 B 接收数据,主机 A Tcp 内的发送缓冲区内的数据 经过 网络 拷贝到对方的接受缓冲区,而如果主机 B 的接受缓冲区内没数据,此时 read 就会阻塞,就相当于应用层进程就卡住,而一旦有数据,就拷贝到 buffer,那么也就是说 recv 也是拷贝,一切皆拷贝

为什么 TCP 支持全双工

  • 收发的时候有接受、发送缓冲区来进行工作,让应用层来进行拷贝
报文

从 内核原码 的角度,我们可以看到 OS 内部存在大量报文,对其管理 (先描述,再组织)

🔥 假设主机 A 的发送缓冲区有 20 个字节,对方接收缓存区假设只剩10个字节的空间了,由于 tcp你不是要进行未来要进行各种控制嘛,如果你要发送的时候对方只剩10个了,tcp 将来以一定的方式就知道了对方只剩10 个字节,所以 tcp 也就只能发送10个字节过去,因为 tcp 要进行流量控制等的,这些东西所以我们对应的tcp它就把这这个报文的一部分发给对方了

此时 read 读取就有问题了,只读取了原始报文的一部分

  • 而 Udp 不一样,它是完整数据报,UDP 规定发送出去的就是完整的报文,所以Udp 交给 OS 然后OS 是完整打包的,不会出现这个问题,因此 Udp 叫作 完整数据报
  • 那么 Tcp 需要在 应用层 保证自己报文的完整性,也就是说我们发送一个消息,发送了一半之后开始 read ,如果发现此时报文不完整就不做处理,然后下次再读一部分拼接到后面,由用户层自己决定是否读完整
  • 结论: Tcp 是面向字节流不对报文做任何完整性处理,所以需要应用层来保证报文完整性

还记得我们之前说的

不完整原因:这里读到 inbuffer 不一定是对方完整发过来的,Tcp 不进行报文完整性处理的

因此我们需要对之前的两个方案进行改变

  1. "x" "oper" "y" 是报文的内容,里面是不存在特殊字符的,因此我们这里用特殊字符隔开就行,比如 head_length\n "x" "oper" "y"\n ,head_length 表示有效载荷的长度。先读到第一个 \n,然后下标加 1,就可以把报文读上了,也把这个我们自己定义的 head_length\n 叫作报头
  2. 在方案二,我们直接加上在上面说的定制报头,形成完整报文就行,然后进行操作

注意:实际在处理的时候还要添加完整的报头

3. 网络版计算器实现

由于我们这里用到是 jsoncpp ,因此 Makefile 文件里面也要带上 -ljsoncpp

代码语言:javascript
代码运行次数:0
复制
.PHONY:all
all:server_tcp client_tcp

server_tcp:TcpServer.cc
	g++ -o $@ $^ -std=c++17 -lpthread -ljsoncpp
client_tcp:TcpClient.cc
	g++ -o $@ $^ -std=c++17 -ljsoncpp

.PHONY:clean
clean:
	rm -f client_tcp server_tcp
3.1 基本框架

根据我们之前在 Socket 网络编程Tcp 文章里面远程命令版本的 EchoServer 来编写

TcpServer.hpp

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
...

using namespace LogModule;
using namespace ThreadPoolModule;

static const uint16_t gport = 8080;
using handler_t = std::function<std::string (std::string&)>;

class TcpServer
{
    using task_t = std::function<void()>;
    struct ThreadData
    {
        int sockfd;
        TcpServer *self;
    };
public:
    TcpServer(handler_t handler, int port = gport)
      : _handler(handler),
        _port(port), 
        _isrunning(false)
    {
    }

    void InitServer()
    {
        // 1. 创建 Tcp Socket
        _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0); // TCP SOCKET
        if(_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket";
            Die(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "socket create success, socked is : " << _listensockfd;  

        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));;
        local.sin_family = AF_INET;
        local.sin_port = htons(gport);
        local.sin_addr.s_addr = INADDR_ANY;

        // 2. bind
        int n = ::bind(_listensockfd, CONV(&local), sizeof(local));
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            Die(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind success, sockfd is : " << _listensockfd;
        
        // 3.cs, tcp 是面向连接的,因此需要 tcp 随时随地等待被连接 
        // tcp 需要将 socket 设置为监听状态
        n = ::listen(_listensockfd, BACKLOG);
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error";
            Die(SOCKET_ERR);
            
        }
        LOG(LogLevel::INFO) << "listen success, sockfd is : " << _listensockfd;
    
    }

    void HandlerRequest(int sockfd) // TCP 同UDP 一样,也全双工通信
    {
        LOG(LogLevel::INFO) << "HandlerRequest, sockfd is : " << sockfd;
        char inbuffer[4096];
        std::string package;
        // 长任务,在线程池中一般是处理短任务的, 因此对应的我们的线程池应该调大点
        while(true)
        {
            memset(inbuffer, 0, sizeof(inbuffer)); // 清空缓冲区

            // 约定:用户发过来的是一个完整命令 string
            ssize_t n = ::recv(sockfd, inbuffer, sizeof(inbuffer) - 1, 0);
            
            if(n > 0)
            {
                LOG(LogLevel::INFO) << inbuffer;
                inbuffer[n] = 0; // 确保字符串以 null 结尾
                package += inbuffer; //  报文风格:len\r\n{json}\r\n

                std::string cmd_result = _handler(package);
                if(cmd_result.empty()) continue;

                ::send(sockfd, cmd_result.c_str(), cmd_result.size(), 0);
            }
            else if(n == 0)
            {
                // read 如果读取返回值为 0,表示 client 退出
                LOG(LogLevel::INFO) << "client quit: " << sockfd;
                break;
            }
            else
            {
                // 处理 read 错误
                if (errno == EINTR) {
                    continue; // 信号中断,重试
                }
                LOG(LogLevel::ERROR) << "read error: " << strerror(errno) ;
                break;
            }
        }
        // 关闭套接字
        ::close(sockfd); // fd 泄露问题

    }

    ......
private:
    int _listensockfd; // 监听 socket
    uint16_t _port;
    bool _isrunning;

    // 处理上层任务的入口
    handler_t _handler;
};

Protocol.hpp

代码语言:javascript
代码运行次数:0
复制
#pragma once

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

bool Encode(std::string message) // 添加报头
{
}

bool Decode(std::string &package)
{
}

// _x _oper _y
class Request
{
public:
    Request(int x, int y, char oper)
    : _x(x),
      _y(y),
      _oper(oper) 
    {
    }

    // 序列化
    bool Serizlize(std::string &out_string)
    {
    }

    // 反序列化
    bool Deserialize(std::string &in_string)
    {
    }

private: 
    int _x;
    int _y;
    char _oper;
};

class Response
{
public:
    Response(int result, int code)
     : _result(result), 
       _code(code)
    {
    }

    // 序列化
    bool Serizlize()
    {}

    // 反序列化
    bool Deserialize()
    {}

private:
    int _result;    // 结果
    int _code;      // 出错码,0,1,2,3,4
};
Request 类
代码语言:javascript
代码运行次数:0
复制
// _x _oper _y
class Request
{
public:
    Request() : _x(0), _y(0), _oper(0)
    {}
    Request(int x, int y, char oper)
    : _x(x),
      _y(y),
      _oper(oper) 
    {}

    // 序列化
    bool Serialize(std::string &out_string)
    {
        Json::Value root;
        root["x"] = _x;
        root["y"] = _y;
        root["oper"] = _oper;

        Json::StreamWriterBuilder wb;
        std::unique_ptr<Json::StreamWriter> w(wb.newStreamWriter());
        std::stringstream ss;
        w->write(root, &ss);
        out_string = ss.str();
        return true;
    }

    // 反序列化
    bool Deserialize(std::string &in_string)
    {
        Json::Value root;
        Json::Reader reader;
        bool parsingSuccessful = reader.parse(in_string, root);
        if (!parsingSuccessful)
        {
            std::cout << "Failed to parse JSON: " << reader.getFormattedErrorMessages() << std::endl;
            return false;
        }

        _x = root["x"].asInt();
        _y = root["y"].asInt();
        _oper = root["oper"].asInt();

        return true;
    }

    void Print()
    {
        std::cout << _x << std::endl;
        std::cout << _oper << std::endl;
        std::cout << _y << std::endl;
    }

    int X() const       {return _x;}
    int Y() const       {return _y;}
    char Oper() const    {return _oper;}

private: 
    int _x;
    int _y;
    char _oper;
};

然后我们检测一下该代码是否编写成功

代码语言:javascript
代码运行次数:0
复制
#include "Protocol.hpp"

int main()
{
    Request req(10, 20, '+');
    std::string s;
    req.Serizlize(s);
    std::cout << s << std::endl;
    
    req.Deserialize(s);
    req.Print();
    
    return 0;
}

编译运行指令如下:

代码语言:javascript
代码运行次数:0
复制
g++ Test.cc -ljsoncpp

运行结果如下:

Response 类

同上面类似,就不做过多检测和解释了,直接看代码

代码语言:javascript
代码运行次数:0
复制
class Response
{
public:
    Response(): _result(0), _code(0)
    {}
    Response(int result, int code)
     : _result(result), 
       _code(code)
    {}

    // 序列化
    bool Serialize(std::string &out_string)
    {
        Json::Value root;
        root["result"] = _result;
        root["code"] = _code;

        Json::StreamWriterBuilder wb;
        std::unique_ptr<Json::StreamWriter> w(wb.newStreamWriter());
        std::stringstream ss;
        w->write(root, &ss);
        out_string = ss.str();
        return true;
    }

    // 反序列化
    bool Deserialize(std::string &in_string)
    {
        Json::Value root;
        Json::Reader reader;
        bool parsingSuccessful = reader.parse(in_string, root);
        if(!parsingSuccessful)
        {
            std::cout << "Failed to parse JSON: " << reader.getFormattedErrorMessages() << std::endl;
            return false;
        }

        _result = root["result"].asInt();
        _code = root["code"].asInt();

        return true;
    }


    int Result() const      {return _result;}
    int Code() const        {return _code;}

    void SetResult(int res) {_result = res;}
    void SetCode(int c)     {_code = c;}
private:
    int _result;    // 结果
    int _code;      // 出错码,0,1,2,3,4
};
Encode 函数
代码语言:javascript
代码运行次数:0
复制
const std::string Sep = "\r\n";
// {json} -> len\r\n{json}\r\n
bool Encode(std::string &message) // 添加报头
{
    if(message.size() == 0) return false;
    std::string package = std::to_string(message.size()) + Sep + message + Sep; // 完整报文
    message = package;
    return true;
}
Decode 函数
代码语言:javascript
代码运行次数:0
复制
// len\r\n{json}\r\n
// 123\r\n{json}\r\n
// 不完整的就不处理
// 123\r\n
// 123\r\n{json}
// 123\r\n{json}\r\n123\r\n{json}\r
bool Decode(std::string &package, std::string *content)
{
    auto pos = package.find(Sep);
    if(pos == std::string::npos) return false;
    std::string content_length_str = package.substr(0, pos);
    int content_length = std::stoi(content_length_str);
    int full_length = content_length_str.size() + content_length + 2*Sep.size();

    if(package.size() < full_length) return false;
    *content = package.substr(pos + Sep.size(), content_length);
    
    // package erase
    package.erase(0, full_length);
    return true;
}
3.2 计算 -- Calculator.hpp
代码语言:javascript
代码运行次数:0
复制
#pragma once
#include <iostream>
#include "Protocol.hpp"

class Calculator
{
public:
    Calculator()
    {}
    Response Execute(const Request &req)
    {
        // 我们拿到的都是结构化数据,拿到就相当于类对象
        Response resp;
        switch(req.Oper())
        {
        case '+':
            resp.SetResult(req.X() + req.Y());
            break;
        case '-':
            resp.SetResult(req.X() - req.Y());
            break;
        case '*':
            resp.SetResult(req.X() * req.Y());
            break;
        case '/':
        {
            if(req.Y() == 0) {
                resp.SetCode(1); // 1 就是除0
            }
            else{
                resp.SetResult(req.X() / req.Y());
            }
        }
            break;
        case '%':
        {
            if(req.Y() == 0) {
                resp.SetCode(2); // 2 就是mod 0
            }
            else{
                resp.SetResult(req.X() % req.Y());
            }
        }
            break;
        default:
            resp.SetCode(3); // 3 表示用户发来的计算类型 无法识别
            break;
        }
        return resp;
    }
    ~Calculator()
    {}
};
3.3 服务器与客户端的代码修改

TcpServer.cc

代码语言:javascript
代码运行次数:0
复制
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "Calculator.hpp"
#include <memory>

using cal_fun = std::function<Response(const Request &req)>;

// 这里 package 不一定有完整报文
// 如果不完整让底层继续读
// 完整 -> 提取 -> 反序列化 -> Request -> 计算模块进行处理
class Parse
{
public:
    Parse(cal_fun c): _cal(c)
    {}
    std::string Entry(std::string &package)
    {
        // 1. 判断报文完整性
        std::string message;
        std::string respstr;
        
        while(Decode(package, &message))   // 一次处理多个请求
        {
            LOG(LogLevel::DEBUG) << "Content: \n" << message;
            if(message.empty()) break;

            // 2. 反序列化:message 是一个曾经被序列化的 request
            Request req;
            if(!req.Deserialize(message)) break;

            std::cout << "##############" << std::endl;
            req.Print();
            std::cout << "##############" << std::endl;

            // 3. 计算
            Response resp = _cal(req);

            // 4. 序列化
            std::string res;
            resp.Serialize(res);
            LOG(LogLevel::DEBUG) << "序列化: \n" << res; 

            // 5. 添加长度报头字段
            Encode(res);
            LOG(LogLevel::DEBUG) << "Encode: \n" << res; 

            // 6. 拼接应答
            respstr += res;
        }
        LOG(LogLevel::DEBUG) << "resptr: \n" << respstr;
        return respstr;
    }

    ~Parse()
    {}
private:
    cal_fun _cal;
};


int main()
{
    ENABLE_CONSOLE_LOG();

    // 1. 计算模块
    Calculator mycal;

    // 2. 解析对象
    Parse myparse([&mycal](const Request& req){
        return mycal.Execute(req);
    });

    // 3. 通信模块
    // 只负责进行 IO
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>([&myparse](std::string &package){
        return myparse.Entry(package);
    });    

    tsvr->InitServer();
    tsvr->start();
    return 0;
}

TcpClient.cc

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "Protocol.hpp" // 形成约定

// ./client_tcp server_ip server_port
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        std::cout << "Usage:./client_tcp server_ip server_port" << std::endl;
        return 1;
    }

    // 获取 server ip 和 port
    std::string server_ip = argv[1]; // "192.168.1.1" 点分十进制的 ip 地址
    int server_port = std::atoi(argv[2]);

    // 创建 socket
    int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0)
    {
        std::cout << "Error: Failed to create socket" << std::endl;
        return 2;
    }

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(server_port);
    server_addr.sin_addr.s_addr = inet_addr(server_ip.c_str());

    // client 不需要显示的进行 bind,tcp 是面向连接的协议,需要先建立连接
    int n = ::connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    if(n < 0)
    {
        std::cout << "Error: Failed to connect to server" << std::endl;
        return 3;
    }
    // 发送数据
    std::string message;
    while(true)
    {
        int x, y;
        char oper;
        std::cout << "input x: ";
        std::cin >> x;

        std::cout << "input y: ";
        std::cin >> y;

        std::cout << "input oper: ";
        std::cin >> oper;

        Request req(x, y, oper);

        // 1. 序列化
        req.Serialize(message);

        // 2. Encode
        Encode(message);

        // 3. 发送
        n = ::send(sockfd, message.c_str(), message.size(), 0);
        if(n > 0)
        {
            char inbuffer[1024];
            // 4. 获得应答
            int m = ::recv(sockfd, inbuffer, sizeof(inbuffer), 0);
            if(m > 0)
            {
                inbuffer[m] = 0;
                std::string package = inbuffer;
                std::string content;

                // 4. 读到应答完整 -- 暂定 decode
                Decode(package, &content);

                // 5. 反序列化
                Response resp;
                resp.Deserialize(content);

                // 6. 得到结构化数据
                std::cout << resp.Result() << "[" << resp.Code()  << "]" << std::endl;
            }
            else break;
        }
        else break;
    }

    ::close(sockfd);
    return 0;
}

结果如下:

4. 补充

4.1 Jsoncpp

Jsoncpp 是一个用于处理 JSON 数据的 C++库。它提供了将 JSON 数据序列化为字符串以及从字符串反序列化为 C++数据结构的功能。Jsoncpp 是一个开源库,广泛用于各种需要处理 JSON 数据的 C++ 项目中。

🐇 特性
  1. 简单易用:Jsoncpp 提供了直观的 API,使得处理 JSON 数据变得简单
  2. 高性能:Jsoncpp 的性能经过优化,能够高效地处理大量JSON数据
  3. 全面支持:支持 JSON 标准中的所有数据类型,包括对象、数组、字符串、数字、布尔值和 null
  4. 错误处理:在解析 JSON 数据时,Jsoncpp 提供了详细的错误信息和位置,方便开发者调试

💡 主要功能

  1. JSON 解析
    • 将 JSON 字符串或文件解析为 C++ 对象(Json::Value)。
    • 支持解析 JSON 对象、数组、字符串、数字、布尔值和 null
  2. JSON 生成
    • 将 C++ 对象(Json::Value)序列化为 JSON 字符串。
    • 支持生成格式化的 JSON 字符串(易于阅读)或紧凑的 JSON 字符串(节省空间)。
  3. 数据访问
    • 提供简单易用的 API 来访问和修改 JSON 数据。
    • 支持通过键(对于对象)或索引(对于数组)访问数据。
  4. 错误处理
    • 提供详细的错误信息,便于调试 JSON 解析或生成过程中的问题。
🐇 安装
代码语言:javascript
代码运行次数:0
复制
C++
ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel

当使用 Jsoncpp 库进行JSON 的序列化和反序列化时,确实存在不同的做法和工具类可供选择。以下是对 Jsoncpp 中序列化和反序列化操作的详细介绍:

🐇 序列化

🔥 序列化指的是将数据结构或对象转换为一种格式,以便在网络上传输或存储到文件中。Jsoncpp 提供了多种方式进行序列化:

① 使用 Json::Valuetostyledstring 方法

  • 优点:将 Json::Value 对象直接转换为格式化的 JSON 字符串。

示例:

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{
	Json::Value root;
	root["name"] = "joe";
	root["sex"] = "男";
	std::string s = root.toStyledString();
	std::cout << s << std::endl;
	return 0;
}

② 使用 Json::StreamWriter

  • 优点:提供了更多的定制选项,如缩进、换行符等

示例:

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
    Json::Value root;
    root["name"] = "joe"; root["sex"] = "男";
    Json::StreamWriterBuilder wbuilder; // StreamWriter 的工厂

    std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());
    std::stringstream ss;
    writer->write(root, &ss);
    std::cout << ss.str() << std::endl;
    return 0;
}

③ 使用 Json::FastWriter

  • 优点:比 StyledWriter 更快,因为它不添加额外的空格和换行符。
代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
	Json::Value root;
	root["name"] = "joe";
	root["sex"] = "男";
	Json::FastWriter writer;
	std::string s = writer.write(root);
	std::cout << s << std::endl;
	return 0;
}

输出:
{ "name":"joe","sex" : "\u7537" }



int main()
{
	Json::Value root;
	root["name"] = "joe";
	root["sex"] = "男";
	// Json::FastWriter writer;
	Json::StyledWriter writer;
	std::string s = writer.write(root);
	std::cout << s << std::endl;
	return 0;
}

// 输出
{
	"name" : "joe",
		"sex" : "\u7537"
}
🐇 反序列化

🔥 反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。Jsoncpp 提供 了以下方法进行反序列化:

① 使用 Json::Reader (已弃用)

  • 用于解析 JSON 字符串(推荐使用 Json::CharReaderBuilder 替代)
  • 优点:提供详细的错误信息和位置,方便调试。

示例:

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main() {
	// JSON 字符串
	std::string json_string = "{\"name\":\"张三\", \"age\":30, \"city\":\"北京\"}";
	// 解析 JSON 字符串
	Json::Reader reader;
	Json::Value root;
	// 从字符串中读取 JSON 数据
	bool parsingSuccessful = reader.parse(json_string,
		root);
	if (!parsingSuccessful) {
		// 解析失败,输出错误信息
		std::cout << "Failed to parse JSON: " <<
			reader.getFormattedErrorMessages() << std::endl;
		return 1;
	}
	// 访问 JSON 数据
	std::string name = root["name"].asString();
	int age = root["age"].asInt();
	std::string city = root["city"].asString();
	// 输出结果
	std::cout << "Name: " << name << std::endl;
	std::cout << "Age: " << age << std::endl;
	std::cout << "City: " << city << std::endl;
	return 0;
}

使用 Json::CharReader 的派生类(不推荐了,上面的足够了):

  • 在某些情况下,你可能需要更精细地控制解析过程,可以直接使用 Json::CharReader 的派生类。
  • 但通常情况下,使用 Json::parseFromStream 或 Json::Reader 的 parse 方法就足够了
🐇 总结
  1. toStyledString、StreamWriter 和 FastWriter 提供了不同的序列化选项,可以根据具体需求选择使用。
  2. Json::Reader 和 parseFromStream 函数是 Jsoncpp 中主要的反序列化工具,它们提供了强大的错误处理机制。
  3. 在进行序列化和反序列化时,请确保处理所有可能的错误情况,并验证输入和输出的有效性。
4.2 Json::Value

Json::Value 是 Jsoncpp 库中的一个重要类,用于表示和操作 JSON 数据结构。以下是一些常用的 Json::Value 操作列表:

1. 构造函数
  • Json::Value():默认构造函数,创建一个空的 Json::Value 对象。
  • Json::Value(ValueType type, bool allocated = false):根据给定的

ValueType(如 nullValue, intValue, stringValue 等)创建一个 Json::Value 对象。

2. 访问元素
  • Json::Value & operator[](const char* key) :通过键(字符串)访问对象中的元素。如果键不存在,则创建一个新的元素。
  • Json::Value& operator[](const std::string& key) :同上,但使用 std::string 类型的键。
  • Json::Value& operator[](ArrayIndex index) :通过索引访问数组中的元素。如果索引超出范围,则创建一个新的元素。
  • Json::Value& at(const char* key) :通过键访问对象中的元素,如果键不存在则抛出异常。
  • Json::Value& at(const std::string& key) :同上,但使用 std::string 类型的键。
3. 类型检查

函数名称

作用

bool isNull()

检查值是否为 null

bool isBool()

检查值是否为布尔类型

bool isInt()

检查值是否为整数类型

bool isInt64()

检查值是否为 64 位整数类型

bool isUInt()

检查值是否为无符号整数类型

bool isUInt64()

检查值是否为 64 位无符号整数类型

bool isIntegral()

检查值是否为整数或可转换为整数的浮点数

bool isDouble()

检查值是否为双精度浮点数

bool isNumeric()

检查值是否为数字(整数或浮点数)

bool isString()

检查值是否为字符串

bool isArray()

检查值是否为数组

bool isObject():

检查值是否为对象(即键值对的集合)

4. 赋值和类型转换

名称

作用

Json::Value& operator=(bool value)

将布尔值赋给 Json::Value 对象

Json::Value& operator=(int value)

将整数赋给 Json::Value 对象

Json::Value& operator=(unsigned int value)

将无符号整数赋给 Json::Value 对象

Json::Value& operator=(Int64 value)

将 64 位整数赋给 Json::Value 对象

Json::Value& operator=(UInt64 value)

将 64 位无符号整数赋给 Json::Value 对象

Json::Value& operator=(double value)

将双精度浮点数赋给 Json::Value 对象

Json::Value& operator=(const char* value)

将 C 字符串赋给Json::Value 对象

Json::Value& operator=(const std::string& value)

将 std::string 赋给 Json::Value 对

bool asBool()

将值转换为布尔类型(如果可能)

int asInt()

将值转换为整数类型(如果可能)

Int64 asInt64()

将值转换为 64 位整数类型(如果可能)

unsigned int asUInt()

将值转换为无符号整数类型(如果可能)

UInt64 asUInt64()

将值转换为 64 位无符号整数类型(如果可能)

double asDouble()

将值转换为双精度浮点数类型(如果可能)

std::string asString()

将值转换为字符串类型(如果可能)

5. 数组和对象操作
  • size_t size():返回数组或对象中的元素数量。
  • bool empty():检查数组或对象是否为空。
  • void resize(ArrayIndex newSize):调整数组的大小。
  • void clear():删除数组或对象中的所有元素。
  • void append(const Json::Value& value):在数组末尾添加一个新元素。
  • Json::Value& operator[](const char* key, const Json::Value&
  • defaultValue = Json::nullValue):在对象中插入或访问一个元素,如果键不存在则使用默认值。
  • Json::Value& operator[](const std::string& key, const Json::Value& defaultValue = Json::nullValue):同上,但使用 std::string类型的

5. 共勉 🔥

上面代码在我的gitee 网络版计算器 里面有

【*★,°*:.☆( ̄▽ ̄)/$:*.°★* 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新关于【Linux】的内容,请持续关注我 !!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-01-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 前言 🚀
  • 2. 应用层
    • 2.1 再谈协议 -- 结构化数据的传输
    • 2.2 序列化反序列化
    • 2.3 网络版计算器
    • 2.4 基本实现 -- 知识补充
      • 协议定好 + 序列化和反序列化的方案
      • Tcp 全双工 + 面向字节流
      • 报文
  • 3. 网络版计算器实现
    • 3.1 基本框架
      • Request 类
      • Response 类
      • Encode 函数
      • Decode 函数
    • 3.2 计算 -- Calculator.hpp
    • 3.3 服务器与客户端的代码修改
  • 4. 补充
    • 4.1 Jsoncpp
      • 🐇 特性
      • 🐇 安装
      • 🐇 序列化
      • 🐇 反序列化
      • 🐇 总结
    • 4.2 Json::Value
      • 1. 构造函数
      • 2. 访问元素
      • 3. 类型检查
      • 4. 赋值和类型转换
      • 5. 数组和对象操作
  • 5. 共勉 🔥
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档