【计网】从零开始掌握序列化 --- JSON实现协议 + 设计 传输\会话\应用 三层结构

news2024/9/22 22:32:43

在这里插入图片描述

唯有梦想才配让你不安,
唯有行动才能解除你的不安。
--- 卢思浩 ---

从零开始掌握序列化

  • 1 知识回顾
  • 2 序列化与编写协议
    • 2.1 使用Json进行序列化
    • 2.2 编写协议
  • 3 封装IOService
  • 4 应用层 --- 网络计算器
  • 5 总结

1 知识回顾

上一篇文章我们讲解了协议的本质是双方能够看到的结构化数据。并通过传输层的底层理解了为什么read系列函数时全双工支持同时读写的:TCP传输层有两个缓冲区,分别接收和发送。最重要的是我们将TCP通信的代码进行的重构:

  1. 我们将Socket通信单独封装为一个类,负责Socket套接字的创建,bind绑定服务器端口号,进入监听模式…工作,基类Socket并不进行定义,只进行声明!具体实现由派生类TcpServer和UdpServer来进行
  2. TcpServer继承Socket类的所有方法,然后进行具体的函数定义!
  3. 上层的TcpServer直接底层使用TcpSocket对象就可以完成Socket系列操作,十分方便!

接下来我们要实现是这样的一个结构:
在这里插入图片描述
通信过程整体分为三层

  1. 传输层TcpServer:负责从Socket文件中获取链接,传输层不需要进行IO,获取到连接就让会话层通过连接获取数据!
  2. 会话层Service:根据传输层给的连接,从Sockfd文件中读取数据,解析出报文结构中的数据字符串,然后通过协议分离出结构化数据。该层只负责数据的解析,数据的处理交给应用层进行!
  3. 应用层Process:应用层是具有的业务逻辑,根据会话层解析出的数据,进行数据处理!

这样是一个非常非常优雅的封装操作!!!

2 序列化与编写协议

2.1 使用Json进行序列化

协议是IO的基础,只有协议确定下来,才可以进行通信。
我们这里想要实现一个网络计算器的应用,所以协议分为了两个类:Request和Response。分别作为传入的数据和传出的数据:

  1. Request:两个数字和一个运算符
  2. Response:结果数字 , 错误码 ,退出信息

他们是作为结构化的数据进行传输,那么想要进行传输就来到了最重要的部分序列化与反序列化!序列化与反序列化可以使用第三方库也可以自己进行编写。这里我们先使用第三方的Json库进行实现:

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

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

在Linux中使用需要进行安装对应的JSON库:

ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel

安装之后就可以进行使用了:

使用起来是十分方便的:

  1. Json::Value是最重要的类,这是对Json数据结构进程操作和表示的关键类
  2. 建立好类Json::Value之后就可以通过[ ]操作root["x"] = _x;,像这样就可以进行赋值
  3. 将Json数据结构转换为字符串依靠 Json::FastWriter 或 Json::StreamWriter都可以转换成字符串
Json::StyledWriter writer;
std::string s = writer.write(root)
  1. 通过Json::Reader可以快速将字符串反序列化得到Json结构!
bool parsingSuccessful = reader.parse(json_string,root);
// 访问 JSON 数据 
std::string name = root["name"].asString();
int age = root["age"].asInt(); std::string city =
root["city"].asString();

通过这样就就可以简洁的完成序列化与反序列化的工作!

2.2 编写协议

根据我们的需求在加入Json操作我们就可以把协议写出来,代码虽然很长但是很好理解:

  1. Request类中需要根据 int x , int y , char oper进行序列化生成字符串,也要能够通过字符串反序列化得到三个变量
  2. Response类中需要根据 int res , int code , std::string desc进行序列化生成字符串,也要能够通过字符串反序列化得到三个变量
