【计网】从零开始掌握序列化与反序列化 --- 基础知识储备与程序重构

news2024/9/21 22:16:14

在这里插入图片描述


从零开始掌握序列化与反序列化

  • 1 初识序列化与反序列化
  • 2 再谈Tcp协议
  • 3 程序重构
    • 3.1 Socket类
    • 3.2 回调函数设计
    • 3.3 最终的Tcp服务器类

1 初识序列化与反序列化

在刚学习计算机网络时,我们谈到过网络协议栈,其中最上层的就是应用层,那么这个应用层到底是什么呢?

前几篇文章中编写的程序就是应用层!但是应用层协议应该有一个双方都认识的结构啊?我们之前写的双方都是以字符串结构进行的通信!所以我们写的应用层协议的基础就是双方都可以读取识别字符串!!!而我们使用的socket等函数是传输层!

协议就是双方都认识的结构化的数据!

前面我们通过字符串来实现协议,那么以后如果想要传输结构体这样结构化的数据应该如何传递?

假设我们想要实现一个网络计算器,那么用户需要传递两个数字和一个运算符。
此时我们需要自己设计协议

struct request
{
	int x ;
	int y ;
	char oper ;
}
struct result
{
	int result ;
	int code ;//退出码
}

双方需要同时认识这样的一个结构体,也就是确定协议!

对于这样的协议应该如何传输呢?有两个方案:

  1. 约定方案一:
    • 客户端发送一个形如"1+1"的字符串;
    • 这个字符串中有两个操作数, 都是整形;
    • 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
    • 数字和运算符之间没有空格;
  2. 约定方案二:
    • 定义结构体来表示我们需要交互的信息;
    • 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;

因为C语言支持二进制读写,那么我们如果直接通过write将结构体写入到sockfd文件中,对方再以同样方式读取出来可不可以呢?
不可以!因为你无法保证对方是和自己一样的系统!Linux64位与Linux32位的对齐方式就不一样,更何况一些移动端系统或者其他语言了!!!这样就不可能保证可以正常读取!!!技术上就不推荐这样操作!!!再来说应用层面上,这样相当于把写入与读取写死了,如果产品需求改变,那么将会是一场改代码的大灾难!!!

所以不能直接传输结构体!就需要使用方案二,方案二这个过程叫做 “序列化” 和 “反序列化”!为什么要转换成字符串在发送呢?

那么什么是序列化和反序列化呢?

在群聊中,小明现在发送了一条消息,那么发送的消息不单单是这单独的消息,还会带着小明的昵称,发送的时间等信息一并打包发送!这样才能保证其他人知道是何人何时发的消息。这就叫序列化 !
其他人收到消息,会从这一串字符串中进行解析,将时间,昵称,信息都读取出来。这就叫反序列化!

在这里插入图片描述

向上通过反序列化读取消息,向下通过序列化包装消息。而TCP/UDP不关心发送的是什么,都按照字符串进行传输!

2 再谈Tcp协议

在这里我们重新探讨一下 read、 write、 recv、 send 和 tcp 为什么支持全双工?

客户端与服务端进行通信时,双方需要使用套接字。当使用Tcp套接字时,传输层会创建两个缓冲区:发送缓冲区和接收缓冲区。
在这里插入图片描述

缓冲区我们在学习文件时详细讲过,当我们打开一个文件时,会创建一个文件描述符fd,指向struct file结构体。其中就维护了一个文件缓冲区。当用户向fd写入时会先将数据拷贝到缓冲区,再按照一定的刷新策略刷新到磁盘中去!

那么传输层的缓冲区也是一样:一个fd,代表一个链接;一个链接有两个缓冲区!每当应用层写入数据时(write,send…)本质是将数据拷贝到发送缓冲区中,读取数据时(read, recv…)本质上也是从读取缓冲区中进行读取。所以read , write , send , recv本质上都是拷贝函数!

网络传输的本质:从发送方的发送缓冲区把数据通过网络协议栈和网络拷贝发送给接收方的接收缓冲区!

