【Linux】自定义协议与序列化和反序列化

news2024/12/24 8:12:51

一、自定义协议

1.1 自定义报文格式

       在前面的博客中,我们可以知道在TCP协议中,面向的是字节流;而UDP协议中面向的是数据报。因此,在手写简单的TCP和UDP服务器中,所使用的是接收函数和发送函数不同。因此,在TCP协议中,我们需要分清楚一个完整的报文,并将其分离出来,因此,我们应该如何进行分离出一个完整的报文呢??

       如果一个报文中什么标志也没有,那么必然是不可能将一个完整的报文分离出来。因此,我们可以重新定义一下报文格式:为了简单起见,我们采用LV格式,在后面的学习中,当我们学习了TCP报文和UDP报文后,就会知道报文 = 报头 + 有效载荷。报头中存放有效载荷的长度,我们定义的一个简单的报文格式如图所示:

       在上一篇博客中,我们也简单的学习如何使用Json来进行序列化和反序列化的操作。我们可以在协议层中定义出来将请求和响应的报文进行序列化和反序列化操作,然后将数据通过传输层进行传输。在根据具体的报文的格式进行分离出一个完整的报文。

1.2 协议类(表示层)

       在协议类中,我们创建了请求类和响应类,并且创建出添加报头函数和分解完整报文函数,最后在定义出一个工厂用来创建请求和响应。

1.2.1 请求类

    class Request
    {
    public:
        // 我们自定义协议
        // 报文 = 包头 + 有效载荷
        // LV格式  固定字段长——后续字符串的长度  正文内容\n\t
        // 对报文进行分析
        // "len\r\n"_x_op_y\r\n"
        Request()
        {
        }
        Request(int x, int y, char oper)
            : _x(x), _y(y), _oper(oper)
        {
        }

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

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

        bool Deserialize(std::string &in) // 反序列化
        {
            Json::Value root;
            Json::Reader reader;
            bool ret = reader.parse(in, root);
            _x = root["x"].asInt();
            _y = root["y"].asInt();
            _oper = root["oper"].asInt();
            return ret;
        }

        ~Request()
        {
        }

    public:
        int _x;
        int _y;
        char _oper; // "+-*/%"
    };

1.2.2 响应类

    class Response
    {
    public:
        Response()
        {
        }
        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::Reader reader;
            Json::Value root;
            bool ret = reader.parse(in, root);
            _result = root["result"].asInt();
            _code = root["code"].asInt();
            return ret;
        }

        ~Response()
        {
        }

    public:
        int _result;
        int _code;
    };

1.2.3 添加报文函数

       在将创建出来的请求或者响应进行序列化后,将其进行添加报头方式,将其构造成我们所需要的报文格式,以便在之后进行分离出完整报文的操作。

    const std::string SEP = "\n\t";

    // 进行拼装报文
    std::string Encode(const std::string &json_str)
    {
        int json_str_len = json_str.size();
        std::string proto_str = std::to_string(json_str_len);
        proto_str += SEP;
        proto_str += json_str;
        proto_str += SEP;
        return proto_str;
    }

1.2.4 分解完整报文函数

       解决粘包问题,我们根据报文格式,我们可以得出:在第一次遇见“\n\t”的时候,其前面的字符串会有两种情况:要么是空串,要么是报头。当我们解析到报头后,计算出一个完整的报文的总长度,然后个根据总长度将完整报文解析出来。

    const std::string SEP = "\n\t";

    // 处理粘包问题
    std::string Decode(std::string &inbuffer) // const 不能修改
    {
        // 先找出SEP,然后截取出来len的长度,最后将完整报文截取出来
        auto pos = inbuffer.find(SEP);
        if (pos == std::string::npos)
            return "";
        std::string len_str = inbuffer.substr(0, pos);
        if (len_str.empty())
            return "";
        int len = std::stoi(len_str);
        int total = len + len_str.size() + SEP.size() * 2;
        if (inbuffer.size() < total)
            return "";

        std::string package = inbuffer.substr(pos + SEP.size(), len);
        inbuffer.erase(0, total); // yichu
        return package;
    }

1.2.5 工厂类

       在这里采用简单工厂模式,我们可以将请求类和响应类设置为私有类,不想外部暴漏;只将工厂类向外暴漏,方便用户进行注册请求和响应。

    // 简单工厂模式
    class Factory
    {
    public:
        Factory()
        {
            srand(time(nullptr) ^ getpid());
        }
        // 生产数据   利用随机值将报文填充
        std::shared_ptr<Request> BuildRequest()
        {
            opers = "+-*/%&^";
            int x = rand() % 10;
            int y = rand() % 5;
            char oper = opers[rand() % 8];
            std::shared_ptr<Request> req = std::make_shared<Request>(x, y, oper);
            return req;
        }
        std::shared_ptr<Response> BuildResponse()
        {
            return std::make_shared<Response>();
        }
        ~Factory() {}

    private:
        std::string opers;
    };