#pragma once
#include <jsoncpp/json/json.h>
#include <string>
// 协议就是双方都认识的结构化数据
// "len"\r\n"{json}"\r\n
const std::string sep = "\r\n";

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

    bool Serialize(std::string *out)
    {
        // 使用现成的 Json 库
        Json::Value root;
        root["x"] = _x;
        root["y"] = _y;
        root["oper"] = _oper;
        Json::FastWriter writer;

        std::string s = writer.write(root);
        *out = s;
        return true;
    }
    bool Deserialize(std::string &in)
    {
        Json::Value root;    // 创建json对象
        Json::Reader reader; // 读取
        bool res = reader.parse(in, root);
        if (res == false)
            return false;
        _x = root["x"].asInt();
        _y = root["y"].asInt();
        _oper = root["oper"].asInt();
        return true;
    }
    int X() { return _x; }
    int Y() { return _y; }
    char Oper() { return _oper; }

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

struct Response
{
    Response() {}
    Response(int res, int code, std::string desc) : _res(res), _code(code), _desc(desc)
    {
    }
    ~Response()
    {
    }
    bool Serialize(std::string *out)
    {
        // 使用现成的 Json 库
        Json::Value root;
        root["res"] = _res;
        root["code"] = _code;
        root["desc"] = _desc;
        Json::FastWriter writer;

        std::string s = writer.write(root);
        *out = s;
        return true;
    }
    bool Deserialize(std::string &in)
    {
        Json::Value root;    // 创建json对象
        Json::Reader reader; // 读取
        bool res = reader.parse(in, root);
        if (res == false)
            return false;
        _res = root["res"].asInt();
        _code = root["code"].asInt();
        _desc = root["desc"].asInt();
        return true;
    }
    int _res;
    int _code; // 退出码 0:success 1:div zero 2:非法操作
    std::string _desc;
};

看一下效果:
在这里插入图片描述

完成了基础的序列化和反序列化之后,我们就可以做到从sockfd流中读取数据了吗??不可以!因为不知道Json字符串的长度,就不知道应该读取多少字节!这样可就做不到正确的从数据中获取json字符串!

所以我们还有做一步特殊处理:

  • 需要对生成的Json字符串加入报头len记录json字符串的长度,中间以sep分隔符分割!
  • 需要对获得到的数据进行解析,去除报头得到一个Json字符串!
// "len"\r\n"{json}"\r\n
const std::string sep = "\r\n";
// 加入报头
std::string Encode(const std::string &jsonstr)
{
    int len = jsonstr.size();
    std::string lenstr = std::to_string(len);
    return lenstr + sep + jsonstr + sep;
}

std::string Decode(std::string &packagestream)
{
    auto pos = packagestream.find(sep);
    if (pos == std::string::npos)
        return std::string();
    // 获取到len
    std::string lenstr = packagestream.substr(0, pos);
    int len = std::stoi(lenstr);
    //算上报头的完整长度!
    int total = lenstr.size() + len + 2 * sep.size();
    if (total > packagestream.size())
        return std::string();
    // 到这里说明可以读取完整数据
    std::string jsonstr = packagestream.substr(pos + sep.size(), len);
    packagestream.erase(total);
    return jsonstr;
}

经过这样的操作,可以保证:

  • 上层想要发送数据时,可以将数据包装为json字符串,并加入报头形成完整报文!
  • 上层获取数据进行反序列化时可以获取到完整的json字符串!并成功解析为数据

3 封装IOService

将来我们的线程会执行将会执行这个回调函数方法,现在我们不再需要TcpServer来进行IO操作,TcpServer只负责进行获取链接,获取到连接后通过ThreadData结构体将数据传到线程中的回调函数中:

	class ThreadData
    {
    public:
        SockSPtr _sockfd;
        InetAddr _addr;
        TcpServer *_this;

    public:
        ThreadData(SockSPtr sockfd, InetAddr addr, TcpServer *p) : _sockfd(sockfd),
                                                                   _this(p),
                                                                   _addr(addr)
        {
        }
    };

在回调函数Execute中:

// 注意设置为静态函数 , 不然参数默认会有TcpServer* this!!!
    static void *Execute(void *args)
    {
        pthread_detach(pthread_self()); // 线程分离!!!
        // 执行Service函数
        TcpServer::ThreadData *td = static_cast<TcpServer::ThreadData *>(args);
        td->_this->_service(td->_sockfd, td->_addr);
        td->_sockfd->Close();
        delete td;
        return nullptr;
    }

