当前位置: 首页 > 编程日记 > 正文

IO复用之epoll系列

epoll是什么?

epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率

----摘自百度百科

在linux的网络编程中,很长的时间都在使用select来做事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll。

相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。并且,在include/linux/posix_types.h头文件有这样的声明:

#define __FD_SETSIZE    1024

表示select最多同时监听1024个fd,当然,可以通过修改头文件再重编译内核来扩大这个数目。

epoll的相关接口

创建一个文件句柄

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

创建一个 epoll 对象,这里类似于创建管道,但是这里返回的是一个标识该软件资源的文件描述符,在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽

epoll的事件注册函数

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

第一个参数是epoll_create()的返回值,

第二个参数表示动作,用三个宏来表示:

EPOLL_CTL_ADD:注册新的fd到epfd中;

EPOLL_CTL_MOD:修改已经注册的fd的监听事件;

EPOLL_CTL_DEL:从epfd中删除一个fd;

第三个参数是需要监听的文件描述符,

第四个参数为一个结构体指针,这个结构体中的信息为告诉内核需要监听什么事件

struct epoll_event结构如下:

struct epoll_event {__uint32_t events;  /* Epoll events */epoll_data_t data;  /* User data variable */};
typedef union epoll_data {void        *ptr;int          fd;uint32_t     u32;uint64_t     u64;} epoll_data_t;
 

events可以是以下几个宏的集合:

EPOLLIN :     表示对应的文件描述符可以读(包括对端SOCKET正常关闭);

EPOLLOUT:    表示对应的文件描述符可以写;

EPOLLPRI:      表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);

EPOLLERR:     表示对应的文件描述符发生错误;

EPOLLHUP:     表示对应的文件描述符被挂断;

EPOLLET:      将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。

EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

data 为联合体,用来保存用户自定制数据,传什么类型的数据,就对联合体里面的哪个数据进行赋值

这里的data 在一般情况下用保存对应的文件描述符

epoll的事件等待函数

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

第一个参数为我们创建的epoll模型

第二个参数为事件数组

第三个为数组大小

第四个参数为超时时间

epoll对文件描述符的两种操作模式

Edge Triggered (ET)  边缘触发只有数据到来,才触发,不管缓存区中是否还有数据。

Level Triggered (LT)  电平触发只要有数据都会触发。

假如有这样一个例子:

1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符

2. 这个时候从管道的另一端被写入了2KB的数据

3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作

4. 然后我们读取了1KB的数据

5. 调用epoll_wait(2)......

Edge Triggered 工作模式:

如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。

i    基于非阻塞文件句柄

ii   只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read()时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read()返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。

Level Triggered 工作模式

相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。

然后详细解释ET, LT:

LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.

ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认(在许多测试中我们会看到如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当我们遇到大量的idle- connection(例如WAN环境中存在大量的慢速连接),就会发现epoll的效率大大高于select/poll。

另外,当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,

读数据的时候需要考虑的是当recv()返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取

while(rs){buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);if(buflen < 0) {// 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读// 在这里就当作是该次事件已处理处.if(errno == EAGAIN)break;elsereturn;} else if(buflen == 0) {// 这里表示对端的socket已正常关闭.
        }if(buflen == sizeof(buf)rs = 1;   // 需要再次读取elsers = 0;}

还有,假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),由于是非阻塞的socket,那么send()函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发,当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会这次请求发送的数据.所以,需要封装socket_send()的函数用来处理这种情况,该函数会尽量将数据写完再返回,返回-1表示出错。在socket_send()内部,当写缓冲已满(send()返回-1,且errno为EAGAIN),那么会等待后再重试.这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send()内部,但暂没有更好的办法.

epoll的工作原理

1.创建epoll模型 
调用epoll_create()之后,内核会做3件事情 
(1)在操作系统底层(硬件驱动,网卡)构建会调机制 
(2)在操作系统层构建一颗红黑树(一种相对平衡的二叉搜索树),树的每个节点用来保存用户关心的事件(即用户关心的文件描述符和所关心的事件类型) 
(3)在操作系统层构建一个就绪队列,保存众多事件中已经就绪的事件 
2.用户控制事件 
(1) 用户通过调用epoll_ctl()实现实现告诉操作系统,你现在要关心的文件描述符和关心的事件类型 
(2)操作系统会将这一事件保存在红黑树中 
3.内核激活事件 
(1)操作系统得知网卡(文件)上面有数据就绪时(硬件机制),激活该事件,将其存入就绪队列中 
(2)用户调用epoll_wait()返回时,返回的为就绪队列中就绪的事件 
我们说的epoll_wait()实现是O(1)的时间复杂度,只需要关注就绪队列是否为空,不为空就将事件复制到用户态

代码实例:

#include <iostream>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
using namespace std;
#define MAXLINE 5
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5000
#define INFTIM 1000
void setnonblocking(int sock)//将套接字设置为非阻塞
{int opts;opts=fcntl(sock,F_GETFL);if(opts<0){perror("fcntl(sock,GETFL)");exit(1);}opts = opts|O_NONBLOCK;if(fcntl(sock,F_SETFL,opts)<0){perror("fcntl(sock,SETFL,opts)");exit(1);}
}
int main(int argc, char* argv[])
{int i, maxi, listenfd, connfd, sockfd,epfd,nfds, portnumber;ssize_t n;char line[MAXLINE];socklen_t clilen;if ( 2 == argc ){if( (portnumber = atoi(argv[1])) < 0 ){fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);return 1;}}else{fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);return 1;}struct epoll_event ev,events[20]; //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件epfd=epoll_create(256); //生成用于处理accept的epoll专用的文件描述符struct sockaddr_in clientaddr;struct sockaddr_in serveraddr;listenfd = socket(AF_INET, SOCK_STREAM, 0);setnonblocking(listenfd); //把socket设置为非阻塞方式ev.data.fd=listenfd; //设置与要处理的事件相关的文件描述符ev.events=EPOLLIN|EPOLLET;  //设置要处理的事件类型    
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); //注册epoll事件bzero(&serveraddr, sizeof(serveraddr));serveraddr.sin_family = AF_INET;char *local_addr="127.0.0.1";inet_aton(local_addr,&(serveraddr.sin_addr)); serveraddr.sin_port=htons(portnumber);bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));listen(listenfd, LISTENQ);maxi = 0;for ( ; ; ) {nfds=epoll_wait(epfd,events,20,500); //等待epoll事件的发生for(i=0;i<nfds;++i) //处理所发生的所有事件
        {if(events[i].data.fd==listenfd)//如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。
            {connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);if(connfd<0){perror("connfd<0");exit(1);}char *str = inet_ntoa(clientaddr.sin_addr);cout << "accapt a connection from " << str << endl;ev.data.fd=connfd; //设置用于读操作的文件描述符ev.events=EPOLLIN|EPOLLET; //设置用于注测的读操作事件epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //注册ev
            }else if(events[i].events&EPOLLIN)//如果是已经连接的用户,并且收到数据,那么进行读入。
            {cout << "EPOLLIN" << endl;if ( (sockfd = events[i].data.fd) < 0)continue;if ( (n = read(sockfd, line, MAXLINE)) < 0) {if (errno == ECONNRESET) {close(sockfd);events[i].data.fd = -1;} elsestd::cout<<"readline error"<<std::endl;} else if (n == 0) {close(sockfd);events[i].data.fd = -1;}line[n] = '/0';cout << "read " << line << endl;ev.data.fd=sockfd;  //设置用于写操作的文件描述符ev.events=EPOLLOUT|EPOLLET; //设置用于注测的写操作事件epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改sockfd上要处理的事件为EPOLLOUT
            }else if(events[i].events&EPOLLOUT) // 如果有数据发送
            {sockfd = events[i].data.fd;write(sockfd, line, n);ev.data.fd=sockfd; //设置用于读操作的文件描述符ev.events=EPOLLIN|EPOLLET; //设置用于注测的读操作事件epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);  //修改sockfd上要处理的事件为EPOLIN
            }}}return 0;
}

转载于:https://www.cnblogs.com/GHzz/p/9486836.html

相关文章:

MVP Summit 2008 照片纪实(二)- 旧金山,Google总部和Stanford大学

坐在洛杉矶机场里&#xff0c;终于为这次MVP峰会的美国之行画上了句号。从旧金山到拉斯维加斯&#xff0c;从拉斯维加斯到大峡谷&#xff0c;最后从大峡谷返回洛杉矶&#xff0c;3天之中总共驾驶历程超过1600英里&#xff08;据说可以赶上出租车司机了&#xff09;。3天之中经历…

(C++)1025 PAT Ranking

#include<cstdio> #include<algorithm> #include<cstring>using namespace std;const int M 100*300;struct testee{//考生 char reg_num[14];//准考证号 int score;//分数 int final_rank;//最终排名 int loc_no;//考场号 int local_rank;//考场内排名 }te…

模态视图(转)

转载请注明出处&#xff0c;原文网址&#xff1a;http://blog.csdn.net/m_changgong/article/details/8127894 作者&#xff1a;张燕广 模态视图不是专门的某个类&#xff0c;而是通过视图控制器的presentViewController方法弹出的视图&#xff0c;我们称为模态视图。 模态视图…

MHA二种高可用架构切换演练

高可用架构一 proxysqlkeepalivedmysqlmha优势&#xff0c;最大程序的降低脑裂风险&#xff0c;可以读写分离&#xff08;需要开启相应的插件支持&#xff09; 一、proxysql 1、安装 tar -zxvf proxysql.tar.gz -C /usr/local/chmod -R 700 /usr/local/proxysqlcd /usr/local/p…

