IO多路复用,使用select,poll,epoll三个函数分别实现

目录

一,使用select实现IO多路复用 

二,使用poll实现IO多路复用

三,使用epoll实现IO多路复用

四,select,poll和epoll的特点


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也不需要重新构造表,只需要从用户空间向内核空间拷贝一次数据。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值