1.poll

1.1 简单介绍

poll实际上实在select的基础上进行了改进,但是本质上还是要将维护的集合copy到内核进行线性遍历的计算,然后将整个集合copy返回给用户。

不过poll维护的集合更加直观,对一个结构体进行维护,官方介绍如下:

//The set of file descriptors to be monitored is specified in the fds argument, which is an array of //structures of the following form:

struct pollfd {
    int   fd;         /* file descriptor */
    short events;     /* requested events */
    short revents;    /* returned events */
};

可以看到整个结构体有三个变量。

  • fd表示我们需要监测的文件描述符
  • events表示我们需要检测的事件
  • revents表示经过内核检测这个文件描述符后返回的事件

下面我们简单介绍下poll函数。

NAME
    poll, ppoll - wait for some event on a file descriptor

SYNOPSIS
    #include <poll.h>
    int poll(struct pollfd *fds, nfds_t nfds, int timeout);

RETURN VALUE
       On success, poll() returns a nonnegative value which is the number of elements in the pollfds 			whose revents fields have  been  set  to  a  nonzero
       value (indicating an event or an error).  A return value of zero indicates that the system call 			timed out before any file descriptors became read.

       On error, -1 is returned, and errno is set to indicate the cause of the error.

从官方文档我们可以看到,poll的第一个参数为我们我们维护的集合(结构体变量)、nfds表示需要检测的文件描述符个数、timeout表示阻塞等待事件。

返回值:

成功:返回就绪的fd数量

​失败:返回-1

1.2 代码解读

1.2.1 网络编程基础

//1.创建一个用于监听请求的文件文件描述符(fd),命名为listen_fd
int listen_fd = socket(AF_INET,SOCK_STREAM,0);
if( listen_fd < 0){
    perror("socket:");
    exit(0);
}

//2.用sockaddr_in类型的结构体来存储本地server的信息
//包括地址族协议,端口号,IP地址
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(PORT);
serverAddr.sin_addr.s_addr = INADDR_ANY;
if(bind(listen_fd,(struct sockaddr *)&serverAddr,sizeof(serverAddr)) < 0){
    perror("bind:");
    exit(0);
}

//3.将设置好的listen_fd状态设置为监听,这个listen_fd会在内核中一直监听,不会主动关闭
//一次最多可以监听5个请求,然后将这些请求放到内核的缓冲区中,并用队列进行存储
printf("server is listening..\n");
if(listen(listen_fd,5) < 0){
    perror("listen:");
    exit(0);
}

1.2.2 初始化fd集合

//4.初始化需要维护并监听的结构体数组
struct pollfd myfd[MAX_POLLFD] = {0};
for(int i = 0;i < MAX_POLLFD;i++){
    myfd[i].fd = -1;
}

//首先加入用于监听连接的文件描述符listen_fd
myfd[0].fd = listen_fd;
myfd[0].events = POLLIN;
int nfds = 1; //监听的文件描述符数量

###1.2.3 循环监听并处理

再阅读代码之前需要先了解下位运算&的含义,先举个例子。

int x = 0b1010;   // 10,0b是二进制前缀
int mask = 0b0010; 

if(x & mask){
    printf("1");
}

x&mask的计算方式如下:

1010  //x
0010 //mask
----
0010 != 0  →判断了x从右往左的第二位不是0,x包含了mask事件

那么我们再实际编程中这个&运算有什么意义呢,请看如下代码:

//5.2判断用于监听新的请求的listen_fd是否就绪
if(myfd[0].revents & POLLIN){
	//略
}

我第一次看这个代码的时候不知道这是再干嘛,一脸懵,别急我们一步步拆解。

POLLIN代表读取数据的事件,来看看POLLIN的实际值是多少:

/* Event types that can be polled for.  These bits may be set in `events'
   to indicate the interesting event types; they will appear in `revents'
   to indicate the status of the file descriptor.  */
#define POLLIN		0x001		/* There is data to read.  */
#define POLLPRI		0x002		/* There is urgent data to read.  */
#define POLLOUT		0x004		/* Writing now will not block.  */

可以看到这是一个16进制的数字(0x表示16进制的前缀),POLLIN相关事件可以二进制详细表示如下:

#define POLLIN   0x001   // 0000 0001
#define POLLPRI  0x002   // 0000 0010
#define POLLOUT  0x004   // 0000 0100

因为这里的事件只有一个bit位为1,其他的位置为0,所以使用&可以直接判断其他事件的包含关系,怎么做到的呢?

假设myfd[0].revents = POLLIN|POLLOUT,即包含读取和写入事件,那么myfd[0].revents & POLLIN的计算如下:

0000 0101  //myfd[0].revents
0000 0001 //POLLIN
----
0000 0001 != 0  →说明判断事件revents从右往左的第1位不是0,则myfd[0].revents包含了POLLIN,进入if判断语句

接下来我们就可以进入while循环进行事件的监听和事件类型的判断,然后对其进行相关操作。

