一、使用alarm 函數設置超時
void handler(int sig) { } signal(SIGALRM, handler); alarm(5); int ret = read(fd, buf, sizeof(buf)); if (ret == -1 && errno == EINTR) errno = ETIMEOUT; else if (ret >= 0) alarm(0); .................
程序大概框架如上所示,如果read在5s內被SIGALRM信號中斷而返回,則表示超時,否則未超 時已讀取到數據,取消鬧鐘。但這種方法不常用,因為有時可能在其他地方使用了alarm會造成混亂。
二、使用套接 字選項SO_SNDTIMEO、SO_RCVTIMEO
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, 5); int ret = read(sock, buf, sizeof(buf)); if (ret == -1 && errno == EWOULDBLOCK) errno = ETIMEOUT; ..........
即使用setsockopt 函數進行設置,但這種方法可移植性比較差,不是每種系統實現都有這些選項。
三、使用select 實現超時
下面程序包含read_timeout、write_timeout、accept_timeout、 connect_timeout 四個函數封裝
/************************************************************************* > File Name: sysutil.c > Author: Simba > Mail: [email protected] > Created Time: Sat 02 Mar 2013 10:53:06 PM CST ************************************************************************/ #include "sysutil.h" /* read_timeout - 讀超時檢測函數,不含讀操作 * fd:文件描述符 * wait_seconds:等待超時秒數, 如果為0表示不檢測超時; * 成功(未超時)返回0,失敗返回-1,超時返回-1並且errno = ETIMEDOUT */ int read_timeout(int fd, unsigned int wait_seconds) { int ret = 0; if (wait_seconds > 0) { fd_set read_fdset; struct timeval timeout; FD_ZERO(&read_fdset); FD_SET(fd, &read_fdset); timeout.tv_sec = wait_seconds; timeout.tv_usec = 0; do { ret = select(fd + 1, &read_fdset, NULL, NULL, &timeout); //select會阻塞直到檢測到事件或者超時 // 如果select檢測到可讀事件發送,則此時調用read不會阻塞 } while (ret < 0 && errno == EINTR); if (ret == 0) { ret = -1; errno = ETIMEDOUT; } else if (ret == 1) return 0; } return ret; } /* write_timeout - 寫超時檢測函數,不含寫操作 * fd:文件描述符 * wait_seconds:等待超時秒數, 如果為0表示不檢測超時; * 成功(未超時)返回0,失敗返回-1,超時返回-1並且errno = ETIMEDOUT */ int write_timeout(int fd, unsigned int wait_seconds) { int ret = 0; if (wait_seconds > 0) { fd_set write_fdset; struct timeval timeout; FD_ZERO(&write_fdset); FD_SET(fd, &write_fdset); timeout.tv_sec = wait_seconds; timeout.tv_usec = 0; do { ret = select(fd + 1, NULL, &write_fdset, NULL, &timeout); } while (ret < 0 && errno == EINTR); if (ret == 0) { ret = -1; errno = ETIMEDOUT; } else if (ret == 1) return 0; } return ret; } /* accept_timeout - 帶超時的accept * fd: 套接字 * addr: 輸出參數,返回對方地址 * wait_seconds: 等待超時秒數,如果為0表示正常模式 * 成功(未超時)返回已連接套接字,失敗返回-1,超時返回-1並且errno = ETIMEDOUT */ int accept_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds) { int ret; socklen_t addrlen = sizeof(struct sockaddr_in); if (wait_seconds > 0) { fd_set accept_fdset; struct timeval timeout; FD_ZERO(&accept_fdset); FD_SET(fd, &accept_fdset); timeout.tv_sec = wait_seconds; timeout.tv_usec = 0; do { ret = select(fd + 1, &accept_fdset, NULL, NULL, &timeout); } while (ret < 0 && errno == EINTR); if (ret == -1) return -1; else if (ret == 0) { errno = ETIMEDOUT; return -1; } } if (addr != NULL) ret = accept(fd, (struct sockaddr *)addr, &addrlen); else ret = accept(fd, NULL, NULL); if (ret == -1) ERR_EXIT("accpet error"); return ret; } /* activate_nonblock - 設置IO為非阻塞模式 * fd: 文件描述符 */ void activate_nonblock(int fd) { int ret; int flags = fcntl(fd, F_GETFL); if (flags == -1) ERR_EXIT("fcntl error"); flags |= O_NONBLOCK; ret = fcntl(fd, F_SETFL, flags); if (ret == -1) ERR_EXIT("fcntl error"); } /* deactivate_nonblock - 設置IO為阻塞模式 * fd: 文件描述符 */ void deactivate_nonblock(int fd) { int ret; int flags = fcntl(fd, F_GETFL); if (flags == -1) ERR_EXIT("fcntl error"); flags &= ~O_NONBLOCK; ret = fcntl(fd, F_SETFL, flags); if (ret == -1) ERR_EXIT("fcntl error"); } /* connect_timeout - 帶超時的connect * fd: 套接字 * addr: 輸出參數,返回對方地址 * wait_seconds: 等待超時秒數,如果為0表示正常模式 * 成功(未超時)返回0,失敗返回-1,超時返回-1並且errno = ETIMEDOUT */ int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds) { int ret; socklen_t addrlen = sizeof(struct sockaddr_in); if (wait_seconds > 0) activate_nonblock(fd); ret = connect(fd, (struct sockaddr *)addr, addrlen); if (ret < 0 && errno == EINPROGRESS) { fd_set connect_fdset; struct timeval timeout; FD_ZERO(&connect_fdset); FD_SET(fd, &connect_fdset); timeout.tv_sec = wait_seconds; timeout.tv_usec = 0; do { /* 一旦連接建立,套接字就可寫 */ ret = select(fd + 1, NULL, &connect_fdset, NULL, &timeout); } while (ret < 0 && errno == EINTR); if (ret == 0) { errno = ETIMEDOUT; return -1; } else if (ret < 0) return -1; else if (ret == 1) { /* ret返回為1,可能有兩種情況,一種是連接建立成功,一種是套接字產生錯誤 * 此時錯誤信息不會保存至errno變量中(select沒出錯),因此,需要調用 * getsockopt來獲取 */ int err; socklen_t socklen = sizeof(err); int sockoptret = getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &socklen); if (sockoptret == -1) return -1; if (err == 0) ret = 0; else { errno = err; ret = -1; } } } if (wait_seconds > 0) deactivate_nonblock(fd); return ret; }
下面來解析一下這些函數的封裝:
1、read_timeout :如注釋所寫,這只是讀超時檢測函數,並不包含 讀操作,如果從此函數成功返回,則此時調用read將不再阻塞,測試代碼可以這樣寫:
int ret; ret = read_timeout(fd, 5); if (ret == 0) read(fd, buf, sizeof(buf)); else if (ret == -1 && errno == ETIMEOUT) printf("timeout...\n"); else ERR_EXIT("read_timeout");
如果 read_timeout(fd, 0); 則表示不檢測超時,函數直接返回為0,此時再調用read 將會阻塞。
當 wait_seconds 參數大於0,則進入if 括號執行,將超時時間設置為select函數的超時時間結構體,select會阻塞直到檢測 到事件發生或者超時。如果select返回-1且errno 為EINTR,說明是被信號中斷,需要重啟select;如果select返回0表示超 時;如果select返回1表示檢測到可讀事件;否則select返回-1 表示出錯。
2、write_timeout :此函數跟 read_timeout 函數類似,只是select 關心的是可寫事件,不再贅述。
3、accept_timeout :此函數是帶超時的 accept 函數,如果能從if (wait_seconds > 0) 括號執行後向下執行,說明select 返回為1,檢測到已連接隊列不為空 ,此時再調用accept 不再阻塞,當然如果wait_seconds == 0 則像正常模式一樣,accept 阻塞等待,注意,accept 返回 的是已連接套接字。
4、connect_timeout :在調用connect前需要使用fcntl 函數將套接字標志設置為非阻塞,如 果網絡環境很好,則connect立即返回0,不進入if 大括號執行;如果網絡環境擁塞,則connect返回-1且errno == EINPROGRESS,表示正在處理。此後調用select與前面3個函數類似,但這裡關注的是可寫事件,因為一旦連接建立,套接字 就可寫。還需要注意的是當select 返回1,可能有兩種情況,一種是連接成功,一種是套接字產生錯誤,由這裡可知,這兩 種情況都會產生可寫事件,所以需要使用getsockopt來獲取一下。退出之前還需重新將套接字設置為阻塞。
我們可 以寫個小程序測試一下connect_timeout 函數,客戶端程序如下:
#include "sysutil.h" int main(void) { int sock; if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) ERR_EXIT("socket"); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); int ret = connect_timeout(sock, &servaddr, 5); if (ret == -1 && errno == ETIMEDOUT) { printf("timeout...\n"); return 1; } else if (ret == -1) ERR_EXIT("connect_timeout"); struct sockaddr_in localaddr; socklen_t addrlen = sizeof(localaddr); if (getsockname(sock, (struct sockaddr *)&localaddr, &addrlen) < 0) ERR_EXIT("getsockname"); printf("ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port)); return 0; }
因為是在本機上測試,所以不會出現超時的情況,但出錯的情況還是可以看到的,比如不要啟動服務器端程序,而直接 啟動客戶端程序,輸出如下:
simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echocli_timeout
connect_timeout: Connection refused
很明顯是connect_timeout 函數返回了-1 ,我們也可以推算出connect_timeout 函數中,select返回1,但卻是套接字發生錯誤的情況,errno = ECONNREFUSED,所 以打印出Connection refused。