就可以解析出来套接字文件描述符和客户端信息了!解析出信息之后就去执行会话层的回调函数进行IO操作:

  1. Service内部只有一个成员变量,就是应用层的回调函数,Service解析出来数据之后就可以传入到应用层中进行使用
  2. IO中主要需要进行从sockfd文件中获取数据,然后通过协议进行解析,获取到真正的数据。再调用回调函数对数据进行操作!得到结果之后就可以进行序列化,加入报头,再发送给客户端!
  3. 应用层的操作逻辑,Service并不关心,只要回调函数可以传回需要的结构体就可以!
class Service
{
public:
    Service(process_t process) : _process(process)
    {
    }
    void IOExecute(SockSPtr sock, InetAddr &addr)
    {
        LOG(INFO, "service start!!!\n");
        std::string message;
        while (true)
        {
            // 1. 进行读取
            ssize_t n = sock->Recv(&message);
            if (n < 0)
            {
                LOG(ERROR, "read error: %s\n", addr.AddrStr().c_str());
                break;
            }
            // 此时获取到客户端发送的数据
            // 但是不能保证是否是完整的报文
            // 2.报文解析
            std::string str = Decode(message); // 通过去报头获取报文
            if (str.empty())
                continue; // 说明没有完整的报文!
            // 到这里说明有完整的报文!!!
            auto req = Factory::BuildRequestDefault();
            // 3.反序列化初始化Request
            req->Deserialize(str);
            auto res = Factory::BuildResponseDefault();
            // 4.业务处理
            res = _process(req);
            // 5.进行序列化处理
            std::string ret;
            res->Serialize(&ret);
            // 6.加入报头
            Encode(ret);
            // 7.将获取的数据发送回去
            sock->Send(ret);
        }
    }
    ~Service()
    {
    }

private:
    process_t _process;
};

4 应用层 — 网络计算器

应用层根据具体需要可以随时改变,我这里以网络计算器为例子进行书写:

#include "Protocol.hpp"
class NetCal
{
public:
    NetCal() {}
    std::shared_ptr<Response> Calculator(std::shared_ptr<Request> req)
    {
        std::shared_ptr<Response> res = Factory::BuildResponseDefault();

        switch (req->Oper())
        {
        case '+':
            res->_res = req->X() + req->Y();
            res->_code = 0;
            res->_desc = "success";
            break;
        case '-':
            res->_res = req->X() - req->Y();
            res->_code = 0;
            res->_desc = "success";
            break;
        case '*':
            res->_res = req->X() * req->Y();
            res->_code = 0;
            res->_desc = "success";
            break;
        case '/':
        {
            if (req->Y() == 0)
            {
                res->_code = 1;
                res->_desc = "div zero";
            }
            res->_res = req->X() / req->Y();
            res->_code = 0;
            res->_desc = "success";
        }
        break;
        case '%':
        {
            if (req->Y() == 0)
            {
                res->_code = 1;
                res->_desc = "mod zero";
            }
            res->_res = req->X() % req->Y();
            res->_code = 0;
            res->_desc = "success";
        }
        break;
        default:
            res->_code = 2;
            res->_desc = "illegal operations";
            break;
        }
        return res;
    }
    ~NetCal() {}
};

逻辑很简单不在多加赘述!

5 总结

现在我们的程序分为了三层结构:
在这里插入图片描述
我们做到了最大程度的解耦!

  • 传输层只负责获取链接,我们应用层要进行什么工作,只要是进行网络通信传输层的工作就是唯一的!
  • 会话层进行IO操作!只要传输层提供了链接,会话层就可以获取数据,然后根据具体的协议进行数据的解析工作。协议根据实际情况改变,但是会话层的工作逻辑是不变的!
  • 应用层只管进行数据处理即可,什么但不不需要考虑!完成工作后返回给会话层数据即可!

这样的结构逻辑十分清晰,并且解耦的非常优雅,值得反复品味!!!

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

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

