poll系统调用和select系统调用相似,也是在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者。
poll系统调用原型:
#include<poll.h>
int poll(struct pollfd* fds,nfds_t nfds,int timeout);
1、fds参数是一个pollfd结构类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件。
pollfd结构体定义如下:
struct pollfd
{
int fd;//文件描述符
short events;//注册的事件
short revents;//实际发生的事件,由内核填充
};
其中,fd成员指定文件描述符;events成员告诉poll监听fd上的哪些事件,它是一系列事件的按位或;revents成员则由内核修改,以通知应用程序fd上实际发生了哪些事件。
事 件 | 描 述 |
POLLIN | 数据(包括普通数据和优先数据)可读 |
POLLRDNORM | 普通数据可读 |
POLLRDBAND | 优先级带数据可读(Linux不支持) |
POLLPRI | 高优先级数据可读,比如TCP带外数据 |
POLLOUT | 数据(包括普通数据和优先数据)可写 |
POLLWRNORM | 普通数据可写 |
POLLWRBAND | 优先级带数据可写 |
POLLRDHUP | TCP连接被对方关闭,或者对方关闭了写操作 |
POLLERR | 错误 |
POLLHUP | 挂起。比如管道的写端被关闭后,读端描述符上将收到POLLHUP事件 POLLNVAL 文件描述符没有打开 |
POLLNVAL | 文件描述符没有打开 |
上表中,POLLRDNORM、POLLRDBAND、POLLWRNORM、POLLWRBAND由XOPEN规范定义。它们实际上是将POLLIN和POLLOUT事件分得更细致,以区分对待普通数据和优先数据,但Linux不完全支持。
通常,应用程序需要根据recv调用的返回值来区分socket上接收到的是有效数据还是对方关闭连接的请求,并做相应的处理。不过,自LInux内核2.6.17开始,GNU为poll系统调用增加了一个POLLRDHUP时间,它在socket上接收到对方关闭连接的请求之后触发。但使用POLLRDHUP时间时,我们需要在代码最开始处定义_GNU_SOURCE。
2、nfds参数指定被监听事件集合fds的大小。其类型nfds_t的定义如下:
typedef unsigned long int nfds_t;
3、timeout参数指定poll的超时值,单位是毫秒(1s = 1000ms)。当timeout为-1时,poll调用将永远阻塞,直到某个事件发生;当timeout为0时,poll调用将立即返回。
4、poll系统调用的返回值与select相同:
- poll成功时返回就绪(可读、可写和异常)文件描述符的总数。
- 如果在超时时间内没有任何文件描述符就绪。poll将返回0。
- 失败时返回-1并设置errno。如果在poll等待期间,程序接收到信号,则select立即返回-1,并设置errno为EINTR
5、poll 和 select的相同点和不同点
不同点:
- poll long(可描述的文件描述符多) POLLIN、POLLOUT等,类型多
- select 1024bit 读/写/异常
相同点:
- select/poll 描述符和事件传给内核
- 内核实现:轮询检测,时间效率相当O(n)
- 找到就绪|检索就绪描述符O(n)
- 内核返回之后仍会遍历一遍
6、使用poll编写实现服务器
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<unistd.h>
#include<poll.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define MAXFD 10
int create_sockfd();//函数声明
//向fds数组中添加文件描述符fd和事件信息
void fds_add(struct pollfd fds[],int fd)
{
int i = 0;
for(;i<MAXFD;i++)
{
if(fds[i].fd == -1)
{
fds[i].fd = fd;
fds[i].events = POLLIN;
fds[i].revents = 0;
break;
}
}
}
//删除fds数组中文件描述符fd和事件信息
void fds_del(struct pollfd fds[],int fd)
{
int i = 0;
for(;i<MAXFD;i++)
{
if(fds[i].fd == fd)
{
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
}
}
}
//初始化fds数组
void fds_init(struct pollfd fds[])
{
int i = 0;
for(;i<MAXFD;i++)
{
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
}
}
int main()
{
int sockfd = create_sockfd();
assert(sockfd != -1);
struct pollfd fds[MAXFD];//定义pollfd类型的结构体数组fds
fds_init(fds);//初始化fds数组
fds_add(fds,sockfd);//添加信息
while(1)
{
int n = poll(fds,MAXFD,5000);//poll系统调用
if(n == -1)//失败
{
perror("poll error");
}
else if(n == 0)//超时
{
printf("time out\n");
}
else//当前数组有文件描述符就绪
{
int i = 0;
for(;i<MAXFD;i++)//循环遍历数组
{
if(fds[i].fd == -1)
{
continue;
}
if(fds[i].revents & POLLIN)
{
/*
此时有两种情况,若fds[i].fd == sockfd
说明监听队列中有连接待处理,使用accept建立一个连接
否则,说明没有新连接产生,是有客户端发来了数据,直接使
用recv接收客端数据,并打印,就ok
*/
if(fds[i].fd == sockfd)
{
//accept
struct sockaddr_in caddr;
int len = sizeof(caddr);
//接收一个套接字已建立的连接,得到连接套接字connfd
int c = accept(sockfd,(struct sockaddr*)&caddr,&len);
if(c<0)
{
continue;
}
printf("accept c = %d\n",c);
fds_add(fds,c);//将新的连接套接字加入fds数组
}
else
{
//recv
char buff[128] = {0};
int num = recv(fds[i].fd,buff,127,0);//接受客户端发来的数据
if (num <= 0)//说明客户端已经关闭
{
//close
close(fds[i].fd);//先关闭文件描述符
fds_del(fds,fds[i].fd);//将此文件描述符在fds数组里删除
printf("one client over\n");
}
else
{
printf("recv(%d) = %s\n",fds[i].fd,buff);
send(fds[i].fd,"ok",2,0);
}
}
}
}
}
}
}
int create_sockfd()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);//创建监听套接字
if(sockfd == -1)
{
return -1;
}
struct sockaddr_in saddr;//socket专用地址信息设置
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");
//命名套接字,将socket专用地址绑定到socket文件描述符上
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(res == -1)
{
return -1;
}
listen(sockfd,5);//创建监听队列
return sockfd;
}