目录
IO多路复用可以满足在应用程序中,同时处理多路输入输出流的功能;
IO多路复用的基本思想:
①先构造一张有关文件描述符的表(或集合,数组);
②然后调用一个相关函数(select,poll或epoll);
③表中的文件描述符有一个或多个准备好进行IO操作时函数返回;
④函数返回时,告诉进程对应的文件描述符已就绪,可以进行IO操作;
一,使用select实现IO多路复用
函数:int select(int nfds, fd_set readfds, fd_set writefds,fd_set exceptfds, struct timeval timeout);
功能:监测是哪个或哪些文件描述符产生了事件;
参数:① nfds - 监测的文件描述符的个数;(nfds 和程序中最后一次打开的文件描述符有关,描述符从系统自动打开的0,1,2开始,也就是从0开始计数,所以nfds 的值为最大文件描述符+1,也就是最后创建的文件描述符+1);
②readfds - 读事件集合;//NULL表示不关心
③writefds - 写事件集合;//NULL表示不关心
④exceptfds - 异常事件集合;//NULL表示不关心
⑤timeout - 超时检测;//NULL表示不做超时检测
超时检测结构体:
struct timeval{
long tv_sec; //seconds 秒
long tv_usec; //microseconds 微秒
}
返回值:①无超时检测时 - 小于0失败,大于0则表示有事件产生;
②有超时检测时 - 小于0失败,大于0表示有事件产生,等于0表示超时时间到;
相关操作函数:
①将表中的文件描述符删除
void FD_CLR(int fd,fd_set *set);
②清空表
void FD_ZERO(fd_set *set);
③将文件描述符加入表
void FD_SET(int fd,fd_set *set);
④判断文件描述符是否在表中,在则返回1,不在则返回0;
void FD_ISSET(int fd,fd_set *set);
注意:由于在使用select函数之后,没有产生事件的文件描述符会在表中被清除;当想要循环监听事件,或者想监听其他事件时,可能就监听不到了,所以在使用select函数时,需要定义一个临时表,在调用select函数时将原有的表赋值给这个临时表;
select实现-服务器部分代码如下:
//创建文件描述符表
fd_set readfds, tempfds;
//清空表
FD_ZERO(&readfds);
//将关心的文件描述符添加到表中
FD_SET(0, &readfds); //标准输入
FD_SET(sockfd, &readfds); //套接字描述符
//监测的文件描述符中,最大的为套接字描述符
maxfd = sockfd;
int recvbyte;
char buf[128];
//循环检测表中是否有事件发生
while (1)
{
tempfds = readfds; //没有产生事件的文件描述副会从表中清除,需要重新拿到原表
if (select(maxfd + 1, &tempfds, NULL, NULL, NULL) < 0)
{
perror("selcet err.");
return -1;
}
for (int i = 0; i <= maxfd; i++)
{
//发生事件的文件描述符如果在表中,则对比是标准输入还是套接字
if (FD_ISSET(i, &tempfds))
{
if (i == sockfd)
{
if((acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &len)) < 0)
{
perror("accept err.");
return -1;
}
//ntohs网络字节序到主机字节序;
//inet_ntoa将32位的网络字节序二进制地址转换为点分十进制的字符串
printf("client:ip=%s,port=%d\n", inet_ntoa(clientaddr.sin_addr),
ntohs(clientaddr.sin_port));
//客户端链接成功,需要将用于通信的文件描述符添加到检测表(原表)
FD_SET(acceptfd, &readfds);
//确定maxfd
if (maxfd < acceptfd)
maxfd = acceptfd;
}
else if (i == 0)
{
fgets(buf, sizeof(buf), stdin);
printf("key:%s\n", buf);
for (int i = 0; i <= maxfd; i++)
{
if (FD_ISSET(i, &readfds))
{
//将消息发给每个客户端
if (i > sockfd)
{
send(i, buf, sizeof(buf), 0);
}
}
}
}
else
{
recvbyte = recv(i, buf, sizeof(buf), 0);
if (recvbyte < 0)
{
perror("recv err.");
return -1;
}
else if (recvbyte == 0)
{
printf("%d client exit\n", i);
FD_CLR(i, &readfds);
close(i);
}
else
printf("%d:%s\n", i, buf);
}
}
}
}
二,使用poll实现IO多路复用
函数:int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:①fds - 存放关心的文件描述符的结构体数组;
②nfds - 数组的长度,也是定义的存放文件描述符的最大个数(没有限制);
③timeout - 超时检测,以毫秒为单位;//-1表示无限超时
返回值:同select函数返回值;
结构体原型:
struct pollfd{
int fd; //关心的文件描述符
short events; //关心的事件,读时间写事件等;
short revents;//如果产生了事件,将会自动填充该成员;(当文件描述符产生事件时,poll函数就会自动把产生得事件填充到该成员)
}
poll实现-服务器部分代码如下:
//创建表,存放描述符的结构体数组
int nfds;
struct pollfd fds[20];
//将关心的文件描述符和关心的事件添加到表中
fds[0].fd = 0;
fds[0].events = POLLIN;
fds[1].fd = sockfd;
fds[1].events = POLLIN;
nfds = 2;
char buf[128];
//循环检测表中是否有事件发生
while (1)
{
if (poll(fds, nfds, -1) < 0)
{
perror("poll err.");
return -1;
}
if (fds[1].revents == POLLIN)
{
if ((acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &len)) < 0)
{
perror("accept err.");
return -1;
}
//ntohs网络字节序到主机字节序;
//inet_ntoa将32位的网络字节序二进制地址转换为点分十进制的字符串
printf("client:ip=%s,port=%d\n", inet_ntoa(clientaddr.sin_addr),
ntohs(clientaddr.sin_port));
}
if (fds[0].revents == POLLIN)
{
fgets(buf, sizeof(buf), stdin);
printf("key:%s\n", buf);
}
}
三,使用epoll实现IO多路复用
三个功能函数:
①epoll_create;
②epoll_ctl;
③epoll_wait;
①int epoll_create(int size);
功能:创建一个epoll句柄;
参数:size - 需要填不为0的任意正整数;
返回值:成功时返回句柄epfd,失败时返回 -1;
使用示例:
int epfd = epoll_create(1);
if(epfd < 0)
{
perror("epoll_create err");
return -1;
}
②int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
功能:epoll的事件注册函数,注册要监听的事件的属性;
参数:①epfd - epoll的句柄;
②op - 事件的动作,有三个表示动作的宏;
(1)EPOLL_CTL_ADD - 注册新的文件描述符;
(2)EPOLL_CTL_MOD - 修改已经注册的文件描述符的监听事件;
(3)EPOLL_CTL_DEL - 删除注册的文件描述符;
③fd - 要监听的文件描述符;
④event - 告诉内核需要监听什么事件;
(1)EPOLLIN - 告诉内核需要监听什么事件;
(2)EPOLLOUT - 表示对应的文件描述符可读
(3)EPOLLET - 将EPOLL设置为边缘触发(上升沿,下降沿)
(4)EPOLLPRI - 表示对应的文件描述符有紧急数据可读;
(5)EPOLLERR - 表示对应的文件描述符发生错误;
(6)EPOLLHUP - 表示对应的文件描述符被挂断;
返回值:成功时返回0,失败时返回-1;
在使用epoll_ctl之前,要填充两个结构体变量的内容;
下图为结构体的原型:
定义一个结构体变量struct epoll_event event,填充:
1.关心的文件描述符
event.data.fd;
2.文件描述符的事件
event.events = EPOLLIN | EPOLLET;
使用示例:
struct epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN|EPOLLET;
//1.向epfd中注册新的fd:
epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&event);
//2.从epfd中删除fd;
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
③int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);
功能:收集在epoll监听的事件中已经发生的事件;
参数:①epfd - epoll句柄;
②events - 保存从内核中得到的已经发生的事件的集合;
③maxevents - 每次能处理的事件的最大个数;
④timeout - 超时检测,用法同poll函数;
返回值:成功返回发生事件的文件描述符的个数,失败返回-1;
使用示例:
//1.定义保存发生事件的数组,数组长度20
struct epoll_event events[20];
//2.创建epoll句柄
int epfd = epoll_create(1);
//3.注册关心的文件描述符的属性
event.data.fd = 0; //标准输入
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epdf,EPOLL_CTL_ADD,0,&event);
//4.等待并收集事件的发生
int ret_epoll = epoll_wait(epfd,events,20,-1);
//5.循环遍历事件集合,对比是哪个文件描述符发生了事件,然后进行相应的操作
for(int i = 0;i < ret_epoll;i++)
{
if(events[i].data.fd == 0)
{
//可执行程序;
}
}
epoll实现-服务器部分代码:
//用epoll实现IO多路复用
struct epoll_event event; //保存添加的文件描述符的信息
struct epoll_event events[20]; //保存内核发生事件的描述符
//创建epoll句柄
int epfd = epoll_create(1);
if (epfd < 0)
{
perror("epfd_create err");
return -1;
}
//注册关心事件的属性
event.data.fd = 0;
event.events = EPOLLIN | EPOLLET;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event) < 0)
{
perror(" 0 epoll_ctl err");
return -1;
}
//
event.data.fd = sockfd;
event.events = EPOLLIN | EPOLLET;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event) < 0)
{
perror("sockfd epoll_ctl err");
return -1;
}
int recvbytes;
char buf[128];
int ret_epoll, i;
//循环等待事件,循环执行相应事件的操作
while (1)
{
//等待事件的发生
if ((ret_epoll = epoll_wait(epfd, events, 20, -1)) < 0)
{
perror("epoll_wait err");
return -1;
}
//循环对比相应事件的文件描述符,然后进行相应操作
for (i = 0; i < ret_epoll; i++)
{
if (events[i].data.fd == 0)
{
fgets(buf, sizeof(buf), stdin);
printf("buf:%s", buf);
}
else if (events[i].data.fd == sockfd)
{
//阻塞等待客户端连接
if ((acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &len)) < 0)
{
perror("accept err");
return -1;
}
printf("accept ok\n");
//打印客户端信息
printf("client:ip=%s,port=%d\n", inet_ntoa(clientaddr.sin_addr),
ntohs(clientaddr.sin_port));
//将acceptfd添加到epfd中
event.data.fd = acceptfd;
event.events = EPOLLIN | EPOLLET;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, acceptfd, &event) < 0)
{
perror("epoll_ctl err");
return -1;
}
}
else if (events[i].data.fd == acceptfd)
{
if ((recvbytes = recv(events[i].data.fd, buf, sizeof(buf), 0)) < 0)
{
perror("recv err");
return -1;
}
else if (recvbytes == 0)
{
printf("client exit\n");
//客户端退出后,将对应文件描述符从epoll中删除
epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
close(events[i].data.fd);
}
else
{
printf("buf:%s\n", buf);
}
}
}
}
四,select,poll和epoll的特点
1.select
①一个进程最多监听1024个文件描述;
②select每次被唤醒,都需要重新轮询一遍驱动函数,消耗CPU资源,效率较低;
③select每次都会清空表,每次都需要拷贝用户空间的表到内核空间,效率低。
2.poll
①没有监听的文件描述符的数量限制;
②poll被唤醒后,也需要重新轮询;
③poll不需要重新构造表,只需要从用户空间向内核空间拷贝一次数据。
3.epoll
①没有监听的文件描述符的数量限制(取决于使用的系统);
②使用异步IO,监听的事件产生后,epoll函数被唤醒,文件描述符主动调用callback(回调函数)直接拿到产生事件的文件描述符,不需要轮询,效率高;
③epoll也不需要重新构造表,只需要从用户空间向内核空间拷贝一次数据。