【计算机网络】多路复用的三种方案

news2025/1/24 14:43:12

文章目录

  • 1. select
    • select函数
    • select的工作特性
    • select的缺点
  • 2. poll
    • poll函数
    • poll与select的对比
  • 3. epoll
    • epoll的三个接口
    • epoll的工作原理
    • epoll的优点
    • LT和ET模式
    • epoll的应用场景


🔎Linux提供三种不同的多路转接(又称多路复用)的方案,分别是:select,poll和epoll。它们表现为不同的系统调用接口。

前置知识:“IO事件就绪”

即读事件就绪或写事件就绪

  • 读事件就绪,指接收缓冲区中有数据可以读取
  • 写事件就绪,指发送缓冲区中有空间可以写入

1. select

select是Linux中用于同时监视多个文件描述符是否就绪的系统调用接口,当程序运行到select调用处,默认会阻塞等待(也可以设为非阻塞)监视的文件描述符至少有一个IO事件就绪为止。

select函数

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数介绍:

  • nfds:需要监视的文件描述符最大值+1;
  • readfds/writefds/exceptfds:分别是监视的读文件描述符集合,写文件描述符集合,异常文件描述符集合。这三个fd_set*类型的参数是输入输出型函数,如:你需要关心某个fd的读事件,就将其设置到readfds中并调用select,select返回时,若该fd的读事件就绪,则readfds存在该fds,否则不存在。writefdsexceptfds同理。总而言之,这三个参数既让用户告知内核"我"要关心哪个文件fd,又让内核向用户通知哪些文件fd的哪些事件已就绪。
  • timeout:select等待超时时间,设为nullptr则为阻塞等待。

返回值:

  • 成功,则返回三个fd_set中事件就绪的fd的个数,若为0,表示超时timeout了也没有fd的事件就绪

  • 失败,返回-1,错误码被设置,此时参数readfds, writefds,exceptfds和timeout的值失效

fd_set结构:

fd_set底层是一个long int的数组,上层视作一个位图结构,每个比特位,下标代表文件fd,内容表示fd是否有效。若调用select时,参数readfds的位图结构为...00001000 (前面全0省略),则表示用户需要监视3号文件描述符的读事件,select返回时,若readfds依然为...00001000,表示3号文件描述符读事件就绪,否则未就绪。

在这里插入图片描述

提供了一组操作fd_set的接口,来比较方便的操作位图

void FD_CLR(int fd, fd_set *set);  // 将fd从set中去除
int  FD_ISSET(int fd, fd_set *set);// 判断fd是否存在于set中
void FD_SET(int fd, fd_set *set);  // 将fd设置入set中
void FD_ZERO(fd_set *set);		   // 清空set

select的工作特性

  1. select在内核中会去遍历三个fd_set,对于每个fd,若用户关心,则检测该fd的对应事件是否就绪,就绪则比特位置1,反之置0,若用户不关心,则跳过。参数nfds是底层遍历的终点。这种遍历在阻塞情况下会持续进行,直到检测到有一个或多个fd的IO事件就绪为止。

  2. select能够监视的文件fd是有限的,这受限于fd_set的大小,fd个数 = sizeof(fd_set) * 8。不同环境下的fd_set大小不同,在我的本地测试sizeof(fd_set) = 128,那么能够监视的fd个数即为1024。

  3. 将关心的fd加入select的监控集合后,还需要在上层维护一个数组array来保存这些关心的fd。原因如下:

    • 一,select的三个fd_set都是输入输出型参数,那么select前设置的fd,在select后可能就不存在了(因为该fd的事件未就绪),因此每次select之前都需要重置fd_set,重置就需要有一个数组array始终保存着用户关心的fd。

    • 二,select返回后,用户需要手动遍历fd_set,判断哪些fd已就绪(FD_ISSET),而上层保存fd的数组array就是判断的根据。

select的缺点

  1. 每次select之前都要手动重置fd_set,太麻烦了
  2. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  3. 每次调用select,都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  4. select支持的文件描述符数量太小

2. poll

poll的作用和工作特性与select基本相同,但使用方法不同。

poll函数

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

