这个函数有点奇怪,不过它很好用。看看下面这个情况:如果你是一个 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);这个函数以 readfds丶writefds 及 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 而定。
哇!我们有微秒精度的计时器了! 是的,不过别依赖它。无论你将 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。 |
07-高等技术 >