二、复习一下TCP服务器的写法

2.1 回忆一下Socket类

       在上一节课中,我们将Socket进行封装。我们将TCP套接字和UDP套接字的方法进行封装成一个类中,来回忆一下他们的方法总共有哪些?创建套接字,绑定套接字,监听套接字,TCP接收数据,TCP连接,返回套接字,接收数据,发送数据。

// 在抽象类中,我们可以定义出一些虚函数,供TCPSocket和UDPSocket方便构造各自的函数
virtual void CreateSocketOrDie() = 0;             // 创建套接字
virtual void BindSocketOrDie(InetAddr &addr) = 0; // 绑定套接字
virtual void ListenSocketOrDie() = 0;             // 监听套接字
virtual socket_sptr Accepter(InetAddr *addr) = 0; // 接受数据
virtual bool Connector(InetAddr &addr) = 0;       // 连接
virtual int Sockfd() = 0;                         // 返回套接字
virtual int Recv(std::string *out) = 0;           // 接收数据
virtual int Send(const std::string &in) = 0;      // 发送数据

2.2 TcpServer类

       在这个类中,我们可以通过2.1中的TcpSocket类进行创建,我们可以创建出智能指针,通过智能指针来进行管理这个变量,调用里面的函数。在执行函数中,我们可以利用Acceptor函数创建出新的套接字,以便于进行与客户端通信。在这个函数中,我们可以使用多线程来进行。

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <unistd.h>
#include <functional>
#include "Log.hpp"
#include "InetAddr.hpp"
#include "Socket.hpp"
#include <memory>

using namespace socket_ns;
using io_service_t = std::function<void(socket_sptr, InetAddr)>;
class TcpServer;
const static int backlog = 16; // 连接队列
class ThreadDate
{
public:
    ThreadDate(socket_sptr sockfd, InetAddr addr, TcpServer *s) : _sockfd(sockfd), _addr(addr), self(s) {}

    InetAddr _addr;
    socket_sptr _sockfd;
    TcpServer *self;
};

class TcpServer
{
public:
    TcpServer(int port, io_service_t service)
        : _local("0", port), _isrunning(false), _listensock(std::make_unique<TcpSocket>()), _service(service)
    {
        _listensock->BuildListenSocket(_local); // 创建套接字,绑定套接字,监听套接字
    }
    ~TcpServer()
    {
    }
    static void *HandlerSock(void *args)
    {
        pthread_detach(pthread_self());
        ThreadDate *td = static_cast<ThreadDate *>(args);
        td->self->_service(td->_sockfd, td->_addr); // 这个地址就是客户端的地址
        ::close(td->_sockfd->Sockfd());
        delete td;
        return nullptr;
    }
    void Loop()
    {
        // 4. 不能直接收数据,先获取连接
        // accept在通信之前先获取客户端的地址, 返回值是文件描述符
        // 这个文件描述符是什么?? 每建立一个链接就会有一个套接字, 用于IO操作
        _isrunning = true;
        while (_isrunning)
        {
            // 利用accept函数进行创建出新的套接字
            InetAddr peeraddr;
            socket_sptr s = _listensock->Accepter(&peeraddr);
            if (s == nullptr)
                continue;
            // version 2 : 采用多线程
            pthread_t t;
            ThreadDate *td = new ThreadDate(s, peeraddr, this);
            pthread_create(&t, nullptr, HandlerSock, td);
        }
        _isrunning = false;
    }
private:
    InetAddr _local;
    std::unique_ptr<Socket> _listensock;
    bool _isrunning;
    io_service_t _service;
};

三、业务服务类(应用层)

       为了使业务服务与通信服务进一步解耦合,我们可以将业务服务单独封装成一个类,使得整体布局更加具有层次性。

#pragma once
#include <iostream>
#include "Protocol.hpp"
using namespace protocol_ns;

// 解决计算层
class Calculate
{
public:
    Calculate()
    {
    }
    // 处理函数
    Response Excute(const Request &req)
    {
        Response rsp(0, 0);
        switch (req._oper)
        {
        case '+':
            rsp._result = req._x + req._y;
            break;
        case '-':
            rsp._result = req._x - req._y;
            break;
        case '*':
            rsp._result = req._x * req._y;
            break;
        case '/':
        {
            if (req._y == 0)
            {
                rsp._code = 1;
            }
            else
            {
                rsp._result = req._x / req._y;
            }
        }
        break;
        case '%':
        {
            if (req._y == 0)
            {
                rsp._code = 2;
            }
            else
            {
                rsp._result = req._x % req._y;
            }
        }
        break;
        default:
            rsp._code = 3;
            break;
        }
        return rsp;
    }
    ~Calculate()
    {
    }
private:
};