相关文章

【JavaEE】多线程编程引入——认识Thread类

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯&#xff0c;你们的点赞收藏是我前进最大的动力&#xff01;&#xff01;希望本文内容能帮到你&#xff01; 目录 引入&#xff1a; 一&#xff1a;Thread类 1&#xff1a;Thread类可以直接调用 2&#xff1a;run方法 &a…

SpringBoot+thymeleaf竞赛报名系统

一、介绍 > 这是一个基于Spring Boot的后台管理系统。 > 使用了Mybatis Plus作为持久层框架&#xff0c;EasyExcel用于Excel操作&#xff0c;Thymeleaf作为前端模板引擎。 > 界面简洁&#xff0c;功能丰富&#xff0c;完成度比较高&#xff0c;适用于JAVA初学者作…

安国U盘量产工具系列下载地址

来源地址&#xff08;访问需要科学工具&#xff09;&#xff1a;AlcorMP (Последняя версия ALCOR U2 MP v23.08.07.00.H) – [USBDev.ru] 版本列表&#xff1a; AlcorMP&#xff08;最新版本的 ALCOR U2 MP v23.08.07.00.H&#xff09; AlcorMP是在Alcor Mic…

SpringBoot项目License证书生成与验证(TrueLicense) 【记录】

SpringBoot项目License证书生成与验证(TrueLicense) 【记录】 在非开源产品、商业软件、收费软件等系统的使用上&#xff0c;需要考虑系统的使用版权问题&#xff0c;不能随便一个人拿去在任何环境都能用。应用部署一般分为两种情况&#xff1a; 应用部署在开发者自己的云服务…

数据集-目标检测系列-火车检测数据集 train >> DataBall

数据集-目标检测系列-火车检测数据集 train >> DataBall 数据集-目标检测系列-火车检测数据集 数据量&#xff1a;1W 想要进一步了解&#xff0c;请联系 DataBall。 DataBall 助力快速掌握数据集的信息和使用方式&#xff0c;会员享有 百种数据集&#xff0c;不断增加…

跟李沐学AI:注意力机制、注意力分数

目录 不随意线索 随意线索 注意力机制 非参注意力池化层 参数化的注意力机制 注意力机制总结 注意力分数 拓展到高维度 加性模型&#xff08;Additive Attention&#xff09; 点积注意力机制&#xff08;Dot Product Attention&#xff09; 注意力分数总结 不随意线…

vscode 顶部 Command Center,minimap

目录 vscode 顶部 Command Center 设置显示步骤: minimap设置 方法一:使用设置界面 方法二:使用命令面板 方法三:编辑 settings.json 文件 左侧目录树和编辑器字体不一致: vscode 顶部 Command Center Visual Studio Code (VSCode) 中的 Command Center 是一个集中…

240912-设置WSL中的Ollama可在局域网访问

A. 最终效果 B. 设置Ollama&#xff08;前提&#xff09; sudo vim /etc/systemd/system/ollama.service[Unit] DescriptionOllama Service Afternetwork-online.target[Service] ExecStart/usr/bin/ollama serve Userollama Groupollama Restartalways RestartSec3 Environme…

Python redis 安装和使用介绍

python redis安装和使用 一、Redis 安装1.1、Windows安装 二、安装 redis 模块二、使用redis 实例1.1、简单使用1.2、连接池1.3、redis 基本命令 String1.3.1、ex - 过期时间&#xff08;秒&#xff09;1.3.2、nx - 如果设置为True&#xff0c;则只有name不存在时&#xff0c;当…

fiddler抓包08_抓Android手机请求

课程大纲 手机抓包&#xff0c;电脑端的设置和IOS端相同&#xff0c;设置一次即可&#xff0c;无需重复设置。 前提&#xff1a;电脑和手机连接同一个局域网 土小帽电脑和手机都连了自己的无线网“tuxiaomao”。 Step1. 电脑端设置 ① 打开Fiddler - 开启抓包&#xff08;F12…

django项目——图片上传到阿里云OSS对象存储

