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

目录

一、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对应会话层。

相关推荐
Titan20241 小时前
网络学习:网络层与数据链路层知识梳理
服务器·网络·学习·tcp/ip
志栋智能1 小时前
运维超自动化中的配置管理实践
运维·网络·数据库·自动化
一条泥憨鱼1 小时前
【从0开始学习计算机网络】| TIME_WAIT 为什么是 2MSL,CLOSE_WAIT / TIME_WAIT 堆积怎么排查
服务器·开发语言·网络·计算机网络·网络安全
FW-Linker1 小时前
多卡聚合的数据包级并行传输原理在广电直播场景中详解
网络·5g·架构·智能路由器
Tiny2141 小时前
解决VMware中win10虚拟机无法上网
网络·windows·vmware
Multipath7121 小时前
无人驾驶车的多链路聚合传输应用方案
网络·5g·安全·智能路由器·实时音视频
Yang96112 小时前
六重保护加自动放电:鼎讯信通DLG-1让高压测试更安心
网络
薛之谦_2 小时前
国际SSL证书与国密SSL证书:专业视角下的全面解读
网络·网络协议·ssl
云运维笔记2 小时前
华为VRP系统全解析:网络设备的智能核心
服务器·网络·华为