欢迎光临
我们一直在努力

【网络】应用层自定义协议——序列化和反序列化的概念与方法(Json),以一个网络计算器为例

文章目录

  • 一、应用层协议的必要性
    • 1. 什么是序列化和反序列化
    • 2. TCP的收发缓冲区
  • 二、序列化和反序列化的方法(json)
  • 三、以实现一个网络计算器为例
    • 1. 项目结构
    • 2. 核心代码

一、应用层协议的必要性

1. 什么是序列化和反序列化

我们程序员写的一个个应用软件,几乎都是在应用层实现的。

但是,应用层发送数据,不一定是只发送简单类型数据。例如,微信聊天,你跟别人发送消息,实际上发送的数据包含“用户名称、头像图片、聊天内容、时间”等等信息。 此时,我们思考两种数据传输方法:

  • 构造一种结构体类型,包含上述成员内容,直接发送该结构体对象。
  • 按照某种规则,将上述数据构造成一个字符串,发送字符串,接收方再按照规则复原数据。
  • 如果使用方法1,发送结构体对象,这种方法的可扩展性就十分不好,收通信双方系统、编程语言影响很大。 所以,绝大多数人都会使用方法2,按照一定规则,转换为字符串或字节流信息,收到时在转为结构体。这就是序列化和反序列化,这种规则就是应用层自定义协议!

    在这里插入图片描述

    但是,对于传输层TCP协议,它是面向字节流发送的:发送方的数据不一定会一次发送完整,接收方可能会需要多次接收才是完整的数据包。读到不完整数据,叫做数据粘包问题。 因此,应用层协议必须保证数据的完整性,常用做法之一是应用层添加一个固定长度的报头,表示有效载荷的大小;读端先解析报头,再根据这个大小读取有效载荷。

    UDP协议不存在数据粘包问题,因为UDP协议面向数据报发送,它保证数据发一次、收一次。

    2. TCP的收发缓冲区

    在这里插入图片描述

    在任意一台主机上,TCP协议会为每个连接维护独立的发送缓冲区与接收缓冲区,使得内核可以同时进行数据的发送与接收操作,即全双工(这就是为什么TCP的读写sockfd是同一个)。 在通信过程中,用户态调用write/send函数,本质上只是将数据从用户空间拷贝到内核的 TCP发送缓冲区中;调用read/recv函数,则只是从内核的TCP接收缓冲区中将数据拷贝到用户空间。至于数据何时被发送到网络、每次发送多少字节、出错后如何重传或处理,这些逻辑都由TCP协议自动控制,对用户程序完全透明。

    二、序列化和反序列化的方法(json)

    市面上已经有了许多成熟的序列化和反序列化工具,如json、xml、protobuf等,我们今天学习使用json。

    在C++中,jsoncpp是一个用于处理json数据的C++库,它提供了能将json数据序列化为字符串以及将字符串反序列化为C++数据结构的功能。 ubuntu系统中,使用命令sudo apt-get install libjsoncpp-dev安装json库

    库: 在这里插入图片描述

    头文件:

    在这里插入图片描述

    当然,这是一个第三方库,在g++编译时需要手动链接-ljsoncpp

    简单的使用方法演示:

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

    int main()
    {
    // 1. 构造Json核心类型Value,所有要处理数据都会先存放在这个对象里,支持几乎所有类型数据
    Json::Value root;
    root["name"] = "zhangsan";
    root["age"] = 18;
    root["height"] = 178;

    // 2. 序列化, 将Json::Value类型对象转换为一个Json字符串,有多种做法:
    // ver1:
    // Json::StreamWriterBuilder wbuilder;
    // std::unique_ptr<Json::StreamWriter> swriter(wbuilder.newStreamWriter());
    // std::stringstream ss;
    // swriter->write(root, &ss);
    // std::cout << ss.str() <<std::endl;

    // ver2:
    // Json::FastWriter writer;
    // std::string json_s = writer.write(root);
    // std::cout << json_s << std::endl;

    // ver3: 以这种方法为例演示
    Json::StyledWriter writer;
    std::string json_s = writer.write(root);
    std::cout << json_s << std::endl;

    // 将Json字符串通过网络发送。假设这里已经完成
    // 3. 反序列化
    Json::Reader reader;
    Json::Value root1;
    bool ret = reader.parse(json_s, root1); // 将Json字符串解析到root1对象中
    if(!ret)
    {
    std::cerr << "解析失败" << std::endl;
    exit(1);
    }

    // 因为协议是双方约定好的,收数据方一定知道有什么成员
    std::string name = root1["name"].asString();
    int age = root1["age"].asInt();
    int height = root1["height"].asInt();

    std::cout << "name: " << name << std::endl;
    std::cout << "age: " << age << std::endl;
    std::cout << "height: " << height << std::endl;

    return 0;
    }

    在这里插入图片描述

    三、以实现一个网络计算器为例

    完整项目代码已提交在我的github仓库中

    1. 项目结构

    完整项目文件结构:

    在这里插入图片描述

    这个项目的逻辑,可以划分为如下层次:

    在这里插入图片描述

    其中,核心三层正好对应着OSI模型的前三层!

    • 业务处理层 -> 应用层
    • 协议层 -> 表示层
    • 服务通信层 -> 会话层

    好的软件工程设计,和OSI模型背后的 “分层解耦” 思想,在网络场景下不谋而合。这也侧面体现OSI模型其实设计非常好!

    2. 核心代码

    首先,我们可以把套接字封装成类,便于使用。由于UDP套接字和TCP套接字有很多接口是相似的,我们这里引入一种设计模式“模版方法模式”,将普通套接字设计为基类,具体协议套接字类继承为子类:

    // Socket.hpp
    #pragma once
    #include "InetAddr.hpp"
    #include "Logger.hpp"
    #include <memory>
    #include <netinet/in.h>
    #include <string>
    #include <sys/socket.h>
    #include <sys/types.h>
    #include <unistd.h>

    const int gbacklog = 16;

    // 设计模式, 模版方法模式
    class Socket
    {
    public:
    ~Socket() {}

    protected:
    virtual void CreateSocket() = 0;
    virtual void BindSocket(uint16_t port) = 0;
    virtual void ListenSocket() = 0;

    public:
    // Tcp服务端创建套接字方法
    void BuildTcpServerSock(uint16_t port)
    {
    CreateSocket();
    BindSocket(port);
    ListenSocket();
    }

    // Tcp客户端创建套接字方法
    void BuildTcpClientSock()
    {
    CreateSocket();
    }

    virtual std::shared_ptr<Socket> Accept(InetAddr* clientaddr) = 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;

    // 今天我们只使用TCP实现,UDP不考虑了
    // void BuildUdpServerSock(uint16_t port)
    // {
    // CreateSocket();
    // BindSocket(port);
    // }
    };

    // class UdpSocket : public Socket
    // {…}

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

    ~TcpSocket() {}

    void CreateSocket() override
    {
    _sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (_sockfd < 0)
    {
    LOG(LogLevel::FATAL) << "creat socket error";
    exit(1);
    }
    }

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

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

    std::shared_ptr<Socket> Accept(InetAddr* clientaddr) override
    {
    struct sockaddr_in addr;
    socklen_t len = sizeof(addr);
    int sockfd = accept(_sockfd, (struct sockaddr*)&addr, &len);
    if (sockfd < 0)
    {
    LOG(LogLevel::WARNING) << "accpet 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 n;
    }

    int Send(const std::string& in) override
    {
    return 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.GetNetAddress(), addr.Len());
    if (n < 0)
    {
    return false;
    }
    return true;
    }

    private:
    int _sockfd;
    };

    服务器通信方法模块:

    // TcpServer.hpp
    #pragma once
    #include "InetAddr.hpp"
    #include "Logger.hpp"
    #include "Socket.hpp"
    #include "ThreadPool.hpp"
    #include <functional>
    #include <memory>
    #include <string>

    using task_t = std::function<void()>;

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

    class TcpServer
    {
    public:
    TcpServer(Hander_t handler, uint16_t port = gport)
    : _port(port), _handler(handler), _listensock(std::make_unique<TcpSocket>())
    {
    _listensock->BuildTcpServerSock(port);
    LOG(LogLevel::INFO) << "create listen socket success: " << _listensock->Sockfd();
    }

    void Start()
    {
    while (1)
    {
    InetAddr clientaddr;
    auto sockfd = _listensock->Accept(&clientaddr);
    if (!sockfd)
    {
    continue;
    }
    LOG(LogLevel::INFO) << "accept client success, sockfd: " << sockfd->Sockfd();

    // 处理通信任务可以多进程、多线程、线程池,这里使用线程池
    // 将通信任务放入线程池
    ThreadPool<task_t>::Instance()->Enqueue([this, sockfd, clientaddr]() -> void
    { this->serviceIO(sockfd, clientaddr); });
    }
    }

    ~TcpServer()
    {
    _listensock->Close();
    }

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

    // 处理获取的数据流,返回需要发回的数据流
    outbuffer += _handler(inbuffer);
    if (outbuffer.empty())
    {
    continue;
    }
    LOG(LogLevel::INFO) << "outbuffer:\\n" << outbuffer;

    n = sockfd->Send(outbuffer);
    // 每次发送完的内容清空
    outbuffer.clear();
    if (n < 0)
    {
    LOG(LogLevel::WARNING) << "send, client quit: " << clientaddr.ToString();
    break;
    }
    }
    sockfd->Close();
    }

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

    协议内容模块:

    // Protocol.hpp
    #pragma once
    #include "Logger.hpp"
    #include <functional>
    #include <jsoncpp/json/json.h>
    #include <jsoncpp/json/reader.h>
    #include <jsoncpp/json/value.h>
    #include <jsoncpp/json/writer.h>
    #include <string>

    // 请求报文
    // 一个请求,形如x+y,由三个成员组成
    class Request
    {
    public:
    Request() {}

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

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

    Json::FastWriter writer;
    *out = writer.write(root);
    }

    // 反序列化
    void Deserialize(std::string& in)
    {
    Json::Value root;
    Json::Reader reader;
    if (!reader.parse(in, root))
    {
    LOG(LogLevel::WARNING) << "Json parse error";
    return;
    }

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

    int Getx() const
    {
    return _x;
    }

    int Gety() const
    {
    return _y;
    }

    char Getoper() const
    {
    return _oper;
    }

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

    // 应答报文
    // 一个应答,需要包含运算结果和错误标志(如除零等错误)
    class Response
    {
    public:
    Response() {}

    Response(const std::string& res, int code) : _res(res), _code(code) {}

    // 序列化
    void Serialize(std::string* out)
    {
    Json::Value root;
    root["res"] = _res;
    root["code"] = _code;

    Json::FastWriter writer;
    *out = writer.write(root);
    }

    // 反序列化
    void Deserialize(std::string& in)
    {
    Json::Value root;
    Json::Reader reader;
    if (!reader.parse(in, root))
    {
    LOG(LogLevel::WARNING) << "Json parse error";
    return;
    }

    _res = root["res"].asString();
    _code = root["code"].asInt();
    }

    std::string Getres() const
    {
    return _res;
    }

    int Getcode() const
    {
    return _code;
    }

    private:
    std::string _res;
    int _code;
    };

    // 协议类,需要包含封装方法、解包方法、分析报文方法
    // 我们的协议,约定报文格式为: "有效载荷长度/r/n序列化json字符串/r/n"
    const std::string gsep = "\\r\\n";

    // 需要提供怎么处理请求和应答的业务方法函数, 服务端需要知道怎么处理请求并返回应答,客户端需要知道怎么处理应答
    using HanderRequest_t = std::function<Response(Request&)>;
    using HanderResponse_t = std::function<void(Response&)>;

    class Protocol
    {
    public:
    Protocol(HanderRequest_t handler_request) : _handler_request(handler_request) {}

    Protocol(HanderResponse_t handler_response) : _handler_response(handler_response) {}

    // 封装报文方法
    std::string Packet(const std::string& json_string)
    {
    // "有效载荷长度/r/n序列化json字符串/r/n"
    return std::to_string(json_string.size()) + gsep + json_string + gsep;
    }

    // 解包方法
    // packet表示当前读到的字符串
    // 如果能在packet中读到一个完整报文,返回1。否则返回0.读到的json串记录到第二个参数中
    // 如果出现其他错误,返回-1
    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);
    int len = std::stoi(lenstr);
    int total_len = lenstr.size() + len + 2 * gsep.size();

    // 如果当前packet长度小于这条报文总长,肯定读不到; 反之则能
    if (packet.size() < total_len)
    {
    return 0;
    }

    *json_string = packet.substr(pos + gsep.size(), len);
    packet.erase(0, total_len);
    return 1;
    }

    // 分析请求
    // 如果读到不是完整的报文,什么都不做,继续读取
    // 如果读到了完整的报文,交给计算函数处理
    std::string ParseRequest(std::string& inbuffer)
    {
    // 如果能读到多个请求报文,都处理完再一次性返回所有的应答报文
    std::string result;

    while (1)
    {
    // 1. 获取请求json字符串, 解包
    std::string json_string;
    int n = Unpack(inbuffer, &json_string);
    if (n < 0)
    {
    LOG(LogLevel::WARNING) << "nullptr error";
    return std::string();
    }
    if (n == 0)
    {
    // 读不到更多报文了
    LOG(LogLevel::INFO) << inbuffer << "parse done";
    return result;
    }
    if (n == 1)
    {
    LOG(LogLevel::INFO) << "json_string:\\n" << json_string;
    LOG(LogLevel::INFO) << "inbuffer:\\n" << inbuffer;

    // 现在,有了一个完整的json串,是一个请求
    // 2. 请求反序列化
    Request req;
    req.Deserialize(json_string);

    // 3. 业务处理。我协议层不知道怎么处理,交给业务处理层,只要返回一个应答就行了
    Response resp;
    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 (1)
    {
    // 1. 获取应答json字符串, 解包
    std::string json_string;
    int n = Unpack(inbuffer, &json_string);
    if (n < 0)
    {
    LOG(LogLevel::WARNING) << "nullptr error";
    return std::string();
    }
    if (n == 0)
    {
    // 读不到更多报文了
    LOG(LogLevel::INFO) << inbuffer << "parse done";
    return std::string();
    }
    if (n == 1)
    {
    // 2. 应答反序列化
    Response resp;
    resp.Deserialize(json_string);

    // 3. 业务处理,我协议层不知道怎么处理应答,回调函数
    _handler_response(resp);
    }
    }
    }

    private:
    HanderRequest_t _handler_request;
    HanderResponse_t _handler_response;
    };

    计算器模块:

    // Calculator.hpp
    #pragma once
    #include "Protocol.hpp"
    #include <string>
    class Calculator
    {
    public:
    Response Execute(const Request& req)
    {
    int x = req.Getx();
    int y = req.Gety();
    char oper = req.Getoper();

    int res = 0;
    int code = 0;
    switch (oper)
    {
    case '+':
    res = x + y;
    break;
    case '-':
    res = x y;
    break;
    case '*':
    res = x * y;
    break;
    case '/':
    if (y == 0)
    code = 1;
    else
    res = x / y;
    break;
    case '%':
    if (y == 0)
    code = 2;
    else
    res = x % y;
    break;
    default:
    code = 3;
    break;
    }
    std::string res_string = "error";
    if (code == 0)
    {
    res_string = std::to_string(x) + oper + std::to_string(y) + '=' + std::to_string(res);
    }
    Response resp(res_string, code);
    return resp;
    }
    };

    服务端主函数:

    // NetCalServer.cc
    #include "Calculator.hpp"
    #include "Protocol.hpp"
    #include "TcpServer.hpp"
    #include <iostream>
    #include <memory>
    #include <string>

    int main(int argc, char* argv[])
    {
    if (argc != 2)
    {
    std::cerr << "./Server.exe port" << std::endl;
    return 1;
    }
    uint16_t port = std::stoi(argv[1]);

    // 定义计算器对象
    std::unique_ptr<Calculator> cal = std::make_unique<Calculator>();

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

    // 定义服务器对象
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(
    [&protocol](std::string& inbuffer)->std::string
    {
    return protocol->ParseRequest(inbuffer);
    },
    port
    );

    // 启动
    tsvr->Start();

    return 0;
    }

    客户端主函数:

    // NetCalClient.cc
    #include "InetAddr.hpp"
    #include "Protocol.hpp"
    #include "Socket.hpp"
    #include <iostream>
    #include <memory>
    #include <string>

    int main(int argc, char* argv[])
    {
    if (argc != 3)
    {
    std::cerr << "Client.exe ip port" << std::endl;
    return 1;
    }
    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);
    InetAddr serveraddr(server_port, server_ip);

    std::unique_ptr<Socket> socket = std::make_unique<TcpSocket>();
    socket->BuildTcpClientSock();
    bool n = socket->Connect(serveraddr);
    if (!n)
    {
    std::cerr << "connect error: " << serveraddr.ToString() << std::endl;
    return 2;
    }

    Protocol protocol([](Response& resp){
    std::cout << "result: " << resp.Getres() << '[' << resp.Getcode() << ']' << std::endl;
    });

    std::string inbuffer;
    while(1)
    {
    // 攒三次数据再发送
    std::string outbuffer;
    for(int i = 0; i < 3; i++)
    {
    int x, y;
    char oper;
    std::cout << "输入x:" << std::endl;
    std::cin >> x;
    std::cout << "输入y:" << std::endl;
    std::cin >> y;
    std::cout << "输入oper:" << std::endl;
    std::cin >> oper;

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

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

    // 添加报头
    std::string send_req = protocol.Packet(req_json);
    outbuffer += send_req;
    }
    std::cout << '\\n' << outbuffer << std::endl;

    // 发送请求
    socket->Send(outbuffer);
    // 接收应答
    socket->Recv(&inbuffer);
    // 解析应答
    protocol.ParseResponse(inbuffer);
    }
    return 0;
    }

    Makefile文件:

    .PHONY:all
    all:Server.exe Client.exe

    Server.exe:NetCalServer.cc
    g++ -o $@ $^ -std=c++17 -ljsoncpp

    Client.exe:NetCalClient.cc
    g++ -o $@ $^ -std=c++17 -ljsoncpp

    .PHONY:clean
    clean:
    rm -r Server.exe Client.exe

    演示效果:

    在这里插入图片描述 没有问题!

    本篇完,感谢阅读

    赞(0)
    未经允许不得转载:171主机测评 » 【网络】应用层自定义协议——序列化和反序列化的概念与方法(Json),以一个网络计算器为例
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址