select、poll和epoll的区别
文章目录
- 概要
- 一、多路复用I/O模型的诞生
- 1.1 多线程或进程方式
- 1.2 通过数组,链表等方式保存socket fd,不断轮询
- 二、select
- 三、poll
- 四、epoll
- 五、小结
- 六、参考
概要
在Unix五种I/O模型一文中,提到了I/O多路复用模型,其在Linux下有3种实现方式:select、poll、epoll,本文主要深入介绍下它们各自特点。
事先说明:I/O多路复用模型,select和poll核心就是【轮询+内核I/O事件就绪通知】,epoll的核心是内核I/O事件就绪通知。
多路:多个socket连接(即多个客户端连接)
复用:允许内核监听多个socket描述符,一旦发现进程指定的一个或多个scoket的I/O事件就绪(TCP三次握手成功、可读,可写),就通知该进程
要想更好的了解,最好根据代码来说,下面是代码的基本框架:
void main(int argc, char **argv)
{int listenfd, connfd;struct sockaddr_in srv_addr;//创建socket套接字if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);return;}//设置绑定地址的内容memset(&srv_addr, 0, sizeof(srv_addr));srv_addr.sin_family = AF_INET; //ipv4srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);//ip 0.0.0.0srv_addr.sin_port = htons(8888); //端口//绑定地址if (bind(listenfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)) == -1){printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);return;}//listenif (listen(listenfd, SOMAXCONN) == -1) //指定监听的套接字描述符、TCP半连接和全连接队列大小{printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);return;}//【在这个区域分别使用阻塞,多线程,select,poll,epoll等多种方式实现连接】close(listenfd); //关闭listen socketreturn;
}
一、多路复用I/O模型的诞生
之所以诞生多路复用I/O模型,肯定是旧的I/O模型无法满足需要了,首先回顾下基础的阻塞I/O模型:
代码如下:
char buff[MAXLNE];int n;struct sockaddr_in cli_addr;socklen_t len = sizeof(client);if ((connfd = accept(listenfd, (struct sockaddr *)&cli_addr, &len)) == -1){printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);return;}while(1){n = recv(connfd, buff, MAXLNE, 0); //阻塞if (n > 0){//加上字符串的尾部,以便显示和转发buff[n] = '\0';printf("recv msg: %s \n", buff);send(connfd, buff, n, 0);}else if (n == 0){close(connfd); //关闭client socket连接}else //n==-1{printf("recv errno: %d\n", errno);}}
此时while在accept API之后,那么只能处理一个client,并维持长连接。
那么while在accept API之前会如何呢,显而易见,此时能处理多个client,但只能处理每个client一条消息,不能未出长连接。
如果想与多个client维持长连接,该如何做呢?于是基础阻塞I/O模型有了以下两种方式:
- 多线程或进程;
- 通过数组,链表等方式保存socket fd,不断轮询;
1.1 多线程或进程方式
代码如下(以多进程为例):
signal(SIGCHLD, sig_child); //注册子进程退出处理函数pid_t child_pid;while(1){struct sockaddr_in cli_addr;socklen_t len = sizeof(client);if ((connfd = accept(listenfd, (struct sockaddr *)&cli_addr, &len)) == -1){printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);return;}if ((child_pid = fork()) == 0) //为每个client派生一个子进程处理{ close(listenfd);str_echo(connfd);exit(0);}}
子进程处理客户端请求函数
void str_echo(int connfd)
{int n;char buff[MAXLNE];again:while ((n = recv(connfd, buf, MAXLINE)) > 0){printf("recv msg: %s \n", buff);send(sockfd, buf, n);}if (n < 0 && errno == EINTR){goto again;} else if (n == 0){close(connfd); //结束return; //退出进程}else //n==-1{printf("recv errno: %d\n", errno);}
}
子进程退出函数
void sig_child(int signo)
{pid_t pid;int stat;//等待所有子进程退出while ((pid = waitpid(-1, &stat, WNOHANG)) > 0)printf("child %d exit\n", pid);return;
}
但是这方式有个弊端,每个client由一个进程/线程去处理,系统开销相当大,很难维持大量客户端。
1.2 通过数组,链表等方式保存socket fd,不断轮询
这种模式是将客户端socket fd通过数组,链表等方式保存下来,然后不断地轮询,如果客户端太多,轮询也是很慢的。
伪代码如下:
int client_fds[FD_SETSIZE];
int sockfd;
while(1)
{int connfd = accept() //阻塞for (i = 0; i < FD_SETSIZE; i++) { if (client[i] < 0) {client_fds[i] = connfd;break;}}for (i = 0; i < FD_SETSIZE; i++) {if ((sockfd = client_fds[i]) <= 0){continue; }n = recv(sockfd, buf, MAXLINE)//阻塞if(n > 0){printf("recv msg: %s \n", buff);send(sockfd, buf, n);}else if (n < 0 ){printf("recv fd:%d, errno: %d\n", sockfd, errno);} else// n == 0{close(connfd); //结束client_fds[i] = 0; //标记一下} }
}
可以看到通过client_fds数组
将客户端连接的描述符保存下来,后续对其进行轮询,来达到与多个client维持长连接
的目的。
但accept,recv等函数都是阻塞的,如果此时I/O事件(比如accept的TCP三次握手成功,recv的可读,send的可写)没就绪,那岂不永远卡死了。所以我们需要一种机制,告诉我们client_fds数组和监听socket listenfd
中哪些socket有I/O就绪事件,基于此多路复用I/O模型诞生了,没错,该模型本质就是告诉进程哪些socket有I/O就绪事件,然后我们基于此去轮询那些有I/O就绪事件的scoket,这样就不会卡住了。
PS:这种方式是没有实际应用的,主要是为了引出多路复用I/O模型。
那select、poll、epoll是如何告诉进程哪些socket有I/O就绪事件呢?下面依次探究下吧。
二、select
select api函数(还有个pselect函数,不是很常用,不过二者核心逻辑是一样的):
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数:
nfds:最大的文件描述符数量加1的值
readfds:监听可读集合
writefds:监听可写集合
exceptfds:监听异常集合
timeout:超时时间
返回值:
大于0,表示I/O就绪事件的socket描述符个数;
等于0表示没有描述符有状态变化,并且调用超时;
小于0表示出错,此时全局变量errno保存错误码。
我们使用Glibc库select或pselect函数时,都会走Linux内核do_select函数,可以看到本质就是轮询readfds、writefds、exceptfds这三个集合,每次调用会轮询两次:
- 第一次会将当前进程(本质是I/O事件就绪时的回调函数)加入到readfds、writefds、exceptfds这三个集合中socket的等待队列中(socket有I/O事件就会触发回调函数),然后就通过poll_schedule_timeout函数挂起;
- 一旦有某个socket描述符I/O事件就绪,会立即通知进程,就会开始第二次轮询,本次轮询会确定readfds、writefds、exceptfds这三个集合中到底是哪些socket描述符有就绪的I/O事件;
- 明明两次,为啥会有三呢,这里主要是在do_select最后会调用一次
poll_freewait
函数,该函数会将当前进程从readfds、writefds、exceptfds这三个集合中socket的等待队列中移除。
从上面描述就可以看出,select主要工作:维护所有socket的监听(添加【步骤1】和移除【步骤三】)、判定是否有I/O事件就绪的socket
。
该方式虽相比基于阻塞I/O的多进程/线程方式能更便捷的实现与多个客户端维持长连接了,但缺点多多:
- 由于无法准确识别哪些socket描述符I/O事件就绪,所以会进行无差别轮询,时间复杂度O(N);
- Linux下readfds、writefds、exceptfds这三个集合大小默认1024,所以维持长连接的客户端数量是有限的,源码如下:
typedef __kernel_fd_set fd_set
__kernel_fd_set
#undef __FD_SETSIZE
#define __FD_SETSIZE 1024typedef struct {unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;
可以看到无论32位还是64位操作系统下fds_bits大小都是1024位,注意fd_set集合是通过位运算标识第几个scoket描述符有无I/O事件。
- 每次调用select函数,都需要把所有fd_set从用户空间拷贝到内核空间,如果fd_set比较大,对性能影响就非常大;
优点:
- 相比基于阻塞I/O的多进程/线程方式,更便捷的实现与多个客户端维持长连接;
- 相比非阻塞I/O,主动轮询socket是否有I/O事件,调整为等待内核通知,这样一次系统调用就实现多个client事件的管理,更有优势。
综合来看相比基于阻塞I/O的多进程/线程方式优势并不大。
三、poll
poll api函数:
struct pollfd {int fd; //要监听的文件描述符short events; //要监听的事件short revents; //事件结果
};
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
fds:这是一个数组,每一个数组元素表示要监听的文件描述符以及相应的事件
nfds:数组的个数
timeout:超时时间
返回值:
大于0:表示结构体数组fds中有fd描述符的状态发生变化,或可以读取、或可以写入、或出错。并且返回的值表示这些状态有变化的socket描述符的总数量;此时可以对fds数组进行遍历,以寻找那些revents不空的描述符,然后判断这个里面有哪些事件以读取数据。
等于0:表示没有描述符有状态变化,并且调用超时。
小于0:此时表示有错误发生,此时全局变量errno保存错误码。
我们使用Glibc库poll函数时,会走Linux内核do_sys_poll和do_poll函数,可以看到:
- do_sys_poll函数会将fds数组转化为
struct poll_list
链表,根据代码可知结构如下图:
- do_poll函数被 do_sys_poll调用,其核心逻辑与do_select差不多,每次被调用会轮询两次:
1):第一次会将当前进程(本质是I/O事件就绪时的回调函数)加入到struct poll_list
链表中socket的等待队列中(socket有I/O事件就会触发回调函数),然后就通过poll_schedule_timeout函数挂起;
2):一旦有某个socket描述符I/O事件就绪,会立即通知进程,就会开始第二次轮询,本次轮询会确定struct poll_list
链表中到底是哪些socket描述符有就绪的I/O事件。 - do_sys_poll调用do_poll函数结束后,还有一次循环,即
poll_freewait
函数,此时会将当前进程从struct poll_list
链表中socket的等待队列中移除。
从上面描述就可以看出,相比select,唯一改进的地方在于没有了最大连接数的限制,但相应的也需要关注随着客户端连接数增加,轮询的效率和会急剧下降的问题。
四、epoll
epoll全名event poll,其api函数有:
- epoll_create,创建一个epoll实例
struct eventpoll
,其实其返回一个int值,可以称之为epoll描述符,Linux内核会管理该值与具体epoll实例的映射关系。对应内核源码do_epoll_create
//epoll 结构体,即epoll实例
struct eventpoll {/* 等待队列头,被sys_epoll_wait使用 */wait_queue_head_t wq;/* 保准准备就绪的文件描述符的一个链表 */struct list_head rdllist;/* 红黑树节点,epoll使用红黑树存储事件信息 */struct rb_root rbr;...
};
int epoll_create(int size);
参数:
size:一个int值,实际没有任何用,只要不小于等于0即可;
返回值:
小于0表示错误,此时全局变量errno保存错误码
- epoll_ctl,添加、删除、修改事件。对应内核源码do_epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数:
epfd:epoll_create函数的返回值,即epoll描述符;
op:事件类型,EPOLL_CTL_ADD(添加事件)、EPOLL_CTL_MOD(修改事件)、EPOLL_CTL_DEL(删除事件);
fd: socket描述符;
event:告诉内核需要监听哪个事件。
返回值:
小于0表示错误,此时全局变量errno保存错误码
- epoll_wait,等待I/O事件。对应内核源码do_epoll_wait
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
参数:
epfd:epoll_create函数的返回值,即epoll描述符;
events:表示准备就绪的事件数组;
maxevents: 要等待的最大事件数;
timeout:超时时间。
返回值:
大于0:表示事件就绪的socket个数。
等于0:表示没有描述符有状态变化,并且调用超时。
小于0:此时表示有错误发生,此时全局变量errno保存错误码。
根据api就可以观察出,相比select和poll,epoll拆成了三个,实际上是两个epoll_ctl和epoll_wait。即select和poll是将维护监听scoket描述符
和等待I/O事件
合为一体的,epoll拆开了,epoll_ctl来维护监听scoket描述符
,epoll_wait来等待I/O事件`。
为什么要这样做呢,在分析select和epoll缺点时,其实主要集中两点:
1:轮询:每次调用select或poll时都需要将进程加入到所有socket的等待队列中,等到socket有I/O事件立马唤醒进程,此时会轮询整个socket列表以确定哪些socket有I/O事件,最后会将进程从每个socket等队列中移除。这里涉及到对socket列表的三次轮询,随着socket列表的增加,会造成性能急剧下降的。
2:复制:每次调用select或poll时都要将整个socket列表从用户区复制到内核区,也是有一定开销的。
知道了缺点,该如何解决呢?
1:针对每次调用需要将将进程加入到所有socket的等待队列中,最后将进程从每个socket等队列中移除的操作,这部分交由epoll_ctl处理,按需对某个socket进行EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL操作;
2:针对每次调用都要将整个socket列表从用户区复制到内核区的问题,这部分交由epoll_create创建的epoll实例替代,epoll_ctl通过EPOLL_CTL_ADD操作,会被插入到epoll实例的红黑树中,也就是说只会被复制一次;
3:针对进程被唤醒后,还需要轮询整个socket列表以确定哪些socket有I/O事件的问题(即不知道哪些socket的I/O事件就绪,只能一个个遍历),调整成每个socket有I/O事件时会将该socket加入到epoll实例的rdllink双向链表中,进程调用epoll_wait时只需判断rdllink双向链表长度即可,大于0立即返回,等于0就挂起进程,等待被某个socketI/O事件唤醒。
所以说epoll通过对select/poll功能的拆分,解决了前两者的缺点,相对于前两者优点如下:
- 没有最大并发连接的限制,当然了还受操作系统限制,比如资源、配置等等;
- 性能高,没有了轮询,不会随着socket数量的增加而导致性能下降。
- epoll支持边缘触发(EPOLLET)和水平触发(EPOLLLT),前两者仅支持水平触发。
缺点:
目前缺点就是进程去读写I/O事件就绪的socket时,还需要将数据从内核区复制到用户区,当然了select/poll也有该问题,这一点需要异步I/O去解决了。目前epoll是够用的,Nginx,Redis都是基于epoll的,足以应对处理C10K,C100K问题。
LT(level triggered) 是 缺省 的工作方式 ,并且同时支持 block 和 no-block I/O。 在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,下次内核还会继续通知你的,所以,这种模式编程出错误可能性要小一点。 select/poll 都是这种模型的代表。
ET(edge-triggered) 是高速工作方式 ,只支持 no-block I/O 。在这种模式下,当描述符从未就绪变为就绪时,内核通过 epoll 告诉你,然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了 ( 比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个 EWOULDBLOCK 错误)。但是请注意,如果一直不对这个 socket做 IO 操作 ( 从而使它再次变成未就绪 ) ,内核不会发送更多的通知 (only once), 不过在 TCP 协议中, ET 模式的加速效用仍需要更多的 benchmark 确认。
Epoll 工作在 ET 模式的时候,必须使用非阻塞I/O,以避免由于一个文件句柄的阻塞读 / 阻塞写操作把处理多个文件描述符的任务饿死。
五、小结
从整体来看,epoll的实现性能是比select/poll更好的,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调,一般情况下选epoll就对了。
本人研究Redis相关源码时,观察到其在Linux下就是通过epoll+EPOLLET+非阻塞I/O+Reactor设计模式组合来处理I/O事件,是其高性能的一个关键点。
六、参考
1]:Linux下实现单客户连接的tcp服务端
2]:从网络I/O模型到Netty,先深入了解下I/O多路复用
3]:epoll的本质
4]:深入了解epoll模型
5]:Linux epoll内核源码剖析
相关文章:

select、poll和epoll的区别
文章目录 概要一、多路复用I/O模型的诞生1.1 多线程或进程方式1.2 通过数组,链表等方式保存socket fd,不断轮询 二、select三、poll四、epoll五、小结六、参考 概要 在Unix五种I/O模型一文中,提到了I/O多路复用模型,其在Linux下有…...
gpt今日最新新闻:gpts的广泛应用
最近,OpenAI给ChatGPT带来了一个备受期待的更新——“GPT提及(mentions)”功能。这项创新不仅增强了ChatGPT的实用性,也为AI在日常业务中的运用开辟了新路径。在本文中,我将分享我对这项新功能的初步体验,并…...

【进入游戏行业选游戏特效还是技术美术?】
进入游戏行业选游戏特效还是技术美术? 游戏行业正处于蓬勃发展的黄金时期,科技的进步推动了游戏技术和视觉艺术的飞速革新。在这个创意和技术挑战交织的领域里,游戏特效和技术美术岗位成为了许多人追求的职业目标。 这两个岗位虽然紧密关联…...
(delphi11最新学习资料) Object Pascal 学习笔记---第4章第2.3节(常量参数)
4.2.3 常量参数 作为引用参数的替代,您可以使用const参数。由于您无法在例程内为const参数赋予新值,因此编译器可以优化参数传递。编译器可以选择与引用参数相似的方法(或者在C术语中是const引用),但行为类似于值参…...
事件在状态流程图中的工作方式
什么是事件? 事件是一个Stateflow对象,它可以触发以下对象中一个动作: Simulink触发子系统 Simulink函数调用子系统 状态流程图 何时使用事件 当你想: 激活Simulink触发的子系统 激活Simulink函数调用子系统 在状态流程图…...

幻兽帕鲁能在Mac上运行吗?幻兽帕鲁Palworld新手攻略
幻兽帕鲁能在Mac上运行吗? 《幻兽帕鲁》目前还未正式登陆Mac平台,不过通过一些方法是可以让游戏在该平台运行的。 虽然游戏不能在最高配置下运行,但如果你安装了CrossOver这个软件,就可以玩了。这是为Mac、Linux和ChromeOS等设计…...

elementPlus实现动态表格单元格合并span-method方法总结
最近在做PC端需求的时候,需要把首列中相邻的同名称单元格合并。 我看了一下elementPlus官网中的table表格,span-method可以实现单元格合并。 我们先看一下官网的例子: 合并行或列 多行或多列共用一个数据时,可以合并行或列。 …...

视频上传 - 断点续传那点事
在上一篇文章中,我们讲解了分片上传的实现方式。在讲解断点续传之前,我要把上篇文章中留下的问题讲解一下。读过上一篇文章的小伙伴们都知道,对于分片上传来说,它的传输方式分为2种,一种是按顺序传输,一种是…...
Scala 和 Java在继承机制方面的区别
Scala 和 Java 都是面向对象编程语言,都支持类的继承机制。然而,尽管两者在基础概念上有很多相似之处,但在具体的实现和语法上,Scala 的继承机制有其独特之处。以下是 Scala 和 Java 在继承方面的一些主要区别: 多重继…...

spark sql上线前的调试工作实现
背景 每个公司应该都有大数据的平台的吧,平台的作用就是可以在上面执行各种spark sql以及定时任务,不过一般来说,由于这些spark sql的上线不经过测试,所以可能会影响到生产的数据,这种情况下大数据平台提供一个上线前…...
java -jar启动SpringBoot项目时配置文件加载位置与优先级
服务部署启动时,我们经常需要指定配置文件启动. 一般有四种,优先级如下 spring.config.location > spring.profiles.active > spring.config.additional-location > 默认的 application.yml 1.spring.config.location 外部配置文件优先级最高 一般配置文件在服务…...
每日一题 力扣LCP30.魔塔游戏
题目描述: 小扣当前位于魔塔游戏第一层,共有 N 个房间,编号为 0 ~ N-1。每个房间的补血道具/怪物对于血量影响记于数组 nums,其中正数表示道具补血数值,即血量增加对应数值;负数表示怪物造成伤害值&#x…...
iPhone搞机记录
-iPhone 8 或以上 设备进入DFU模式的方法: (适用:iPhone 8/8 Plus、iPhone X 系列、iPad Pro3 (11-inch)/(12.9-inch)) 1.保持设备处于开机或恢复模式下,插入数据线。 2.按一次设备的“音量加键”松开、再按一次“音量…...

Linux中共享内存(mmap函数的使用)
内存映射的基本使用 内存映射 概念: 使一个磁盘文件与内存中的一个缓冲区相映射,进程可以像访问普通内存一样对文件进行访问,不必再调用read,write。 mmap()的优点: 实现了用户空间和内核空间的高效交互方式 优化前:优…...

Golang与Erlang有什么差异
Golang和Erlang是两种备受关注的编程语言,它们各自具有独特的特点和优势。下面我将简单的探讨一下Golang和Erlang之间的差异,并且分析它们在并发模型、运行环境、函数式编程和领域特性等多个方面的不同之处。 并发模型 Golang使用goroutines和channels…...

cesium系列篇:Entity vs Primitive 源码解析(从Entity到Primitive)02
上篇文章中,我们介绍了使用viewer.entities.add添加entity之后的信号传递以及最后entity对象被传递到GeometryVisualizer; 这篇文章,我们则介绍如何在逐帧渲染的过程中根据GeometryVisualizer中的entity对象创建相应的primitive 这是下文中…...

golang windows 环境搭建 环境配置
golang windows 环境搭建 环境配置 Golang学习之路一环境搭建 MacBook Linux 树莓派raspberrypi安装Golang环境 官网下载地址: https://go.dev/dl/ https://golang.google.cn/dl/ 下载对应系统版本,例如windows 64位系统,下载:xxx.window…...

【Git】06 常用场景
文章目录 前言一、场景11.1 删除分支1.2 修改message信息1.2.1 最新一次commit的message1.2.2 过去commit的message 1.3 合并commit1.3.1 多个连续commit合并1.3.2 不连续commit合并 二、场景22.1 比较暂存区和HEAD所含文件的差异2.2 比较工作区和暂存区所含文件的差异2.3 将暂…...

docker下nacos(1.2.0)的持久化
一、创建数据库 运行以下代码自动创建数据库和表 CREATE DATABASE IF NOT EXISTS nacos_config /*!40100 DEFAULT CHARACTER SET utf8 */; USE nacos_config;SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS 0;-- ---------------------------- -- Table structure for config_…...

Win32 SDK Gui编程系列之--弹出式菜单
1.弹出式菜单 例如,在命令提示窗口中点击鼠标右键,会出现如下图所示的弹出菜单(下拉菜单)。 这种弹出式菜单的实现很简单。不创建菜单栏,用CreatePopupMenu函数创建的菜单是最顶端的菜单就可以了。 菜单的显示使用TrackPopupMenu函数进行。 例如,点击鼠标右键显示弹出…...
反向工程与模型迁移:打造未来商品详情API的可持续创新体系
在电商行业蓬勃发展的当下,商品详情API作为连接电商平台与开发者、商家及用户的关键纽带,其重要性日益凸显。传统商品详情API主要聚焦于商品基本信息(如名称、价格、库存等)的获取与展示,已难以满足市场对个性化、智能…...
可靠性+灵活性:电力载波技术在楼宇自控中的核心价值
可靠性灵活性:电力载波技术在楼宇自控中的核心价值 在智能楼宇的自动化控制中,电力载波技术(PLC)凭借其独特的优势,正成为构建高效、稳定、灵活系统的核心解决方案。它利用现有电力线路传输数据,无需额外布…...

Java面试专项一-准备篇
一、企业简历筛选规则 一般企业的简历筛选流程:首先由HR先筛选一部分简历后,在将简历给到对应的项目负责人后再进行下一步的操作。 HR如何筛选简历 例如:Boss直聘(招聘方平台) 直接按照条件进行筛选 例如:…...
.Net Framework 4/C# 关键字(非常用,持续更新...)
一、is 关键字 is 关键字用于检查对象是否于给定类型兼容,如果兼容将返回 true,如果不兼容则返回 false,在进行类型转换前,可以先使用 is 关键字判断对象是否与指定类型兼容,如果兼容才进行转换,这样的转换是安全的。 例如有:首先创建一个字符串对象,然后将字符串对象隐…...
JS设计模式(4):观察者模式
JS设计模式(4):观察者模式 一、引入 在开发中,我们经常会遇到这样的场景:一个对象的状态变化需要自动通知其他对象,比如: 电商平台中,商品库存变化时需要通知所有订阅该商品的用户;新闻网站中࿰…...

浪潮交换机配置track检测实现高速公路收费网络主备切换NQA
浪潮交换机track配置 项目背景高速网络拓扑网络情况分析通信线路收费网络路由 收费汇聚交换机相应配置收费汇聚track配置 项目背景 在实施省内一条高速公路时遇到的需求,本次涉及的主要是收费汇聚交换机的配置,浪潮网络设备在高速项目很少,通…...

使用Spring AI和MCP协议构建图片搜索服务
目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式(本地调用) SSE模式(远程调用) 4. 注册工具提…...

FFmpeg:Windows系统小白安装及其使用
一、安装 1.访问官网 Download FFmpeg 2.点击版本目录 3.选择版本点击安装 注意这里选择的是【release buids】,注意左上角标题 例如我安装在目录 F:\FFmpeg 4.解压 5.添加环境变量 把你解压后的bin目录(即exe所在文件夹)加入系统变量…...

【Linux】Linux安装并配置RabbitMQ
目录 1. 安装 Erlang 2. 安装 RabbitMQ 2.1.添加 RabbitMQ 仓库 2.2.安装 RabbitMQ 3.配置 3.1.启动和管理服务 4. 访问管理界面 5.安装问题 6.修改密码 7.修改端口 7.1.找到文件 7.2.修改文件 1. 安装 Erlang 由于 RabbitMQ 是用 Erlang 编写的,需要先安…...

归并排序:分治思想的高效排序
目录 基本原理 流程图解 实现方法 递归实现 非递归实现 演示过程 时间复杂度 基本原理 归并排序(Merge Sort)是一种基于分治思想的排序算法,由约翰冯诺伊曼在1945年提出。其核心思想包括: 分割(Divide):将待排序数组递归地分成两个子…...