当前位置: 首页 > news >正文

【Linux】从零开始使用多路转接IO --- epoll

在这里插入图片描述

当你偶尔发现语言变得无力时,
不妨安静下来,
让沉默替你发声。
--- 里则林 ---

从零开始认识多路转接

  • 1 epoll的作用和定位
  • 2 epoll 的接口
  • 3 epoll工作原理
  • 4 实现epollserverV1

1 epoll的作用和定位

之前提过的多路转接方案select和poll 都有致命缺点:底层都是暴力的遍历,效率不高!
对此,诞生出了epoll这个更好的方案!

按照 man 手册的说法: 是为处理大批量句柄而作了改进的 poll。它是在 2.5.44 内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)。它几乎具备了之前所说的一切优点, 被公认为 Linux2.6 下性能最好的多路 I/O 就绪通知方法.

2 epoll 的接口

epoll的相关接口有三个:

epoll_create

EPOLL_CREATE(2)                Linux Programmer's Manual                                                           EPOLL_CREATE(2)NAMEepoll_create, epoll_create1 - open an epoll file descriptorSYNOPSIS#include <sys/epoll.h>int epoll_create(int size);int epoll_create1(int flags);

epoll_create接口只有一个参数,其功能是在内核创建一个epoll模型!这个模型我们后面详细谈。这个size我们只有设置为一个大于零的数即可。创建成功之后会给我们返回一个文件描述符,现在我们还理解不了,后续讲解。

epoll_ctl

EPOLL_CTL(2)                                                              Linux Programmer's Manual                                                              EPOLL_CTL(2)NAMEepoll_ctl - control interface for an epoll file descriptorSYNOPSIS#include <sys/epoll.h>int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);DESCRIPTIONThis  system  call  is used to add, modify, or remove entries in the interest list of the epoll(7) instance referred to by the file descriptor epfd.  It requests thatthe operation op be performed for the target file descriptor, fd.

epoll_ctl有四个参数:

  1. int epfd:这个就是通过epoll_create获得的文件描述符
  2. int op:这个是操作选项,我们这个函数共用三种选项:EPOLL_CTL_ADD增加 EPOLL_CTL_MOD 修改EPOLL_CTL_DEL删除。
  3. int fd:对这个文件描述符进行操作。
  4. struct epoll_event * event:这时一个结构体,类似struct pollfd,但内部更加复杂:
    typedef union epoll_data {void        *ptr;int          fd;uint32_t     u32;uint64_t     u64;} epoll_data_t;struct epoll_event {uint32_t     events;      /* Epoll events */epoll_data_t data;        /* User data variable */};
    其中的events位图就可以设置读事件,写事件…注意这里没有返回事件!

epoll_wait

EPOLL_WAIT(2)                                                             Linux Programmer's Manual                                                             EPOLL_WAIT(2)NAMEepoll_wait, epoll_pwait - wait for an I/O event on an epoll file descriptorSYNOPSIS#include <sys/epoll.h>int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);int epoll_pwait(int epfd, struct epoll_event *events,int maxevents, int timeout,const sigset_t *sigmask);DESCRIPTIONThe epoll_wait() system call waits for events on the epoll(7) instance referred to by the file descriptor epfd.  The buffer pointed to by events is used to return in‐formation from the ready list about file descriptors in the interest list that have some events available.  Up to maxevents are returned by  epoll_wait().   The  max‐events argument must be greater than zero.

epoll_wait有四个参数:

  1. int epfd:这个就是通过epoll_create获得的文件描述符。
  2. *struct epoll_event events :这是一个数组,向内核输入一个缓冲区,想让内核提供这个数组将就绪事件返回来!
  3. ** int maxevents**:数组的元素个数。
  4. int timeout:等价于poll接口的timeout,以毫秒为单位!
  5. 返回值等价于poll!

总而言之:epoll将传入与传出分成了两个接口来进行

3 epoll工作原理

对于epoll更深入的理解我们需要从底层进行讲解:

数据到达主机时,数据首先会到达物理层,那么操作系统如何知道网卡里有数据呢?通过硬件中断!通过针脚中断,就可以通知操作系统!从而数据链路层从网络层读取数据!

