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
事件组成。