四、创建客户端(会话层)

       由于之前的套接字封装,我们可以简单地通过智能指针创建出TcpSocket,通过智能指针可以直接创建出客户端套接字并连接服务器成功。我们之后就可以进行通信。

客户端与服务器通信的步骤:创建一个请求;将请求进行序列化;将请求添加报头;发送报文;读取应答;判断应答是否是一个完整的报文;将报文反序列化,最后拿到了结构化的应答。

void Usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
              << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(2);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);
    InetAddr serveraddr(serverip, serverport);
    std::unique_ptr<Socket> cli = std::make_unique<TcpSocket>();
    bool res = cli->BuildClientSocket(serveraddr);
    std::string buffer;
    while (res)
    {
        Factory factory;
        // 1. 创建一个请求
        auto req = factory.BuildRequest();
        // 2. 将请求进行序列化
        std::string request;
        req->Serialize(&request);
        // 3. 添加报文长度
        request = Encode(request);
        // 4. 发送报文
        cli->Send(request);
        // 5. 读取应答
        int n = cli->Recv(&buffer);
        if (n < 0)
        {
            break; // 出错了
        }
        buffer = Encode(buffer);
        // 6. 判断应答是否是一个完整的报文
        auto resp = factory.BuildResponse();
        resp->Deserialize(buffer);
        // 7. 拿到了结构化的应答
        std::cout << resp->_result << "[" << resp->_code << "]" << std::endl;
    }
    return 0;
}

五、总结

       将OSI七层模式与TCP/IP分层模式进行对比,我们会发现TCP/IP分层模型将会话层、表示层与应用层合并为一层。因为应用层必须要在用户层完成,用户决定了与谁构建连接,用户决定了采用什么样的报文格式以及协议,用户决定了采用什么服务。

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

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

相关文章

docker images

docker 装好docker之后&#xff0c;先掌握一下docker启动与停止 docker启动关闭状态 systemctl 命令是系统服务管理器指令&#xff0c;它是 service 和 chkconfig 两个命令组合。 查看 docker 的启动状态 systemctl status docker关闭 docker systemctl stop docker启动 …

基于空间结构光场照明的三维单像素成像

单像素成像是一种新兴的计算成像技术。该技术使用不具备空间分辨能力的单像素探测器来获取目标物体或场景的空间信息。单像素探测器具有高的时间分辨率、光探测效率和探测带宽&#xff0c;因此单像素光学成像技术在散射、弱光等复杂环境下相较于传统面阵成像技术展现了很大优势…

A题 农村公交与异构无人机协同配送优化

1.1问题背景 农村地区的独特地理和社会结构带来了配送上的特殊挑战。复杂的地形&#xff0c;如山地和河流等自然障碍&#xff0c;使得道路建设困难重重&#xff0c;导致道路网络稀疏&#xff0c;而分散的配送点进一步增加了物流的复杂性。这些因素叠加&#xff0c;使得传统配送…

linux top命令介绍以及使用

文章目录 介绍 top 命令1. top 的基本功能2. 如何启动 top3. top 的输出解释系统概况任务和 CPU 使用情况内存和交换空间进程信息 4. 常用操作 总结查看逻辑CPU的个数查看系统运行时间 介绍 top 命令 top 是一个在类 Unix 系统中广泛使用的命令行工具&#xff0c;用于实时显示…

建模导论的最后一个视频笔记

建模的目的&#xff1a;解决贴合题意的问题&#xff0c;用合适的方法解决问题前提&#xff1a;理解题意&#xff0c;知道题目在说什么&#xff0c;前提的前提&#xff1a;了解题目的背景&#xff0c;知道题目这类问题的常见概念&#xff0c;了解这方面的知识如果是数据题&#…

常见的网络安全服务大全(汇总详解)

吉祥知识星球http://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247485367&idx1&sn837891059c360ad60db7e9ac980a3321&chksmc0e47eebf793f7fdb8fcd7eed8ce29160cf79ba303b59858ba3a6660c6dac536774afb2a6330&scene21#wechat_redirect 《网安面试指南》…

若楠带你初识OpenCV(4) -- 图像边缘检测

文章目录 OpenCV图像边缘检测sobel 算子1. x方向上的边缘2. 保存负值3. 取绝对值4. y方向上的边缘5. 加权运算不要双方向同时卷积读取猪猪侠查看效果 Scharr 算子Laplacian 算子 canny边缘检测优点理论步骤四部分1. 图像降噪2. 梯度计算3. 非极大值抑制4. 双阈值边界跟踪 示例应…

【QT | 开发环境搭建】windows系统(Win10)安装 QT 5.12.12 开发环境

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; ⏰发布时间⏰&#xff1a; 本文未经允许…

