0voice-2.1.1-io多路复用select/poll/epoll
select
-
之前的模式:\(1\) 请求 , \(1\) 线程
- 好处:代码逻辑简单
- 缺点:不利于并发, \(1 \ k\) 并发量左右
-
连接三种集合代表的行为模式 (
readfds、writefds和exceptfds)-
readfds:- 监听套接字 (
listening socket) 可读:
表示有新的客户端连接请求到达了服务器的监听队列,可以调用accept()来接受这个新连接了。 - 连接套接字 (
connected socket) 可读:
表示套接字接收缓冲区中有数据可读。你可以调用read()或recv()函数来从这个套接字中读取数据,而且调用不会阻塞(或者至少不会无限期阻塞)。 - 远程关闭连接:如果连接的另一端(客户端)正常关闭了连接(发送了
FIN包),那么这个套接字也会被标记为可读,此时read()或recv()会返回 0。 readfds关注的是是否有数据可以从FD中读取,或者是否有新的连接可以建立。
- 监听套接字 (
-
writefds:- 连接套接字 (
connected socket) 可写:
表示套接字的发送缓冲区有足够的空间来写入数据。你可以调用write()或send()函数来向这个套接字中写入数据,而且调用不会阻塞(或者至少不会无限期阻塞)。通常,在TCP连接建立后,连接套接字大部分时间都是可写的,除非你正在发送大量数据,导致发送缓冲区被填满。 writefds关注的是是否可以向FD中写入数据而不会阻塞。
- 连接套接字 (
-
exceptfds:- 套接字接收到带外数据 (
Out-of-Band Data - OOB):
这是最常见的用途。TCP协议支持发送“带外数据”,它是一种紧急数据,优先级高于普通数据。当套接字接收到OOB数据时,它会被标记为异常。你可以使用MSG_OOB标志的recv()来读取它。 - 连接错误:在某些情况下,如果套接字发生了一些严重的错误(例如,连接的另一端重置了连接
RST),可能会在exceptfds中报告(尽管更多时候,这些错误也会在readfds或writefds上报告,导致read/write返回错误。 exceptfds关注的是是否有带外数据到达,或连接是否发生了某些不寻常的、可能需要立即处理的错误或特殊条件。
- 套接字接收到带外数据 (
-
-
理解
select语句int nready = select(参数1 , 参数2 , 参数3 , 参数4 , 参数5);- 参数 \(2\) : 前文提及的
readfds,读集合 ,注意它是一个fd的集合形式。 - 参数 \(3\) : 前文提及的
writefds, 写集合。 - 参数 \(4\) : 前文提及的
exceptfds, 异常集合。 - 参数 \(5\) : 阻塞时长,等待触发,指的是三个集合中的待定元素,有触发的现象。
NULL代表着一直阻塞。
- 参数 \(1\) : 三个集合中最大 \(fd\) 编号 (\(maxfd\)) + \(1\) , 注意 \(maxfd\) ,由于有 \(fd\) 的创建和回收的机制 ,需要我们写代码动态去更新的。
nready: 代表三个集合中有触发次数的总数。- 注意每次
select过后,三个集合都会被修改的,只有真正触发对应事件的 \(fd\) 才会被留下。
- 参数 \(2\) : 前文提及的
-
主要代码
fd_set rfds,rset;
FD_ZERO(&rfds);
FD_SET(sockfd, &rfds);
int maxfd = sockfd; //集合fd最大值
while (1) {
//rfds 主集合或持久化集合的角色
//rset 工作集合或临时集合的角色
rset = rfds;
//最大fd + 1 , 可读集合 , 可写集合 , 出错集合 , timeout
int nready = select(maxfd + 1 , &rset , NULL , NULL , NULL);
//accept
if (FD_ISSET(sockfd , &rset)) { //在 sockfd 的位置上有没有被设置 (ISSET)
int clientfd = accept(sockfd , (struct sockaddr*)&clientaddr , &len);
printf("accept finished: %d\n",clientfd);
FD_SET(clientfd, &rfds);
if (clientfd > maxfd) {
maxfd = clientfd;
}
}
//recv
int i = 0;
for (i = sockfd + 1 ; i <= maxfd ; ++i) {
if (FD_ISSET(i , &rset)) {
char buffer[1024] = {0};
int count = recv(i , buffer , 1024 , 0);
printf("RECV %s\n", buffer);
if (count == 0) { //discount
printf("client disconnect: %d\n",i);
close(i);
FD_CLR(i , &rfds);
continue;
}
count = send(i , buffer , count , 0);
printf("SEND: %d\n", count);
}
}
}
- 注意
rfds和rset两个集合的含义- 注意判断都是用
rset, 因为只在乎 这次循环 触发事件的fd。 - 而
rfds在fd的增加和回收中会及时变化的。 rset在select中是要进入内核的。
- 注意判断都是用
- 思考为什么能并行 ?
- 阻塞 (
NULL状态下) 的语句就只有select,而且一旦有对应的事件发生就进行。 - 集合的作用。不在关注服务个体,只有在对应触发事件的集合下,都会被遍历到发生,这样我们就不会像一服务一线程一样,仅关注一个服务上触发的事件。
- 阻塞 (
maxfd可以无限大吗?fd_set跟c++中的bitset一样,它是位图,肯定有大小限制的,有默认大小。
- 上述代码能用
nready优化- 每次完成一个触发事件,
nready--,nready 变成0时,不在循环找触发事件。
- 每次完成一个触发事件,
poll
-
select的优点与缺点- 实现了
io多路复用 - 参数太多
- 实现了
-
参数解析
struct pollfd {
int fd;
short events;
short revents;
}
events: 想要监听的事件 , 如POLLIN(可读事件)revents: 实际触发的事件 (由poll返回)
poll(参数1,参数2,参数3)
- 参数 \(1\) :
fd集合 - 参数 \(2\) : 集合大小
- 参数 \(3\) :
Timeout, \(-1\) 意为着一直等待
- 代码
struct pollfd fds[1024] = {0};
fds[sockfd].fd = sockfd;
fds[sockfd].events = POLLIN;
int maxfd = sockfd;
while (1) {
int nready = poll(fds , maxfd + 1 , -1); //fds copy到内核里面
if (fds[sockfd].revents & POLLIN) { //accept
int clientfd = accept(sockfd , (struct sockaddr*)&clientaddr , &len);
printf("accept finished: %d\n",clientfd);
fds[clientfd].fd = clientfd;
fds[clientfd].events = POLLIN;
if (clientfd > maxfd) {
maxfd = clientfd;
}
}
//recv
int i = 0;
for (i = sockfd + 1 ; i <= maxfd ; ++i) {
if (fds[i].revents & POLLIN) {
char buffer[1024] = {0};
int count = recv(i , buffer , 1024 , 0);
printf("RECV %s\n", buffer);
if (count == 0) { //discount
printf("client disconnect: %d\n",i);
close(i);
fds[i].fd = -1;
fds[i].events = 0;
continue;
}
count = send(i , buffer , count , 0);
printf("SEND: %d\n", count);
}
}
}
-
select的限制:在select中,文件描述符的数量是有限制的,通常FD_SETSIZE定义(在许多系统中,默认值为1024)。这意味着,如果有超过1024个文件描述符需要监听,select就不再适用,需要手动调整编译时参数或者使用其他方法。 -
poll没有这个限制:poll使用一个数组来存储待监听的文件描述符,数组的大小是动态的,不受固定的限制。因此,可以轻松地监听成千上万的文件描述符。 -
poll本身会检测套接字的 可写 状态:如果套接字的发送缓冲区已经有足够的空间,poll会自动把该套接字标记为可写(POLLOUT)。因此你不需要手动设置POLLOUT,只要你在pollfd中监听了POLLOUT事件,poll会在合适的时候通知你该套接字可以写数据。(解释为什么没有在send的时候设置可写状态)。
epoll
int epfd = epoll_create(1);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd , EPOLL_CTL_ADD , sockfd , &ev);
while (1) {
struct epoll_event events[1024] = {0};
int nready = epoll_wait(epfd , events, 1024 , -1);
//events 数组存放返回的事件信息,注意它也是
int i = 0;
for (i = 0 ; i < nready ; i++) {
int connfd = events[i].data.fd;
if (connfd == sockfd) { //accept
int clientfd = accept(sockfd , (struct sockaddr*)&clientaddr , &len);
printf("accept finished: %d\n",clientfd);
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd , EPOLL_CTL_ADD , clientfd , &ev);
} else if (events[i].events & EPOLLIN) {
char buffer[1024] = {0};
int count = recv(connfd, buffer , 1024 , 0);
printf("RECV %s\n", buffer);
if (count == 0) { //discount
printf("client disconnect: %d\n",connfd);
close(i);
epoll_ctl(epfd , EPOLL_CTL_DEL , connfd , &ev);
continue;
}
count = send(connfd , buffer , count , 0);
printf("SEND: %d\n", count);
}
}
}
-
int epfd = epoll_create(int size)- 这里的
size参数不等于 0 均可,基本没用,为的是兼容旧版本。 epoll_create它会在内核里创建一个epoll实例,它这个epoll实例里面,可以保存很多fd。- 以后你要对某个
socket关注EPOLLIN(可读)、EPOLLOUT(可写),都要先把它“放进epoll实例里”。 epfd是内核中那张epoll事件表的“编号”。
- 这里的
-
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *_Nullable event);op:epoll_ctl_add(添加)、epoll_ctl_mod(修改) 和
epoll_ctl_del(删除)。
-
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);- 等待
epfd中的事件响应
- 等待
-
epoll_ctl(epfd , EPOLL_CTL_ADD , sockfd , &ev);-
将套接字
sockfd添加到epoll监听集合 -
epfd为ev的编号 -
EPOLL_CTL_ADD添加fd,EPOLL_CTL_DEL删除fd,EPOLL_CTL_MOD修改fd的监听事件。
-
-
相比较于
select而言,对于大并发的优势?select需要把整个集合copy进内核里面去。- 积累起来的,就绪就
ok。 - 就绪是我们需要处理的事件。
-
select、poll和epoll- 均解决
io事件触发 - 未涉及到应用层
- 均解决
-
io的生命周期内,无数个io事件组成。

浙公网安备 33010602011771号