以上就是tcp支持全双工通信的本质原因!!!

接下来Tcp就需要解决发送缓冲区的一些问题:

  1. 什么时候将数据发送?
  2. 一次发送多少数据?
  3. 发送出错了怎么办?

这些都是由Tcp协议来决定的!使用时不需要管这些问题,因为传输层是属于操作系统的,传输层的问题都是由OS自主来决定的!那么这不就是相当于文件吗!

Tcp设计也是符合生产者消费者模型!因为发送缓冲区和接收缓冲区都是属于操作系统的,所以一定是临界资源!会有多个生产者,多个消费者!而IO发生阻塞也就是为了维护同步关系,保证缓冲区的正确使用!

传输层什么时候发,发多少,出错怎么办都是由OS决定,有没有一种可能 :对方的接收缓冲区写满了,对方一种不读,那么我们的发送缓冲区就积压了很多同样的请求,如果一次性刷新过去,对方就读取到多条信息;又或者只发送了一条请求的一半过去,那么接受方读取就读取一半了,就不可能进行反序列化!这个过程就叫面向字节流!!!客户端发的不一定是服务端收的!!!
所以怎么保证读取的是一个完整的请求呢???

所以对序列化和反序列化中还需要进行特别处理

3 程序重构

在我们将序列化与发序列化加入我们的程序中之前我们先来将我们的代码进行一个重构。之前我们编写的Tcp代码的服务器类并没有做到绝对的解耦:

  1. 服务器类中进行了Socket套接字的创建,bind绑定服务器端口号,进入监听模式。都是通过初始化函数来进行
  2. 服务器类中的在工作中需要做到从套接字文件中获取链接,然后通过sockfd获取数据,也要向客户端发送数据
  3. 服务类类中还需要进行回调函数的处理!

服务器类的工作是比较冗杂的,我们可以将对于套接字文件的操作提取出来,封装为一个Socket类来完成对于套接字的操作。而服务器类只负责建立连接和执行回调函数(回调函数由上层传递),不用在处理套接相关的操作!!!

3.1 Socket类

  1. 将socket系列操作分类封装,设计为基类,派生出Tcp和Udp两种具体的Socket!基类都需要进行创建socket文件 、进行绑定、 进入listen 、获取链接、 申请链接…由于两种类的操作方式不一致,所以基类只需要进行一个声明就可以,具体实现在派生类中完成!
  2. TcpSocket继承Socket类 成员变量 sockfd(可以是listensockfd 也可以是普通套接字)
    • 构造与析构
    • 创建套接字 直接CV原本的代码就可以
    • 进行绑定 需要本地端口号 uint16_t port
    • 进行监听 需要gblcklog
    • 获取链接 输出型参数客户端InetAddr
      using SockSPtr = std::shared_ptr;进行封装,返回一个指针
    • 进行链接 通过远端IP 端口号进行链接
    • Recv 接口负责读
    • Send 接口负责发送
  3. 通过这些操作的组合,可以进行建立监听链接 ,建立客户端连接等操作,十分方便!这种设计模式是模版方法设计模式!!!

整体的框架如下:

namespace socket_ns
{
    class Socket;
    using SockSPtr = std::shared_ptr<Socket>;

    const int gblocklog = 8;

    enum
    {
        SOCKET_FD = 1,
        SOCKET_BIND,
        SOCKET_LISTNE
    };

    using namespace log_ns;
    // 模版方法类!
    class Socket
    {
    public:
        virtual void CreateSocketOrDie() = 0;
        virtual void CreateBindOrDie(uint16_t port) = 0;
        virtual void CreateListenOrDie(int blocklog = gblocklog) = 0;
        virtual SockSPtr Accepter(InetAddr *addr) = 0;
        virtual bool Connector(const std::string &peerip, uint16_t peerport) = 0;
        virtual int GetSockfd() = 0;
        virtual void Close() = 0;
        virtual ssize_t Recv(std::string *out) = 0;
        virtual ssize_t Send(std::string &in) = 0;