poll以结构体struct pollfd的形式规定了监视的fd,其中包含更多信息。

在这里插入图片描述

fd:监视的fd

events:这是用户设置的,用于用户告知内核本fd要监视什么事件,是输入型参数

revents:这是poll函数返回的,用于内核通知用户本fd的哪些事件已就绪,是输出型参数

events/revents的事件类型:

POLLIN:读事件

POLLOUT:写事件

POLLERR:错误异常

POLLHUP:挂起异常,可能是对端关闭了连接

以上事件类型都是<poll.h>中定义的宏,events/revents可以同时承载多个事件,只需多个事件类型进行与操作即可。

poll的参数介绍

  • fdsstruct pollfd类型的指针,指向一段连续存放多个pollfd的空间。fds由用户自己维护。
  • nfds:fds指向空间的大小
  • timeout:超时时间,单位是毫秒ms

💭一些小细节:

  1. 在pollfd中,若fd为负数,则events无效,调用poll后,revents被设为0
  2. 在pollfd中,若events为0,表示不关心该fd上的任何时间,那么调用poll后,revents也将被设为0。
  3. poll只会监视fd用户关心的事件,并通过revents返回

poll的返回值:

  • 成功,返回有事件就绪的fd的个数,即revents不为0的pollfd的个数。若为0,表示超时timeout了也没有fd的事件就绪。
  • 失败,返回-1,错误码被设置。

poll与select的对比

  1. poll相比于select的优势有两点:
    • 输入与输出分离,不用在每次调用poll前手动设置关心的fd
    • 能够关心的fd个数不受限制,fds指向的空间用户可以扩容,参数nfds也可以修改
  2. 与select一样存在频繁遍历的劣势。监视的fd一多,每次调用poll,要将大量的pollfd结构从用户态拷贝到内核中,内核要遍历传递进来的所有pollfd结构,检查有哪些事件就绪。每次poll返回,都需要遍历上层维护的pollfd结构,根据revents判断哪些fd的什么事件就绪了。
  3. 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,遍历速度降低,其效率也会线性下降。

3. epoll

epoll是目前公认的效率最高,性能最佳的多路复用组件。

epoll的三个接口

  1. epoll_create

    功能:创建一个epoll模型的句柄,返回该句柄的文件描述符

    函数原型:

    #include <sys/epoll.h>
    
    int epoll_create(int size);
    

    注意事项:

    • 自从linux2.6.8之后,size参数是被忽略的,随便给一个就行
    • 使用结束,必须调用close()关闭epoll句柄
  2. epoll_ctl

    功能:对epoll模型进行增、删、改的操作(本质是对红黑树的操作)

    函数原型:

    #include <sys/epoll.h>
    
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    

    参数:

    epfd:epoll句柄的文件描述符,由epoll_create获得;

    fd:目标文件描述符;

    op:操作类型,有如下三种:

    • EPOLL_CTL_ADD:将新的fd注册到epoll模型中,表示关心fd的事件发生
    • EPOLL_CTL_MOD:修改fd的关心事件
    • EPOLL_CTL_DEL:将fd从epoll模型中移除,表示不再关心fd的事件发生

    event:struct epoll_event结构体类型如下

               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:表示fd关心的事件。events可以是以下几个宏的集合:

    宏名称意义用户设置内核返回
    EPOLLIN表示对应的文件描述符可以读 (包括对端SOCKET正常关闭)
    EPOLLOUT表示对应的文件描述符可以写
    EPOLLERR表示对应的文件描述符发生错误
    EPOLLHUP表示对应的文件描述符被挂断
    EPOLLET将EPOLL设为边缘触发(Edge Triggered)模式

    data:epoll_data类型的联合体,包含文件描述符的信息,可选择四种不同类型表示,一般选用int fd

    返回值:成功返回0,错误返回-1,错误码被设置。

  3. epoll_wait

    功能:等待epoll模型上的就绪事件

    函数原型:

    #include <sys/epoll.h>
    
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    

    参数:

    epfd:epoll句柄的文件描述符,由epoll_create获得;

    events:指向一组连续存放的epoll_event结构体,epoll_wait会从内核拷贝已就绪的事件到events指向的空间中;

    maxevents:events最多存epoll_event数量,maxevents的值不能大于创建epoll_create()时的size;

    timeout:超时时间,单位是毫秒ms。-1表示阻塞等待。

    epol_wait的返回值:

    • 成功,返回有事件就绪的fd的个数,即events的单元个数。若为0,表示超时timeout了也没有fd的事件就绪。
    • 失败,返回-1,错误码被设置。

