1. poll函数
poll的机制与select类似,与select在本质上没有多大差别,使用方法也类似,下面的是对于二者的对比:
- 内核对应文件描述符的检测也是以线性的方式进行轮询,根据描述符的状态进行处理
- poll和select检测的文件描述符集合会在检测过程中频繁的进行用户区和内核区的拷贝,它的开销随着文件描述符数量的增加而线性增大,从而效率也会越来越低。
select检测的文件描述符个数上限是1024,poll没有最大文件描述符数量的限制
select可以跨平台使用,poll只能在Linux平台使用
poll函数的函数原型如下:
1 2 3 4 5 6 7 8 9 10
| #include <poll.h>
struct pollfd { int fd; short events; short revents; };
struct pollfd myfd[100]; int poll(struct pollfd *fds, nfds_t nfds, int timeout);
|
函数参数:
fds: 这是一个struct pollfd
类型的数组, 里边存储了待检测的文件描述符的信息,这个数组中有三个成员:
- fd:委托内核检测的文件描述符
- events:委托内核检测的fd事件(输入、输出、错误),每一个事件有多个取值
- revents:这是一个传出参数,数据由内核写入,存储内核检测之后的结果
nfds: 这是第一个参数数组中最后一个有效元素的下标 + 1(也可以指定参数1数组的元素总个数)
timeout: 指定poll函数的阻塞时长
- -1:一直阻塞,直到检测的集合中有就绪的文件描述符(有事件产生)解除阻塞
- 0:不阻塞,不管检测集合中有没有已就绪的文件描述符,函数马上返回
- 大于0:阻塞指定的毫秒(ms)数之后,解除阻塞
函数返回值:
- 失败: 返回-1
- 成功:返回一个大于0的整数,表示检测的集合中已就绪的文件描述符的总个数
2. 测试代码
服务器端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
| #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <sys/select.h> #include <poll.h>
int main() { int lfd = socket(AF_INET, SOCK_STREAM, 0); if(lfd == -1) { perror("socket"); exit(0); } struct sockaddr_in addr; addr.sin_port = htons(9999); addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr)); if(ret == -1) { perror("bind"); exit(0); } ret = listen(lfd, 100); if(ret == -1) { perror("listen"); exit(0); } struct pollfd fds[1024]; for(int i=0; i<1024; ++i) { fds[i].fd = -1; fds[i].events = POLLIN; } fds[0].fd = lfd;
int maxfd = 0; while(1) { ret = poll(fds, maxfd+1, -1); if(ret == -1) { perror("select"); exit(0); }
if(fds[0].revents & POLLIN) { struct sockaddr_in sockcli; int len = sizeof(sockcli); int connfd = accept(lfd, (struct sockaddr*)&sockcli, &len); int i; for(i=0; i<1024; ++i) { if(fds[i].fd == -1) { fds[i].fd = connfd; break; } } maxfd = i > maxfd ? i : maxfd; } for(int i=1; i<=maxfd; ++i) { if(fds[i].revents & POLLIN) { char buf[128]; int ret = read(fds[i].fd, buf, sizeof(buf)); if(ret == -1) { perror("read"); exit(0); } else if(ret == 0) { printf("对方已经关闭了连接...\n"); close(fds[i].fd); fds[i].fd = -1; } else { printf("客户端say: %s\n", buf); write(fds[i].fd, buf, strlen(buf)+1); } } } } close(lfd); return 0; }
|
从上面的测试代码可以得知,使用poll和select进行IO多路转接的处理思路是完全相同的,但是使用poll编写的代码看起来会更直观一些,select使用的位图的方式来标记要委托内核检测的文件描述符(每个比特位对应一个唯一的文件描述符),并且对这个fd_set
类型的位图变量进行读写还需要借助一系列的宏函数,操作比较麻烦。而poll直接将要检测的文件描述符的相关信息封装到了一个结构体struct pollfd
中,我们可以直接读写这个结构体变量。
另外poll的第二个参数有两种赋值方式,但是都和第一个参数的数组有关系:
- 使用参数1数组的元素个数
- 使用参数1数组中存储的最后一个有效元素对应的下标值 + 1
内核会根据第二个参数传递的值对参数1数组中的文件描述符进行线性遍历,这一点和select也是类似的。
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h>
int main() { int fd = socket(AF_INET, SOCK_STREAM, 0); if(fd == -1) { perror("socket"); exit(0); }
struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(9999); inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr); int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr)); if(ret == -1) { perror("connect"); exit(0); }
while(1) { char recvBuf[1024]; fgets(recvBuf, sizeof(recvBuf), stdin); write(fd, recvBuf, strlen(recvBuf)+1); read(fd, recvBuf, sizeof(recvBuf)); printf("recv buf: %s\n", recvBuf); sleep(1); } close(fd); return 0; }
|
客户端不需要使用IO多路转接进行处理,因为客户端和服务器的对应关系是 1:N,也就是说客户端是比较专一的,只能和一个连接成功的服务器通信。