WebStorm用Debug模式调试Vue项目

问题说明 开发前端代码时&#xff0c;一直很苦恼调试前端代码的麻烦。 简单的内容可以通过console.log()在控制台打印变量值&#xff0c;来验证预期结果。 涉及到稍复杂的逻辑&#xff0c;就需要在代码中侵入增加debugger&#xff0c;或者在浏览器中找到js文件&#xff0c;再手…

Linux 之 mysql-5.7.44 下载/安装(离线)

下载 官网 MySQL :: Download MySQL Community Server (Archived Versions) 安装 1.解压并放到指定目录&#xff08;/home/mysql&#xff09; tar -zxvf mysql-5.7.44-el7-x86_64.tar.gz 移动到指定安装位置&#xff08;我的&#xff1a;/home 下&#xff09; mv mysql-5.…

实操在聆思CSK6大模型开发板的英文评测SDK中自定义添加单词、短语、句子资源

引言 英文评测示例通过对用户语音输入的英文单词进行精准识别&#xff0c;提供 单词、短语、句子 三种类型&#xff0c;用户在选择好类型后&#xff0c;可根据屏幕上的提示进行语音输入&#xff0c;评测算法将对输入的英文语音进行精准识别&#xff0c;并对单词的发音、错读、漏…

Codeforces Round 970 (Div. 3)(ABCDEF)

Codeforces Round 970 (Div. 3) A:Sakurakos Exams 签到 题意:给定1,2的数量,判断是否能用加减符号使得这些1,2计算出0 void solve() {cin>>n>>m;if(n%2)cout<<"NO\n";else{if(m%20||n)cout<<"YES\n";else cout<<"…

作为前端,感受一下MemFire Cloud带来的魅力

作为一名前端开发者&#xff0c;你是否曾为后台开发的繁琐和复杂而头疼&#xff1f;是否想过只需要专注于前端界面&#xff0c;而所有的后端工作都能一键搞定&#xff1f;如果你有这些想法&#xff0c;那么MemFire Cloud 无疑会成为你开发路上的新神器。 什么是MemFire Cloud…

SpringBoot学习(8)RabbitMQ详解

RabbitMQ 即一个消息队列&#xff0c;主要是用来实现应用程序的异步和解耦&#xff0c;同时也能起到消息缓冲&#xff0c;消息分发的作用。 消息中间件最主要的作用是解耦&#xff0c;中间件最标准的用法是生产者生产消息传送到队列&#xff0c;消费者从队列中拿取消息并处理&…

搞定JavaScript异步原理,深入学习Promise

JS异步及Promise基础 一、从浏览器运行机制说起 来破案啦&#xff0c;常说的“多线程的浏览器、单线程的Javascript”到底是什么意思&#xff1f;单线程的Javascript是否真的需要抽身去计时、去发http请求&#xff1f;第一部分我们重新从浏览器运行机制开始认识一下JavaScript…

828华为云征文 | Flexus X实例与华为云EulerOS的Tomcat安装指南

文章目录 前言安全组设置操作步骤软件安装配置软件验证Tomcat安装是否成功 总结 前言 Tomcat是一个由Apache软件基金会开发并维护的免费、开源的Web应用服务器。它主要用于处理Java Servlet、JavaServer Pages&#xff08;JSP&#xff09;和JavaServer Pages Standard Tag Lib…

新增一个数组传递给后端

实现的效果&#xff1a; 页面 <div style"margin-bottom: 10px" v-if"totalPrice"><p style"font-weight: bolder;margin-bottom: 10px">支付计划<el-button type"text" size"small" click"addPayInf…

53 mysql pid 文件的创建

前言 接上一篇文章 mysql 启动过程中常见的相关报错信息 在 mysql 中文我们在 “service mysql start”, “service mysql stop” 经常会碰到 mysql.pid 相关的错误信息 比如 “The server quit without updating PID file” 我们这里来看一下 mysql 中 mysql.pid 文件的…

软件工程知识点总结(2):需求分析(一)——用例建模

1 软件项目开发流程&#xff1a; 需求分析→概要设计→详细设计→编码实施→测试→产品提 交→维护 2 系统必须做什么&#xff1f; 获取用户需求&#xff0c;从用户角度考虑&#xff0c;用户需要系统必须完成哪些工作&#xff0c;也就是对目 标系统提出完整、准确、清晰、具体…

算法day22|组合总和 (含剪枝)、40.组合总和II、131.分割回文串

算法day22|组合总和 &#xff08;含剪枝&#xff09;、40.组合总和II、131.分割回文串 39. 组合总和 &#xff08;含剪枝&#xff09;40.组合总和II131.分割回文串 39. 组合总和 &#xff08;含剪枝&#xff09; 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 ta…