epoll的工作原理

epoll的内核实现,采用所谓的"epoll模型",是由一个红黑树(rbtree)和一个就绪队列(rdlist)组成,用于高效的监控文件描述符的状态变化。在内核中搭建epoll模型(创建rbtree和rdlist),并创建一个eventpoll句柄,用户持有eventpoll句柄的文件描述符,eventpoll句柄存有rbtree的根节点指针和rdlist的头结点指针,能找到内核中的epoll模型。eventpoll句柄成为用户与内核epoll沟通的桥梁。

在这里插入图片描述

🔎epoll的工作机制,由以下三部分构成:

  1. 红黑树rbtree

    根据先描述再组织的管理思想,epoll对于每一个关心的文件描述符,先将其描述为epitem结构体,再挂载到红黑树rbtree当中,并在rbtree中完成各种操作。如:epoll_ctl的EPOLL_CTL_ADD操作,就是先将新的文件描述符描述为一个epitem结构体,再插入到内核epoll的红黑树中。同理,EPOLL_CTL_MOD和EPOLL_CTL_DEL就是对红黑树的改和删的工作。

    struct epitem{
    	struct rb_node rbn;		  //红黑树节点
    	struct list_head rdllink; //双向链表节点
    	struct epoll_filefd ffd;  //文件描述符的句柄信息
    	struct eventpoll *ep; 	  //指向其所属的eventpoll对象
    	struct epoll_event event; //期待发生的事件类型
    }
    

    struct epoll_event是epitem的key值,也是用户告知内核关心事件,内核通知用户就绪事件的结构体。

  2. 就绪队列rdlist

    rdlist就绪队列,其实是一个双向链表结构,用于存放rbtree中已就绪的事件节点。epoll会将rbtree中事件就绪的epitem节点,推送到rdlist中,用户调用epoll_wait实际上就是将rdlist中的就绪事件拷贝到参数events中,时间复杂度为O(1)。值得注意的是,rdlist中的节点并不是rbtree中的副本,“节点从rbtree推送到rdlist”这个动作实际上只是修改节点的连接关系(epitem中的struct list_head rdllink),而不是拷贝一份到rdlist中。

    在这里插入图片描述

    关于rdlist的增与删

    rdlist的增由回调机制决定,当某个fd的IO事件就绪,执行回调函数将fd对应的epitem节点连接到rdlist中。对于LT模式,只要fd的IO事件依然就绪,就继续保留fd对应的epitem节点在rdlist中;对于ET模式,epoll_wait读取一次,就将节点从rdlist中移除。

  3. 回调机制ep_poll_callback

    “节点从rbtree推送到rdlist”是怎么做到的?首先,在调用epoll_ctl注册新的文件描述符时,会为这个文件描述符对应的epitem在底层的注册一个回调函数,这个回调的功能是将节点连接到rdlist中(回调函数注册到fd的文件句柄中)。此后,当底层IO事件就绪(协议栈决定), 检测到对应文件句柄的回调函数存在,就会调用这个回调函数,将epitem节点连接到rdlist中。

epoll的优点

  1. 内核采用红黑树结构管理关心的文件描述符,上层不需要再像select/poll那样自行维护关心的文件描述符,也不需要再手动设置每次要关心的文件描述符。
  2. 红黑树结构的增删查改效率很高(时间复杂度是O(lgN)),不会因为关心fd的增多,导致新文件描述符的添加、关心事件的修改、文件描述符的移除等操作的效率降低。
  3. 不需要再像select/poll那样轮询检测哪些fd的事件就绪,而是采用回调机制将活跃节点连接到rdlist中,用户仅需通过就绪队列rdlist获取就绪事件,这样一来避免了频繁的遍历,即使文件描述符很多,也没有影响。