    public:
    	//将若干接口合并,完成复杂任务!
        void BuildListenSocket(uint16_t port)
        {
            CreateSocketOrDie();
            CreateBindOrDie(port);
            CreateListenOrDie();
        }
        void BuildClientSocket(const std::string &peerip, uint16_t peerport)
        {
            CreateSocketOrDie();
            Connector(peerip, peerport);
        }
        void BuildUdpSocket()
        {
        }
    };
    class TcpSocket : public Socket
    {
    public:
        TcpSocket() {}
        TcpSocket(int sockfd) : _sockfd(sockfd)  {}
        ~TcpSocket() {}
        void CreateSocketOrDie() override
        {
        }
        void CreateBindOrDie(uint16_t port) override
        {
        }
        void CreateListenOrDie(int blocklog = gblocklog) override
        {
        }
        SockSPtr Accepter(InetAddr *addr) override
        {
        }
        bool Connector(const std::string &peerip, uint16_t peerport) override
        {
        }
        ssize_t Recv(std::string *out) override
        {
        }
        ssize_t Send(std::string &in) override
        {    
        }
        int GetSockfd()
        {
            return _sockfd;
        }
        void Close()
        {
            if (_sockfd > 0)
            {
                ::close(_sockfd);
            }
        }

    private:
        int _sockfd = -1;//可以是listensSockfd 也可以是 Sockfd
    };

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

}

在这里插入图片描述
我们从原本的代码中可以拆分出三个部分:

  • 创建套接字 直接CV原本的代码就可以
  • 进行绑定 需要本地端口号 uint16_t port
  • 进行监听 需要gblcklog
		void CreateSocketOrDie() override
        {
            // 创建socket文件 --- 字节流方式
            _sockfd = socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
            {
                LOG(FATAL, "socket error!!!\n");
                return;
            }
            LOG(INFO, "socket create success!!! _listensockfd: %d\n", _sockfd);
        }
        void CreateBindOrDie(uint16_t port) override
        {
            // 建立server结构体
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_addr.s_addr = INADDR_ANY; // 服务器IP一般设置为0
            local.sin_port = htons(port);

            // 进行绑定
            if (::bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                LOG(FATAL, "bind error!!!\n");
                return;
            }
            LOG(INFO, "bind success!!!\n");
        }
        void CreateListenOrDie(int blocklog = gblocklog) override
        {
            // 将_listensockfd文件转换为listening状态!!!
            if (::listen(_sockfd, blocklog) < 0)
            {
                LOG(FATAL, "listen error!!!\n");
                exit(SOCKET_LISTNE);
            }
            LOG(INFO, "listen success!!!\n");
        }

注意添加具体的参数,让操作更加顺畅!
然后就是加入获取链接与进行链接,这也可以从之前的服务器端和客户端代码中拆分出来,之后服务端和客户端只需要调用对应的接口即可,非常方便 !!!
在这里插入图片描述

		SockSPtr Accepter(InetAddr *addr) override
        {
            // accept接收sockfd
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = ::accept(_sockfd, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                LOG(WARNING, "accept error\n");
                return nullptr;
            }
            *addr = InetAddr(client);
            // 读取数据
            return std::make_shared<TcpSocket>(sockfd);
        }
        bool Connector(const std::string &peerip, uint16_t peerport) override
        {
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server)); // 数据归零
            server.sin_family = AF_INET;
            server.sin_port = htons(peerport); // 端口号
            ::inet_pton(AF_INET, peerip.c_str(), &server.sin_addr);

            // 进行发送数据
            int n = ::connect(_sockfd, (struct sockaddr *)&server, sizeof(server));
            if (n < 0)
            {
                std::cerr << "connect socket error" << std::endl;
                return false;
            }
            return true;
        }

最后是包装一下发送与接收数据,这个比较简单:

		ssize_t Recv(std::string *out) override
        {
            char buffer[4096];
            ssize_t n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);

            if (n > 0)
            {
                buffer[n] = 0;
                *out += buffer;
            }
            return n;
        }
        ssize_t Send(std::string &in) override
        {
            return ::send(_sockfd, in.c_str(), in.size(), 0);
        }

