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

在这里插入图片描述


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

  • 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一般设置为0local.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接收sockfdstruct 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接收sockfdInetAddr 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.xdnf.cn/news/149099.html

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

相关文章

97、配置 VXLAN 不同子网互访 (分布式网关)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、基础配置SW1SW2IGP IS-IS 二、VXLAN1.引入库 总结 前言 一、基础配置 SW1 vlan 10 vlan 20interface GigabitEthernet0/0/1port link-type accessport de…

springboot+阿里云物联网教程

需求背景 最近有一个项目,需要用到阿里云物联网,不是MQ。发现使用原来EMQX的代码去连接阿里云MQTT直接报错,试了很多种方案都不行。最终还是把错误分析和教程都整理一下。 需要注意的是,阿里云物联网平台和MQ不一样。方向别走偏了。 概念描述 EMQX和阿里云MQTT有什么区别…

python编程开发“人机猜拳”游戏

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;开发者-曼亿点 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 曼亿点 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a…

利用Accelerate()进行pytorch的多GPU加速

简介 官方Github&#xff1a;https://github.com/huggingface/accelerate Accelerate 是为喜欢编写PyTorch模型的训练循环但不愿意编写和维护使用多GPU/TPU/fp16所需的样板代码的PyTorch用户创建的。 它可以仅加速与多 GPU/TPU/fp16 相关的样板代码&#xff0c;并保持其余代…

代码提交消息自动生成助手 | OPENAIGC开发者大赛高校组AI创新之星奖

在第二届拯救者杯OPENAIGC开发者大赛中&#xff0c;涌现出一批技术突出、创意卓越的作品。为了让这些优秀项目被更多人看到&#xff0c;我们特意开设了优秀作品报道专栏&#xff0c;旨在展示其独特之处和开发者的精彩故事。 无论您是技术专家还是爱好者&#xff0c;希望能带给…

hive建表指定列分隔符为多字符分隔符实战(默认只支持单字符)_hive row formate ###

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。 需要这份系统化资料的朋友,可以戳这里获取 一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎…

我国以人名命名的城市有哪些?

我国幅员辽阔&#xff0c;国内的城市非常多&#xff0c;每个城市的名字或许都有其背后的故事。 其中不乏一些以人物之名命名的城市&#xff0c;有些是上古传说中的人物&#xff0c;有些则是历史上有重要影响的人物。 湖北神农架林区&#xff0c;因炎帝神农氏而得名 而我国198…

【Linux网络 —— 网络基础概念】

Linux网络 —— 网络基础概念 计算机网络背景网络发展 初始协议协议分层协议分层的好处 OSI七层模型TCP/IP五层(或四层)模型 再识协议为什么要有TCP/IP协议&#xff1f;什么是TCP/IP协议&#xff1f;TCP/IP协议与操作系统的关系所以究竟什么是协议&#xff1f; 网络传输基本流程…

软件供应链安全管理实践之中国联通

软件供应链安全管理是保护软件开发和交付过程中所有组件的安全性和完整性的重要环节&#xff0c;软件供应链安全国家标准及政策的发布&#xff0c;为企业软件供应链安全管理提供了依据。 本文摘选自软件供应链安全推进工作组指导、苏州棱镜七彩信息科技有限公司主笔的《2023软…

编曲为什么这么难学 编曲应该从何下手,想要学习编曲,一定要有扎实的乐理基础知识

很多小伙伴在刚刚接触编曲的时候&#xff0c;可能会感觉只是学习怎么创作旋律&#xff0c;并不会很难。但在真正开始接触编曲的时候&#xff0c;却发现想要创作出好的曲目&#xff0c;要学习的知识实在是太多了&#xff0c;因此小伙伴也会感慨编曲太难学了。下面给大家详细讲解…

Python画笔案例-062 绘制彩花之太阳花

1、绘制彩花之太阳花 通过 python 的turtle 库绘制 彩花之太阳花,如下图: 2、实现代码 绘制 彩花之太阳花,以下为实现代码: """彩花之太阳花.py本程序需要coloradd模块支持,安装方法:pip install coloradd""" import turtle from coloradd…