总而言之,epoll只关注活跃的fd,不会像select/poll一样总是全局扫描所有的fd,这大大提高了它的效率。

LT和ET模式

epoll有两种工作模式,水平触发(LT, Level Triggered)和边缘触发(ET, Edge Triggered)

  • LT模式:只要fd的IO事件一直就绪,就一直通知用户。在epoll底层表现为,一个epitem节点通过回调被连接到rdlist就绪队列中,只要该epitem对应的fd上IO事件还就绪着(比如对于读事件,socket接收缓冲区还有数据),就不会将其从rdlist中移除,因此用户每次调用epoll_wait都能获知该fd上的IO事件就绪。这是epoll的默认工作模式。

  • ET模式只在fd的事件状态变化时通知用户一次。在epoll底层表现为,一个epitem节点通过回调被连接到rdlist就绪队列中,说明该epitem对应的fd上IO事件就绪,这里以读事件为例,用户调用epoll_wait后,获知该fd上的读事件就绪,用户可能把缓冲区中所有数据读完,也可能只读了一部分,epoll不管缓冲区中还有没有数据,即无论fd上的读事件是否依然就绪,fd对应的epitem直接从rdlist中移除,用户下次调用epoll_wait就读不到了。只有在下次新数据包到来时,ET才会再通知上层一次,这就是在“事件状态变化”时通知用户,即边缘触发。

🔎LT与ET的区别

从效率层面

  • 从效率角度来说,ET的效率高于LT。反复通知代表上层要多次调用epoll_wait来获取就绪事件信息,而一次通知只需要一次系统调用,系统调用从用户态到内核态开销较大,ET模式有效地减少了系统调用的次数。

ET与LT的区别更显著地体现在对上层的影响

  • LT会反复通知事件就绪,这样一来,用户可能不会立刻处理事件,而是在需要的时候再处理。

  • ET只会在fd事件状态变化时通知用户一次,这样一来,就倒逼用户必须立刻处理完就绪事件,否则可能会错过事件。例如,fd读事件就绪,接收缓冲区上有2KB的数据,如果是ET模式通知用户,用户收到后就必须尽快将fd接收缓冲区上的所有数据读完,如果这次通知只读了1KB数据,且往后该fd没有新数据到来了,那么剩下的1KB数据就会丢失,因为ET模式不会再通知一次!

  • ET倒逼用户尽快取走数据,本质也是提高效率:使得底层的TCP接收窗口更大,从而在较大概率上使得对端的滑动窗口更大,提高通信效率。

如何设置ET模式?

设置fd的event为EPOLLET即可,这会让epoll对于该fd以ET模式工作。

如何保证一次处理完就绪事件?

以读事件为例,一次处理完读事件,就是一次性将接收缓冲区上的数据全部读完。调用read/recv接口循环读取fd上的数据,默认情况下,如果数据读完了,read/recv会阻塞等待,这样虽然能读完数据,但是上层无法获知。因此,必须使用非阻塞的方式读取数据!以非阻塞方式循环读取数据,当数据读完时,非阻塞read/recv不会挂起等待,而是以错误的形式返回,错误码为 EAGAIN or EWOULDBLOCK,这样一来,用户就可以通过对错误码的判断,获知数据是否读完了。对于写事件也是一样的,以非阻塞方式write/send,若发送缓冲区被写满了,表示写事件未就绪,错误码也会被设为EAGAIN or EWOULDBLOCK。

综上所述:使用 ET 模式的 epoll,需要将文件描述设置为非阻塞。 这个不是接口上的要求,而是"工程实践"上的要求,因为ET模式的机制总是要求程序员一次就绪响应就将事件处理完毕。

💭其它的理解细节:

事实上,LT也可以通过非阻塞的方式,通知一次就将所有数据取完,但由于LT是反复通知上层,就算不将数据一次读完,上层也不会错过就绪事件,只有ET的机制才倒逼用户必须立刻处理完就绪事件

epoll的应用场景

