- 英文小册原文地址:beej.us/guide/bgnet…
- 作者:Beej
- 中文翻译地址:www.chanmufeng.com/posts/netwo…
我假设你已经读过poll()
的用法了,因此直接进入主题。
select()
可以同时监听多个socket,当有你感兴趣的(多个)事件中的任何一个发生,内核才会唤醒select()
。如果你真的想知道的话,select()
会告诉你哪些socket是可以读取的,哪些是可以写入的,哪些引发了异常。
警告:随着连接数越来越多,
select()
函数会变得巨慢!这种情况下,推荐你使用libevent这样的事件库。它会尝试使用你系统上可用的最快方法,获得更好的性能。
看一下select()
的语法:
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int numfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
select()
函数监听多种类型文件描述符的集合,尤其是readfd
、writefd
和exceptfd
。如果你想要知道你是否能从标准输入(standard input)及某个socket descriptor(用 sockfd
表示)中进行读取,只要将标准输入的文件描述符表——0
与 sockfd
新增到 readfds
中。参数numfds
应设置为最高文件描述符的值加1。在本例中,它应该设置为sockfd+1
,因为它肯定高于标准输入——0
。
当select()
返回时,readfds
将被修改,来反映你选择的哪些file descriptor可以读取。你可以使用下面的宏FD_ISSET()
测试它们。
在进一步讨论之前,我先说一下如何操作这些file descriptor集合,每个集合都是fd_set
类型,下面的宏在此类型上运行:
函数 | 描述 |
FD_SET(int fd, fd_set *set); | 将fd加入到set |
FD_CLR(int fd, fd_set *set); | 从set种移除fd |
FD_ISSET(int fd, fd_set *set); | 若fd在set中,返回true |
FD_ZERO(fd_set *set); | 清空set |
最后,这个奇怪的struct timeval
是什么?
有时候,你不想永远一直等着别人给你发送数据。也许每隔一段时间你就想在终端上打印“Still Going…”,即使什么都没有发生。这个struct允许你指定超时时间段。如果超过了时间,select()
仍然没有找到任何就绪的file descriptor,它将返回以便你可以继续进行处理。
struct timeval
长这样:
struct timeval {
int tv_sec; // seconds
int tv_usec; // microseconds
};
只需将tv_sec
设置为等待的秒数,将tv_usec
设置成等待的微秒数。你没看错,这是_micro_seconds,而不是毫秒。一毫秒有1000微秒,一秒钟有1000毫秒。因此,每秒有1000000微秒。为什么是“usec”?“u”应该看起来像我们用来表示“micro”的希腊字母μ(Mu)。
此外,当函数返回时,可能会更新timeout
以显示剩余时间。这取决于你正在运行的Unix类型。
哇!我们有一个微秒级别的计时器!好吧,别指望它。无论你将struct timeval
设置得多么小,你可能还是要等待一小段的 standard Unix timeslice(标准 Unix 时间片段)。
另一件有意思的事情是:如果将struct timeval
中的字段设置为0,select()
会在轮询过 sets 中的每个 file descriptor 之后立即timeout。如果将参数timeout
设置为NULL
,它将永远不会timeout,而是陷入等待状态,直到至少一个file descriptor已经就绪。如果你不在乎等待时间,可以在select()
中将其设置为NULL
。
下面的代码段等待2.5秒,等待标准输入中出现某些内容:
/*
** select.c -- a select() demo
*/
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#define STDIN 0 // file descriptor for standard input
int main(void)
{
struct timeval tv;
fd_set readfds;
tv.tv_sec = 2;
tv.tv_usec = 500000;
FD_ZERO(&readfds);
FD_SET(STDIN, &readfds);
// don't care about writefds and exceptfds:
select(STDIN+1, &readfds, NULL, NULL, &tv);
if (FD_ISSET(STDIN, &readfds))
printf("A key was pressed!\n");
else
printf("Timed out.\n");
return 0;
}
如果你用的是行缓冲(line buffered)的终端,那么你从键盘输入数据后应该要尽快按下 Enter,否则程序就会发生 timeout。
行缓存:标准输出流遇到换行符\n时冲刷缓存。
你现在可能在想,这个方法用在需要等待数据的 datagram socket 上应该会很棒,你是对的:这可能确实是个不错的方法。有些Unix可以以这种方式使用select()
,有些则不能。如果你想尝试的话,你应该参考一下你系统上的man手册上是怎么写的。
有些Unix系统会更新 struct timeval
的时间,用来反映 select()
还剩下多少时间才会 timeout;但是有些并不会这样。如果你想要程序具备可移植性,那就不要依赖这个特性。(如果你确实需要追踪剩下的时间,可以使用 gettimeofday()
,我知道这让你有点不爽,这也是没有办法的事情。)
如果在 read set 中的 socket 关闭连接了,会怎样?
在这种情况下,select()
返回时,socket descriptor会被设置为“ready to read”。当你对其执行recv()
时,recv()
将返回0
。这样你就知道客户端已经断开连接了。