应用层自定义协议与序列化

目录

一、TCP协议数据接收

[1.1 重新理解read、write、send](#1.1 重新理解read、write、send)

[1.2 TCP为什么支持全双工](#1.2 TCP为什么支持全双工)

[1.3 存在的问题](#1.3 存在的问题)

二、应用层自定义协议

[2.1 了解应用层](#2.1 了解应用层)

[2.2 了解协议](#2.2 了解协议)

[2.3 序列化和反序列化](#2.3 序列化和反序列化)

三、网络版计数器

[3.1 Socket封装](#3.1 Socket封装)

[3.2 定制协议](#3.2 定制协议)

[3.3 流式数据的处理](#3.3 流式数据的处理)

[3.3.1 Jsoncpp](#3.3.1 Jsoncpp)

3.3.1.1了解Jsoncpp

3.3.1.2Jsoncpp的安装

[3.3.1.3 Json::Value](#3.3.1.3 Json::Value)

[3.3.2 Jsoncpp的使用](#3.3.2 Jsoncpp的使用)

[3.3.2.1 序列化](#3.3.2.1 序列化)

[1.Json::Value的toStyledString 方法](#1.Json::Value的toStyledString 方法)

2.Json::StreamWriter

3.Json::FastWriter

[3.3.2.2 反序列化](#3.3.2.2 反序列化)

1.Json::Reader

2.Json::CharReader

[3.4 规定报文格式](#3.4 规定报文格式)

[3.5 计算业务](#3.5 计算业务)

[3.6 完整代码](#3.6 完整代码)

四、与OSI七层模型的对应关系


一、TCP协议数据接收

在前面的TCP和UDP socket编程中,我们使用read来接收数据。但是UDP中使用read不会出现问题,而TCP中接收数据使用到的read都是有问题的,原因是:

TCP是流式协议,内核只维护一份连续字节缓冲区,协议本身不存在天然数据包边界,直接调用一次 read 读取数据会出现两类典型问题:

1.粘包:多条业务数据在内核缓冲区被合并,单次 read 直接读到多条完整业务消息;

2.拆包:单条业务数据长度超过本次读取容量,一次 read 仅读到消息片段,剩余字节仍留存在内核缓冲区。

我们当前客户端、服务端代码直接固定缓冲区 1024 字节调用 read,完全没有做分包边界处理,运行时一定会出现命令错乱、输出截断等异常。 但以上都只是浮于表面的现象,没有触及问题根源。想要彻底看透这类边界问题的本质、并给出可靠解决方案,我们需要先重新吃透read、write、send这三个IO 接口,同时搞懂 TCP 天然支持全双工通信的底层逻辑。

1.1 重新理解read、write、send

前面我们所写的echoserver、dictserver、Command这些都属于应用层的业务逻辑。以TCP通信为例,对每一个文件描述符(一组链接),TCP在底层都会为我们提供一个数据接收缓冲区和发送缓冲区,建立链接的双方(主机A和主机B)都会有这两个缓冲区。

以主机A的视角来看,当要发送数据时,write并不是直接把数据发送到网络空间中进行传输,而是先将用户空间中的数据先拷贝到 发送缓冲区中。发送缓冲区的数据什么时候发、能存多少、出错了怎么办,这些问题都是由传输层TCP自己通过传输控制协议来维护;而这两个缓冲区属于操作系统内核进行维护。

进行数据发送时,操作系统使用TCP逻辑,将主机A的数据从发送缓冲区中发送到主机B的接收缓冲区中。当主机B的接收缓存区中有数据,就将缓冲区中的数据拷贝到用户空间中;如果缓冲区中没有数据,则read阻塞。当写入数据时写得太快,发送缓冲区被写满了,那么write就会阻塞住。

所以read、write本质上就是拷贝函数。而网络间的通信本质上是通信双方(主机A、B)的操作系统在进行网络通信,用户只是通过主机将要发送的数据交给操作系统,然后由操作系统来进行完整的网络通信的服务。

1.2 TCP为什么支持全双工

由前面的分析我们可以得到,通信双方各有一对缓冲区。所以一端在进行信息的发送时既可以发送,同时也可以接收来自另一端发送的信息,这一对缓冲区的工作互相不影响。所以TCP就能支持全双工通信了。而用户往缓冲区里写数据,操作系统从缓冲区中拿数据再发送到网络中去,本质上可以看作一个局部的生产者消费者模型。

所以前面提到的read和write会产生阻塞实际上就是用户和内核这两个角色的直接的同步过程。

那前面所提到的缓冲区在哪里呢?

在上述的操作系统与用户进行数据交换的过程中,就有可能会出现操作系统内部有很多的报文,而有些报文还没来得及放入到接收缓冲区中。于是操作系统就需要对收到的报文进行管理 。要管理就必须先描述,再组织!操作系统中存在着保存和管理这些报文的数据结构------struct sk_buff。

当我们从网络中收到报文时,操作系统会分配sk_buff结构体存储报文数据,记录协议、序列号等元信息,再挂载 到对应TCP连接的内核接收队列。用户进程调用read读取数据时,内核再把sk_buff里的字节拷贝到用户缓冲区,读完后释放对应的sk_buff。

进程PCB(task_struct)中维护文件描述符表,每个文件描述符对应一个struct file;socket 类型的struct file关联struct socket,struct socket再指向内核协议栈的struct sock。

而在sock这个结构体中包含了下面的这两个队列:

当我们收到**报文(sk_buff)**时,操作系统会将报文插入到receive_queue中,发送报文时会将报文插入write_queue中。而这就是接收和发送缓冲区!

1.3 存在的问题

当用户调用write输入数据之后,这些数据有没有发送到网络中,我们并不能确定。前面提到过,发送缓冲区的数据什么时候发、能存多少、出错了怎么办,这些问题都是由TCP传输控制协议来维护。

以远程命令执行服务器为例,假设主机A向服务器发送命令ls -a -l,当用户调用write把数据交给操作系统后,操作系统通过TCP逻辑将信息发送到服务器。但是由于TCP协议是面向字节流的,所以就有可能操作系统先发送了ls,并没有发送完一整条指令。然后服务器接收到这条指令,并执行ls。后续主机A再发送后续的指令-a -l,服务器认为这条指令是错误的指令,无法执行。

换句话来说,你所接收到的字符串数据有可能只是别人发送过来的一部分!站在TCP角度是没有错的。这种问题就是数据的拆包问题!

同理可得,由于TCP协议是面向字节流的,你接收到的字符串数据,也有可能不只是一段,而是别人好几次发送的数据拼在一起堆过来的!这种问题就是数据的粘包问题!

那在前面我们进行TCP socket编程时,为什么没有出现这种情况呢?

其实只是巧合,要么是我们收发的数据量太小,要么受操作系统缓冲区调度影响,这类问题属于偶发问题,不是每次运行都会复现。

二、应用层自定义协议

粘包、拆包这类问题没法靠 TCP 协议本身去解决,站在 TCP 的设计层面,字节流无边界本身就是正常机制,不存在任何错误。于是就需要上层用户自己来保证数据的完整性 !而这种解决方法就是应用层自定义协议!

2.1 了解应用层

在 TCP/IP 四层模型中,应用层位于最顶层,是直接面向用户程序的层级,下层传输层、网络层、网络接口层仅负责数据传输,不会区分业务消息边界,粘包、拆包问题都需要应用层自行处理。我们程序员写的一个个解决我们实际问题,满足我们日常需求的网络程序,都是在应用层。

2.2 了解协议

协议是一种 "约定"。socket api 的接口,在读写数据时,都是按 "字符串" 的方式来发送接收的。如果我们要传输一些 "结构化的数据" 怎么办呢?

其实,协议就是双方约定好的结构化的数据。

我们以网络版计算器举例。 我们要实现一个服务器加法器,客户端把参与计算的数据发给服务端,服务端完成运算之后再把结果回传给客户端。

Socket 只能收发字节流,不能直接在网络上传输程序内存中的结构体对象。

方案一:可以直接约定文本格式,客户端发送形如1+2这样的字符串,服务端解析字符串提取出两个整数完成计算。

方案二:我们可以定义结构体,用来存放操作数、运算符这类交互信息。然后再在发送数据时将这个结构体按照一个规则转换成字符串,接收到数据的时候再按照相同的规则把字符串转化回结构体。

2.3 序列化和反序列化

上述方案二中的过程就是序列化和反序列化。

发送方需要把结构体,按照双方提前约定好的规则转换成字节字符串,这个过程就是序列化 ; 接收方拿到字节数据之后,再依照完全相同的规则,把字节还原回程序中的结构体,这个过程就是反序列化。

序列化与反序列化,就是应用层自定义协议当中,处理结构化数据收发的核心手段 。同时,我们还可以在这套协议里增加消息长度字段,一并解决 TCP 带来的粘包、拆包问题。

下面是一个收发一条聊天信息时序列化和反序列化的例子:

序列化和反序列化的好处,就在于它可以让我们编写出来的代码具有很好的拓展性!通信双方不需要使用完全相同的编程语言,也不需要运行在同一套硬件平台上,只要遵循同一套协议格式,就可以完成网络数据交互。

为了让我们深刻理解协议,我们使用自定义实现一下协议的过程。

1.我们采用方案二,要体现协议定制的细节;

2.引入序列化和反序列化,我们采用现成的方案--jsoncpp库;

3.我们要对socket进行字节流的读取处理;

三、网络版计数器

3.1 Socket封装

无论是TCP还是UDP我们都需要先创建套接字、绑定套接字、监听套接字。所以第一步先要封装创建套接字的函数,这些函数放在Socket类中。但由于TCP和UDP的接口不一样,所以我们把这些函数设计成虚函数,然后再设计TcpSocket类和UdpSocket类继承父类Socket类,分开来实现各自的函数。

由于UDP不需要监听套接字,所以在UDP类中具体实现的时候使用空方法就行了。定义模板方法BuildTcpSocketMethod()和BuildUdpSocketMethod(),固定步骤顺序,然后把步骤的具体实现延迟交给子类来完成。这种模式我们称之为模板方法模式!这种模式比较适合解决完成某项业务的执行流程相对固定、步骤套路化,但流程中每一步的底层实现逻辑存在较大差异。

cpp 复制代码
namespace NS_SOCKET_MODULE
{
    class Socket
    {
        public:
            ~Socket()
            {
            }

        public:
            virtual void CreateSocketOrDie() = 0;
            virtual void BindSocketOrDie(uint16_t port) = 0;
            virtual void ListenSocketOrDie() = 0;

        public:
            void BuildTcpSocketMethod()//模板方法
            {
                CreateSocketOrDie();
                BindSocketOrDie();
                ListenSocketOrDie();
            }

            void BuildUdpSocketMethod()
           {
               CreateSocketOrDie();
               BindSocketOrDie();
           }
    };

    };

    class TcpSocket:public Socket
    {

    };
    class TcpSocket:public Socket
    {

    };
}

接下来分别实现子类TCP和UDP中的实现方法(以实现子类TCP为例进行讲解实现过程)。CreateSocketOrDie()、BindSocketOrDie()、ListenSocketOrDie()三个函数的完整实现:

cpp 复制代码
void CreateSocketOrDie() override
    {
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(SOCKET_ERR);
        }
    }

    void BindSocketOrDie(uint16_t port) override
    {
        InetAddr addr(port);
        if (bind(_sockfd, addr.NetAddress(), addr.Len()) != 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(BIND_ERR);
        }
    }

    void ListenSocketOrDie() override
    {
        if (listen(_sockfd, gbacklog) != 0)
        {
            LOG(LogLevel::FATAL) << "listen socket error";
            exit(LISTEN_ERR);
        }
    }

逐步完善后的完整代码:

Socket.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <memory>
#include "Logger.hpp"
#include "InetAddr.hpp"

#define CONV(addr) ((struct sockaddr*)addr)

namespace NS_SOCKET_MODULE
{
using namespace NS_LOG_MODULE;

static const int gbacklog = 16;
enum
{
    OK = 0,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR
};

// 模版方法模式!
class Socket
{
public:
    virtual ~Socket() = default;
protected:
    // 模板方法拆分的基础步骤接口,仅子类内部调用,不对外暴露
    virtual void CreateSocketOrDie() = 0;
    virtual void BindSocketOrDie(uint16_t port) = 0;
    virtual void ListenSocketOrDie() = 0;

public:
    virtual std::shared_ptr<Socket> Accepter(InetAddr &addr) = 0;
    virtual int Sockfd() = 0;
    virtual int Recv(std::string *out) = 0;
    virtual int Send(const std::string &in) = 0;
    virtual void Close() = 0;
    virtual bool Connect(InetAddr &addr) = 0;

public:
    void BuildTcpSocketMethod(uint16_t port) // 模版方法
    {
        CreateSocketOrDie();
        BindSocketOrDie(port);
        ListenSocketOrDie();
    }

    void BuildTcpClientSockMethod()
    {
        CreateSocketOrDie();
    }

    void BuildUdpSocketMethod(uint16_t port)
    {
        CreateSocketOrDie();
        BindSocketOrDie(port);
    }
};

class TcpSocket : public Socket
{
public:
    TcpSocket() : _sockfd(-1)
    {}
    TcpSocket(int sockfd): _sockfd(sockfd)
    {}

    void CreateSocketOrDie() override
    {
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(SOCKET_ERR);
        }
    }

    void BindSocketOrDie(uint16_t port) override
    {
        InetAddr addr(port);
        if (bind(_sockfd, addr.NetAddress(), addr.Len()) != 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(BIND_ERR);
        }
    }

    void ListenSocketOrDie() override
    {
        if (listen(_sockfd, gbacklog) != 0)
        {
            LOG(LogLevel::FATAL) << "listen socket error";
            exit(LISTEN_ERR);
        }
    }

    std::shared_ptr<Socket> Accepter(InetAddr &clientaddr) override
    {
        struct sockaddr_in addr;
        socklen_t len = sizeof(addr);
        int sockfd = accept(_sockfd, CONV(&addr), &len);
        if(sockfd < 0)
        {
            LOG(LogLevel::WARNING) << "accept error";
            return nullptr;
        }
        clientaddr = addr;
        return std::make_shared<TcpSocket>(sockfd);//基类指针返回子类对象
    }

    int Sockfd() override
    {
        return _sockfd;
    }

    int Recv(std::string *out) override
    {
        char inbuffer[1024];
        ssize_t n = recv(_sockfd, inbuffer, sizeof(inbuffer)-1, 0);
        if(n > 0)
        {
            inbuffer[n] = 0;
            *out += inbuffer; // 追加写入的
        }
        return static_cast<int>(n);
    }

    int Send(const std::string &in) override
    {
        return static_cast<int>(send(_sockfd, in.c_str(), in.size(), 0));
    }

    void Close() override
    {
        if(_sockfd >= 0)
        {
            close(_sockfd);
            _sockfd = -1;
        }
    }

    bool Connect(InetAddr &addr) override
    {
        int n = connect(_sockfd, addr.NetAddress(), addr.Len());
        return n >= 0;
    }

    ~TcpSocket()
    {
        Close();
    }

private:
    int _sockfd;
};

// 完整实现UdpSocket
class UdpSocket: public Socket
{
public:
    UdpSocket() : _sockfd(-1)
    {}
    UdpSocket(int sockfd) : _sockfd(sockfd)
    {}

protected:
    void CreateSocketOrDie() override
    {
        // UDP使用SOCK_DGRAM
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create udp socket error";
            exit(SOCKET_ERR);
        }
    }

    void BindSocketOrDie(uint16_t port) override
    {
        InetAddr addr(port);
        if (bind(_sockfd, addr.NetAddress(), addr.Len()) != 0)
        {
            LOG(LogLevel::FATAL) << "bind udp socket error";
            exit(BIND_ERR);
        }
    }

    // UDP无listen,空实现,仅满足纯虚函数要求
    void ListenSocketOrDie() override
    {
        LOG(LogLevel::WARNING) << "UDP socket does not support listen, skip";
    }

public:
    // UDP无accept,返回空指针
    std::shared_ptr<Socket> Accepter(InetAddr &addr) override
    {
        LOG(LogLevel::WARNING) << "UDP socket does not support accept";
        return nullptr;
    }

    int Sockfd() override
    {
        return _sockfd;
    }

    // UDP专用recvfrom,读出对端地址到addr
    int Recv(std::string *out) override
    {
        char buf[1024];
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        ssize_t n = recvfrom(_sockfd, buf, sizeof(buf)-1, 0, CONV(&peer), &len);
        if (n > 0)
        {
            buf[n] = '\0';
            *out += buf;
        }
        return static_cast<int>(n);
    }

    // 重载:可指定接收对端地址
    int Recv(std::string *out, InetAddr &peerAddr)
    {
        char buf[1024];
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        ssize_t n = recvfrom(_sockfd, buf, sizeof(buf)-1, 0, CONV(&peer), &len);
        if (n > 0)
        {
            buf[n] = '\0';
            *out += buf;
            peerAddr = peer;
        }
        return static_cast<int>(n);
    }

    // Send默认发送给已connect的对端
    int Send(const std::string &in) override
    {
        return static_cast<int>(send(_sockfd, in.c_str(), in.size(), 0));
    }

    // 重载:指定地址发送数据
    int SendTo(const std::string &in, InetAddr &peerAddr)
    {
        return static_cast<int>(sendto(
            _sockfd,
            in.c_str(),
            in.size(),
            0,
            peerAddr.NetAddress(),
            peerAddr.Len()
        ));
    }

    void Close() override
    {
        if (_sockfd >= 0)
        {
            close(_sockfd);
            _sockfd = -1;
        }
    }

    // UDP connect仅绑定默认通信对端,不建立连接
    bool Connect(InetAddr &addr) override
    {
        int ret = connect(_sockfd, addr.NetAddress(), addr.Len());
        if (ret < 0)
        {
            LOG(LogLevel::WARNING) << "udp connect failed";
            return false;
        }
        return true;
    }

    ~UdpSocket()
    {
        Close();
    }

private:
    int _sockfd;
};

} // namespace NS_SOCKET_MODULE

无论要实现哪种服务,都需要有一个部分,专门用于实现网络通信的逻辑:

TcpServer.hpp

cpp 复制代码
#include "Logger.hpp"
#include "InetAddr.hpp"
#include "Socket.hpp"
#include <memory>
#include <unistd.h>
#include <signal.h>
#include <functional>

static uint16_t gport = 8080;

using namespace NS_SOCKET_MODULE;

using Handler_t = std::function<std::string(std::string&)>;

class TcpServer
{
public:
    TcpServer(Handler_t handler, uint16_t port = gport) 
    : _port(port),
     _listensock(std::make_unique<TcpSocket>()),
     _handler(handler)
    {
        _listensock->BuildTcpSocketMethod(_port); // 使用创建listensockfd的模版方法!
        LOG(LogLevel::INFO) << "create listen socket success: " << _listensock->Sockfd();
    }
    void Loop()
    {
        signal(SIGCHLD, SIG_IGN);
        while (true)
        {
            InetAddr clientaddr;
            auto sockfd = _listensock->Accepter(clientaddr);
            if (!sockfd)
                continue;
            LOG(LogLevel::DEBUG) << "get a new link, socket address: " << clientaddr.ToString() << " sockfd: " << sockfd->Sockfd();

            if (fork() == 0)
            {
                // child
                service(sockfd, clientaddr);
                sockfd->Close();
                exit(0);
            }
        }
    }
    ~TcpServer()
    {
    }

private:
    void service(std::shared_ptr<Socket> sockfd, InetAddr &clientaddr)
    {
        std::string inbuffer, outbuffer;
        while (true)
        {
            outbuffer.clear();
            
            int n = sockfd->Recv(&inbuffer);
            if(n <= 0)
            {
                LOG(LogLevel::WARNING) << "recv: client quit, " << clientaddr.ToString();
                break;
            }

            LOG(LogLevel::DEBUG) << "inbuffer:\n" << inbuffer;

            if(_handler)
                outbuffer += _handler(inbuffer);

            if(outbuffer.empty())
                continue;

            LOG(LogLevel::DEBUG) << "outbuffer:\n" << outbuffer;
            n = sockfd->Send(outbuffer);
            if(n < 0)
            {
                LOG(LogLevel::WARNING) << "send: client quit, " << clientaddr.ToString();
                break;
            }
        }
    }

private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensock;
    Handler_t _handler;
};

3.2 定制协议

在序列化和反序列化时,我们有两种方法,一种是自己手动编写序列化与反序列化的代码,另一种是使用别人封装好的,现成的代码(json、protobuf、xml)。json是可视化的、可读性非常好的序列化方法;而protobuf的可视化效果不好,但效率最高。由于手动编写过程比较繁杂,这里我们使用可读性较好的json来实现序列化与反序列化。

3.3 流式数据的处理

3.3.1 Jsoncpp

3.3.1.1了解Jsoncpp

在使用json进行序列化和反序列化之前,我们先要了解C++中用于处理json数据的库--Jsoncpp。

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

Jsoncpp有几个特性:

1.简单易用:Jsoncpp 提供了直观的 API,使得处理 JSON 数据变得简单。

2.高性能:Jsoncpp 的性能经过优化,能够高效地处理大量 JSON 数据。

3.全面支持:支持 JSON 标准中的所有数据类型,包括对象、数组、字符串、数字、布尔值和 null。

4.错误处理:在解析 JSON 数据时,Jsoncpp 提供了详细的错误信息和位置,方便开发者调试。

3.3.1.2Jsoncpp的安装
bash 复制代码
1  ubuntu: sudo apt-get install -y libjsoncpp-dev
2  Centos: sudo yum install -y jsoncpp-devel

安装Jsoncpp其实是在安装一个库,所以安装成功后其头文件一般会放在include目录下(以ubantu为例):

而json中包含了json的头文件:

而库文件会被放在:

3.3.1.3 Json::Value

无论是在使用jsoncpp进行序列化还是反序列化的过程中,都离不开一种非常重要的一种数据类型--Json::Value,这个数据类型贯穿序列化和反序列化的始终。Json::Value就类似于一种万能对象,在其内部都对任意类型的中括号操作都进行了重载。

3.3.2 Jsoncpp的使用

3.3.2.1 序列化
1.Json::Value的toStyledString 方法

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

使用示例:

cpp 复制代码
#include <iostream>
#include <jsoncpp/json/json.h>

int main()
{
    // 构造Json::Value对象
    Json::Value root;
    root["name"] = "zhangsan";
    root["age"] = 20;
    root["major"] = "computer";

    // toStyledString:格式化JSON字符串(带缩进换行)
    std::string str = root.toStyledString();

    std::cout << str << std::endl;
    return 0;
}

输出结果:

复制代码
{
   "name" : "zhangsan",
   "age" : 20,
   "major" : "computer"
}
2.Json::StreamWriter

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

使用示例:

1.直接返回字符串:

cpp 复制代码
#include <iostream>
#include <sstream>
#include <jsoncpp/json/json.h>

int main()
{
    Json::Value root;
    root["name"] = "lisi";
    root["age"] = 21;
    root["hobby"] = "coding";

    // 构造输出配置:自定义缩进为4个空格
    Json::StreamWriterBuilder builder;
    builder["indentation"] = "    ";   // 设置缩进4空格

    // 将Json::Value序列化为字符串
    std::string jsonStr = Json::writeString(builder, root);

    std::cout << jsonStr << std::endl;
    return 0;
}

2.流式写法(输出到cout流、文件中):

cpp 复制代码
#include <iostream>
#include <memory>
#include <jsoncpp/json/json.h>

int main()
{
    Json::Value root;
    root["name"] = "lisi";
    root["age"] = 21;
    root["hobby"] = "coding";

    Json::StreamWriterBuilder builder;
    builder["indentation"] = "  ";

    // builder.newStreamWriter() 返回 Json::StreamWriter* 裸指针
    // unique_ptr 智能指针接管这个指针,出作用域自动 delete
    std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());

    //swriter->write(待序列化JsonValue, 输出流指针)
    //把内存中的"root"JSON树,序列化文本写入"ss"这个stringstream缓冲区。
    std::stringstream ss;
    swriter->write(root, &ss);

    std::cout << ss.str() << std::endl;

    // 离开大括号,writer销毁,自动调用delete,不需要手动释放
    return 0;
}

输出结果:

cpp 复制代码
{
    "name" : "lisi",
    "age" : 21,
    "hobby" : "coding"
}
3.Json::FastWriter

优点:比StyledWriter 更快,因为它不添加额外的空格和换行符。

使用示例:

cpp 复制代码
#include <iostream>
#include <jsoncpp/json/json.h>

int main()
{
    Json::Value root;
    root["name"] = "wangwu";
    root["age"] = 24;
    root["score"] = 95;

    Json::FastWriter writer;
    std::string jsonStr = writer.write(root);

    std::cout << jsonStr << std::endl;
    return 0;
}

输出结果:

复制代码
{"name":"wangwu","age":24,"score":95}

JSON 的 value 可以不是简单数字字符串,也可以是另外一个完整 JSON 对象:

cpp 复制代码
int main()
{
    //子对象1:人的基本信息
    Json::Value sub_root1;
    sub_root1["name"] = "zhangsan";
    sub_root1["sex"]  = "man";
    sub_root1["age"]  = 18;

    //子对象2:考试分数
    Json::Value sub_root2;
    sub_root2["math"]    = 88;
    sub_root2["chinese"] = 98;

    //最外层根对象
    Json::Value root;
    root["who"]   = sub_root1;   // 把整个子对象赋值给key
    root["score"] = sub_root2;   
}

得到的json文本:

bash 复制代码
{
    "who" : 
    {
        "name" : "zhangsan",
        "sex"  : "man",
        "age"  : 18
    },
    "score" : 
    {
        "math"    : 88,
        "chinese" : 98
    }
}
3.3.2.2 反序列化
1.Json::Reader

优点:解析出错时,能输出详细错误信息、出错行号、位置,方便调试找 JSON 语法错误。

使用示例:

cpp 复制代码
#include <jsoncpp/json/json.h>
#include <iostream>
#include <string>

int main()
{
    // JSON字符串
    std::string json_str = R"({"name":"zhangsan","age":18})";

    Json::Reader reader;
    Json::Value root;

    // 解析字符串到root对象
    bool ok = reader.parse(json_str, root);

    if(!ok)
    {
        // 获取详细错误:出错位置、错误描述
        std::string err_msg = reader.getFormattedErrorMessages();
        std::cout << "解析错误:\n" << err_msg << std::endl;
        return -1;
    }
    
    std::string name = root["name"].asString();
    int age = root["age"].asInt();

    // 读取数据
    std::cout << name << std::endl;
    std::cout << age << std::endl;
    return 0;
}

输出结果:

复制代码
zhangsan
18
2.Json::CharReader

优点:可灵活配置解析策略,解析出错能够提供详细错误信息及出错位置便于调试。

使用示例:

cpp 复制代码
#include <iostream>
#include <memory>
#include <string>
#include <jsoncpp/json/json.h>

int main()
{
    std::string json_str = R"({"name":"zhangsan","age":18})";
    Json::Value root;
    std::string err; //保存错误信息

    Json::CharReaderBuilder rbuilder;
    std::unique_ptr<Json::CharReader> reader(rbuilder.newCharReader());

    // parse参数:字符串起始、字符串末尾指针,输出root,输出错误信息
    bool ok = reader->parse(json_str.c_str(),
                            json_str.c_str() + json_str.size(),
                            &root, &err);

    if(!ok)
    {
        //新版同样拿到完整错误信息
        std::cout << "解析失败:" << err << std::endl;
        return -1;
    }

    std::cout << root["name"].asString() << std::endl;
    std::cout << root["age"].asInt() << std::endl;
    return 0;
}

3.4 规定报文格式

虽然我们已经实现了序列化和反序列化,但是没法判断收发的报文是不是完整数据,所以我们还要提前约定好统一的报文传输格式。

首先规定发送报文的有效载荷的长度,如果长度对应上再从缓冲区中读取对应长度的内容。为了区分有效载荷的长度和内容,我们设置了特殊分隔符\r\n。

cpp 复制代码
class Protocol
{
public:
    Protocol(HandlerRequest_t handler) : _version("1.0"), _handler_request(handler)
    {
    }
    Protocol(HandlerResponse_t handler_response):_version("1.0"), _handler_response(handler_response)
    {
    }
  
    std::string Packet(const std::string &json_string)
    {
        return std::to_string(json_string.size()) + gsep + json_string + gsep;
    }
    
    int Unpack(std::string &packet, std::string *json_string)
    {
        if (packet.empty())
            return 0;
        if (json_string == nullptr)
            return -1;

        // 分析报文
        auto pos = packet.find(gsep);
        if (pos == std::string::npos)
            return 0;
        std::string lenstr = packet.substr(0, pos);

        // lenstr 合法性判断,lenstr -> 123 345

        int len = std::stoi(lenstr);
        int total = lenstr.size() + len + 2 * gsep.size();
        if (packet.size() < total)
            return 0;
        // 提取报文
        *json_string = packet.substr(pos + gsep.size(), len);
        packet.erase(0, total);
        return 1;
    }
    // 如果读到半个报文,什么都不做
    // 如果读到一个报文+,循环处理,把所有合法的报文都进行统一处理
    std::string ParseRequest(std::string &inbuffer)
    {
        std::string result;
        while (true)
        {
            std::string json_string;
            // 1. 解包
            int n = Unpack(inbuffer, &json_string);
            if (n < 0)
            {
                LOG(LogLevel::DEBUG) << "no way !!";
                return std::string();
            }
            if (n == 0)
            {
                LOG(LogLevel::INFO) << inbuffer << " parse done";
                return result;
            }
            LOG(LogLevel::DEBUG) << "json_string:\n" << json_string;
            LOG(LogLevel::DEBUG) << "unpack done, inbuffer:\n" << inbuffer;
            // 2. 反序列化
            // 得到一个完整的报文jsonstring
            Request req;
            if (!req.Deserialize(json_string))
                return std::string();

            // 3. 业务计算
            Response resp;
            if (_handler_request)
                resp = _handler_request(req);

            // 4. 应答序列化
            std::string resp_json_string;
            resp.Serialize(&resp_json_string);

            // 5. 添加报头
            result += Packet(resp_json_string);
        }
    }

3.5 计算业务

上面完成的Protocal部分只关心对数据的序列化和反序列化,以及报文格式的约定,本身并不关心需要完成什么业务逻辑。所以我们要完成网络版计算器的话还需要添加计算业务的逻辑:

Calculator.hpp

cpp 复制代码
#pragma once

#include "Protocol.hpp"
#include <iostream>
#include <string>

class Calculator
{
public:
    Response Execute(const Request &req)
    {
        Response resp;
        switch (req._oper)
        {
        case '+':
            resp._result = req._data_x + req._data_y;
            break;
        case '-':
            resp._result = req._data_x - req._data_y;
            break;
        case '*':
            resp._result = req._data_x * req._data_y;
            break;
        case '/':
        {
            if (req._data_y == 0)
                resp._code = 1; // div error
            else
                resp._result = req._data_x / req._data_y;
        }
        break;
        case '%':
        {
            if (req._data_y == 0)
                resp._code = 2; // mod error
            else
                resp._result = req._data_x % req._data_y;
        }
        break;
        default:
            resp._code = 3; // 非法操作
            break;
        }
        return resp;
    }
};

3.6 完整代码

自定义协议部分代码:

Protocol.hpp

cpp 复制代码
#pragma once

// 自定义协议部分
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
#include <functional>
#include "Logger.hpp"

using namespace NS_LOG_MODULE;

// 请求报文
class Request
{
public:
    Request() : _data_x(0), _data_y(0), _oper(0)
    {
    }
    Request(int x, int y, char oper) : _data_x(x), _data_y(y), _oper(oper)
    {
    }
    bool Serialize(std::string *out)
    {
        // 结构化 -> "_data_x _oper _data_y"
        Json::Value root;
        root["left"] = _data_x;
        root["right"] = _data_y;
        root["oper"] = _oper;

        Json::FastWriter writer;
        *out = writer.write(root);
        return true;
    }
    bool Deserialize(std::string &in) // "_data_x _oper _data_y"
    {
        // "_data_x _oper _data_y" -> 结构化
        Json::Value root;
        Json::Reader reader;
        bool parsesuccess = reader.parse(in, root);
        if (!parsesuccess)
            return false;

        _data_x = root["left"].asInt();
        _data_y = root["right"].asInt();
        _oper = root["oper"].asInt();//JSON标准不存在独立char类型
        //若JSON中oper字段存储数字 43('+'对应的 ASCII 码),root["oper"].asInt()会读取该整数43;
        //C++ 中int类型数值可隐式转换为 char类型,因此赋值给char成员_oper后,变量存储字符'+'。
        return true;
    }
    ~Request()
    {
    }
    // get set

public:
    // 10 20 '-' -> 10 - 20 = ?
    // _data_x _oper _data_y
    int _data_x;
    int _data_y;
    char _oper; // '+' '-' '/' '*' '%'
};

// 应答报文
class Response
{
public:
    Response() : _result(0), _code(0)
    {
    }
    Response(int result, int code) : _result(result), _code(code)
    {
    }
    bool Serialize(std::string *out)
    {
        Json::Value root;
        root["result"] = _result;
        root["code"] = _code;

        Json::FastWriter writer;
        *out = writer.write(root);
        return true;
    }
    bool Deserialize(std::string &in)
    {
        Json::Value root;
        Json::Reader reader;
        bool parsesuccess = reader.parse(in, root);
        if (!parsesuccess)
            return false;

        _result = root["result"].asInt();
        _code = root["code"].asInt();
        return true;
    }
    ~Response()
    {
    }

public:
    int _result; // 结果
    int _code;   // 状态码,用于区分是收到的数据是返回的错误码还是计算后的结果!
};

const std::string gsep = "\r\n";

using HandlerRequest_t = std::function<Response(Request &)>;
using HandlerResponse_t = std::function<void (Response &)>;

class Protocol
{
public:
    Protocol(HandlerRequest_t handler) : _version("1.0"), _handler_request(handler)
    {
    }
    Protocol(HandlerResponse_t handler_response):_version("1.0"), _handler_response(handler_response)
    {
    }
  
    //封包
    std::string Packet(const std::string &json_string)
    {
        return std::to_string(json_string.size()) + gsep + json_string + gsep;
    }
    
    //解包
    int Unpack(std::string &packet, std::string *json_string)
    {
        if (packet.empty())
            return 0;
        if (json_string == nullptr)
            return -1;

        // 分析报文
        auto pos = packet.find(gsep);
        if (pos == std::string::npos)
            return 0;
        std::string lenstr = packet.substr(0, pos);

        // lenstr 合法性判断,lenstr -> 123 345

        int len = std::stoi(lenstr);
        int total = lenstr.size() + len + 2 * gsep.size();
        if (packet.size() < total)
            return 0;
        // 提取报文
        *json_string = packet.substr(pos + gsep.size(), len);
        packet.erase(0, total);
        return 1;
    }
    // 如果读到半个报文,什么都不做
    // 如果读到一个报文+,循环处理,把所有合法的报文都进行统一处理
    std::string ParseRequest(std::string &inbuffer)
    {
        std::string result;
        while (true)
        {
            std::string json_string;
            // 1. 解包
            int n = Unpack(inbuffer, &json_string);
            if (n < 0)
            {
                LOG(LogLevel::DEBUG) << "no way !!";
                return std::string();
            }
            if (n == 0)
            {
                LOG(LogLevel::INFO) << inbuffer << " parse done";
                return result;
            }
            LOG(LogLevel::DEBUG) << "json_string:\n" << json_string;
            LOG(LogLevel::DEBUG) << "unpack done, inbuffer:\n" << inbuffer;
            // 2. 反序列化
            // 得到一个完整的报文jsonstring
            Request req;
            if (!req.Deserialize(json_string))
                return std::string();

            // 3. 业务计算
            Response resp;
            if (_handler_request)
                resp = _handler_request(req);

            // 4. 应答序列化
            std::string resp_json_string;
            resp.Serialize(&resp_json_string);

            // 5. 添加报头
            result += Packet(resp_json_string);
        }
    }

    std::string ParseResponse(std::string &inbuffer)
    {
        while (true)
        {
            std::string json_string;
            // 1. 解包
            int n = Unpack(inbuffer, &json_string);
            if (n < 0)
            {
                LOG(LogLevel::DEBUG) << "no way !!";
                return std::string();
            }
            if (n == 0)
            {
                LOG(LogLevel::INFO) << inbuffer << " parse done";
                return std::string();
            }
            // 2. 反序列化
            // 得到一个完整的报文jsonstring
            Response resp;
            if (!resp.Deserialize(json_string))
                return std::string();
            
            // 3. 回调处理
            if (_handler_response)
                _handler_response(resp);
        }
    }

    ~Protocol()
    {
    }

private:
    std::string _version;
    HandlerRequest_t _handler_request;
    HandlerResponse_t _handler_response;
};

网络版计算器客户端主程序:

NetCalClient.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <memory>
#include "InetAddr.hpp"
#include "Socket.hpp"
#include "Protocol.hpp"

using namespace NS_SOCKET_MODULE;

static void Usage(const std::string &proc)
{
    std::cout << "Usage:\n\t" << proc << " server_ip server_port" << std::endl;
}

static void HanlderResponse(Response &resp)
{
    std::cout << "result: " << resp._result << "[" << resp._code << "]" << std::endl;
}

// ./netcal_client 目标IP 目标主机端口
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    std::unique_ptr<Socket> socket = std::make_unique<TcpSocket>();
    socket->BuildTcpClientSockMethod();

    InetAddr serveraddress(server_port, server_ip);
    bool n = socket->Connect(serveraddress);
    if (!n)
    {
        std::cerr << "connect error: " << serveraddress.ToString() << std::endl;
        exit(2);
    }
    Protocol procotol(HanlderResponse);
    std::string inbuffer;
    while (true)
    {
        int cnt = 3;
        std::string outbuffer;
        while (cnt--)
        {
            // 0. 获取数据
            int x, y;
            char oper;
            std::cout << "Enter Your x: ";
            std::cin >> x;
            std::cout << "Enter Your y: ";
            std::cin >> y;
            std::cout << "Enter Your oper: ";
            std::cin >> oper;

            // 1. 定义请求对象
            Request req(x, y, oper);

            // 2. 序列化
            std::string req_json;
            req.Serialize(&req_json);

            // 3. 封装报头
            std::string send_req_string = procotol.Packet(req_json);
            outbuffer += send_req_string;
        }
        std::cout << "\n" << outbuffer << std::endl;

        // 4. 发送
        socket->Send(outbuffer);

        // 5. 接收
        socket->Recv(&inbuffer);

        // 6. 解析
        procotol.ParseResponse(inbuffer);
    }

    socket->Close();
    // sock
    return 0;
}

网络版计算器服务器端主程序:

NetCalServer.cc

cpp 复制代码
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "Calculator.hpp"
#include <memory>


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

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }
    uint16_t port = std::stoi(argv[1]);
    // 1. 定义计算机
    std::unique_ptr<Calculator> cal = std::make_unique<Calculator>();

    // 2. 定义协议对象
    std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>(
        [&cal](Request &req)->Response{
            return cal->Execute(req);
        }
    );

    // 3. 定义网络对象
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(
        [&protocol](std::string &inbuffer)->std::string{
            return protocol->ParseRequest(inbuffer);
        }, port
    );
    
    // 4. 启动
    tsvr->Loop();

    return 0;
}

四、与OSI七层模型的对应关系

OSI七层协议和TCP/IP 五层模型相比,多了会话层和表示层,把应用层的部分功能拆了出来。

OSI 参考模型 各层的解释
应用层 为应用程序提供服务
表示层 数据格式转化、数据加密
会话层 建立、管理和维护会话
传输层 建立、管理和维护端到端的连接
网络层 IP 选址及路由选择
数据链路层 提供介质访问和链路管理
物理层 比特流物理传输

而上面实现网络版计数器中的Calculator.hpp、Protocol.hpp和TcpServer.hpp同属于TCP/IP五层协议中的应用层。其中,Calculator对应的是OSI七层模型中的应用层,Protocol对应表示层,TcpServer对应会话层。

相关推荐
Cry丶7 小时前
ubuntu安装vsftpd
服务器·网络·ubuntu
SHENGWEI胜为10 小时前
交换机端口显示正常,终端却获取不到 IP 地址?从 DHCP 流程定位故障
服务器·网络·测试工具·缓存·通信工程
IMPYLH12 小时前
Linux 系统备份与镜像
linux·服务器·网络
淘气包海鸟13 小时前
水声网络框架
网络·ns3·水
国科安芯14 小时前
星间激光通信终端CANFD控制网络的时延确定性与辐射可靠性
网络·嵌入式硬件·信息与通信·canfd·低轨卫星·商业航天·抗辐射
中屹指纹浏览器svip15 小时前
网络抖动会标记账号异常?分清两种波动
网络
承渊政道15 小时前
Linux网络学习【传输层协议UDP】
linux·网络·网络协议·学习·ubuntu·udp
小此方15 小时前
Linux网络(三十三):告别O(n)噩梦:从select/poll的挣扎到epoll红黑树+回调的降维打击,一文打通IO多路复用
linux·网络·驱动开发
2301_7779983415 小时前
Linux 网络:数据链路层
linux·服务器·网络
国科安芯15 小时前
空间锂离子电池组管理系统CANFD通信网络的辐射可靠性设计
网络·信息与通信·canfd·电池·低轨卫星·抗辐射