07-高等技术‎ > ‎

7.2. select()-同步 I/O 多工

这个函数有点奇怪,不过它很好用。看看下面这个情况:如果你是一个 server,而你想要 listen 正在进来的连接,如同不断读取已建立的连接 socket 一样。

你说:没问题,只要用 accept() 及一对 recv() 就好了。

慢点,老兄!如果你在 accept() call 时发生了 blocking 该怎麽办呢?你要如何同时进行 recv() 呢?

"那就使用 non-blocking socket!"

不行!你不会想成为浪费 CPU 资源的罪人吧。

嗯,那有什麽好方法吗?

select() 授予你同时监视多个 sockets 的权力,它会告诉你哪些 sockets 已经有数据可以读取丶哪些 sockets 已经可以写入,如果你真的想知道,还会告诉你哪些 sockets 触发了例外。

即使 select() 相当有可移植性,不过却是监视 sockets 最慢的方法。一个比较可行的替代方案是 libevent [24] 或者其它类似的方法,封装全部的系统相依要素,用以取得 socket 的通知。

好了,不罗唆,下面我提供了 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);
这个函数以 readfdswritefds 及 exceptfds 监视 file descriptor(文件描述符)的 "sets(组)"。如果你想要知道你是否能读取 standard input(标准输入)及某个 sockfd socket descriptor,只要将 file descriptor 0 与 sockfd 新增到 readfds set 中。numfds 参数应该要设置为 file descriptor 的最高值加 1。在这个例子中,应该要将 numfds 设置为 sockfd+1,因为它必定大於 standard input(0)。

当 select() 回传时,readfds 会被修改,用来反映你所设置的 file descriptors 中,哪些已经有数据可以读取,你可以用下列的FD_ISSET() macro(宏)来取得这些可读的 file descriptors。

在继续谈下去以前,我想要说说该如何控制这些 sets。

每个 sets 的型别都是 fd_set,下列是用来控制这个型别的 macro:
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 是什麽东西呢?

好,有时你不想要一直花时间在等人家送数据给你,或者明明没什麽事,却每 96 秒就要印出 "运行中 ..." 到终端(terminal),而这个 time structure 让你可以设置 timeout 的周期。

如果时间超过了,而 select() 还没有找到任何就绪的 file descriptor 时,它会回传,让你可以继续做其它事情。

struct timeval 的栏位如下:
struct timeval {
  int tv_sec; // 秒(second)
  int tv_usec; // 微秒(microseconds)
};

只要将 tv_sec 设置为要等待的秒数,并将 tv_usec 设置为要等待的微秒数。是的,就是微秒,不是毫秒。一毫秒有 1,000 微秒,而一秒有 1,000 毫秒。所以,一秒就有 1,000,000 微秒。

为什麽要用 "usec(微秒)" 呢?

"u"看起来很像我们用来表示 "micro(微)"的希腊字母 μ(Mu)。还有,当函数回传时,会更新 timeout,用以表示还剩下多少时间。这个行为取决於你所使用的 Unix 而定。

译注:
因为有些系统平台的 select() 会修改 timeout 的值,而有些系统不会,所以如果要重复调用 select() 的话,每次都应该要重新设置 timeout 的值,以确保程序的行为可以符合预期。

哇!我们有微秒精度的计时器了!

是的,不过别依赖它。无论你将 struct timeval 设置的多小,你可能还要等待一小段 standard Unix timeslice(标准 Unix 时间片段)。

另一件有趣的事:如果你将 struct timeval 的栏位设置为 0,select() 会在轮询过 sets 中的每个 file descriptors 之後,就马上 timeout。如果你将 timeout 参数设置为 NULL,它就永远不会 timeout,并且陷入等待,直到至少一个 file descriptor 已经就绪(ready)。如果你不在乎等待时间,就在调用 select() 时将 timeout 参数设置为 NULL。

