linux裡的select函式和一種併發伺服器的實現
select函式 函式原型: int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); man page 中對select函式的說明如下: select() and pselect() allow a program to monitor multiple file descriptors, waiting until one or more of the file descriptors become "ready" for some class of I/O operation (e.g., input possible). A file descriptor is considered ready if it is possible to perform the corresponding I/O operation (e.g., read(2)) without blocking. 也就是說,select函式可以監控多個檔案描述符,等待直到其中一個或多個檔案可以進行讀取或寫入操作而不被阻塞時,才返回。 引數: nfds: 要監視的fd最大值加1,設定select監視的檔案描述符範圍可以提高效率 readfds: 要進行讀取操作的檔案描述符集 writefds: 要進行寫入操作的檔案描述符集 exceptfds: 要監視異常的檔案描述符集 timeout: 使用struct timeval描述的超時時間,設為NULL時,表示永遠等待 返回值: 返回-1表示出錯 返回0表示沒有檔案描述符準備好 返回正值表示已經準備好的描述符個數 fd_set: fd_set用來描述一個檔案描述符集。可以使用以下四個介面對它進行操作。 #include <sys/select.h> void FD_ZERO(fd_set, *fdset); void FD_CLR(int fd, fd_set, *fdset); void FD_SET(int fd, fd_set, *fdset); int FD_ISSET(int fd, fd_set, *fdset); FD_ZERO 用來清空一個fdset(清除所有位) FD_CLR 用來清除fdset中指定位 FD_SET 用來設定fdset中指定位 FD_ISSET 測試指定位是否設定,已設定返回非零,否則返回0 3個fd_set指標可以為NULL,表示不關心相應的檔案集。 函式在呼叫select前,應該先使用FD_ZERO清空每個要使用的fd_set,然後使用FD_SET設定關心的檔案描述符。在select返回後使用FD_ISSET檢查每個關心的位是否仍舊設定。 select函式在實現併發伺服器時是很好用的,這時我們可以每建立一個連線都加入fdset中進行監視,當任意一個連線可讀時再去讀取資料然後處理,這樣可以很簡單地實現併發。下面是一個使用select實現的TCP併發伺服器的例子: /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // tcp-server.c ////////////////////////////////////////////////////////////////////////////////////////////////////////////// #include <unistd.h> #include <sys/socket.h> #include <sys/types.h> #include <sys/time.h> #include <arpa/inet.h> #include <sys/fcntl.h> #include <errno.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #define MAX_MESSAGE_LEN 128 #define MAX_CONNECTION_NUM 16 typedef struct { int sock; struct sockaddr_in client_addr; void * prvivate; //連線私有資料,用來存放連線上下文等 } conn_t; static conn_t conn[MAX_CONNECTION_NUM] ; int proc_msg(int conn_hd, char *buf, int len); //增加一個連線 //初始化conn_t資料結構,並將socket加入監視的檔案描述符集 static int add_conn(int sck, struct sockaddr_in *client_addr) { int i; for(i=0; i<MAX_CONNECTION_NUM; i++) { if(conn[i].sock == -1) { #if 0 //為連線私有資料申請空間 conn[i].private = malloc(...); if(conn[i].private == NULL) { ERR_PRINT("Alloc memmory failed.\n"); return -1; } memset(conn[i].private, 0, ...); #endif if( (sck+1) > fdmax) { fdmax = sck+1; } conn[i].sock = sck; FD_SET(sck, &fdset_rd); memcpy(&conn[i].client_addr, client_addr, sizeof(struct sockaddr_in)); return i; } } return -1; } //刪除一個連線 //從監視的檔案描述符集中刪除socket //並銷燬conn_t static int del_conn(int hd) { int max=0; int i; if(hd >= MAX_CONNECTION_NUM) return -1; if(conn[hd].sock == -1) return -1; FD_CLR(conn[hd].sock, &fdset_rd); close(conn[hd].sock); conn[hd].sock = -1; if(fdmax == conn[hd].sock+1) { for(i=0;i<MAX_CONNECTION_NUM;i++) { if( (conn[i].sock != -1) && ((conn[i].sock+1)>max) ) max = conn[i].sock+1; } fdmax = max; } #if 0 //釋放申請的空間 if(conn[i].private != NULL) { p = conn[i].private; conn[i].private = NULL; free(conn[i].private); } #endif return 0; } int main(int argc, char* argv[]) { struct sockaddr_in sock_addr, client_addr; int sockfd, clientfd; int i; fd_set rdset; struct timeval tv; int len,ret; char buff[MAX_MESSAGE_LEN+1] = {0}; memset(conn, 0, sizeof(conn)); for(i=0; i<MAX_CONNECTION_NUM; i++) { conn[i].sock = -1; } //建立socket sockfd = socket(AF_INET, SOCK_STREAM, 0); if(sockfd == -1) { ERR_PRINT("Open socket error.\n"); return -1; } //設為非阻塞 if( fcntl(sockfd, F_SETFL, O_NONBLOCK) == -1) { DBG_PRINT("Set server socket nonblock failed\n"); } bzero(&sock_addr, sizeof(sock_addr)); sock_addr.sin_family = AF_INET; sock_addr.sin_port = htons(IOSERVER_TCP_PORT); sock_addr.sin_addr.s_addr = htons(INADDR_ANY); bzero(&(sock_addr.sin_zero), 8); //繫結埠 if(bind(sockfd, (struct sockaddr*)&sock_addr, sizeof(sock_addr)) != 0) { ERR_PRINT("TCP port binding faild. %s\n", strerror(errno)); return -1; } //開始監聽 ret = listen(sockfd, MAX_CONNECTION_NUM); if(ret) { ERR_PRINT("TCP server listen faild. %s\n", strerror(errno)); return -1; } //初始將伺服器監聽socket加入監視檔案描述符集 FD_ZERO(&fdset_rd); FD_SET(sockfd, &fdset_rd); fdmax = sockfd+1; while(1) { //每次呼叫select前都需要重新設定 memcpy(&rdset, &fdset_rd, sizeof(fd_set)); tv.tv_sec = 0; tv.tv_usec = 0; ret = select(fdmax, &rdset, NULL, NULL, &tv); switch(ret) { case -1: ERR_PRINT("select returned %d\n", ret); break; case 0: break; default: //建立一個連線 if(FD_ISSET(sockfd, &rdset)) { len = sizeof(client_addr); clientfd = accept(sockfd, (struct sockaddr*)&client_addr, (socklen_t*)&len); if(clientfd > 0) { DBG_PRINT("get one connection\n"); if(add_conn(clientfd, &client_addr) == -1) { ERR_PRINT("Add connetion failed,close this connection\n"); close(clientfd); } } } //掃描所有連線,並從準備好的連線接收資料並處理 for(i=0; i<MAX_CONNECTION_NUM; i++) { if(conn[i].sock != -1) { memset(buff, 0, sizeof(buff)); if(FD_ISSET(conn[i].sock, &rdset)) { len = recv(conn[i].sock, buff, MAX_MESSAGE_LEN,0); if(len > 0) { porc_msg(&conn[i], buff, len); } } } } break; } } close(sockfd); return 0; }