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
结构体即可。