下列的代码片段 [25] 等待 2.5 秒後,就会出现 standard input(标准输入)所输入的东西:
/*
** select.c -- a select() demo
*/
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#define STDIN 0 // standard input 的 file descriptor
int main(void)
{
  struct timeval tv;
  fd_set readfds;

  tv.tv_sec = 2;
  tv.tv_usec = 500000;

  FD_ZERO(&readfds);
  FD_SET(STDIN, &readfds);

  // 不用管 writefds 与 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;
}
如果你用一行缓冲区(buffer)的终端,那麽你从键盘输入数据後应该要尽快按下 Enter,否则程序就会发生 timeout。

你现在可能在想,这个方法用在需要等待数据的 datagram socket 上很好,而且你是对的:应该是不错的方法。

有些系统会用这个方式来使用 select(),而有些不行,如果你想要用它,你应该要参考你系统上的 man 使用手册说明看是否会有问题。

有些系统会更新 struct timeval 的时间,用来反映 select() 原本还剩下多少时间 timeout;不过有些却不会。如果你想要程序是可移植的,那就不要倚赖这个特性。[如果你需要追踪剩下的时间,可以使用 gettimeofday(),我知道这很令人失望,不过事实就是这样。]

如果在 read set 中的 socket 关闭连接,会怎样吗?

好的,这个例子的 select() 回传时,会在 socket descriptor set 中说明这个 socket 是 "ready to read(就绪可读)"的。而当你真的用recv() 去读取这个 socket 时,recv() 则会回传 0 给你。这样你就能知道是 client 关闭连接了。

再次强调 select() 有趣的地方:如果你正在 listen() 一个 socket,你可以将这个 socket 的 file descriptor 放在 readfds set 中,用来检查是不是有新的连接。

我的朋友阿,这就是万能 select() 函数的速成说明。

不过,应观众要求,这里提供个有深度的范例,毫无疑问地,以前的简单范例和这个范例的难易度会有显着差距。不过你可以先看看,然後读後面的解释。

