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

在这里插入图片描述


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

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

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

相关文章

Rosetta 一:手把手教你用Linux安装Rosetta(全网最简洁)

文章目录 1. Rosetta 介绍2.下载2. Rosetta 安装3. 验证安装 1. Rosetta 介绍 很久很久之前就对Rosetta有所耳闻&#xff0c;有一篇文章叫做denovo protein design&#xff0c;说的就是用rosetta来设计蛋白质。 rosetta是david baker团队设计的软件&#xff0c;早期只是一个蛋…

【Godot4.3】胶囊形的偏移获取法

概述 之前用半圆弧拼接的方式求过胶囊形&#xff0c;在逐渐熟练使用Geometry2D的过程中&#xff0c;发现通过线段求端点是圆角类型的偏移多边形&#xff0c;获得的就是胶囊形。 所以我们有了第二种胶囊形求法。 测试代码 tool extends Node2D## 横向宽度 export var width:…

【工具】Windows|两款开源桌面窗口管理小工具Deskpins和WindowTop

总结 Deskpins 功能单一&#xff0c;拖到窗口上窗口就可以置顶并且标记钉子标签&#xff0c;大小 104 KB&#xff0c;开源位置&#xff1a;https://github.com/thewhitegrizzli/DeskPins/releases WindowTop 功能完善全面强大&#xff0c;包括透明度、置顶、选区置顶等一系列功…

SQL server学习01-SQL server环境配置

目录 一&#xff0c;手动下载及安装 microsoft .net framework 3.5 1&#xff0c;下载 2&#xff0c;安装 二&#xff0c;安装SQL server2014 1&#xff0c;下载 2&#xff0c;安装 3&#xff0c;启动SQL server服务 三&#xff0c;下载及安装Microsoft SQL Server…

C Prime Plus 第6章习题

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

ubuntu 安装minikube,并拉取k8s镜像

不要使用最新版&#xff0c;重要的事情说三遍&#xff0c;刚开始也是最求新一点的版本&#xff0c;但问题很多&#xff0c;主要是版本之间的依赖问题&#xff0c;不是某个依赖的版本不支持某些功能&#xff0c;就是依赖之间的版本不能对应上&#xff0c;所以就降低几个版本&…

行业人工智能研究-Python自监督方式学习图像表示算法

学术界人工智能研究落后于工业界 摘要 行业或工业界在人工智能研究上超出学术界&#xff0c;并占据着大量的计算力&#xff0c;数据集和人才诱人的薪水和明朗的预期吸引大量人才离开学术界&#xff0c;涌入行业或工业界即使&#xff0c;比如Meta开源其人工智能模型&#xff0…

实验:WLAN无线综合实验

无线综合实验的概述&#xff1a; WLAN无线综合实验是一种针对无线网络技术的综合性实验&#xff0c;旨在通过实践操作加深对无线局域网&#xff08;WLAN&#xff09;技术的理解和应用能力。以下是对该实验的详细概述&#xff1a; 实验目的 掌握认证AP上线的配置方法&#xff…

[SAP ABAP] 创建域

我们可以使用事务码SE11创建域 输入要创建的域的名称&#xff0c;然后点击创建 输入简短描述&#xff0c;选择数据类型和输入字符数 激活并保存域&#xff0c;创建的域才能够生效

pg入门18—如何使用pg gis

1. 下载postgre gis镜像 2. 运行镜像 docker run -p 15432:5432 -d -e POSTGRES_PASSWORDAb123456! postgis/postgis:12-3.4-alpine 3. 使用gis # 进入容器&#xff0c;登录pgdocker exec -it bash# 登录数据库psql -U postgres# 创建数据库CREATE DATABASE mygeotest;# 使用…

Spring Boot 入门:解锁 Spring 全家桶

前言 Spring 全家桶是现代 Java 开发者不可或缺的工具集&#xff0c;它提供了从轻量级的框架到微服务架构的完整支持。本文将带你快速了解 Spring 框架、核心概念如 IoC&#xff08;控制反转&#xff09;和 AOP&#xff08;面向切面编程&#xff09;&#xff0c;并深入介绍 Sp…