如何关闭事件跟踪程序

最近经常遇到一些独享服务器用户反应自己的服务器联系万网工程师重起后&#xff0c;重新登陆时遇到的界面不知道该如何操作问题。当您看到此界面时&#xff0c;只需要在“注释”下面的空白处随意输入字符即可激活“确定”按钮&#xff0c;点击“确定”后可以进入系统。 这个界…

(C++)1015 德才论

#include<cstdio> #include<algorithm> #include<cstring> using namespace std; const int M 100000;struct Testee{char no[10];int de;int cai;int type;//第几类 }peo[M10];bool cmp(Testee a,Testee b){//比较顺序依次为总分&#xff0c;德分&#xf…

Vim命令相关

在shell中&#xff0c;记住一些常用的vim命令&#xff0c;会在操作时候事半功倍。 光标移动 h,j,k,l,h #表示往左&#xff0c;j表示往下&#xff0c;k表示往右&#xff0c;l表示往上 Ctrl f #上一页 Ctrl b #下一页 w, e, W, E #跳到单词的后面&#xff0c;小…

做科研的几点体会

刚刚开始做实验的时候&#xff0c;别人怎么说我就怎么做&#xff0c;每天在实验台旁干到深夜&#xff0c;以为这就是科研了。两个月过去&#xff0c;突然发现自己还在原地踏步。那种感觉&#xff0c;只能用”沮丧”来形 容。我开始置疑自己的行为和观念。感觉有种习惯的力量在束…

ICMP报文分析

一.概述&#xff1a;1. ICMP同意主机或路由报告差错情况和提供有关异常情况。ICMP是因特网的标准协议&#xff0c;但ICMP不是高层协议&#xff0c;而是IP层的协议。通常ICMP报文被IP层或更高层协议&#xff08;TCP或UDP&#xff09;使用。一些ICMP报文把差错报文返回给用户进…

(C++)1029 旧键盘

#include<cstdio> #include<cstring>const int M 80;//值得注意的地方是“按照发现顺序 ” //采取的最佳策略是&#xff0c;对于字符串1中的每一个字符&#xff0c;看在字符串2中是否出现int hashmap(char c){int res 0;if(0<c&&c<9){res c-0;}e…

深入理解 python 元类

一、什么的元类 # 思考&#xff1a; # Python 中对象是由实例化类得来的&#xff0c;那么类又是怎么得到的呢&#xff1f; # 疑问&#xff1a; # python 中一切皆对象&#xff0c;那么类是否也是对象&#xff1f;如果是&#xff0c;那么它又是那个类实例化而来的呢&…

使用.NET REACTOR制作软件许可证

使用.NET REACTOR制作软件许可证 原文:使用.NET REACTOR制作软件许可证软件下载地址&#xff1a;http://www.eziriz.com/downloads.htm 做一个简单的许可证系统&#xff0c;下面是具体步骤&#xff1a;1&#xff0c; OPEN ASSEMBLY打开项目可执行文件(debug文件夹里面exe文件…

(C++)CSP 201712-2 游戏

#include<cstdio> #include<algorithm> using namespace std;const int M 1000;int k;bool obsl(int x){if(x%k0||x%10k){return true;//淘汰 }else return false; }int main(){int n;//孩子的个数 scanf("%d%d",&n,&k);int i1;//现在报的数 in…

在wpf中运行EXE文件

最简单的方法&#xff1a;System.Diagnostics.Process.Start("路径");网上的其他方法&#xff1a; Process p new System.Diagnostics.Process(); p.StartInfo.FileName "路径"; p.StartInfo.Arguments ""; …

C语言程序试题

一个无向连通图G点上的哈密尔顿&#xff08;Hamiltion&#xff09;回路是指从图G上的某个顶点出发&#xff0c;经过图上所有其他顶点一次且仅一次&#xff0c;最后回到该顶点的路劲。一种求解无向图上哈密尔顿回路算法的基础实现如下&#xff1a; 假设图G存在一个从顶点V0出发的…

利用OWC创建图表的完美解决方案

http://onlytiancai.cnblogs.com/archive/2005/08/24/221761.html 转载于:https://www.cnblogs.com/Athrun/archive/2008/05/19/1202909.html

(C++)1020 月饼 简单贪心

#include<cstdio> #include<algorithm> using namespace std;int types,weight;//月饼的种类数 struct Mooncake{double totalPrice;double price;double weight;double sell;//卖出了多少 };bool cmp(Mooncake a,Mooncake b){return a.price>b.price; }int ma…

枚举,给枚举赋值

