在涉及套接字IO超時的設置上有一下3種方法:
1、調用alarm,它在指定的時期滿時產生SIGALRM信號。這個方法涉及信號的處理,而信號處理在不同的實現上存在差異,而且可能干擾進程中現有的alarm調用。
程序大概框架如下所示,如果read在5s內被SIGALRM信號中斷而返回,則表示超時,否則未超時已讀取到數據則取消鬧鍾。為了在超時時中斷read函數,可以用信號處理函數來捕捉SIGALRM信號。
void handler(int sig) { return 0; //只是用來中斷read函數,不需要進行處理 } signal(SIGALRM,handler); alarm(5); //開啟鬧鍾 int ret =read(fd,buf,sizeof(buf)); if(ret==-1 && errno == EINTR) //read被超時中斷 { errno = ETIMEDOUT; } else if (ret>0) //讀到數據 { alarm(0); //關閉鬧鍾 }
2、使用SO_RCVTIMEO和SO_SNDTIMEO兩個套接字選項。此方法的問題在於並非所有的實現都支持這兩個套接字選項。此種方法僅適用於套接字描述符。
struct timeval timeout={3,0}; //設置超時時間為3秒 setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,(char*)&timeout,sizeof(struct timeval)); //設置套接字選項 int ret=read(fd,buf,sizeof(buf)); if(ret==-1 && errno == EWOULDBLOCK) //IO超時中斷 errno = ETIMEDOUT; .................
3、在select中阻塞等待的IO(select有內置的時間限制),以此代替直接阻塞在read和write調用上。
(1)利用select封裝read超時函數
如果 read_timeout(fd, 0); 則表示不檢測超時,函數直接返回為0,此時再調用read 將會阻塞。
當wait_seconds 參數大於0,則進入if 括號執行,將超時時間設置為select函數的超時時間結構體,select會阻塞直到檢測到事件發生或者超時。如果select返回-1且errno 為EINTR,說明是被信號中斷,需要重啟select;如果select返回0表示超時;如果select返回1表示檢測到可讀事件;否則select返回-1 表示出錯。
/** *read_timeout 讀超時檢測函數,不含讀操作 * fd:文件描述符 *wait_seconds:等待超時秒數,如果為0則表示不檢測超時 *成功(未超時)返回0,失敗返回-1,超時返回-1並且errno=ETIMEDOUT */ int read_timeout(int fd,unsigned int wait_seconds) { int ret=0; //默認為0,當wait_seconds==0時,不檢測直接返回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會返回0, 如果檢測到事件會返回1,如果異常會返回-1,如果是由於信號中斷引起的異常errno==EINTR*/ }while(ret<0 && errno == EINTR); //如果是有信號引起的異常則繼續阻塞select,直到檢測到事件或則超時 if(ret==0) //select超時退出 { ret=-1; errno= ETIMEDOUT; } else if(ret==1) //select檢測到可讀事件 ret= 0; } return ret; }
測試程序框架
/***********測試程序***************/ int ret; ret=read_timeout(fd,5); if(ret==0) //檢測到套接字可讀 { read(fd,......); //進行讀套接字的操作 } else if(ret==-1 && errno == ETIMEDOUT) //超時即指定事件內為檢測到套接字可讀 { timeout.......... ; //進行超時處理 } else { ERR_EXIT("read_timeout"); //進行錯誤處理 }
(2)利用select封裝write超時函數
write超時函數的封裝與read超時函數的封裝基本一樣,不同的只是把文件描述符加入到寫集合write_fdset中,其關心的只是寫事件。
/** *write_timeout 寫超時檢測函數,不含寫操作 * fd:文件描述符 *wait_seconds:等待超時秒數,如果為0則表示不檢測超時 *成功(未超時)返回0,失敗返回-1,超時返回-1並且errno=ETIMEDOUT */ int write_timeout(int fd,unsigned int wait_seconds) { int ret=0; //默認為0,當wait_seconds==0時,不檢測直接返回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); /*select會阻塞直到檢測到事件或則超時,如果超時,select會返回0, 如果檢測到事件會返回1,如果異常會返回-1,如果是由於信號中斷引起的異常errno==EINTR*/ }while(ret<0 && errno == EINTR); //如果是有信號引起的異常則繼續阻塞select,直到檢測到事件或則超時 if(ret==0) //select超時退出 { ret=-1; errno= ETIMEDOUT; } else if(ret==1) //select檢測到可寫事件 ret= 0; } return ret; }
測試代碼
/***********測試程序***************/ int ret; ret=write_timeout(fd,5); if(ret==0) //檢測到描述符可寫 { write(fd,......); //進行寫描述符的操作 } else if(ret==-1 && errno == ETIMEDOUT) //超時即指定事件內為檢測到套接字可寫 { timeout.......... ; //進行超時處理 } else { ERR_EXIT("read_timeout"); //進行錯誤處理 }
(3)accept超時函數的封裝
accept超時函數是帶超時的accept 函數,如果能從if (wait_seconds > 0) 括號執行后向下執行,說明select 返回為1,檢測到已連接隊列不為空,此時再調用accept 不再阻塞,當然如果wait_seconds == 0 則像正常模式一樣,accept 阻塞等待,注意,accept 返回的是已連接套接字。
/** *accept_timeout 帶超時的accept函數 *fd:文件描述符 *addr 輸出參數,accept返回的對等方的地址結構 *wait_seconds:等待超時秒數,如果為0則表示正常模式 *成功(未超時)返回已連接的套接字,失敗返回-1,超時返回-1並且errno=ETIMEDOUT */ int accept_timeout(int fd,struct sockaddr_in *addr,unsigned int wait_seconds) { int ret=0; //默認為0,當wait_seconds==0時,不檢測直接返回0 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); /*select會阻塞直到檢測到事件或則超時,如果超時,select會返回0, 如果檢測到事件會返回1,如果異常會返回-1,如果是由於信號中斷引起的異常errno==EINTR*/ }while(ret<0 && errno == EINTR); //如果是有信號引起的異常則繼續阻塞select,直到檢測到事件或則超時 if(ret==-1) //失敗 { return -1; } if(ret==0) //select超時退出 { errno= ETIMEDOUT; return -1; } } //select在指定時間內檢測到套接字可連接即三次握手完成或者不需要select檢測 if(addr!=NULL) { ret=accept(fd,(struct sockaddr *)addr,&addr); } else { ret=accept{fd,NULL,NULL}; } if (ret==-1) //連接失敗 ERR_EXIT("accept"); return ret; //返回連接的套接字 }
(4)connect超時函數的封裝
在調用connect前需要使用fcntl 函數將套接字標志設置為非阻塞,如果網絡環境很好,則connect立即返回0,不進入if 大括號執行;如果網絡環境擁塞,則connect返回-1且errno == EINPROGRESS,表示正在處理。此后調用select與前面3個函數類似,但這里關注的是可寫事件,因為一旦連接建立,套接字就可寫。還需要注意的是當select 返回1,可能有兩種情況,一種是連接成功,一種是套接字產生錯誤,由這里可知,這兩種情況都會產生可寫事件,所以需要使用getsockopt來獲取一下。退出之前還需重新將套接字設置為阻塞。
/** * activiate_nonblock 設置IO為非阻塞模式 * fd 文件描述符 */ void activiate_nonblock(int fd) { int ret; int flags = fcntl(fd,F_GETFL); //獲取fd的當前標記 if(flags == -1) ERR_EXIT("fcntl"); flags |= O_NONBLOCK; //與新標記邏輯或 ret = fcntl(fd,F_SETFL,flags); //設置標記 if(ret == -1) ERR_EXIT("fntl"); } /** * deactiviate_nonblock 設置IO為阻塞模式 * fd 文件描述符 */ void deactiviate_nonblock(int fd) { int ret; int flags = fcntl(fd,F_GETFL); //獲取fd的當前標記 if(flags == -1) ERR_EXIT("fcntl"); flags &=~O_NONBLOCK; //與新標記邏輯與 ret = fcntl(fd,F_SETFL,flags); //設置標記 if(ret == -1) ERR_EXIT("fntl"); } /** *connect_timeout 帶超時的accept函數 *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=0; //默認為0,當wait_seconds==0時,不檢測直接返回0 socklen_t addrlen =sizeof(struct sockaddr_in); if(wait_seconds>0) //需要檢測超時 activiate_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); /*select會阻塞直到檢測到事件或則超時,如果超時,select會返回0, 如果檢測到事件會返回1,如果異常會返回-1,如果是由於信號中斷引起的異常errno==EINTR*/ }while(ret<0 && errno == EINTR); //如果是有信號引起的異常則繼續阻塞select,直到檢測到事件或則超時 if(ret==-1) //失敗 { return -1; } else if(ret==0) //select超時退出 { errno= ETIMEDOUT; return -1; } else if(ret==1) { /*ret為1有兩種情況,一種是連接建立成功,一種是套接字產生錯誤 此時錯誤信息不回保存在errno變量中,因此,需要調用getsockopt函數來獲取。*/ int err; socklen_t socklen = sizeof(err); int sockoptret = getsockopt(fd,SOL_SOCKET,SO_ERROR,&err,&socklen);//獲取套接字的錯誤放在err中 if(sockoptret == -1) //調用getsockopt失敗 { return -1; } if(err==0) //表示沒有錯誤即套接字建立連接成功 ret=0; else //套接字產生錯誤 { errno=err; ret=-1; } } } if(wait_seconds>0) { deactiviate_nonblock(fd); //重新將套接字設為阻塞模式 } return ret; }
connect超時測試程序
#include<stdio.h> #include<stdlib.h> #include<errno.h> #include<string.h> #include<sys/types.h> #include<sys/socket.h> #include<sys/un.h> #include<sys/wait.h> //*進程用的頭文件*/ #include<netinet/in.h> #include<arpa/inet.h> #include <unistd.h> //fcntl的頭文件 #include <fcntl.h> #include <sys/time.h> #define MAXBYTEMUN 1024 /** * activiate_nonblock 設置IO為非阻塞模式 * fd 文件描述符 */ void activiate_nonblock(int fd) { int ret; int flags = fcntl(fd,F_GETFL); //獲取fd的當前標記 if(flags == -1) perror("fcntl"); flags |= O_NONBLOCK; //與新標記邏輯或 ret = fcntl(fd,F_SETFL,flags); //設置標記 if(ret == -1) perror("fntl"); } /** * deactiviate_nonblock 設置IO為阻塞模式 * fd 文件描述符 */ void deactiviate_nonblock(int fd) { int ret; int flags = fcntl(fd,F_GETFL); //獲取fd的當前標記 if(flags == -1) perror("fcntl"); flags &=~O_NONBLOCK; //與新標記邏輯與 ret = fcntl(fd,F_SETFL,flags); //設置標記 if(ret == -1) perror("fntl"); } /** *connect_timeout 帶超時的accept函數 *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=0; //默認為0,當wait_seconds==0時,不檢測直接返回0 socklen_t addrlen =sizeof(struct sockaddr_in); if(wait_seconds>0) //需要檢測超時 activiate_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); /*select會阻塞直到檢測到事件或則超時,如果超時,select會返回0, 如果檢測到事件會返回1,如果異常會返回-1,如果是由於信號中斷引起的異常errno==EINTR*/ }while(ret<0 && errno == EINTR); //如果是有信號引起的異常則繼續阻塞select,直到檢測到事件或則超時 if(ret==-1) //失敗 { return -1; } else if(ret==0) //select超時退出 { errno= ETIMEDOUT; return -1; } else if(ret==1) { /*ret為1有兩種情況,一種是連接建立成功,一種是套接字產生錯誤 此時錯誤信息不回保存在errno變量中,因此,需要調用getsockopt函數來獲取。*/ int err; socklen_t socklen = sizeof(err); int sockoptret = getsockopt(fd,SOL_SOCKET,SO_ERROR,&err,&socklen);//獲取套接字的錯誤放在err中 if(sockoptret == -1) //調用getsockopt失敗 { return -1; } if(err==0) //表示沒有錯誤即套接字建立連接成功 ret=0; else //套接字產生錯誤 { errno=err; ret=-1; } } } if(wait_seconds>0) { deactiviate_nonblock(fd); //重新將套接字設為阻塞模式 } return ret; } /***********測試程序***************/ int main(int argc,char *argv[]) { int sock_fd,numbytes,maxfd,fd_stdin,nready; // char buf[MAXBYTEMUN]; struct hostent; struct sockaddr_in client_addr;//客戶機的地址信息 ssize_t ret; char recvbuf[1024]={'0'},sendbuf[1024]={'0'}; fd_set rset; int stdineof; if(argc!=2) { fprintf(stderr,"usage: client IPAddress\n"); //執行客戶端程序時,輸入客戶端程序名稱和其IP地址 exit(1); } /*創建套接字*/ sock_fd=socket(AF_INET,SOCK_STREAM,0);//采用IPv4協議 if(sock_fd==-1) { perror("creat socket failed"); exit(1); } /*服務器地址參數*/ client_addr.sin_family=AF_INET; client_addr.sin_port=htons(3490); client_addr.sin_addr.s_addr=inet_addr(argv[1]); bzero(&client_addr.sin_zero,sizeof(struct sockaddr_in));//bzero位清零函數,將sin_zero清零,sin_zero為填充字段,必須全部為零 /*連接到服務器*/ ret=connect_timeout(sock_fd,(struct sockaddr*)&client_addr,5) if(ret==-1 && errno ==ETIMEDOUT) { perror("connect timedout\n"); exit(1); } else if(ret==-1) perror("connect error\n") if((numbytes=recv(sock_fd,recvbuf,MAXBYTEMUN,0))==-1) { perror("receive failed"); exit(1); } recvbuf[numbytes]='\0';//在字符串末尾加上\0,否則字符串無法輸出 printf("Received: %s\n",recvbuf); return 0; }
因為是在本機上測試,所以不會出現超時的情況,但出錯的情況還是可以看到的,比如不要啟動服務器端程序,而直接啟動客戶端程序,會報錯如下
在connect_timeout函數里面打印輸出信息,可以跟蹤程序的執行
同樣只開啟客戶端程序會出現如下結果
如果開啟服務端之后在開啟客戶端,出現如下結果
根據這些打印輸出的結果可以看到connect_timeout函數的執行路勁