• 英文小册原文地址:​​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​​。这样你就知道客户端已经断开连接了。