当我们使用epoll时,系统内部会建立一个红黑树,这个红黑树创建时是空树。红黑树的节点字段主要存在:文件描述符fd , 事件位图 events ,左右指针,节点颜色...,这个树标识了用户想让OS关心的文件操作符fd以及其对应事件!epoll_ctl接口中的op就是对应的增添修改删除红黑树节点!注意:这个红黑树的键值是fd!

其中还有一个就绪队列,这是一个双向链表,每个节点与红黑树中的节点类似。当网卡中有数据了,网卡通过硬件中断把数据交给网络协议栈。OS可以知道每个文件描述符对应的输入输出缓冲区状态,当回红黑树节点对应fd的EPOLLIN事件等就绪,那么OS就把这个fd的事件放入就绪队列。这个就绪队列就是储存就绪事件的数据结构,当用户调用epoll_wait时,就通过就绪队列进行检测哪个fd对应事件就绪了!将事件依次严格按照顺序放入struct epoll_event *events数组中!

这个检测就绪事件的算法的时间复杂度就是O(1)!只需要判断就绪队列是否为空就可以!而将就绪事件获取的时间复杂度是O(n)!

这就是epoll模型!!!
在这里插入图片描述

而这个epoll模型是可以打开多个的,就和打开多个文件一样。当我们打开多个epoll模型时,那么操作系统如何管理这些epoll模型呢?

在内核中有一个eventpoll,这个是描述epoll模型的结构体,其中就有rbr红黑树与rdllist就绪队列。那为什么创建epoll模型之后会返回一个文件描述符呢?

在内核中有无数个task_struct进程结构体,每个进程都有一张文件描述符表struct files_struct,这个表的元素就指向文件结构体struct file文件结构体中就有一个指针指向epoll模型。那么在进程中想要找到epoll模型就可以通过文件描述符表找到epoll模型!

我们来谈一个十分巧妙的设计。在epoll模型中,存在红黑树和就绪队列。每个节点都有对应的文件描述符。在之前所学的数据结构中,我们每个数据结构的节点都是独属于自身的,比如二叉树的节点不可能是链表的节点。
但是在epoll模型中,一个节点是可以属于多个数据结构的!我们来看是如何实现的:

  1. 首先,有这样一个链表节点listnode,其中只包含左右指针。
  2. 然后在task_struct中,就可以存在listnode link,那么每一个task_struct就可以通过这个link进行连接起来的。
  3. 但是,这个指向的只是下一个task_struct结构体中的link,那么怎么才能访问task_struct全部的数据呢?
  4. 可以先计算这个link在task_struct的偏移量,通过将0地址强制类型转换,得到里面link的地址,就知道了偏移量!然后通过task_struct中link里的指针减去偏移量,我们就得到了task_struct的起始地址,再进行类型转换我们就得到了task_struct!
  5. 同样的,task_struct还可以存在二叉树节点link2 , 队列节点link3,就都可以通过这种方式进行链接,并且是一个节点属于了多个数据结构中!!!

这是十分巧妙的设计!!!而epoll模型中的epitem结构体就是这样设计的!一个节点既属于红黑树,也属于就绪队列!

在这里插入图片描述
其中epitem还有一个status变量,表示其是否被激活。可以判断是否在红黑树或者就绪队列中!
下面我们开始编写v1版本的epollserver

4 实现epollserverV1

下面我们来实现epollserver:

成员变量需要以下:

  1. 端口号_port :用于创建listen套接字
  2. 套接字socket :_listensock监听套接字,使用TCP进行通信。
  3. 文件描述符_epfd :epoll模型的文件操作符,是使用epoll系列接口的必要参数。
  4. epoll_event revs[] 数组:从epoll模型中获取就绪事件的结构体数组。

根据成员变量,进行构造,创建套接字,创建epoll模型。
初始化函数中,建立struct epoll_event ev设置其中的 fd 与events位图;先将_listensock套接字fd添加到epoll中 通过epoll_ctl进行ADD操作。

