poll系统调用和select类似,也是在指定时间内轮询一定数量的文件描述符,以测试器中是否由就绪者。poll原型如下所示:
1
2 3 |
#include <poll.h>
int poll( struct pollfd *fds, nfds_t nfds, int timeout); |
1)fds参数是一个pollfd结构类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件。pollfd结构体的定义如下:
1
2 3 4 5 6 |
struct pollfd
{ int fd; //文件描述符 short events; //注册的事件 short revents; //实际发生事件,由内核填充 }; |
其中,fd成员指定文件描述符:events成员告诉poll监听fd上的那些事件,它是一系列事件的按位或,revents成员则由内核修改,以通知应用程序fd上世纪发生了那些事件。poll支持的事件类型如下所示:
常量
|
说明
|
POLLIN
|
普通或优先级带数据可读
|
POLLRDNORM
|
普通数据可读
|
POLLRDBAND
|
优先级带数据可读
|
POLLPRI
|
高优先级数据可读
|
POLLOUT
|
普通数据可写
|
POLLWRNORM
|
普通数据可写
|
POLLWRBAND
|
优先级带数据可写
|
POLLERR
|
发生错误
|
POLLHUP
|
发生挂起
|
POLLNVAL
|
描述字不是一个打开的文件
|
通常,应用程序需要根据recv调用的返回值来区分socket上接收到的是有效数据还是对方关闭连接的请求,并做相应的处理。不过,自Linux内核2.6.17开始,GNU为poll系统条用增加了一个POLLRDHUP事件,它在socket上接收到对方关闭连接的请求之后出发。这位我们区分上述两种情况提供了一种更简单的方式。但是用POLLRDHUP事件时,我们需要再代码最开始定义_GNU_SOURCE。
2)nfds参数指定被监听事件集合fds的大小。其类型nfds_t的定义如下:
1
|
typedef
unsigned
long
int nfds_t;
|
poll系统调用的返回值的含义与select相同。
聊天室程序:
像ssh这样的登录程序通常要同时处理网络连接和用户输入,这也可以使用I/O复用来实现。本节我们以poll为例子实现一个简单的聊天室程序,以阐述如何使用I/O复用技术来同时处理网络连接和用户输入。该聊天室程序能让所有用户同时在线群聊,它分为客户端和服务器两部分。其中客户端程序有两个功能:一是从标准输入终端读入用户数据,并将用户数据发送至服务器;二是往标准输出终端打印服务器发送给它的数据。服务器的功能是接收客户数据,并把客户数据发送给每一个登录到该服务器上的客户端(数据发送者除外),下面我们依次给出客户端程序和服务器程序的代码。
1、客户端
客户端程序使用poll同时监听用户输入和网络连接,并利用splice函数将用户输入内容直接定向到网络连接上以发送之,从而实现数据零拷贝,提高程序执行效率。客户端程序代码如下所示:
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 |
#define _GNU_SOURCE
1
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <assert.h> #include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <poll.h> #include <fcntl.h> #define BUFFER_SIZE 64 int main( int argc, char *argv[] ) { if( argc <= 2 ) { printf( "usage: %s ip_address port_number\n", basename( argv[ 0] ) ); return 1; } const char *ip = argv[ 1]; int port = atoi( argv[ 2] ); struct sockaddr_in server_address; bzero( &server_address, sizeof( server_address ) ); server_address.sin_family = AF_INET; inet_pton( AF_INET, ip, &server_address.sin_addr ); server_address.sin_port = htons( port ); int sockfd = socket( PF_INET, SOCK_STREAM, 0 ); assert( sockfd >= 0 ); if ( connect( sockfd, ( struct sockaddr * )&server_address, sizeof( server_address ) ) < 0 ) { printf( "connection failed\n" ); close( sockfd ); return 1; } //注册文件描述符0(标准输入)和文件描述符sockfd上的可读事件 pollfd fds[ 2]; fds[ 0].fd = 0; //数据(包括普通数据和优先数据)可读 fds[ 0].events = POLLIN; fds[ 0].revents = 0; fds[ 1].fd = sockfd; //TCP连接被对方关闭,或者对方关闭了写操作。 fds[ 1].events = POLLIN | POLLRDHUP; fds[ 1].revents = 0; char read_buf[BUFFER_SIZE]; int pipefd[ 2]; //无名管道 int ret = pipe( pipefd ); assert( ret != - 1 ); while( 1 ) { ret = poll( fds, 2, - 1 ); //阻塞poll机制 if( ret < 0 ) { printf( "poll failure\n" ); break; } //检测服务关闭事件 if( fds[ 1].revents & POLLRDHUP ) { printf( "server close the connection\n" ); break; } //检测数据可读事件 else if( fds[ 1].revents & POLLIN ) { memset( read_buf, '\0', BUFFER_SIZE ); recv( fds[ 1].fd, read_buf, BUFFER_SIZE - 1, 0 ); printf( "%s\n", read_buf ); } //检测命令行终端是否有数据可读 if( fds[ 0].revents & POLLIN ) { //使用splice将用户输入的数据直接写到sockfd上(零拷贝),通过管道 ret = splice( 0, NULL, pipefd[ 1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE ); ret = splice( pipefd[ 0], NULL, sockfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE ); } } close( sockfd ); return 0; } |
2、服务器
服务程序使用poll同时管理监听socket和连接socket,并且使用牺牲空间换取时间的策略来提高服务器的性能。代码清单如下所示。
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 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 |
#define _GNU_SOURCE
1
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <assert.h> #include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <fcntl.h> #include <stdlib.h> #include <poll.h> //最大用户数量 #define USER_LIMIT 5 //该缓冲区的大小 #define BUFFER_SIZE 64 //文件描述符的数量限制 #define FD_LIMIT 65535 struct client_data { //客户数据:客户端socket地址 sockaddr_in address; //待写到客户端数据的位置 char *write_buf; //从客户端读入的数据 char buf[ BUFFER_SIZE ]; }; //设置非阻塞 int setnonblocking( int fd ) { int old_option = fcntl( fd, F_GETFL ); int new_option = old_option | O_NONBLOCK; fcntl( fd, F_SETFL, new_option ); return old_option; } int main( int argc, char *argv[] ) { if( argc <= 2 ) { printf( "usage: %s ip_address port_number\n", basename( argv[ 0] ) ); return 1; } const char *ip = argv[ 1]; int port = atoi( argv[ 2] ); int ret = 0; struct sockaddr_in address; bzero( &address, sizeof( address ) ); address.sin_family = AF_INET; inet_pton( AF_INET, ip, &address.sin_addr ); address.sin_port = htons( port ); //创建TCP socket,并将其绑定到端口port上 int listenfd = socket( PF_INET, SOCK_STREAM, 0 ); assert( listenfd >= 0 ); ret = bind( listenfd, ( struct sockaddr * )&address, sizeof( address ) ); assert( ret != - 1 ); ret = listen( listenfd, 5 ); assert( ret != - 1 ); //创建user数组,分配FD_LIMIT各Client_data对象。 //可以预期:每个可能的socket连接都可以获得一个这样的对象 //并且socket的值可以直接用来索引socket连接对象的client_data对象 //这是将socket和客户数据管理的简单而高效的方式 client_data *users = new client_data[FD_LIMIT]; //尽管我们分配了足够多的client_data对象,但为了提供poll的性能,仍然有比亚限制用户的数量 pollfd fds[USER_LIMIT + 1]; int user_counter = 0; for( int i = 1; i <= USER_LIMIT; ++i ) { fds[i].fd = - 1; fds[i].events = 0; } fds[ 0].fd = listenfd; fds[ 0].events = POLLIN | POLLERR; //用于数据读入和数据出错 fds[ 0].revents = 0; while( 1 ) { ret = poll( fds, user_counter + 1, - 1 ); //poll阻塞 if ( ret < 0 ) { printf( "poll failure\n" ); break; } //用for循环来检测每个结构体 for( int i = 0; i < user_counter + 1; ++i ) { //监听是否由新的连接进来。 if( ( fds[i].fd == listenfd ) && ( fds[i].revents & POLLIN ) ) //服务器监听端口 { struct sockaddr_in client_address; socklen_t client_addrlength = sizeof( client_address ); int connfd = accept( listenfd, //接受连接 ( struct sockaddr * )&client_address, &client_addrlength ); if ( connfd < 0 ) { printf( "errno is: %d\n", errno ); continue; } //如果请求太多,则关闭新到的连接 if( user_counter >= USER_LIMIT ) { const char *info = "too many users\n"; printf( "%s", info ); send( connfd, info, strlen( info ), 0 ); close( connfd ); continue; } //对于新的连接,同时修改fds和users数组 //users[connfd]对应于新连接文件描述符connfd的客户数据 user_counter++; users[connfd].address = client_address; setnonblocking( connfd ); fds[user_counter].fd = connfd; fds[user_counter].events = POLLIN | POLLRDHUP | POLLERR; fds[user_counter].revents = 0; printf( "comes a new user, now have %d users\n", user_counter ); } //监听错误链接 else if( fds[i].revents & POLLERR ) { printf( "get an error from %d\n", fds[i].fd ); char errors[ 100 ]; memset( errors, '\0', 100 ); socklen_t length = sizeof( errors ); if( getsockopt( fds[i].fd, SOL_SOCKET, SO_ERROR, &errors, &length ) < 0 ) { printf( "get socket option failed\n" ); } continue; } //监听客户端关闭 else if( fds[i].revents & POLLRDHUP ) { users[fds[i].fd] = users[fds[user_counter].fd]; close( fds[i].fd ); fds[i] = fds[user_counter]; i--; user_counter--; printf( "a client left\n" ); } //监听数据可读 else if( fds[i].revents & POLLIN ) { int connfd = fds[i].fd; memset( users[connfd].buf, '\0', BUFFER_SIZE ); ret = recv( connfd, users[connfd].buf, BUFFER_SIZE - 1, 0 ); printf( "get %d bytes of client data %s from %d\n", ret, users[connfd].buf, connfd ); if( ret < 0 ) { if( errno != EAGAIN ) { close( connfd ); users[fds[i].fd] = users[fds[user_counter].fd]; fds[i] = fds[user_counter]; i--; user_counter--; } } else if( ret == 0 ) { printf( "code should not come to here\n" ); } else { //如果接收到客户数据,则通知其他socket连接准备写数据 for( int j = 1; j <= user_counter; ++j ) { if( fds[j].fd == connfd ) { continue; } fds[j].events |= ~POLLIN; fds[j].events |= POLLOUT; users[fds[j].fd].write_buf = users[connfd].buf; } } } else if( fds[i].revents & POLLOUT ) { int connfd = fds[i].fd; //非本机 if( ! users[connfd].write_buf ) { continue; } //发送数据 ret = send( connfd, users[connfd].write_buf, strlen( users[connfd].write_buf ), 0 ); users[connfd].write_buf = NULL; fds[i].events |= ~POLLOUT; fds[i].events |= POLLIN; } } } //删除空间 delete [] users; //关闭连接 close( listenfd ); return 0; } |