这样我们的Socket类就编写好了,之后的Tcp服务器就只需要进行调用类内的接口即可,这样Tcp服务器的逻辑更加直观!!!

3.2 回调函数设计

对于回调函数我们也要单独设计一下,主要是实现IO的功能,这以后就作为回调函数来进行操作!

#include "Socket.hpp"
#include "InetAddr.hpp"

using namespace socket_ns;

class Service
{
public:
    Service()
    {
    }
    void IOExecute(SockSPtr sock, InetAddr &addr)
    {
        LOG(INFO, "service start!!!\n");
        while (true)
        {
            std::string message;
            ssize_t n = sock->Recv(&message);

            if (n > 0)
            {
                LOG(INFO, "sockfd read success!!! buffer: %s\n", message.c_str());
                std::string hello = "hello";
                sock->Send(hello);
            }
            else if (n == 0)
            {
                LOG(INFO, "client %s quit!\n", addr.AddrStr().c_str());
                break;
            }
            else
            {
                LOG(ERROR, "read error: %s\n", addr.AddrStr().c_str());
                break;
            }
        }
        
    }
    ~Service()
    {
    }
};

目前先简单的进行写一下,后续添加业务逻辑!

3.3 最终的Tcp服务器类

我们将Socket类封装好,IO也单独封装好之后,我们的服务器类就会变为这样简洁的形式:

class TcpServer
{
public:
    // 模版方法模式
    TcpServer(service_io_t service, int port = gport) : _port(port),
                                                        _listensock(std::make_shared<TcpSocket>()),
                                                        _isrunning(false),
                                                        _service(service)
    {
        _listensock->BuildListenSocket(_port);
    }

    void Loop()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // accept接收sockfd
            InetAddr client;
            SockSPtr newsock = _listensock->Accepter(&client);
            if (newsock == nullptr)
                continue;
            LOG(INFO, "get a new link, client info : %s, sockfd is : %d\n", client.AddrStr().c_str(), newsock->GetSockfd());

            pthread_t tid;
            ThreadData *td = new ThreadData(newsock, client, this);
            pthread_create(&tid, nullptr, Execute, td);
        }
        _isrunning = false;
    }
    class ThreadData
    {
    public:
        SockSPtr _sockfd;
        InetAddr _addr;
        TcpServer *_this;

    public:
        ThreadData(SockSPtr sockfd, InetAddr addr, TcpServer *p) : _sockfd(sockfd),
                                                                   _this(p),
                                                                   _addr(addr)
        {
        }
    };
    // 注意设置为静态函数 , 不然参数默认会有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;
    }
    ~TcpServer()
    {
    }
private:
    uint16_t _port; // 服务器端口
    SockSPtr _listensock;
    bool _isrunning;
    service_io_t _service;
};

我们可以测试运行一下:
在这里插入图片描述
可以完成基础工作!!!
后面我们就来加入序列化与反序列化!!!

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

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

相关文章

Qt圆角窗口

Qt圆角窗口 问题&#xff1a;自己重写了一个窗口&#xff0c;发现用qss设置圆角了&#xff0c;但是都不生效&#xff0c;不过子窗口圆角都生效了。 无边框移动窗口 bool eventFilter(QObject *watched, QEvent *evt) {static QPoint mousePoint;static bool mousePressed f…

开源、极简的B站第三方,建议所有人收藏

很多人说B站落寞了&#xff0c;但我觉得不是B站落寞&#xff0c;而是长视频落寞了。现代人已经没有充足的耐心&#xff0c;刷完一个十分钟的视频。毕竟&#xff0c;短视频可以把这十分钟切成50份&#xff0c;让我们开心50次。 可怕的是&#xff0c;B站即使落寞&#xff0c;在长…

继承的例题

