zl程序教程

您现在的位置是:首页 >  硬件

当前栏目

高性能服务器编程之I/O复用---poll

服务器编程高性能 --- 复用 poll
2023-09-14 09:15:35 时间

2019年10月27日10:01:07
之前已经分析过了select,其实掌握了select之后 再去理解poll会理解起来容易一些。详见博客:

背景说明

poll系统调用和select是很类似的,也是在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者,poll的原型如下:

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

那么下面咱们就详细分析一下这几个参数:

  • fds参数 它是一个 pollfd 结构类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件。pollfd结构体的定义如下:
struct pollfd 
{
   int   fd;         /* 文件描述符 */
   short events;     /* 注册的事件 */
   short revents;    /* 实际发生的事件,由内核返回 */
};
//注:多看看第3个参数:由内核返回

其中,fd成员指定文件描述符;events成员告诉poll监听fd上的哪些事件,它是一系列事件的按位或 (待会儿,下面有代码演示);revents成员则由内核修改,以通知应用程序fd上实际发生了哪些事件。(大家好好体会一下第3个这个参数
poll支持的事件类型如下:

事件是否能设置events是否能由revents返回详细描述事件类型
POLLIN可以不阻塞的读普通和优先级数据(和POLLRDNORM或POLLRDBAND 等效)可读
POLLRDNORM可以不阻塞的读普通数据可读
POLLRDBAND可以不阻塞的读优先级数据(Linux不支持)可读
POLLPRI可以不阻塞的读高优先级数据(TCP带外数据)可读
POLLRDHUPTCP连接被对方关闭或对方关闭了写操作,由GNU引入可读
POLLOUT可以不阻塞的写普通和优先级数据可写
POLLWRNORM可以不阻塞的写普通数据可写
POLLWRBAND可以不阻塞的写优先级数据可写
POLLERR指定描述符发生错误异常
POLLHUP指定文件描述符挂起事件:比如管道的写端被关闭后,读端描述符上将收到该事件异常
POLLNVAL指定描述符非法:文件描述符没有打开,比如引用一个没有打开文件异常

但是上表的POLLRDNORM、POLLRDBAND、POLLWRNORM、POLLWRBAND 由XOPEN规范定义。它们实际上是将POLLIN事件和POLLOUT事件分得更细致,以区别对待普通数据和优先数据。但Linux并不完全支持它们。

通常情况下:应用程序需要根据recv调用的返回值来区分socket上接收到的是有效数据还是对方关闭连接的请求,并做相应的处理。 不过,自Linux内核2.6.17开始,GNU为poll系统凋用增加了一个POLLRDHUP事件(很重要的),它在sockct上接收到对方关闭连接的请求之后触发。这为我们区分上述两种情况提供了一种更简单的方式。但使用POLLRDHUP事件时,我们需要在代码最开始处定义_GNU_SOURCE。(但是需要注意这么一件事:POLLRDHUP事件发生的话,那么也一定会触发POLLIN事件。也就是说:client关闭连接的时候,会被POLLIN事件读入处理掉。所以解决办法就是:POLLIN事件放在POLLRDHUP事件处理的后面)。
在这里插入图片描述
所以需要在代码最开始处定义_GNU_SOURCE宏。

  • nfds_t nfds 参数指定被监听事件集合 fds 的大小,其类型 nfds_t 的定义如下:
/* Type used for the number of file descriptors.  */
typedef unsigned long int nfds_t;

这个参数 说白了:就是告诉内核前面的这个数组有多大。(用户关注的文描的个数)

  • timeout参数 它主要是指定poll的超时时间,单位是亳秒,当timeout为 -1 时,poll调用将永远阻塞,直到某个事件发生当timeout为0时,poll调用将立即返回
  • 注:poll系统调用的返回值的含义与select一致:成功时返回就绪(可读、可写和异常)的文件描述符的总数;而如果在超时时间内没有任何描述符就绪,返回0(超时);失败返回-1。

基于poll实现的服务器 连接多客户端

这里实现起来相对于select就简便很多了。效果展示如下:
在这里插入图片描述
在这里插入图片描述
源代码如下:

#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>

#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <poll.h>

#define MAX 100

int create_socket()
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd == -1)
    {
        return -1;
    }
    
    struct sockaddr_in saddr;
    
    memset(&saddr,0,sizeof(saddr));

    saddr.sin_family=AF_INET;
    saddr.sin_port=htons(6000);
    saddr.sin_addr.s_addr=inet_addr("127.0.0.1");

    int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(res==-1)
    {
        return -1;
    }
    if(listen(sockfd,5)==-1)
    {
        return -1;
    }
    return sockfd;
}
//初始化fds数组,其中文件描述符fd置为-1,其余都置为0
void fds_init(struct pollfd fds[])
{
    int i=0;
    for(;i<MAX;++i)
    {
        fds[i].fd=-1;
        fds[i].events=0;
        fds[i].revents=0;
    }
}