【研赛D题成品论文】24华为杯数学建模研赛D题成品论文(第一问)+可运行代码丨免费分享

2024华为杯研究生数学建模竞赛D题精品成品论文已出&#xff01; D题 大数据驱动的地理综合问题 一、问题分析 问题一&#xff1a;目标&#xff1a;利用1990-2020年的数据&#xff0c;针对降水量和土地利用的时空演化特征进行描述。数据&#xff1a;两个核心变量&#xff0c;一…

XBOX掌机和新主机或于26年推出

原文转载修改自&#xff08;更多互联网新闻/搞机小知识&#xff09;&#xff1a; XBOX掌机和新主机或于2026年发布&#xff0c;比PS6“早点” XBOX掌机成真 关于下一代XBOX主机&#xff0c;微软相关负责人就曾坦言下一代 Xbox 将是该平台 “最大的技术飞跃”&#xff0c;在饱…

18722 稀疏矩阵的运算

思路&#xff1a; 快速转置算法的基本思想是预先计算出转置后的三元组在新数组中的位置&#xff0c;然后直接将元素放到对应的位置上。这样做的好处是只需要遍历一次原数组&#xff0c;就可以完成转置操作。 步骤如下&#xff1a; 1. 初始化一个新的三元组数组&#xff0c;用于…

“咨询+数智化”双剑合璧,毕马威与用友的“最强拍档” | 商业创新同行者

作为全球“四大”会计师事务所之一&#xff0c;毕马威被很多人熟知&#xff0c;是因为其为很多上市公司提供了财务报告的审计服务。 实际上&#xff0c;审计业务并不是毕马威的全部&#xff0c;甚至不是其最大的业务版块。在审计、税务和咨询这三大业务中&#xff0c;咨询的营…

ABB 机器人与 Profinet 转 EthernetIP 网关的高效连接

Profinet转EthernetIP网关在工业自动化领域发挥着至关重要的作用。它主要的功能就是实现不同网络协议之间的数据交互&#xff0c;为各种设备的连接与协同工作搭建了桥梁。 以连接ABB机器人为例&#xff0c;Profinet转EthernetIP网关能够将ABB机器人高效地接入到不同的网络系统…

基于Java的建筑节能监测系统+公共建筑能耗监测系统+建筑能耗监测系统+节能监测系统

建筑节能监测系统公共建筑能耗监测系统建筑能耗监测系统节能监测系统能耗监测建筑能耗监测能耗分析能耗管理能耗预测能耗监控能耗监测平台建筑能耗 介绍 建筑节能监测系统是基于计算机网络、物联网、大数据和数据可视化等多种技术融合形成的一套节能监测系统 系统实现了对建…

k8s中,pod生命周期,初始化容器,容器探针,事件处理函数,理解其设计思路及作用

k8s中&#xff0c;为什么要设计pod 平台直接管理容器不是挺好的吗 为什么要以pod为单位进行管理&#xff0c; 然后把容器放在pod里面 那么有pod和没pod的区别是什么 也就是pod提供了什么作用 这个可以考虑从pod生命周期管理的角度去思考 如图&#xff0c;pod主容器在运行…

2024.9.24 数据分析

资料 111个Python数据分析实战项目&#xff0c;代码已跑通&#xff0c;数据可下载_python数据分析项目案例-CSDN博客 【数据挖掘六大项目实战】敢说这是全B站讲的最详细最通俗易懂的数据挖掘教程&#xff01;整整60集&#xff01;学不会来找我&#xff01;-数据挖掘、数据挖掘…

idea怎么快速生成get set方法,快捷键是什么?

idea怎么快速生成get set方法 参考文章&#xff1a;IntelliJ IDEA生成get/set方法的快捷键是什么 1、生成某个get set方法altenter 快捷键&#xff1a;altenter 2.生成整个类或者某个get set方法altinsert 快捷键&#xff1a;altinsert 点击后&#xff0c;会出现下图弹窗&…