I/O多路复用
是一种技术,允许一个进程同时监控多个文件描述符,并在其中任何一个文件描述符准备好进行 I/O 操作时通知进程。这种技术可以提高程序的性能和效率,特别是在需要同时处理多个网络连接或文件描述符的情况下。
# select
函数原型:使用 select
这种 IO
多路转接方式需要调用一个同名函数 select
,这个函数是跨平台的, Linux
、 Windows
、 MacOS
等操作系统都是支持的。该函数可以委托内核帮助我们检测若干个文件描述符的状态,其实就是检测这些文件描述符对应的读写缓冲区的状态。
- 读缓冲区状态:检测里面是否有数据,如果有数据该缓冲区对应的文件描述符就绪
- 写缓冲区状态:检测里面是否还有空闲空间,如果有空闲空间该缓冲区对应的文件描述符就绪
- 读取异常状态:检测是否发生异常,比如网络异常、文件异常等,如果发生异常该缓冲区对应的文件描述符就绪
委托检测的文件描述符遍历检测完毕之后,已就绪的这些满足的文件描述符会通过 select () 的参数分为 3 个集合传出,程序员得到这几个集合依次分情况依次处理即可。
函数原型:
#include <sys/select.h> | |
struct timeval { | |
time_t tv_sec; /* 秒 */ | |
suseconds_t tv_usec; /* 微秒 */ | |
} | |
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); |
nfds
:需要检测的文件描述符的范围,即检测的文件描述符从 0 到nfds-1
,nfds
的值通常为待检测的最大文件描述符加 1。(内核允许最大一般是 1024)- 内核需要线程遍历这些集合中的文件描述符,这个值是循环结束的条件。
- 在 windows 中,该参数是无效的,指定为 - 1 即可。
readfds
:需要检测读状态的文件描述符集合,即检测这些文件描述符对应的读缓冲区状态。传入传出参数
,读集合一般情况下是需要检测的,这样测能直到通过哪些文件描述符接收数据。
writefds
:需要检测写状态的文件描述符集合,即检测这些文件描述符对应的写缓冲区状态。传入传出参数
,如果不需要使用该参数可以指定为 NULL。
exceptfds
:需要检测异常状态的文件描述符集合,即检测这些文件描述符对应的异常状态。传入传出参数
,如果不需要使用该参数可以指定为 NULL。
timeout
:超时时间,即检测文件描述符状态的超时时间。- 函数返回值:
- 大于
0
:成功,返回集合中已就绪的文件描述符的个数。 - 等于
-1
:则表示出错。 - 等于
0
:则表示超时。
- 大于
另外初始化 fd_set
类型的参数还需要使用相关的一些列操作函数,具体如下:
void FD_ZERO(fd_set *set); // 将 set 集合中的所有文件描述符对应的标志位都设置为 0,表示集合中没有文件描述符 | |
void FD_SET(int fd, fd_set *set); // 将 set 集合中的 fd 文件描述符对应的标志位设置为 1,表示集合中有该文件描述符 | |
void FD_CLR(int fd, fd_set *set); // 将 set 集合中的 fd 文件描述符对应的标志位设置为 0,表示集合中没有该文件描述符 | |
void FD_ISSET(int fd, fd_set *set); // 判断 set 集合中的 fd 文件描述符对应的标志位是否为 1,如果为 1 则表示集合中有该文件描述符,否则表示集合中没有该文件描述符 |
# poll
函数原型:
#include <poll.h> | |
struct pollfd { | |
int fd; /* 文件描述符 */ | |
short events; /* 监控的事件 */ | |
short revents; /* 实际发生的事件 */ | |
}; | |
int poll(struct pollfd *fds, nfds_t nfds, int timeout); |
fds
:需要检测的文件描述符集合,即检测这些文件描述符对应的读缓冲区状态、写缓冲区状态、异常状态。传入传出参数
,读集合一般情况下是需要检测的,这样测能直到通过哪些文件描述符接收数据。pollfd
结构体数组,每个pollfd
结构体表示一个文件描述符的检测信息,包括文件描述符、检测的事件、实际发生的事件。
nfds
:需要检测的文件描述符的个数,即检测的文件描述符从 0 到nfds-1
,nfds
的值通常为待检测的最大文件描述符加 1。- 内核需要线程遍历这些集合中的文件描述符,这个值是循环结束的条件。
timeout
:超时时间,即检测文件描述符状态的超时时间。- 函数返回值:
- 大于
0
:成功,返回集合中已就绪的文件描述符的个数。 - 等于
-1
:则表示出错。 - 等于
0
:则表示超时。
- 大于
创建监听的套接字 lfd = socket ();
将监听的套接字和本地的 IP 和端口绑定 bind ()
给监听的套接字设置监听 listen ()
创建一个文件描述符集合 fd_set,用于存储需要检测读事件的所有的文件描述符
通过 FD_ZERO () 初始化
通过 FD_SET () 将监听的文件描述符放入检测的读集合中
循环调用 select (),周期性的对所有的文件描述符进行检测
select () 解除阻塞返回,得到内核传出的满足条件的就绪的文件描述符集合
通过 FD_ISSET () 判断集合中的标志位是否为 1
如果这个文件描述符是监听的文件描述符,调用 accept () 和客户端建立连接
将得到的新的通信的文件描述符,通过 FD_SET () 放入到检测集合中
如果这个文件描述符是通信的文件描述符,调用通信函数和客户端通信
如果客户端和服务器断开了连接,使用 FD_CLR () 将这个文件描述符从检测集合中删除
如果没有断开连接,正常通信即可
服务端并发实现:
#include <stdio.h> | |
#include <stdlib.h> | |
#include<unistd.h> | |
#include<string.h> | |
#include<arpa/inet.h> | |
#include <sys/select.h> | |
int main(){ | |
// 1. 创建监听的套接字 | |
int fd = socket(AF_INET, SOCK_STREAM, 0); // IPV4, TCP, 默认协议 | |
if(fd == -1){ | |
perror("socket"); | |
exit(-1); | |
} | |
// 2. 绑定 IP 和端口 | |
struct sockaddr_in saddr; | |
saddr.sin_family = AF_INET; // 初始化 IPV4 | |
saddr.sin_port = htons(9999); // 初始化 端口 | |
saddr.sin_addr.s_addr = INADDR_ANY; // 0=0.0.0.0 | |
int ret = bind(fd,(struct sockaddr *)&saddr,sizeof(saddr)); | |
if(ret == -1){ | |
perror("bind"); | |
return -1; | |
} | |
// 设置监听 | |
ret = listen(fd,128); | |
if(ret == -1){ | |
perror("listen"); | |
return -1; | |
} | |
fd_set redset; | |
FD_ZERO(&redset); | |
FD_SET(fd,&redset); | |
int maxfd = fd; | |
while(1){ | |
fd_set temp = redset; | |
int ret = select(maxfd + 1, &temp, NULL,NULL, NULL); | |
// 判断是不是监听 fd | |
if(FD_ISSET(fd,&temp)){ | |
// 接受客户端的连接 | |
int cfd = accept(fd,NULL,NULL); | |
FD_SET(cfd, & redset); | |
maxfd = cfd > maxfd ?cfd:maxfd ; | |
} | |
for(int i = 0; i<= maxfd;++i){ | |
if(i!=fd && FD_ISSET(i,&temp)){ | |
// 接收数据 | |
char buf[1024]; | |
int len = recv(i,buf,sizeof(buf),0); | |
if(len>0){ | |
printf("client say : %s\n",buf); | |
send(i,buf,len,0); | |
} | |
else if(len == 0){ | |
printf("客户端断开连接。。\n"); | |
FD_CLR(i,&redset); | |
close(i); | |
break; | |
} | |
else{ | |
perror("recv"); | |
break; | |
} | |
} | |
} | |
} | |
// 关闭文件描述符 | |
close(fd); | |
return 0; | |
} |
select 与 poll 的并发性能比较:
select
函数的fd_set
类型参数,在每次调用select
函数时,都需要重新设置,而poll
函数的pollfd
类型参数,只需要在第一次调用poll
函数时设置一次,之后每次调用poll
函数时,只需要修改需要检测的文件描述符的pollfd
结构体即可。