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

news2024/11/17 3:27:37

在这里插入图片描述


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

  • 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/2162817.html

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

相关文章

探索Llama 3.1:开源模型的本地部署与创新应用实践

文章目录 1 Llama 3.1模型的突破性进展2 Llama 3.1模型在业务场景中的实践案例3 使用教程4 Llama 3.1在客户服务中的运用 1 Llama 3.1模型的突破性进展 在数字化转型的浪潮中&#xff0c;大型语言模型&#xff08;LLM&#xff09;以其卓越的处理能力和广泛的应用潜力&#xff…

深度学习500问——Chapter14:超参数调整(3)

文章目录 14.5 如何改善GAN的性能 14.6 AutoML 14.6.1 什么是AutoML 14.6.2 自动化超参数搜索方法有哪些 14.6.3 什么是神经网络架构搜索&#xff08;NAS&#xff09; 14.6.4 NASNet的设计策略 14.6.5 网络设计中&#xff0c;为什么卷积核设计尺寸都是奇数 14.6.6 网络设计中&a…

Spring MVC 参数校验 总结

1. 简介 Sping MVC提供了参数校验的方便注解。 2.代码 在pom.xml中添加依赖&#xff1a; <dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator</artifactId><version>8.0.0.Final</version&g…

Java | Leetcode Java题解之第435题无重叠区间

题目&#xff1a; 题解&#xff1a; class Solution {public int eraseOverlapIntervals(int[][] intervals) {if (intervals.length 0) {return 0;}Arrays.sort(intervals, new Comparator<int[]>() {public int compare(int[] interval1, int[] interval2) {return i…

海外仓系统如何成为数据管理效率的加速器?

在数字化信息时代&#xff0c;海外仓系统的数据管理效率对企业运营的重要性不言而喻。随着电子商务和跨境贸易的快速发展&#xff0c;高效的数据管理不仅影响着货物的流通速度、客户满意度&#xff0c;还关系到企业的盈利能力。为了在海外市场中立足&#xff0c;提升海外仓的数…

《机器学习》周志华-CH8(集成学习)

8.1个体与集成 集成学习(ensemble learning)通过构建并结合多个学习器来完成学习任务&#xff0c;有时也被称为多分类器系统&#xff0c;基于委员会的学习。 同质”集成“&#xff1a;只包含同种类型的个体学习器&#xff0c;同质集成中的个体学习器亦称“基学习器”&#xff0…

【html】基础(二)

本专栏内容为&#xff1a;前端专栏 记录学习前端&#xff0c;分为若干个子专栏&#xff0c;html js css vue等 &#x1f493;博主csdn个人主页&#xff1a;小小unicorn ⏩专栏分类&#xff1a;js专栏 &#x1f69a;代码仓库&#xff1a;小小unicorn的代码仓库&#x1f69a; &am…

WGCLOUD 性能调优笔记

如果主控端server主机内存资源充裕的话&#xff0c;适当增加内存使用&#xff0c;提升server运算能力 修改server/start.sh中的 -Xms256m -Xmx512m &#xff0c;改为 -Xms1024m -Xmx1024m &#xff0c;重启server生效 也可以设置更高些&#xff0c;比如改为 -Xms2048m -Xmx20…

时序预测:LSTM、ARIMA、Holt-Winters、SARIMA模型的分析与比较

引言 近年来&#xff0c;民航旅客周转量一直是衡量国家或地区民航运输总量的重要指标之一。为了揭示民航旅客周转量背后的规律和趋势&#xff0c;本研究旨在综合分析1990年至2023年的相关数据。 通过单位根检验和序列分解&#xff0c;我们确定了民航旅客周转量数据的非平稳性&…

8.13霍夫变换-直线检测

基本概念 霍夫变换&#xff08;Hough Transform&#xff09;是一种用于检测图像中特定形状&#xff08;如直线、圆、椭圆等&#xff09;的技术。在OpenCV中&#xff0c;霍夫变换主要用于检测直线和圆形。这里我们将详细介绍如何使用OpenCV中的霍夫变换来检测直线。 霍夫变换&…