🔎并不是说使用epoll就一定是最高效的多路复用,还是要具体问题具体分析。epoll主要用于处理大规模、多并发、多连接的场景,特别是在高性能的网络服务器应用中。epoll在 Linux 上提供了一种高效的 I/O 多路复用机制,相较于selectpoll具有更好的性能和扩展性。而对于一些较小规模、连接较少的服务器,epoll带来的内存开销可能会比较大。因此,要根据具体问题和环境,选用具体的多路复用IO模型。


END…

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

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

相关文章

禁止安装新软件怎么设置(超详细图文介绍)

很多公司的网管向我们反应&#xff0c;总是有员工随意下载软件&#xff0c;并且不去正规网站、正规官网下载&#xff0c;导致公司的电脑总是又卡又慢&#xff0c;网管的工作很难开展。 此时就需要对公司安装软件的情况&#xff0c;进行统一管控了。 方法一&#xff1a;适合个人…

CodeWhisperer 一款好玩的 AI 插件

忙里抽闲&#xff0c;今天试了试 CodeWhisperer 这款插件&#xff0c;我是在 IDEA 中做的测试&#xff0c;下面是我的一些使用感想&#xff1a; 安装 CodeWhisperer 插件&#xff1a;在 IntelliJ IDEA 中&#xff0c;可以通过插件管理器安装 CodeWhisperer 插件&#xff0c;然…

UE 进阶篇一:动画系统

导语: 下面的动画部分功能比较全,可以参考这种实现方式,根据自己项目的颗粒度选择部分功能参考,我们商业项目动画部分也是这么实现的。 最后实现的效果如下: 最终效果 目录: ------------------------------------------- 文末有视频教程/工程地址链接 -------------…

log4j2远程代码执行漏洞原理与漏洞复现(基于vulhub,保姆级的详细教程)

漏洞原理 啥是log4j2? log4j2是apache下的java应用常见的开源日志库&#xff0c;是一个就Java的日志记录工具。在log4j框架的基础上进行了改进&#xff0c;并引入了丰富的特性&#xff0c;可以控制日志信息输送的目的地为控制台、文件、GUI组建等&#xff0c;被应用于业务系…

Pycharm run 输出界面控制一行能够输出的元素个数

Pycharm run 输出界面控制一行能够输出的元素个数 今天遇到了一个问题&#xff0c;当我们在 Pycharm 中打印输出数组时&#xff0c;如果数组一行的元素个数过多&#xff0c;那么我们在打印时就会出现以下问题。 代码如下&#xff1a; import numpy as npx np.array([[0., 0.7…

jQuery【菜单功能、淡入淡出轮播图(上)、淡入淡出轮播图(下)、折叠面板】(五)-全面详解(学习总结---从入门到深化)

目录 菜单功能 淡入淡出轮播图(上) 淡入淡出轮播图(下) 折叠面板 菜单功能 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><…

某高品质房产企业:借助NineData平台,统一数据库访问权限,保障业务安全

该企业是中国领先的优质房产品开发及生活综合服务供应商。在 2022 年取得了亮眼的业绩表现&#xff0c;销售额市场占有率跻身全国前五。业务涵盖房产开发、房产代建、城市更新、科技装修等多个领域。 2023 年&#xff0c;该企业和玖章算术&#xff08;浙江&#xff09;科技有限…

01-论文阅读-Deep learning for anomaly detection in log data: a survey

01-论文阅读-Deep learning for anomaly detection in log data: a survey 文章目录 01-论文阅读-Deep learning for anomaly detection in log data: a survey摘要I 介绍II 背景A 初步定义B 挑战 III 调查方法A 搜索策略B 审查的功能 IV 调查结果A 文献计量学B 深度学习技术C …

electron入门(一)环境搭建,实现样例

1、首先需要安装git和node&#xff0c;配置环境变量&#xff0c;确保npm和git命令可用 2、 然后安装依赖 npm install -g electronnpm install -g electron-forgenpm install -g electron-prebuilt-compile3、 创建样例工程 electron-forge init my-new-app # 我这里碰见报错…

Spring框架学习 -- 创建与使用

