linux下select、poll、epoll机制使用介绍

718 阅读5分钟

「这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

一、功能介绍

在上一篇文章juejin.cn/post/702874… 里,编写了一个 Linux下基于TCP协议的群聊系统设计(多线程+select) 案例,演示了select函数的使用方法。这篇文章接着介绍剩下的poll、epoll函数。并且也是使用群聊系统的案例编写例子,方便理解实际用法。

下面先介绍这3种函数的原型和相关头文件 虽然select在上篇文章里已经介绍过,这里为了方便比较这3个函数,再把select详细介绍粘贴过来。

1.1 select函数

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
函数功能: 监听指定数量的文件描述符的状态。
函数参数:
int nfds : 监听最大的文件描述符+1的值
fd_set *readfds :监听读事件的文件描述符集合,不想监听读事件这里可以填NULL
fd_set *writefds :监听写事件的文件描述符集合,不想监听事件这里可以填NULL
fd_set *exceptfds :监听其他事件的文件描述符集合,不想监听事件这里可以填NULL
struct timeval *timeout : 指定等待的时间。 如果填NULL表示永久等待,直到任意一个文件描述符产生事件再返回。
                          如果填正常时间,如果在等待的时间内没有事件产生,也会返回。
struct timeval {
        long    tv_sec;         /* seconds */
        long    tv_usec;        /* microseconds */
    };

返回值: 表示产生事件文件描述符数量。  ==0表示没有事件产生。  >0表示事件数量  <0表示错误。

void FD_CLR(int fd, fd_set *set);   //清除某个集合里的指定文件描述符
int  FD_ISSET(int fd, fd_set *set); //判断指定集合里的指定文件描述符是否产生了某个事件。 为真就表示产生了事件
void FD_SET(int fd, fd_set *set);  //将指定的文件描述符添加到指定的集合
void FD_ZERO(fd_set *set);  //清空整个集合。

1.2 poll函数

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
函数功能: 监听多个文件描述符的事件。
函数参数:
struct pollfd *fds :监听的事件,可以填结构体数组。
nfds_t nfds :监听的数量。
int timeout :等待的时间.ms单位.  >0表示正常等待时间。 ==0表示不等待  <0永久等待.
返回值:
    产生事件数量

struct pollfd {
    int   fd;         /* file descriptor  监听的文件描述符*/
    short events;     /* requested events 监听的事件   POLLIN表示可读事件*/
    short revents;    /* returned events  产生的事件--作为判断条件*/
};

1.3 epoll函数

#include <sys/epoll.h>

int epoll_create(int size);
函数功能: 创建epoll专用文件描述符的缓冲区。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数功能: 添加、删除、修改 监听文件描述符。
函数参数:
int epfd  epoll专用的文件描述符
int op    操作命令。EPOLL_CTL_ADD  EPOLL_CTL_MOD  EPOLL_CTL_DEL
int fd    要操作文件描述符
struct epoll_event *event  存放监听文件描述符信息的结构体

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
函数功能: 等待事件发生。
函数参数:
int epfd  epoll专用的文件描述符
struct epoll_event *events : 存放产生事件的文件描述结构体。
int maxevents  :最大监听的数量.
int timeout    :等待事件ms单位.   <0  >0 ==0

返回值:  产生事件的数量。