//向fds数组中添加文件描述符fd、并指定关注的事件
void fds_add(struct pollfd fds[],int fd)//添加到这个数组里面
{
    if(fd<0 || fd>=MAX)
    {
        printf("fd value error\n");
        return ;
    }
    fds[fd].fd=fd;
    fds[fd].events=POLLIN | POLLRDHUP;//读事件 和 退出关闭事件
    fds[fd].revents=0;//由内核填充
}
//在fds数组中删除指定文件描述符fd及其相关事件信息
void fds_del(struct pollfd fds[],int fd)
{
    if(fd<0 || fd>=MAX)
    {
        printf("fd value error\n");
        return;
    }
    fds[fd].fd=-1;
    fds[fd].events=0;
    fds[fd].revents=0;//由内核填充
}

int main()
{
    int sockfd=create_socket();
    assert(sockfd != -1);
    
    //初始化pollfd类型的结构体数组fds
    struct pollfd fds[MAX];
    //初始化fds数组:fd置为-1表示无效文描
    fds_init(fds);

    fds_add(fds,sockfd);//把sockfd添加到数组集合中
    
    
    while(1)
    {
    	//使用poll系统调用轮询,测试其中是否有就绪的文描
        int n=poll(fds,MAX,5000);//5秒时间
        if(n==-1)
        {
            perror("poll error");
            continue;
        }
        else if(n==0)//返回0表示超时:文描都没有变化
        {
            printf("time out\n");
            continue;
        }
        else//fds数组存在就绪的文件描述符
        {
            int i=0;
            for(;i<MAX;++i)//循环遍历fds这个数组
            {
                if(fds[i].fd==-1)//无效的描述符
                {
                    continue;
                }
                
                //可得是哪个文件描述符上就绪,且fds[i]中的成员revents由系统内核修改
                //现在client 退出和强行关闭,而下面的else就不会执行了
                if(fds[i].revents & POLLRDHUP)//对方关闭了
                {
                    close(fds[i].fd);
                    fds_del(fds,fds[i].fd);
                    printf("one client hup\n");
                    continue;
                }
				
				//这个事件处理放在后面
                if(fds[i].revents & POLLIN)//上面有读事件
                {
                	//说明监听队列中有连接待处理,则使用accept拿出一个连接
                    if(fds[i].fd==sockfd)
                    {
                        struct sockaddr_in caddr;
                        int len=sizeof(caddr);
						
						//接收一个套接字已建立的连接,得到连接套接字c
                        int c=accept(sockfd,
                        			(struct sockaddr*)&caddr,&len);
                        if(c<0)
                            continue;
                        printf("accept c=%d\n",c);
                        //将新的连接套接字connfd,添加到pollfd结构类型的数组fds
                        fds_add(fds,c);
                    }
                    else //没有新连接产生,是有客户端发来了数据
                    {
                        char buff[128]={0};
                        //直接使用recv接收客户端数据,并进行打印
                        int num=recv(fds[i].fd,buff,5,0);
						
						//接收服务器端的数据是0,说明客户端已经关闭
                        if(num<=0)
                        {
                            printf("one client close\n");
                            //关闭文件描述符fds[i].fd
                            close(fds[i].fd);
                            //从fds数组中删除此文件描述符
                            fds_del(fds,fds[i].fd);
                            continue;
                        }
                        //n不为0,即接收到了数据,于是打印并向客户端回复
                        printf("read(%d)=%s\n",fds[i].fd,buff);
                        send(fds[i].fd,"ok",2,0);
                    }
                }
            }
        }
    }

}

