這個函式有點特別,不過它很好用。看看下面這個情況:如果你是一個 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 set(檔案描述符集合)。
如果你想要知道你是否能讀取 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 set:master 與 read_fds。前面的 master 記錄全部現有連線的 socket descriptor,與正在 listen 新連線的 socket descriptor 一樣。
我用 master 的理由是因為 select() 實際上會改變你傳送過去的 set,用來反映目前就緒可讀(ready for read)的 socket。因為我必須在在兩次的 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 set 時是用不一樣的系統,你可以看看 poll()。
參考資料
[24] http://www.monkey.org/~provos/libevent/
[25] http://beej.us/guide/bgnet/examples/select.c
[26] http://beej.us/guide/bgnet/examples/selectserver.c
譯者註:在 The Linux Programming Interface 的第 63 章有更深入的說明。