程序 [26] 的行为是简单的多用户聊天室 server,在一个窗口中运行 server,然後在其它多个窗口使用 telnet 连接到 server["telnet hostname 9034"]。当你在其中一个 telnet session 中输入某些文字时,这些文字应该会在其它每个窗口上出现。
/*
** selectserver.c -- 一个 cheezy 的多人聊天室 server
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#define PORT "9034" // 我们正在 listen 的 port

// 取得 sockaddr,IPv4 或 IPv6:
void *get_in_addr(struct sockaddr *sa)
{
  if (sa->sa_family == AF_INET) {
    return &(((struct sockaddr_in*)sa)->sin_addr);
  }

  return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

int main(void)
{
  fd_set master; // master file descriptor 表
  fd_set read_fds; // 给 select() 用的暂时 file descriptor 表
  int fdmax; // 最大的 file descriptor 数目

  int listener; // listening socket descriptor
  int newfd; // 新接受的 accept() socket descriptor
  struct sockaddr_storage remoteaddr; // client address
  socklen_t addrlen;

  char buf[256]; // 储存 client 数据的缓冲区
  int nbytes;

  char remoteIP[INET6_ADDRSTRLEN];

  int yes=1; // 供底下的 setsockopt() 设置 SO_REUSEADDR
  int i, j, rv;

  struct addrinfo hints, *ai, *p;

  FD_ZERO(&master); // 清除 master 与 temp sets
  FD_ZERO(&read_fds);

  // 给我们一个 socket,并且 bind 它
  memset(&hints, 0, sizeof hints);
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;
  hints.ai_flags = AI_PASSIVE;

  if ((rv = getaddrinfo(NULL, PORT, &hints, &ai)) != 0) {
    fprintf(stderr, "selectserver: %s\n", gai_strerror(rv));
    exit(1);
  }

  for(p = ai; p != NULL; p = p->ai_next) {
    listener = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
    if (listener < 0) {
      continue;
    }

    // 避开这个错误信息:"address already in use"
    setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));

    if (bind(listener, p->ai_addr, p->ai_addrlen) < 0) {
      close(listener);
      continue;
    }

    break;
  }

  // 若我们进入这个判断式,则表示我们 bind() 失败
  if (p == NULL) {
    fprintf(stderr, "selectserver: failed to bind\n");
    exit(2);
  }
  freeaddrinfo(ai); // all done with this

  // listen
  if (listen(listener, 10) == -1) {
    perror("listen");
    exit(3);
  }

  // 将 listener 新增到 master set
  FD_SET(listener, &master);

  // 持续追踪最大的 file descriptor
  fdmax = listener; // 到此为止,就是它了

  // 主要循环
  for( ; ; ) {
    read_fds = master; // 复制 master

    if (select(fdmax+1, &read_fds, NULL, NULL, NULL) == -1) {
      perror("select");
      exit(4);
    }

    // 在现存的连接中寻找需要读取的数据
    for(i = 0; i <= fdmax; i++) {
      if (FD_ISSET(i, &read_fds)) { // 我们找到一个!!
        if (i == listener) {
          // handle new connections
          addrlen = sizeof remoteaddr;
          newfd = accept(listener,
            (struct sockaddr *)&remoteaddr,
            &addrlen);

          if (newfd == -1) {
            perror("accept");
          } else {
            FD_SET(newfd, &master); // 新增到 master set
            if (newfd > fdmax) { // 持续追踪最大的 fd
              fdmax = newfd;
            }
            printf("selectserver: new connection from %s on "
              "socket %d\n",
              inet_ntop(remoteaddr.ss_family,
                get_in_addr((struct sockaddr*)&remoteaddr),
                remoteIP, INET6_ADDRSTRLEN),
              newfd);
          }

        } else {
          // 处理来自 client 的数据
          if ((nbytes = recv(i, buf, sizeof buf, 0)) <= 0) {
            // got error or connection closed by client
            if (nbytes == 0) {
              // 关闭连接
              printf("selectserver: socket %d hung up\n", i);
            } else {
              perror("recv");
            }
            close(i); // bye!
            FD_CLR(i, &master); // 从 master set 中移除

          } else {
            // 我们从 client 收到一些数据
            for(j = 0; j <= fdmax; j++) {
              // 送给大家!
              if (FD_ISSET(j, &master)) {
                // 不用送给 listener 跟我们自己
                if (j != listener && j != i) {
                  if (send(j, buf, nbytes, 0) == -1) {
                    perror("send");
                  }
                }
              }
            }
          }
        } // END handle data from client
      } // END got new incoming connection
    } // END looping through file descriptors
  } // END for( ; ; )--and you thought it would never end!

  return 0;
}
我说过在代码中有两个 file descriptor sets:master 与 read_fds。前面的 master 记录全部现有连接的 socket descriptors,与正在 listen 新连接的 socket descriptor 一样。

我用 master 的理由是因为 select() 实际上会改变你传送过去的 set,用来反映目前就绪可读(ready for read)的 sockets。因为我必须在在两次的 select() calls 期间也能够持续追踪连接,所以我必须将这些数据安全地储存在某个地方。最後,我再将 master 复制到read_fds,并接着调用 select()

可是这不就代表每当有新连接时,我就要将它新增到 master set 吗?是的!

而每次连接结束时,我们也要将它从 master set 中移除吗?是的,没有错。

我说过,我们要检查 listen 的 socket 是否就绪可读,如果可读,这代表我有一个待处理的连接,而且我要 accept() 这个连接,并将它新增到 master set。同样地,当 client 连接就绪可读且 recv() 返回 0 时,我们就能知道 client 关闭了连接,而我必须将这个 socket descriptor 从 master set 中移除。

若 client 的 recv() 返回非零的值,因而,我能知道 client 已经收到了一些数据,所以我收下这些数据,并接着到 master 清单,并将数据送给其它已连接的每个 clients。

我的朋友们,以上对万能 select() 函数的概述,这真是不简单的事情。

另外,这里有个福利:一个名为 poll 的函数,它的行为与 select() 很像,但是在管理 file descriptor sets 时用不一样的系统,你可以看看 poll

Comments