目录 (1) 创建spring 项目 ① 创建maven项目 ②添加spring框架支持 ③ 添加启动项 (2) 创建 Bean对象 (3) 将Bean注入到容器 (4) 获取Bean对象 (5) 注意事项 (6) Spring的创建和使用流程图 创作不易多多支持 (1) 创建spring 项目 首先我们使用的开发工具为idea 专业版…

【vue+eltable】修改表格滚动条样式

<style lang"scss" scoped> ::v-deep .el-table__body-wrapper::-webkit-scrollbar {width: 10px; /*纵向滚动条的宽度*/height: 10px; /*横向滚动条的高度*/ } /*定义滚动条轨道 内阴影圆角*/ ::v-deep .el-table__body-wrapper::-webkit-scrollbar-track {bo…

ZKP11.1 From Practice To Theory

ZKP学习笔记 ZK-Learning MOOC课程笔记 Lecture 11: From Practice to Theory (Guest Lecturer: Alex Lombardi) 11.1 The Feasibility of Interactive ZK SMPC: semi-honest protocol ZKP malicious protocol Parties use ZKP to prove that they follow the protocol T…

探索实人认证API:保障在线交互安全的关键一步

前言 在数字化时代&#xff0c;随着人们生活的日益数字化&#xff0c;各种在线服务的普及&#xff0c;安全性成为用户体验的至关重要的一环。特别是在金融、电商、社交等领域&#xff0c;确保用户身份的真实性显得尤为重要。而实人认证API作为一种先进的身份验证技术&#xff…

基于C#实现AC自动机算法

我要检查一篇文章中是否有某些敏感词&#xff0c;这其实就是多模式匹配的问题。当然你也可以用 KMP 算法求出&#xff0c;那么它的时间复杂度为 O(c*(mn))&#xff0c;c&#xff1a;为模式串的个数。m&#xff1a;为模式串的长度,n:为正文的长度&#xff0c;那么这个复杂度就不…

设计模式篇---外观模式

文章目录 概念结构实例总结 概念 外观模式&#xff1a;为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口&#xff0c;这个接口使得这一子系统更加容易使用。 外观模式引入了一个新的外观类&#xff0c;它为多个业务类的调用提供了一个统一的入口。主要优点…

HSV映射到圆锥坐标系

def bgr2hsvcone(img):arr_hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV)h arr_hsv[..., 0] / 180. * 2s arr_hsv[..., 1] / 255.v arr_hsv[..., 2] / 255.x np.cos(h * np.pi) * s * vy np.sin(h * np.pi) * s * vreturn np.stack((x, y, v), axis-1)

Redis 的集群模式实现高可用

来源&#xff1a;Redis高可用&#xff1a;武林秘籍存在集群里&#xff0c;那稳了~ (qq.com) 1. 引言 前面我们已经聊过 Redis 的主从同步&#xff08;复制&#xff09;和哨兵机制&#xff0c;这期我们来聊 Redis 的集群模式。 但是在超大规模的互联网应用中&#xff0c;业务规…

七牛云产品使用介绍之CDN篇

上一篇介绍了七牛云的Kodo对象存储&#xff0c;并用Java SDK实现将本地文件上传到bucekt&#xff0c;接下来是对CDN产品的介绍 CDN&#xff08;内容分发网络&#xff09;&#xff1a;通过多级缓存实现对Kodo中的资源或者自己网站的资源的加速访问&#xff0c;让你的系统更快更强…

ProPresenter 7 for Mac:Mac电脑好用的文稿演示软件

ProPresenter 7是一款专业的多媒体演示软件&#xff0c;主要用于创建和展示高质量的教堂媒体演示、音乐歌词、幻灯片和视频等。它是一款功能强大且易于使用的软件&#xff0c;被广泛应用于教堂、会议、演唱会和其他场合。以下是ProPresenter 7的一些主要特点和功能&#xff1a;…

VSCode新建Vue项目

前言 Vue.js 是一款流行的 JavaScript 前端框架&#xff0c;它可以帮助开发者轻松构建高性能、可扩展的 Web 应用程序。而 VSCode 则是一款功能强大的开源代码编辑器&#xff0c;它提供了许多有用的工具和插件&#xff0c;可以大幅提高开发效率。 在本文中&#xff0c;我们将…