/**************枚举*****************/// public enum Colors{// Red,Yellow,Blue,Black,White// }// public static void main(String[] args) {// Colors c Colors.Yellow;// System.out.println(c);//输出枚举// System.out.println(c.ordinal());//输出枚举对应的序号…

青岛...沙尘暴!太可怕了~什么事儿都有!

受蒙古国和我国内蒙古地区出现沙尘暴天气的影响&#xff0c;28日&#xff0c;山东省青岛、烟台等地出现大范围浮尘天气&#xff0c;空气质量明显下降。 28日&#xff0c;一场大范围的浮尘天气影响到烟台&#xff0c;天空一片浑浊&#xff0c;能见度不足5公里&#xff0c;空气质…

面试题收集最新

Java高级程序员面试题------https://www.cnblogs.com/mengdou/p/7233398.html Java高级工程师面试题总结及参考答案-----https://www.cnblogs.com/java1024/p/8594784.html Java高级程序员&#xff08;5年左右&#xff09;面试的题目集----https://blog.csdn.net/fangqun663775…

(C++)1023 组个最小数 简单贪心

#include<cstdio> //#include<algorithm> //using namespace std; //用hash思想读入数字 //解决最高位放谁 //解决后面的位数 //输出 int main(){int key[10];for(int i0;i<10;i){scanf("%d",&key[i]);}//解决最高位for(int i1;i<10;i){if(ke…

Nginx 在centos linux 安装、部署完整步骤并测试通过

需要先装pcre, zlib&#xff0c;前者为了重写rewrite&#xff0c;后者为了gzip压缩。 1.选定源码目录 选定目录 /usr/local/ cd /usr/local/ 2.安装PCRE库 cd /usr/local/ wget http://exim.mirror.fr/pcre/pcre-8.02.tar.gz tar -zxvf pcre-8.02.tar.gz cd pcre-8.02 ./config…

Ubuntu16.04安装qt

5.11官方下载网站&#xff1a; http://download.qt.io/official_releases/qt/5.11/5.11.1/ 可以直接下载linux系统下的.run安装包&#xff1a; 安装方式&#xff1a;https://www.jb51.net/LINUXjishu/501994.html 切换到.run所在的目录&#xff0c;然后 第一步&#xff1a; chm…

好男人是怎么变坏的

十岁以前&#xff0c;就不说了&#xff0c;无非是淘气和不懂事。 十三、四岁的时候&#xff0c;开始对女孩有好感&#xff0c;但是那时候他离女孩远远的&#xff0c;并且以讨厌女孩自居&#xff0c;生怕被同伴嘲笑。 十五岁的时候&#xff0c;听到大人们说某某男人好花&#xf…

(C++)小明种苹果(续)

#include<cstdio>struct tree{int left;//剩余的果子数量bool fallfalse;//是否发生掉落int falls0;//这颗数前面的树&#xff08;包括自身&#xff09;发生掉落的次数 }trs[1000];int main(){int n;//树的总数scanf("%d",&n);for(int i0;i<n;i){//对于…

MySQL如何判别InnoDB表是独立表空间还是共享表空间

InnoDB采用按表空间&#xff08;tablespace)的方式进行存储数据, 默认配置情况下会有一个初始大小为10MB&#xff0c; 名字为ibdata1的文件&#xff0c; 该文件就是默认的表空间文件&#xff08;tablespce file&#xff09;&#xff0c;用户可以通过参数innodb_data_file_path对…

如何使用WindowsLiveWriter发文章

1.下载wlw最新版本http://download.microsoft.com/download/8/0/9/809604cd-bd08-42c8-b590-49c332059e64/writer.msi 2.在菜单中选择“Weblog”&#xff0c;然后选择“Another Weblog Service”。如图一 &#xff08;图一&#xff09; 3.在Weblog Homepage URL中输入你的Blog主…

很多学ThinkPHP的新手会遇到的问题

在模板传递变量的时候&#xff0c;很多视频教程都使用$v.channel的方式&#xff0c;如下&#xff1a; <a href"{:U(Chat/set,array(id>$v.channel))}" title"设置" class"btn btn-mini tip"> 这会导致URL在解析的时候出现问题&#xff…

(C++)1040 有几个PAT

#include<cstdio> #include<cstring> const int MOD 1000000007; const int maxn 100010;int main(){char str[maxn];scanf("%s",str);int len strlen(str);//数出每个元素左侧的P的个数int leftnumP[maxn];leftnumP[0] 0;for(int i1;i<len;i){if…

C#进行Visio二次开发之电气线路停电分析逻辑

停电分析&#xff0c;顾名思义&#xff0c;是对图纸进行停电的逻辑分析。在电气化线路中&#xff0c;一条线路是从一个电源出来&#xff0c;连接着很多很多的设备的&#xff0c;进行停电分析&#xff0c;有两个重要的作用&#xff1a;一是看图纸上的Shape元件是否连接正常&…