文章目录 实现图片上传到阿里云OSS对象存储1. 创建阿里云OSS对象存储2. 查询获取接口访问key和秘钥3. 安装阿里云的SDK集成到项目中使用3.1 python直接操作oss23.2 django配置自定义文件存储上传文件到oss 实现图片上传到阿里云OSS对象存储 1. 创建阿里云OSS对象存储 开发文档…

重磅!人工智能等级考试来了,考试免费,上海落户可以加分

目录 简要介绍 一、关心的问题 1. 什么是上海市高等学校信息技术水平考试&#xff1f; 2. 考试分几个级别&#xff1f;有哪些科目&#xff1f; 3. 哪些人可以进行报名&#xff1f; 4. 每名学生可以报考几个科目&#xff1f; 5. 有没有考试大纲&#xff1f; 6. 考试是否有…

[笔记]23年度展会信息— 吊钩 起升机构

1.吊钩的规格参数 5吨吊钩重26公斤 10吨64公斤。 另外一套型号&#xff0c;更轻&#xff1a; 不确定是结构设计还是用钢材质达到了减重效果。 看看重载双滑轮吊钩&#xff1a; 50吨&#xff0c;400公斤&#xff0c;只是吊钩。 然后是行车吊钩与钢丝绳的直径。这在计算空载吊…

Web Components之继承

我们在使用Web Components自定义组件的时候&#xff0c;我们需要继承HTMLElement这个浏览器内置对象&#xff0c;但是如果我要一些高级封装&#xff0c;给组件内置一些方法的话。我们就需要使用继承的方式&#xff0c;在父类中实现基本功能的封装。 1 父类的封装 以下是我的继…

一文搞懂UEFI

Hi&#xff01;早哦。今天又是宠读者的一天&#xff0c;应允聊聊UEFI。 文章目录 前言UEFI是什么&#xff1f;传统BIOSBIOS作为标准BIOS作为实现BIOS的工作原理传统BIOS的局限性传统BIOS启动过程 BIOS VS UEFIUEFI&#xff1f;UEFI概念EFI 系统分区EFI 变量EFI 的启动过程EFI 变…

【高分系列卫星简介——高分三号卫星(GF-3)】

高分三号卫星&#xff08;GF-3&#xff09; 高分三号&#xff08;GF-3&#xff09;是我国首颗高分辨率、C频段、多极化合成孔径雷达&#xff08;SAR&#xff09;卫星&#xff0c;由中国空间技术研究院北京空间飞行器总部设计部研制&#xff0c;并于2016年8月10日成功发射。该卫…

thop计算模型复杂度(params,flops)

thop安装 -pip install thop在线安装失败 -离线安装 github网址&#xff1a; pytorch-OpCounter:Count the MACs / FLOPs of your PyTorch model. - GitCode python setup.py install 测试&#xff1a; from options import config as c import os os.environ["CUD…

【Flink实战】flink消费http数据并将数组展开多行

文章目录 一. 需求描述二. 方案思路1. 解决思路2. flink json 解析2.1. 通过json path解析非array数据2.2. 通过json path解析array数据 3. CROSS JOIN逻辑 三. 方案实现1. http json数据样例2. flink sql 说明 一. 需求描述 flink消费http接口的数据&#xff0c;将json中的数…

【JavaEE初阶】多线程7(面试要点)

欢迎关注个人主页&#xff1a;逸狼 创造不易&#xff0c;可以点点赞吗~ 如有错误&#xff0c;欢迎指出~ 目录 常见的锁策略 乐观锁vs悲观锁 重量级锁vs轻量级锁 挂起等待锁vs自旋锁 公平锁vs非公平锁 可重入锁vs不可重入锁 读写锁 synchronized的加锁过程 锁升级的过程 偏向锁 …

博途TIA v18下载时,需要重启才能安装下载路径是灰色改不了

一、需要重启才能安装 删除下面注册表P开头的文件&#xff1a; 二、下载路径是灰色改不了 注册表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion里找到C:\Program Files或者C:\Program Files&#xff08;x86&#xff09;&#xff0c;具体哪个看安装的时候对应…