答案&#xff1a;D 解析&#xff1a;C允许一个子类继承多个父类 知识点&#xff1a; 子类是父类的特殊化&#xff0c;父类是子类的泛化 解析&#xff1a;子类可以共享父类的属性和方法&#xff0c;选项A正确 面向对象关系中&#xff0c;类与类的关系包含继承&#xff0c;包…

IntelliJ IDEA 2024.1.4 (Ultimate Edition)找不到Add Framework Support解决方法

目录 背景: 解决方法&#xff1a; 步骤1: 步骤2&#xff1a; 步骤3&#xff1a; 创建Web项目的完整流程&#xff1a; 步骤1: 步骤2: 步骤3&#xff1a; 步骤4&#xff1a; Web优点: 背景: 我的IDE版本是IntelliJ IDEA 2024.1.4 (Ultimate Edition)&#xff0c;当我…

【优选算法之双指针】No.2--- 经典双指针算法(下)

文章目录 前言一、双指针示例&#xff1a;1.1 ⽔果成篮1.2 和为s的两个数字1.3 三数之和1.4 四数之和 二、双指针总结&#xff1a; 前言 &#x1f467;个人主页&#xff1a;小沈YO. &#x1f61a;小编介绍&#xff1a;欢迎来到我的乱七八糟小星球&#x1f31d; &#x1f4cb;专…

前后端分离,使用MOCK进行数据模拟开发,让前端攻城师独立于后端进行开发

mock是什么 Mock生成随机数据,拦截Ajax 请求&#xff0c;前后端分离&#xff0c;让前端攻城师独立于后端进行开发。 增加单元测试的真实性 通过随机数据,模拟各种场景。 在实际开发过程中&#xff0c;前端是通过axios来请求数据的&#xff0c;很多时候前端开发者就是通过写固定…

【Git必看系列】—— Git巨好用的神器之git stash篇

应用场景 当我们开发一个新功能时会先从master拉出一个分支dev&#xff0c;然后在这个dev分支下吭哧吭哧的开始写代码开发新功能&#xff0c;就如下代码所示&#xff0c;我们在dev分支下开发Person类的新功能getId() public class Person {private int id;private String nam…

Vue3:v-model实现组件通信

目录 一.性质 1.双向绑定 2.语法糖 3.响应式系统 4.灵活性 5.可配置性 6.多属性绑定 7.修饰符支持 8.defineModel使用 二.使用 1.父组件 2.子组件 三.代码 1.父组件代码 2.子组件代码 四.效果 一.性质 在Vue3中&#xff0c;v-model指令的性质和作用主要体现在…

vue3 快速入门系列 —— 基础

基础 前面我们已经用 vue2 和 react 做过开发了。 AIAutoPrediction 从 vue2 升级到 vue3 成本较大&#xff0c;特别是较大的项目。所以许多公司对旧项目继续使用vue2&#xff0c;新项目则使用 vue3。 有些UI框架&#xff0c;比如ant design vue1.x 使用的 vue2。但现在 an…

5.C++面向对象2(类对象大小计算,结构体内存对齐,大小端存储方式,this指针)

⭐本篇文章为C学习第4章&#xff0c;主要了解类对象大小和this指针 ⭐本人C代码仓库&#xff1a;yzc的c学习: 小川c的学习记录 - Gitee.com 目录 一. 类对象模型 1.1 类成员函数和成员变量的分布 1.2 如何计算类的大小&#xff1f;&#xff08;结构体内存对齐&#xff09…

卸载node,安装nvm的详细使用方法

一、nvm是什么&#xff1f; nvm是一个node的版本管理工具&#xff0c;可以简单切换的node版本、安装、查看。。。等等&#xff0c;与npm不同的是&#xff0c;npm是依赖包的管理工具。 二、nvm下载安装 安装之前需要先把 自己电脑上边的node给卸载了&#xff0c;不然安装不好…

【数据结构初阶】链式二叉树接口实现超详解

