1.10 poll函数
poll提供的功能与select类似,不过在处理流设备时,它能够提供额外的信息。
/* Poll the file descriptors described by the NFDS structures starting at
FDS. If TIMEOUT is nonzero and not -1, allow TIMEOUT milliseconds for
an event to occur; if TIMEOUT is -1, block until an event occurs.
Returns the number of file descriptors with events, zero if timed out,
or -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);
第一个参数是指向一个结构数组第一个元素的指针。每个数组元素都是一个pollfd,用于指定测试某个给定描述符fd的条件。
/* Data structure describing a polling request. */
struct pollfd
{
int fd; /* File descriptor to poll. */
short int events; /* Types of events poller cares about. */
short int revents; /* Types of events that actually occurred. */
};
要测试的条件由event成员指定,函数在相应的revents成员中返回该描述符的状态。(每个描述符都有两个变量,一个为调用值,另一个为返回结果,从而避免使用值-结果参数。回想select函数的中间三个参数都使值-结果参数)这两个成员中的每一个都由指定某个特定条件的一位或多位构成。图1-19列出了用于指定events标志以及测试revents标志的一些常值。
图1-19 poll函数的输入events和返回revents
我们将该图分为三个部分:第一部分是处理输入的四个常值,第二部分是处理输出的三个常值,第三部分是处理错误的三个常值。其中第三部分的三个常值不能在events中设置,但是当相应条件存在时就在revents中返回。
poll识别三类数据:普通(normal)、优先级带(priority band)和高优先级(high priority)。这些术语均出自基于流的实现。
就TCP和UDP套接字而言,以下条件引起poll返回特定的revent。不幸的是POSIX在其poll的定义中留了许多空洞(也就是说有多种方法可返回相同的条件)。
-
所有正规TCP数据和所有UDP数据都被认为是普通数据。
-
TCP的带外数据被认为是优先级带数据。
-
当TCP连接的读半部关闭时(譬如收到了一个来自对端的FIN),也被认为是普通数据,随后的读操作将返回0。
-
TCP连接存在错误即可认为是普通数据,也可认为是错误(POLLERR)。无论哪种情况,随后的读操作将返回-1,并把errno设置成合适的值。这可用于处理诸如接收到RST或发生超时等条件。
-
在监听套接字上有新的连接可用既可认为是普通数据,也可认为是优先级数据。大多数实现视之为普通数据。
-
非阻塞式connect的完成被认为是使相应套接字可写。
结构数组中元素的个数是由__nfds参数指定。
/* Type used for the number of file descriptors. */
typedef unsigned long int nfds_t;
timeout参数指定poll函数返回前等待多长时间。它是一个指定应等待毫秒数的正值。图1-20给出了它的可能取值。
图1-20 poll的timeout参数值
INFTIM常值被定义为一个负值。如果系统不能提供毫秒级精度的定时器,该值就向上舍入到最接近的支持值。
当发生错误时,poll函数的返回值为-1,若定时器到时之前没有任何描述符就绪,则返回0,否则返回就绪描述符的个数,即revents成员值非0的描述符个数。
如果我们不再关心某个特定描述符,那么可以把与它对应的pollfd结构的fd成员设置成一个负值。poll函数将忽略这样的pollfd结构的events成员,返回时将它的revents成员的值置为0。
1.11 TCP回射服务器程序(再修订版)
我们用poll代替select重写TCP回射服务器程序。在使用select早先那个版本中,我们必须分配一个client数组以及一个名为rset的描述符集。改用poll后,我们只需分配一个pollfd结构的数组来维护客户信息,而不必分配另一个数组。我们用前面处理client数组相同的方法处理该数组的fd成员:值-1表示所在项未用,否则即为描述符值。我们直到传递给poll的pollfd结构数组中的任何fd成员为负值的项都被poll忽略。
下面是服务器程序。
int main()
{
int i, maxi, listenfd, sockfd, connectfd;
int nready;
ssize_t n;
char buf[ MAX_MESG_SIZE ];
socklen_t clilen;
// 我们声明在pollfd结构数组中存在OPEN_MAX个元素。确定一个进程任何时刻能够打开的最大描述符数目并不容易
// 方法之一是以参数_SC_OPEN_MAX调用POSIX的sysconf函数,然后动态分配一个合适大小的数组。然而sysconf的可能
// 返回之一是“ indeterminate ”(不确定),意味着我们仍然不得不猜测一个值
struct pollfd client[ OPEN_MAX ];
struct sockaddr_in cliaddr, servaddr;
bzero( &servaddr, sizeof( servaddr ) );
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons( SERV_PORT );
servaddr.sin_addr.s_addr = htonl( INADDR_ANY );
// 创建一个TCP套接字
if( ( listenfd = socket( AF_INET, SOCK_STREAM, 0 ) ) < 0 )
{
printf( " socket error!\n " );
return -1;
}
// 在待绑定到TCP套接字的网际套接字地址结构中填入通配地址(INADDR_ANY)和服务器的众所周知端口(SERV_PORT,
// 这里定义为5566)。绑定通配地址是在告知系统:要是系统是多宿主机,我们将接受目的地址为任何本地接口的连接。
// 我们选择TCP端口号应该比1023大(我们不需要一个保留端口),比5000大(以免与许多源自Berkeley的实现分配临
// 时端口的范围冲突),比49152小(以免与临时端口号的“正确”范围冲突),而且不应该与任何已注册的端口冲突。
// listen把该套接字转换成一个监听套接字。
if( ( bind( listenfd, ( struct sockaddr* ) &servaddr, sizeof(servaddr) ) ) < 0 )
{
printf( " bind error!\n " );
return -1;
}
if( listen( listenfd, LISTENQ ) < 0 )
{
printf( " listen error!\n " );
return -1;
}
// 我们把client数组的第一项用于监听套接字,并把其余各项的描述符成员置为-1.我们还给第一项设置POLLRDNORM事件,这样
// 当有新的连接准备好被接收时poll将通知我们。maxi变量含有client数组当前正在使用的最大下标值
client[ 0 ].fd = listenfd;
client[ 0 ].events = POLLRDNORM;
for( i = 1; i < OPEN_MAX; i++ )
client[ i ].fd = -1; // -1 indicates available entry
maxi = 0; // max index into client[] array
signal( SIGCHLD, sig_chld );
for( ; ; )
{
// 我们调用poll以等待新的连接或者现有连接上有数据可读。当一个新的连接被接受后,我们在client数组中查找第一个描述符成员为负
// 的可用项。注意,我们从下标1开始搜索,因为client[ 0 ]固定用于监听套接字。找到一个可用项之后,我们把新连接的描述符保存到
// 其中,并设置POLLRDNORM事件。
nready = poll( client, maxi + 1, -1 );
if( client[ 0 ].revents & POLLRDNORM ) // new client connection
{
clilen = sizeof( cliaddr );
connectfd = accept( listenfd, ( struct sockaddr* ) &servaddr, &clilen );
for( i = 1; i < OPEN_MAX; i++ )
{
if( client[ i ].fd < 0 )
{
client[ i ].fd = connectfd; // save descriptor
break;
}
if( i == OPEN_MAX )
{
printf( " too many clients " );
exit( 1 );
}
client[ i ].events = POLLRDNORM;
if( i > maxi )
maxi = i; // max index in client[] array
if( --nready <= 0 )
continue; // no more readable descriptors
}
}
// 我们检查的两个返回事件是POLLRDNORM和POLLERR。其中我们并没有在event成员中设置第二个事件,因为它在条件成立时总是
// 返回。我们检查POLLERR的原因在于:有些实现在一个连接上接收到RST时返回的是POLLERR事件,而其他实现返回的只是
// POLLRDNORM事件。不论哪种情形,我们都掉用read,当有错误发生时,read将返回这个错误。当一个现有连接由它的客户终止时,
// 我们就把它的fd成员置为1.
for( i = 1; i <= maxi; i++ ) //check all clients for data
{
if( ( sockfd = client[ i ].fd ) < 0 )
continue;
if( client[ i ].revents & ( POLLRDNORM | POLLERR ) )
{
if( ( n = read( sockfd, buf, MAX_MESG_SIZE ) ) < 0 )
{
if( errno == ECONNRESET ) // connection reset by client
{
close( sockfd );
client[ i ].fd = -1;
}
else
{
printf( " read error " );
exit( 1 );
}
}
else if( n == 0 ) // connection closed by client
{
close( sockfd );
client[ i ].fd = -1;
}
else
write( sockfd, buf, n );
if( --nready <= 0 )
break; // no more readble descriptors
}
}
}
}