//5.循环监听并处理事件
while(1){
    //5.1使用poll函数来监听准备好的fd,返回就绪的fd数量,-1表示阻塞知道有事件就绪
    int nready = poll(myfd,nfds,-1);

    //5.2判断用于监听新的请求的listen_fd是否就绪
    if(myfd[0].revents & POLLIN){
        struct sockaddr_in clientAddr;
        socklen_t len = sizeof(clientAddr);

        int client_fd = accept(listen_fd,(struct sockaddr *)&clientAddr,&len);
        if(client_fd < 0){
            perror("accept:");
            continue;
        }

        printf("Connect client(%d)\n",client_fd);
        myfd[nfds].fd = client_fd;
        myfd[nfds].events = POLLIN;
        nfds++;
    }

    //5.3线性遍历结构体数组中的所有文件描述符,从1开始是因为存储listenfd信息的下标是0
    for(int i = 1;i <=nfds ;i++){
        int client_fd = myfd[i].fd;
        char buffer[BUFFER_MAXSIZE] = {0};

        //判断是否可以读取数据
        if(myfd[i].revents & POLLIN){
            int recv_len = recv(myfd[i].fd,buffer,BUFFER_MAXSIZE,0);
            if(recv_len == 0){
                printf("Client(%d) lose connection\n",client_fd);
                myfd[i].fd = -1;
                close(client_fd);
                continue;
            }
            printf("Client(%d) sends data(%dbytes):%s\n",client_fd,recv_len,buffer);   

            int send_len = send(client_fd,buffer,recv_len,0);
            if(send_len <= 0){
                perror("send:");
                continue;         
            }
        }
    }
}

1.2.3 完整代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <poll.h>

#define MAX_POLLFD 1024
#define BUFFER_MAXSIZE 1024
#define PORT 8080

int main(){
    //1.创建一个用于监听请求的文件文件描述符(fd),命名为listen_fd
    int listen_fd = socket(AF_INET,SOCK_STREAM,0);
    if( listen_fd < 0){
        perror("socket:");
        exit(0);
    }

    //2.用sockaddr_in类型的结构体来存储本地server的信息
    //包括地址族协议,端口号,IP地址
    struct sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(PORT);
    serverAddr.sin_addr.s_addr = INADDR_ANY;
    if(bind(listen_fd,(struct sockaddr *)&serverAddr,sizeof(serverAddr)) < 0){
        perror("bind:");
        exit(0);
    }

    //3.将设置好的listen_fd状态设置为监听,这个listen_fd会在内核中一直监听,不会主动关闭
    //一次最多可以监听5个请求,然后将这些请求放到内核的缓冲区中,并用队列进行存储
    printf("server is listening..\n");
    if(listen(listen_fd,5) < 0){
        perror("listen:");
        exit(0);
    }


    //4.初始化需要维护并监听的结构体数组
    struct pollfd myfd[MAX_POLLFD] = {0};
    for(int i = 0;i < MAX_POLLFD;i++){
        myfd[i].fd = -1;
    }

    //首先加入用于监听连接的文件描述符listen_fd
    myfd[0].fd = listen_fd;
    myfd[0].events = POLLIN;
    int nfds = 1;



    //5.循环监听并处理事件
    while(1){
        //5.1使用poll函数来监听准备好的fd,返回就绪的fd数量,-1表示阻塞知道有事件就绪
        int nready = poll(myfd,nfds,-1);

        //5.2判断用于监听新的请求的listen_fd是否就绪
        if(myfd[0].revents & POLLIN){
                struct sockaddr_in clientAddr;
                socklen_t len = sizeof(clientAddr);
               
                int client_fd = accept(listen_fd,(struct sockaddr *)&clientAddr,&len);
                if(client_fd < 0){
                    perror("accept:");
                    continue;
                }
                
                printf("Connect client(%d)\n",client_fd);
                myfd[nfds].fd = client_fd;
                myfd[nfds].events = POLLIN;
                nfds++;
        }

        //5.3线性遍历结构体数组中的所有文件描述符,从1开始是因为存储listenfd信息的下标是0
        for(int i = 1;i <=nfds ;i++){
                int client_fd = myfd[i].fd;
                char buffer[BUFFER_MAXSIZE] = {0};

                //判断是否可以读取数据
                if(myfd[i].revents & POLLIN){
                    int recv_len = recv(myfd[i].fd,buffer,BUFFER_MAXSIZE,0);
                    if(recv_len == 0){
                        printf("Client(%d) lose connection\n",client_fd);
                        myfd[i].fd = -1;
                        close(client_fd);
                        continue;
                    }
                printf("Client(%d) sends data(%dbytes):%s\n",client_fd,recv_len,buffer);   

                int send_len = send(client_fd,buffer,recv_len,0);
                if(send_len <= 0){
                    perror("send:");
                    continue;         
                }
        }
    }
    }

    return 0;
}

1.3 poll缺点简单分析

poll是在select的基础上进行改进的,所以二者使用的逻辑基本是一样的,但是poll的集合维护更加直观,而且poll监听的fd数量没有限制,select监听fd的·最大数量为1024poll本质上还是会在用户和内核之间反复copy 并线性循环的计算,效率上没有太大的提高。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