文章目录 1. 节点定义2. 前中后序遍历2. 1 遍历规则2. 2 遍历实现2. 3 结点个数2. 3. 1 二叉树节点个数2. 3. 2 二叉树叶子节点个数2. 3. 3 二叉树第k层节点个数 2. 4 二叉树查找值为x的节点2. 5 二叉树层序遍历2. 6 判断二叉树是否是完全二叉树 3. 二叉树性质 1. 节点定义 用…

日志系统第三弹:日志消息和格式化模块的实现

日志系统第三弹&#xff1a;日志消息和格式化模块的实现 一、日志消息模块的实现二、日志格式化模块的设计1.格式化占位符的约定2.如何打印1.各种零件类1.抽象类2.简单的零件类3.日期零件类4.非格式化数据零件类 2.Formatter 3.如何解析 三、日志格式化模块的实现1.解析函数2.c…

一文详解Unity下RTMP推送|轻量级RTSP服务|RTSP|RTMP播放模块说明

技术背景 好多开发者&#xff0c;对Unity下的模块&#xff0c;不甚了解&#xff0c;实际上&#xff0c;除了Windows/Linux/Android/iOS Native SDK&#xff0c;大牛直播SDK发布了Unity环境下的RTMP推流|轻量级RTSP服务&#xff08;Windows平台Linux平台Android平台&#xff09…

Windows安装Oracle11gR2(图文教程)

本章教程&#xff0c;记录在Windows10上安装Oracle11gR2过程。 一、下载安装包 通过网盘分享的文件&#xff1a;oracle11g 链接: https://pan.baidu.com/s/15ilciQ5NlKWtClklmdAH_w?pwds4dd 提取码: s4dd 二、下载并解压文件 将网盘中的安装包文件下载到本地&#xff0c;在此之…

谷歌收录查询工具,好用的谷歌收录查询工具应具备的这5个特性

在探讨如何高效利用谷歌收录查询工具以优化网站可见性和搜索引擎排名时&#xff0c;好用这一标准往往涵盖了工具的准确性、易用性、功能全面性以及对搜索引擎算法变化的适应性等多个方面。 1.准确性 首先&#xff0c;一款好用的谷歌收录查询工具必须能够提供高度准确的数…

C Prime Plus 第6章习题

你该逆袭了 红色标注的是&#xff1a;错误的答案 蓝色标注的是&#xff1a;正确的答案 绿色标注的是&#xff1a;做题时有疑问的地方 橙色标注的是&#xff1a;答案中需要着重注意的地方 练习题 一、复习题1、2、3、4、5、我的答案&#xff1a;错误正确答案&#xff1a; 6、7、…

窥探 引用拷贝、浅拷贝、深拷贝 的那些事 (clone版)

谁家玉笛暗飞声 散入春风满洛城 往期回顾✨内部类 目录✨ 引用拷贝 介绍 总结 浅拷贝 介绍 浅拷贝的步骤 深拷贝 介绍 引用拷贝 介绍 引用拷贝就是我们常用的 “赋值” &#xff0c;只是复制了原对象的引用&#xff0c;即两个对象指向同一块内存堆地址。修改其中的一个对象会影…

【图灵完备 Turing Complete】游戏经验攻略分享 Part.6 处理器架构2 函数

新的架构来了&#xff0c;本游戏的最后一个攻略分享&#xff0c;最后汇编部分无非是对于操作码的熟练&#xff0c;硬件没有问题&#xff0c;那么也就无关痛痒了。 汇编实现&#xff0c;两数相或和两数相与非一起相与即可。 八位异或器&#xff0c;整就完事了。 有手就行。 利…

【梯度下降算法学习笔记】

梯度下降单参数求解 经过之前的学习我们来引入梯度下降的理念 α \alpha α为学习率 w 1 w 初 − α ∂ J ( w ) ∂ w w_1w_初-\alpha\frac{\partial J(w)}{\partial w} w1​w初​−α∂w∂J(w)​ w 2 w 1 − α ∂ J ( w 1 ) ∂ w 1 w_2w_1-\alpha\frac{\partial J(w_1)}…