YOLOv10多模态 结合Transformer与NMS-Free 融合可见光+红外光(RGB+IR)双输入【附代码】

文章目录 前言视频效果代码获取文章概述必要环境一、模型训练1、 定义数据1.1、数据集结构1.2、定义data.yaml 2、 运行方法运行效果 二、模型验证运行方法运行效果 三、模型推理3.1. 推理图像1. 参数定义2. 运行方法运行效果 3.2. 推理视频1. 参数定义2. 运行方法运行效果 四、…

构建高可用和高防御力的云服务架构第一部分:深入解析DDoS高防(1/5)

引言 在数字化时代&#xff0c;网络安全已成为全球关注的焦点。随着互联网技术的快速发展和应用的广泛深入&#xff0c;网络安全形势日益严峻。特别是分布式拒绝服务&#xff08;DDoS&#xff09;攻击&#xff0c;以其破坏性强、难以防范的特点&#xff0c;对个人、企业乃至国…

Go-知识-定时器

Go-知识-定时器 1. 介绍2. Timer使用场景2.1 设定超时时间2.2 延迟执行某个方法 3. Timer 对外接口3.1 创建定时器3.2 停止定时器3.3 重置定时器3.4 After3.5 AfterFunc 4. Timer 的实现原理4.1 Timer数据结构4.1.1 Timer4.1.2 runtimeTimer 4.2 Timer 实现原理4.2.1 创建Timer…

Type-C 诱骗取电快充协议芯片,支持取电电压5V、9V、12V、15V、20V

‌XSP01A快充协议芯片‌是一款集成USB Power Delivery(PD) 2.0/3.0快充协议的USB-C/Type-C多功能取电芯片 它支持从手机充电器、车充等电源上取电给产品供电。这款芯片的优势在于其价格便宜&#xff0c;同时能够实现快充&#xff0c;对于不需要支持太多协议的设备来说&#x…

DRV8825步进电机驱动详细说明书————含接线图

最近玩步进电机时候&#xff0c;发现步进电机驱动种类多&#xff1b;A4988&#xff0c;drv8825,tb6600,lv8731……&#xff1b;tb6600驱动电流可达4A&#xff0c;1600细分&#xff0c;十分强大&#xff0c;但是体积大&#xff0c;用在平衡车上不太合适。 drv8825加散热器驱动电…

安装SQL Server遇到的问题

出现了一和二的问题&#xff0c;最后还是通过三完全卸载sqlserver安装成功了 一.安装过程中依次报错 1.MOF编译器无法连接WMI服务器。原因可能是语义错误(例如&#xff0c;与现有WMI知识库不兼容)或实际错误(例如WMI服务器启动失败)。 2.PerfLib 2.0计数器removal失败&#xf…

HarmonyOS鸿蒙开发实战(5.0)表情图片聊天案例实践

鸿蒙HarmonyOS NEXT开发实战往期文章必看&#xff08;持续更新......&#xff09; HarmonyOS NEXT应用开发性能实践总结 HarmonyOS NEXT应用开发案例实践总结合集 最新版&#xff01;“非常详细的” 鸿蒙HarmonyOS Next应用开发学习路线&#xff01;&#xff08;从零基础入门…

混合开发-JSBridge

1.1 什么是混合开发? 混合开发是一种融合了原生开发和Web开发优势的移动应用开发方式。 具体来说&#xff0c;混合开发通常指的是利用一种框架或平台来创建应用程序&#xff0c;这种程序结合了原生应用的一些功能和特性&#xff08;比如访问设备的摄像头、相册、GPS、蓝牙等…

DAMODEL——Llama3.1的部署与使用指南

Llama3.1的部署与使用指南 在自然语言处理&#xff08;NLP&#xff09;领域&#xff0c;大模型&#xff08;LLM&#xff09;是基于深度学习算法训练而成的重要工具&#xff0c;应用范围包括自然语言理解和生成。随着技术的发展&#xff0c;开源的LLM不断涌现&#xff0c;涵盖了…