#pragma once#include <string>
#include <iostream>
#include <memory>
#include <sys/epoll.h>#include "Log.hpp"
#include "Socket.hpp"using namespace log_ns;
using namespace socket_ns;class EpollServer
{
private:const static int gnum = 1024;const static int size = 128;public:EpollServer(uint16_t port) : _port(port),_listensock(std::make_unique<TcpSocket>()){// 建立监听套接字_listensock->BuildListenSocket(port);// 建立epoll模型_epollfd = ::epoll_create(size);if (_epollfd < 0){// 创建失败LOG(FATAL, "epoll_create failed!\n");exit(1);}}void InitServer(){// 将监听套接字放入epoll模型struct epoll_event ev;ev.data.fd = _listensock->GetSockfd();ev.events = EPOLLIN;// 放入int n = ::epoll_ctl(_epollfd, EPOLL_CTL_ADD, _listensock->GetSockfd(), &ev);// 根据返回值判断if (n < 0){// 发生错误LOG(FATAL, "epoll_ctl failed ,errno :%d", errno);exit(1);}}void Accepter(){}void HandlerIO(int fd){// 普通fdIO 就绪}void HandlerEvent(int n){}void Loop(){}~EpollServer(){// 关闭epoll模型if (_epollfd > 0)close(_epollfd);// 关闭监听套接字_listensock->Close();}private:// 端口号uint16_t _port;// 套接字std::unique_ptr<Socket> _listensock;// epoll模型描述符int _epollfd;// 文件描述符struct epoll_event revs[gnum];
};

Loop 循环函数,设置timeout 调用epoll_wait接口进行等待事件就绪 ,将就绪的事件放入到revs数组中。根据返回值进行判断结果:

void Loop(){int timeout = 2000;while (true){// 进行等待int n = ::epoll_wait(_epollfd, revs, gnum, timeout);// 判断结果switch (n){case 0:LOG(INFO, "epoll timeout...\n");break;case -1:LOG(ERROR, "epoll error\n");break;default:LOG(INFO, "haved event happened! , n :%d\n", n);// 处理事件HandlerEvent(n);break;}}}

HandlerEvent处理事件,将数组中的n个事件全部处理遍历一遍, 根据就绪的文件描述符种类进行区分判断 (设计一个简单的接口可以通过事件级返回事件种类);读事件就绪 我们进行处理

  • _listensock套接字事件获取连接 Accepter 将新的fd加入到epoll模型 打印客户端信息
  • 普通fd 事件HandlerIO 进行读取recv ;读取失败的话要从epoll删除后再close ,处理后Send回去。
    std::string PrintEvent(uint32_t revents){std::string ret;if (revents & EPOLLIN)ret += "EPOLLIN";if (revents & EPOLLOUT)ret += "| EPOLLOUT";return ret;}void Accepter(){// 获取_listensock的新fdInetAddr addr;int sockfd = _listensock->Accepter(&addr);if (sockfd < 0){LOG(ERROR, "Accepter error\n");exit(1);}// 成功获取连接LOG(INFO, "成功获取连接 ,客户端: %s\n", addr.AddrStr().c_str());// 将连接添加到epoll模型中struct epoll_event ev;ev.data.fd = sockfd;ev.events = EPOLLIN;int n = ::epoll_ctl(_epollfd, EPOLL_CTL_ADD, sockfd, &ev);// 根据返回值判断if (n < 0){// 发生错误LOG(FATAL, "epoll_ctl failed ,errno :%d", errno);exit(1);}}void HandlerIO(int fd){// 普通fdIO 就绪char buffer[4096];int n = ::recv(fd, buffer, sizeof(buffer), 0);if (n > 0){// 读取到了数据buffer[n] = 0;std::string echo_str = "[client say]#";echo_str += buffer;std::cout << echo_str << std::endl;// 返回一个报文std::string content = "<html><body><h1>hello bite</h1></body></html>";std::string ret_str = "HTTP/1.0 200 OK\r\n";ret_str += "Content-Type: text/html\r\n";ret_str += "Content-Length: " + std::to_string(content.size()) + "\r\n\r\n";ret_str += content;// echo_str += buffer;::send(fd, ret_str.c_str(), ret_str.size(), 0); // 临时方案}else if (n == 0){// 此时fd退出了LOG(INFO, "fd:%d quit!\n", fd);//先对epoll中的节点进行删除,因为epoll中的节点必须是合法fd ,不能进行close::epoll_ctl(_epollfd , EPOLL_CTL_DEL , fd , nullptr);::close(fd);}else{LOG(ERROR, "recv error! errno:%d\n", errno);::epoll_ctl(_epollfd , EPOLL_CTL_DEL , fd , nullptr);::close(fd);}}void HandlerEvent(int n){// 处理事件for (int i = 0; i < n; i++){int fd = revs[i].data.fd;uint32_t revents = revs[i].events;LOG(INFO, "fd:%d , %s事件就绪\n", fd, PrintEvent(revents).c_str());// 判断fd类型if (fd == _listensock->GetSockfd()){// 进行AccepterAccepter();}// 普通fdelse{HandlerIO(fd);}}}

这样我们就成功的完成了epollserver的基础服务,来看效果:
在这里插入图片描述
非常好!!!

相关文章:

【Linux】从零开始使用多路转接IO --- epoll

当你偶尔发现语言变得无力时&#xff0c; 不妨安静下来&#xff0c; 让沉默替你发声。 --- 里则林 --- 从零开始认识多路转接 1 epoll的作用和定位2 epoll 的接口3 epoll工作原理4 实现epollserverV1 1 epoll的作用和定位 之前提过的多路转接方案select和poll 都有致命缺点…...

爬虫学习4

from threading import Thread#创建任务 def func(name):for i in range(100):print(name,i)if __name__ __main__:#创建线程t1 Thread(targetfunc,args("1"))t2 Thread(targetfunc, args("2"))t1.start()t2.start()print("我是诛仙剑")from …...

CTF之web题集详情随手笔记

《Web安全》http://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247484238&idx1&snca66551c31e37b8d726f151265fc9211&chksmc0e47a12f793f3049fefde6e9ebe9ec4e2c7626b8594511bd314783719c216bd9929962a71e6&scene21#wechat_redirect 1 WEB 1 靶场目…...

TDengine 集群能力:超越 InfluxDB 的水平扩展与开源优势

随着物联网、车联网等领域的快速发展&#xff0c;企业所面临的数据采集量呈爆炸式增长&#xff0c;这对 IT 基础设施和数据库提出了严峻挑战。传统单机版数据库逐渐无法应对高并发的数据写入和复杂的查询需求。因此&#xff0c;底层数据库必须具备水平扩展能力&#xff0c;以确…...

MATCH_DIRECT_BOOT_AWARE和MATCH_DIRECT_BOOT_UNAWARE

PackageManager.MATCH_DIRECT_BOOT_AWARE和PackageManager.MATCH_DIRECT_BOOT_UNAWARE 在Android系统中&#xff0c;PackageManager类提供了一些标志位&#xff0c;用于控制查询系统中的应用和组件时的行为。其中&#xff0c;MATCH_DIRECT_BOOT_AWARE和MATCH_DIRECT_BOOT_UNAWA…...

LabVIEW离心泵性能优化测试系统

开发了一套基于LabVIEW平台开发的离心泵性能优化测试系统。系统集成了数据采集、流量控制、数据存储、报表生成等功能&#xff0c;提供了低成本、便捷操作的解决方案&#xff0c;适用于工业场景中对离心泵性能的精确测评。 项目背景 随着工业化进程的加速&#xff0c;离心泵在…...

token和jwt区别

Token 和 JSON Web Token (JWT) 都是用于身份验证和授权的技术,但它们之间有一些重要的区别。下面是它们的主要区别和各自的特性: 1. 概念上的区别 Token: 广义概念:Token 是一个通用术语,指的是任何形式的令牌,用于在客户端和服务器之间传递身份验证和授权信息。实现方…...

新闻稿件管理:SpringBoot框架实战指南

3系统分析 3.1可行性分析 通过对本新闻稿件管理系统实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本新闻稿件管理系统采用SSM框架&#xff0c;JAVA作为开发语…...

AI运动小程序开发常见问题集锦二

截止到现在写博文时&#xff0c;我们的AI运动识别小程序插件已经迭代了23个版本&#xff0c;成功应用于健身、体育、体测、AR互动等场景&#xff1b;为了让正在集成或者计划进行功能扩展优化的用户&#xff0c;少走弯路、投入更少的开发资源&#xff0c;针对近期的咨询问题&…...

nginx安装

下载地址 https://nginx.org/en/download.html选择 把下载好的压缩包放在 解压 tar -zxf nginx-1.27.2.tar.gz下载 yum install -y gcc-c pcre pcre-devel zlib zlib-devel openssl openssl-devel ./configuremake&&make install这样表示安装成功 接下去启动nginx…...

【Linux驱动开发】内核定时器的配置和使用

【Linux驱动开发】内核定时器的配置和使用 文章目录 Linux内核时钟定时器调用方式延时函数 应用附录&#xff1a;嵌入式Linux驱动开发基本步骤开发环境驱动文件编译驱动安装驱动自动创建设备节点文件 驱动开发驱动设备号地址映射&#xff0c;虚拟内存和硬件内存地址字符驱动旧…...

Kubernetes架构及核心组件

一、基本架构 Kubernetes集群可以被看作是一个工厂,而各个组件则是这个工厂里的不同部门: Kubernetes API服务器:就像是这个工厂的总经理,负责接收所有的请求并将它们分配给相应的部门进行处理。 etcd:就像是这个工厂的记事本,负责记录所有的配置信息和状态信息,以便其…...

Fastflow工作流系统源码

可视化工作流程审批插件&#xff0c;作为一款高效的企业管理工具&#xff0c;其核心价值在于帮助用户根据企业独特的业务模式和管理模式&#xff0c;灵活自定义所需的各种流程应用。这一功能极大地提升了企业的自主性和灵活性&#xff0c;使得企业能够迅速构建出贴合自身运营需…...

小林渗透入门:burpsuite+proxifier抓取小程序流量

目录 前提&#xff1a; 代理&#xff1a; proxifier&#xff1a; 步骤&#xff1a; bp证书安装 bp设置代理端口&#xff1a; proxifier设置规则&#xff1a; proxifier应用规则&#xff1a; 结果&#xff1a; 前提&#xff1a; 在介绍这两个工具具体实现方法之前&#xff0…...

AiPPT - 全智能 AI 一键生成 PPT

一、产品介绍 AiPPT是一款基于人工智能技术的智能演示文稿制作工具。它结合了先进的AI算法与用户友好的界面设计&#xff0c;旨在帮助用户快速、高效地创建出专业且富有吸引力的PPT演示文稿。AiPPT不仅能够自动排版、优化内容布局&#xff0c;还能根据用户输入的关键词或主题&…...

React 前端使用 Input 输入框的样式上传一个 Excel 文件并读取内容对象数组

本文讲解了关于如何在 React 前端使用 Input 输入框上传一个 Excel 文件&#xff0c;并读取文件内容转成 json 数据格式&#xff08;对象数组&#xff09;。 文章目录 1、Excel 文件展示2、完整代码3、数据结果展示4、前端样式展示5、使用 button 按钮的前端样式 1、Excel 文件…...

【测试工具】Fastbot 客户端稳定性测试

背景 做这个主要为了发版之前提前发现崩溃&#xff0c;风险前置。适合客户端很重的业务。 优点&#xff1a;你不改动也能用&#xff0c; 维护成本不高。 缺点&#xff1a;容易进入H5页面无法返回&#xff0c;效果有限。 备注&#xff1a;我这边接手别人维护&#xff0c;公司…...

软件测试学习笔记丨Vue常用指令-输入绑定(v-model)

本文转自测试人社区&#xff0c;原文链接&#xff1a;https://ceshiren.com/t/topic/23461 指令 指令是将一些特殊行为应用到页面DOM元素的特殊属性 格式都是以v-开始的&#xff0c;例如&#xff1a; v-model&#xff1a;双向绑定v-if和v-else&#xff1a;元素是否存在v-sho…...

C#、C和C++的主要区别

C&#xff03;和C的区别在于&#xff1a;C&#xff03;代码首先会被编译为CLR&#xff08;公共语言运行库&#xff09;&#xff0c;然后由.NET框架解析&#xff1b;它是在虚拟机上执行&#xff0c;会自动进行内存管理&#xff0c;不支持使用指针。C将会直接被编译为机器代码&am…...

我们来学mysql -- 连接(原理版)

我们来学mysql -- 连接 题记两张表驱动表 题记 回到初学者的视角&#xff0c;navicat或命令窗口&#xff0c;呈现一行行数据&#xff0c;类比为excel工作薄更是深入人心通过join将多表的记录关联起来&#xff0c;这似乎也没啥问题只是好像是那么回事&#xff0c;又…似乎有想说…...

PyQt5的安装与简介

目录 一、介绍 二、PyQt5的安装 1、安装PyQt5 2、安装Qt的工具包 三、配置Qt工具 1、配置Designer &#xff08;1)、打开pycharm&#xff0c;找到设置选项 &#xff08;2&#xff09;、找到工具-->外部工具 &#xff08;3&#xff09;、点击号&#xff0c;创建外部工…...

100种算法【Python版】第43篇——优化算法之模拟退火算法

本文目录 1 算法说明2 算法示例:Rosenbrock函数极值3 算法应用1:复杂函数极值4 算法应用2:TSP问题1 算法说明 模拟退火(Simulated Annealing, SA)算法最早由斯图尔特西尔伯特和约瑟夫斯图尔特于1983年提出,灵感来源于金属退火过程。金属在加热后会变得更加柔软,冷却时逐…...

初识动态规划(由浅入深)

&#x1f913; 动态规划入门与进阶指南 &#x1f4d8; 动态规划&#xff08;Dynamic Programming, DP&#xff09;是一种非常经典的&#x1f4d0;算法方法&#xff0c;特别适合用来解决那些有大量重复计算的问题&#x1f300;。它可以将复杂的问题拆分为小问题&#x1f9e9;&a…...

关于大模型微调与训练的问题,大模型训练的难点在哪里?

前言 “ 大模型训练的难点不在于大模型本身&#xff0c;而在于训练数据 ” 这两天有一个小兄弟问我关于大模型训练的问题&#xff0c;然后他想自己训练一个小模型&#xff0c;但又不知道该怎么操作&#xff1b;所以&#xff0c;今天就再来讨论一下大模型的训练问题&#xff0…...

如何对数据库的表字段加密解密处理?

对于表格数据的加密处理&#xff0c;通常涉及到对数据库中存储的数据进行加密&#xff0c;以保护敏感信息。 Java示例&#xff08;使用AES算法加密数据库表数据&#xff09; 首先&#xff0c;你需要一个数据库连接&#xff0c;这里假设你使用的是JDBC连接MySQL数据库。以下是…...

六、Go语言快速入门之数组和切片

文章目录 数组和切片数组:one: 数组初始化:two: 数组的遍历:three: 多维数组:four: 将数组传递给函数 切片(Slice):one: 切片的初始化:star: new和make区别 :two: 切片的使用:three: 将切片传递给函数:four: 多维切片:four: Bytes包:four: 切片和垃圾回收 &#x1f4c5; 2024年…...

Java:数组的定义和使用(万字解析)

目录 1. 数组的概念 2. 数组的基础知识 2.1 数组的创建 \1. 基础创建格式&#xff1a; \2. 类似C语言的创建格式&#xff1a; 【错误的创建(初始化)格式】 2.2 数组的数据类型 2.3 数组的初始化 —— 两种方式 \1.动态初始化&#xff1a;(完全默认初始化) \2. 静态初…...

密码学简要介绍

密码学是研究编制密码和破译密码的技术科学&#xff0c;它研究密码变化的客观规律&#xff0c;主要包括编码学和破译学两大部分。 一、定义与起源 定义&#xff1a;密码学是研究如何隐密地传递信息的学科&#xff0c;在现代特别指对信息以及其传输的数学性研究&#xff0c;常被…...

2024.11月最新智能问答AI创作系统源码,GPT4.0多模态模型+AI换脸+AI智能体GPTs应用+AI绘画(Midjourney)+详细搭建部署教程

一、前言 SparkAi创作系统是一款基于ChatGPT和Midjourney开发的智能问答和绘画系统&#xff0c;提供一站式 AI B/C 端解决方案&#xff0c;AI大模型提问、AI绘画、专业版AI视频生成、文档分析、多模态识图理解、TTS & 语音识别对话、AI换脸、支持AI智能体应用&#xff08;…...

江协科技STM32学习- P34 I2C通信外设

&#x1f680;write in front&#x1f680; &#x1f50e;大家好&#xff0c;我是黄桃罐头&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流 &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd;​…...