typedef union epoll_data {
    void    *ptr;
    int      fd;    //文件描述符
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

struct epoll_event {
    uint32_t     events;    /* Epoll events      EPOLLIN  输入事件 */
    epoll_data_t data;      /* User data variable */
};

二、epoll、poll函数使用案例

在上篇文章里已经贴出了详细的群聊系统的源码,并且有运行效果图、思路讲解。

这里就只贴出客户端的代码,把原来select监听的客户端的代码分别改成了epoll、poll两种。

2.1 poll函数使用案例: 应用在TCP客户端

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <sys/select.h>
#include <sys/time.h>
#include <poll.h>

//消息结构体
struct MSG_DATA
{
    char type; //消息类型.  0表示有聊天的消息数据  1表示好友上线  2表示好友下线
    char name[50]; //好友名称
    int number;   //在线人数的数量
    unsigned char buff[100];  //发送的聊天数据消息
};
struct MSG_DATA msg_data;

//文件接收端
int main(int argc,char **argv)
{   
    if(argc!=4)
    {
        printf("./app  <IP地址> <端口号> <名称>\n");
        return 0;
    }
    int sockfd;
    //忽略 SIGPIPE 信号--方式服务器向无效的套接字写数据导致进程退出
    signal(SIGPIPE,SIG_IGN); 

    /*1. 创建socket套接字*/
    sockfd=socket(AF_INET,SOCK_STREAM,0);
    /*2. 连接服务器*/
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;
    addr.sin_port=htons(atoi(argv[2])); // 端口号0~65535
    addr.sin_addr.s_addr=inet_addr(argv[1]); //IP地址
    if(connect(sockfd,(const struct sockaddr *)&addr,sizeof(struct sockaddr_in))!=0)
    {
        printf("客户端:服务器连接失败.\n");
        return 0;
    }

    /*3. 发送消息表示上线*/
    msg_data.type=1;
    strcpy(msg_data.name,argv[3]);
    write(sockfd,&msg_data,sizeof(struct MSG_DATA));

    int cnt;
    struct pollfd fds[2];
    fds[0].fd=sockfd;
    fds[0].events=POLLIN;
    fds[1].fd=0;
    fds[1].events=POLLIN;
    
    while(1)
    {
       //监听事件
       cnt=poll(fds,2,-1);
        if(cnt)
        {
            if(fds[0].events&fds[0].revents) //判断收到服务器的消息
            {
                cnt=read(sockfd,&msg_data,sizeof(struct MSG_DATA));
                if(cnt<=0) //判断服务器是否断开了连接
                {
                    printf("服务器已经退出.\n");
                    break;
                }
                else if(cnt>0)
                {
                    if(msg_data.type==0)
                    {
                        printf("%s:%s  在线人数:%d\n",msg_data.name,msg_data.buff,msg_data.number);
                    }
                    else if(msg_data.type==1)
                    {
                        printf("%s 好友上线. 在线人数:%d\n",msg_data.name,msg_data.number);
                    }
                    else if(msg_data.type==2)
                    {
                        printf("%s 好友下线. 在线人数:%d\n",msg_data.name,msg_data.number);
                    }
                }
            }
            
            if(fds[1].events&fds[1].revents)  //判断键盘上有输入
            {
                gets(msg_data.buff); //读取键盘上的消息
                msg_data.type=0; //表示正常消息
                strcpy(msg_data.name,argv[3]); //名称
                write(sockfd,&msg_data,sizeof(struct MSG_DATA));
            }
        }
    }
    close(sockfd);
    return 0;
}

2.2 epoll函数使用案例: 应用再TCP客户端

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <sys/select.h>
#include <sys/time.h>
#include <poll.h>
#include <sys/epoll.h>

//消息结构体
struct MSG_DATA
{
    char type; //消息类型.  0表示有聊天的消息数据  1表示好友上线  2表示好友下线
    char name[50]; //好友名称
    int number;   //在线人数的数量
    unsigned char buff[100];  //发送的聊天数据消息
};
struct MSG_DATA msg_data;

#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int epollfd;
int nfds;

//文件接收端
int main(int argc,char **argv)
{   
    if(argc!=4)
    {
        printf("./app  <IP地址> <端口号> <名称>\n");
        return 0;
    }
    int sockfd;
    //忽略 SIGPIPE 信号--方式服务器向无效的套接字写数据导致进程退出
    signal(SIGPIPE,SIG_IGN); 

    /*1. 创建socket套接字*/
    sockfd=socket(AF_INET,SOCK_STREAM,0);
    /*2. 连接服务器*/
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;
    addr.sin_port=htons(atoi(argv[2])); // 端口号0~65535
    addr.sin_addr.s_addr=inet_addr(argv[1]); //IP地址
    if(connect(sockfd,(const struct sockaddr *)&addr,sizeof(struct sockaddr_in))!=0)
    {
        printf("客户端:服务器连接失败.\n");
        return 0;
    }

    /*3. 发送消息表示上线*/
    msg_data.type=1;
    strcpy(msg_data.name,argv[3]);
    write(sockfd,&msg_data,sizeof(struct MSG_DATA));

    int cnt;
    int i;
    //创建专用文件描述符
    epollfd = epoll_create(10);
    //添加要监听的文件描述符
    ev.events = EPOLLIN;
    ev.data.fd = sockfd;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev);

    ev.events = EPOLLIN;
    ev.data.fd = 0; //标准输入文件描述符
    epoll_ctl(epollfd, EPOLL_CTL_ADD, 0, &ev);

    while(1)
    {
        //监听事件
        nfds=epoll_wait(epollfd,events,MAX_EVENTS,-1);
        if(nfds)
        {
            for(i=0;i<nfds;i++)
            {
                if(events[i].data.fd==sockfd) //判断收到服务器的消息
                {
                    cnt=read(sockfd,&msg_data,sizeof(struct MSG_DATA));
                    if(cnt<=0) //判断服务器是否断开了连接
                    {
                        printf("服务器已经退出.\n");
                        goto SERVER_ERROR;
                    }
                    else if(cnt>0)
                    {
                        if(msg_data.type==0)
                        {
                            printf("%s:%s  在线人数:%d\n",msg_data.name,msg_data.buff,msg_data.number);
                        }
                        else if(msg_data.type==1)
                        {
                            printf("%s 好友上线. 在线人数:%d\n",msg_data.name,msg_data.number);
                        }
                        else if(msg_data.type==2)
                        {
                            printf("%s 好友下线. 在线人数:%d\n",msg_data.name,msg_data.number);
                        }
                    }
                }
                else if(events[i].data.fd==0) //表示键盘上有数据输入
                {
                    gets(msg_data.buff); //读取键盘上的消息
                    msg_data.type=0; //表示正常消息
                    strcpy(msg_data.name,argv[3]); //名称
                    write(sockfd,&msg_data,sizeof(struct MSG_DATA));
                }
            }
        }
    }
SERVER_ERROR:
    close(sockfd);
    return 0;
}