poll的小结

与select的区别如下:

  • poll将描述符和事件统一到一个结构体中:关注的事件不再是通过3个变量去传递,而是借助于这里的成员变量:short events; 表示。因此类型就可以很多了。
  • poll能够同时监听的文件描述符比select多,select的最大为1023(监听1024个),也即:文描不再是按位来表示,直接通过无符号长整型类型。且由于nfds_t nfds的存在,前面的是一个用户开辟的数组的首地址,那么我就可以开nfds个元素的个数。
  • poll事件类型比select多:包括优先级带数据可读,高优先级数据可读等事件类型
  • 最后用户关注的 和 内核修改的 分开表示,这样的话 每次调用就不用重新设置了

poll的基本实现原理: poll函数与select函数原理基本相同,只是它不需要每次将位集合清0,因为其有一个结构体数组,每个结构体数组都包含一个文件描述符和关注事件以及实际发生的事件。调用poll函数之后,内核会将当前正在监听的该文件描述符上的实际发生的事件存储在revents中,我们只需要核对该文件描述符上实际发生的事件是否是我们想要关注的事件。如果是,则判断是否是当前我们关心的监听文件描述符,如果是,执行相应的业务操作进行客户端连接,完成三次握手;否则,就有可能是客户端已经连接的文件描述符上的数据已经准备就绪,这时我们只需要调用recv函数进行读取数据就可以了。

而且见我上面的程序:

char buff[128]={0};
//直接使用recv接收客户端数据,并进行打印
int num=recv(fds[i].fd,buff,5,0);

这里每次值读5,那么肯定有时候一次读不完的。那么服务器端就会调用poll一共字符串的长度加1次(包含回车),最终服务器端的终端上会依次将输入的字符串以一次5个字符的形式打印显示出来,再输出一个回车字符。原因在于内核会一直提醒我们该文件描述符上有数据准备就绪,一直等到我们将所有数据处理完成后才停止。

客户端的代码

最近有小伙伴问我要客户端的代码,这件事情 我没太注意,这所有发的代码都是服务器端的。至于client的都是一样的,以后也是这套代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>//转化端口号的
#include <arpa/inet.h>//转化IP地址的


int main()
{
    //创建socket
    int sockfd = socket(AF_INET,SOCK_STREAM,0);//创建socket
    assert(sockfd != -1);

    struct sockaddr_in saddr,caddr;
    memset(&saddr,0,sizeof(saddr));
    
    saddr.sin_family = AF_INET;
    saddr.sin_port   =htons(6000);//给定端口号
    //下面要求是大端形式
    saddr.sin_addr.s_addr= inet_addr("127.0.0.1");

    //saddr.sin_addr.s_addr= inet_addr("192.168.31.225");

    int res=connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    assert(res != -1);
    
    while(1)
    {
        char buff[128]={0};
        printf("input:\n");
        fgets(buff,128,stdin);
        
        buff[strlen(buff)-1] = 0;//去掉回车
        if(strcmp(buff,"end")==0)
        {
            break;
        }

        send(sockfd,buff,strlen(buff),0);
        memset(buff,0,128);
        recv(sockfd,buff,127,0);

        printf("buff=%s\n",buff);

    }
    close(sockfd);
}