Linux 基础IO(个人笔记)

Linux基础 IO 1.C文件IO操作1.1 hello.c写文件1.2 hello.c读文件1.3 stdin&stdout&stderr 2.系统文件I/O2.1 hello.c写文件2.2 hello.c读文件2.3 open函数介绍2.4 文件描述符 fd2.4.1 文件描述符的分配规则2.4.2 重定向2.4.3 dup2系统调用2.4.4 C文件结构体FILE2.4.5 C…

文件上传、amrkdown编辑器

一、文件上传 这里我以图片为例&#xff0c;进行上传&#xff0c;上传到阿里云oss&#xff08;对象存在中&#xff09; 首先&#xff0c;我们先梳理一下&#xff0c;图片上传的流程 1、前端选择文件&#xff0c;提交文件 前端提交文件&#xff0c;我们可以使用ElementUI中的…

python如何实现日期加减

首先通过import datetime&#xff0c;导入日期处理库。 然后把日期转化成datetime标准格式&#xff0c;使用datetime.datetime.strptime()方法将字符串格式的时间转化为标准格式。 其中"%Y/%m/%d %H:%M:%S"为time字符串的时间格式&#xff1a;Y为年&#xff0c;m为月…

思维导图在线工具哪家强?2024年最新评测

你用过思维导图工具吗&#xff1f;如果品尝需要对事情进行逻辑理顺操作或者需要增强记忆点那我比较推荐使用思维导图在线工具来解决这些问题。这篇文章我将介绍几款思维导图工具来提高我们的效率。 1.福晰思维导图 链接一下&#xff1a;https://www.pdf365.cn/naotu/ 这款思…

JVM的基本组成

一、JDK\JRE\JVM JDK: 全称 "Java Development Kit" &#xff0c;Java 开发工具包&#xff0c;提供 javac 编译器、jheap、jconsole 等监控工具;JRE: 全称"Java Runtime Environment"&#xff0c;Java 运行环境&#xff0c;提供Class Library 核心类库 JV…

【leetcode练习·二叉树】用「遍历」思维解题 I

本文参考labuladong算法笔记[【强化练习】用「遍历」思维解题 I | labuladong 的算法笔记] 257. 二叉树的所有路径 | 力扣 | LeetCode | 给你一个二叉树的根节点 root &#xff0c;按 任意顺序 &#xff0c;返回所有从根节点到叶子节点的路径。 叶子节点 是指没有子节点的节点…

spring-boot、spring-cloud、spring-cloud-alibaba的常用依赖的依赖声明及pom文件

copy自若依 父工程pom文件&#xff0c;主要定义了依赖的版本号 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:sch…

什么是电商云手机?可以用来干什么?

随着电商行业的迅速发展&#xff0c;云手机作为一种创新工具正逐渐进入出海电商领域。专为外贸市场量身定制的出海电商云手机&#xff0c;已经成为许多外贸企业和出海电商卖家的必备。本文将详细介绍电商云手机是什么以及可以用来做什么。 与国内云手机偏向于游戏场景不同&…

什么是启发式过滤(Heuristic Filtering)?

定义 启发式过滤是一种技术方法&#xff0c;利用解决问题的技术和算法来识别数据中的模式、趋势或特征。 这种方法通常涉及使用预测分析和近似方法&#xff0c;以便快速做出决策或对信息进行分类。 启发式过滤通常应用于反垃圾邮件软件、防病毒程序和人工智能等领域&#xff0…

openeuler22.03 LTS 源码编译安装nginx1.22.1

openeuler22.03 LTS 源码编译安装nginx1.22.1 下载安装包 #官网下载nginx1.22.1 wget http://nginx.org/download/nginx-1.22.1.tar.gz安装依赖包 #安装依赖包&#xff0c;NGINX是C语言写的&#xff0c;pcre-devel支持正则表达式&#xff0c;openssl 开启加密 [rootproxy ~]…