一. 使用alarm函數設置超時
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
alarm也稱爲鬧鐘函數,它可以在進程中設置一個定時器,當定時器指定的時間到時,它向進程發送SIGALRM信號。如果忽略或者不捕獲此信號,則其默認動作是終止調用該alarm函數的進程。
要注意的是,一個進程只能有一個鬧鐘時間,如果在調用alarm之前已設置過鬧鐘時間,則任何以前的鬧鐘時間都被新值所代替。需要注意的是,經過指定的秒數後,信號由內核產生,由於進程調度的延遲,所以進程得到控制從而能夠處理該信號還需要一些時間。
void handle(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); http://write.blog.csdn.net/postedit/40829321
像上面那樣,如果在5s內被SIGALRM信號中斷,表示超時,否則,已經讀取到數據.但是這麼方法在網絡編程中不太常用,因爲有時可能在其他地方使用了alarm會造成混亂。
二. 使用套接字選項SO_SNDTIMEO,SO_RECVTIMEO實現超時
struct timeval timeout={3,0};
setsockopt(sock,SOL_SOCKET,SO_RECVTIME0,(char*)&timeout,sizeof(struct timeval));
int ret=read(sock,buf,sizeof(buf));
if(ret == -1 && errno==EWOULDBLOCK)
ERRNO=ETIMEOUT;
即使用setsockopt 函數進行設置,但這種方法可移植性比較差,不是每種系統實現都有這些選項。
三. 使用select實現超時
(1) read_timeout
/*
read_time-------讀超時檢測函數,不含讀操作
fd--------------文件描述符
wait_seconds----等待的秒數,
成功(未超時)返回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會阻塞直到檢測到事件或者超時
// 如果selcel檢測到可讀事件,,則此時調用read不會阻塞
}while(ret < 0 && errno == EINTR);
if(ret == 0)
{
ret = -1;
errno = ETIMEDOUT;
}
else if(ret == 1)// 檢測到一個事件
return 0;
}
return ret;
}
簡析: read_time,只是超時檢測函數,沒有讀操作,如果從此函數成功返回,則此時調用read將不再阻塞.
如果 read_timeout(fd, 0); 則表示不檢測超時,函數直接返回爲0,此時再調用read 將會阻塞。
當wait_seconds 參數大於0,則進入if 括號執行,將超時時間設置爲select函數的超時時間結構體,select會阻塞直到檢測到事件發生或者超時。如果select返回-1且errno 爲EINTR,說明是被信號中斷,需要重啓select;如果select返回0表示超時;如果select返回1表示檢測到可讀事件;否則select返回-1 表示出錯。
應用模式:
int ret;
ret = read_timeout(fd,5);
if(ret == 0)//未超時或不檢測超時
{
read(fd,....);
}
else if(ret ==-1 && errno == ETIMEDOUT)// 超時
{
printf("timeout...\n");
}
els
{
ERR_EXIT("READ_EXIT");
}
(2)write_timeout
/*
write_timeout-------寫超時檢測函數,不含讀操作
fd--------------文件描述符
wait_seconds----等待的秒數,
成功(未超時)返回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;
}
(3)accept_timeout
/*
accept_time-------帶超時的accept
fd--------------文件描述符
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; //創建accept文件描述集合
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);//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("accept err");
return ret;
}
解析:accept_timeout :此函數是帶超時的accept 函數,如果能從if (wait_seconds > 0) 括號執行後向下執行,說明select 返回爲1,檢測到已連接隊列不爲空,此時再調用accept 不再阻塞,當然如果wait_seconds == 0 則像正常模式一樣,accept 阻塞等待,注意,accept 返回的是已連接套接字。
(4)connect_timeout
非阻塞I/O使我們的操作要麼成功,要麼立即返回錯誤,不被阻塞。
對於一個給定的描述符兩種方法對其指定非阻塞I/O:
(1)調用open獲得描述符,並指定O_NONBLOCK標誌
(2)對已經打開的文件描述符,調用fcntl,打開O_NONBLOCK文件狀態標誌。
flags = fcntl( s, F_GETFL, 0 ) )
fcntl( s, F_SETFL, flags | O_NONBLOCK )
--------------------------------------------------------------------------------------
fcntl的返回值與命令有關。如果出錯,所有命令都返回-1,如果成功則返回某個其他值。下列四個命令有特定返回值:F_DUPFD、F_GETFD、F_GETFL、F_GETOWN.第一個返回新的文件描述符,接下來的兩個返回相應標誌,最後一個返回一個正的進程ID或負的進程組ID。
//activate_nonblock --- 設置IO爲非阻塞模式
void activate_nonblock(int fd)
{
int ret;
int flags = fcntl(fd,F_GETFL,NULL);
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爲阻塞模式
*/
void deactivate_nonblock(int fd)
{
int ret;
int flags = fcntl(fd,F_GETFL,NULL);
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
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);//設置IO爲非阻塞模式
ret = connect(fd,(struct sockaddr*)addr,addrlen);
if(ret < 0 && errno == EINPROGRESS)//Linux 非阻塞connect,錯誤碼:EINPROGRESS,連接正在處理當中
{
printf("aaaaaaaa\n");
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
{
// 這裏select使用寫集合,因爲一旦建立連接,套接字就寫
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來獲取
// 獲取套接字的錯誤
printf("bbbbbbbbbbb\n");
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)//沒有錯誤
{
printf("dddddddddd\n");
ret = 0;
}
else// 產生錯誤
{
printf("cccccccccc\n");
errno = err;
ret = -1;
}
}
}
if(wait_seconds > 0)/// 重新置爲阻塞模式
deactivate_nonblock(fd);
return ret;
}
解析: 在調用connect前需要使用fcntl 函數將套接字標誌設置爲非阻塞,如果網絡環境很好,則connect立即返回0,不進入if 大括號執行;如果網絡環境擁塞,則connect返回-1且errno == EINPROGRESS,表示正在處理。此後調用select與前面3個函數類似,但這裏關注的是可寫事件,因爲一旦連接建立,套接字就可寫。還需要注意的是當select 返回1,可能有兩種情況,一種是連接成功,一種是套接字產生錯誤,這兩種情況都會產生可寫事件,所以需要使用getsockopt來獲取一下。退出之前還需重新將套接字設置爲阻塞。
完整的C/S測試程序如下:
服務器程序:
/// srv.c
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
int main()
{
int listenfd;
printf("%d",INADDR_ANY);
if((listenfd = socket(PF_INET,SOCK_STREAM,0)) < 0)
ERR_EXIT("socket err");
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
// 設定地址重複利用
int on=1;
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)) < 0)
ERR_EXIT("setsockopt err");
if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
ERR_EXIT("bind err");
if(listen(listenfd,SOMAXCONN) < 0)
ERR_EXIT("listen err");
struct sockaddr_in peeraddr;
socklen_t peerlen=sizeof(peeraddr);
int conn;
if((conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen)) < 0)
ERR_EXIT("accept err");
//連接成功,打印對方IP地址
printf("ip=%s ,port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
return 0;
}
客戶端程序:
// sys.h
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
#include<signal.h>
#include<sys/wait.h>
#include<fcntl.h>
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
/*
read_time-------讀超時檢測函數,不含讀操作
fd--------------文件描述符
wait_seconds----等待的秒數,
成功(未超時)返回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會阻塞直到檢測到事件或者超時
// 如果selcel檢測到可讀事件,,則此時調用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,失敗返回-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_time-------帶超時的accept
fd--------------文件描述符
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; //創建accept文件描述集合
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);//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("accept err");
return ret;
}
//activate_nonblock --- 設置IO爲非阻塞模式
void activate_nonblock(int fd)
{
int ret;
int flags = fcntl(fd,F_GETFL,NULL);
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爲阻塞模式
*/
void deactivate_nonblock(int fd)
{
int ret;
int flags = fcntl(fd,F_GETFL,NULL);
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
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);//設置IO爲非阻塞模式
ret = connect(fd,(struct sockaddr*)addr,addrlen);
if(ret < 0 && errno == EINPROGRESS)//Linux 非阻塞connect,錯誤碼:EINPROGRESS,連接正在處理當中
{
//// printf("aaaaaaaa\n");
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
{
// 這裏select使用寫集合,因爲一旦建立連接,套接字就寫
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來獲取
// 獲取套接字的錯誤
//// printf("bbbbbbbbbbb\n");
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)//沒有錯誤
{
//printf("dddddddddd\n");
ret = 0;
}
else// 產生錯誤
{
//printf("cccccccccc\n");
errno = err;
ret = -1;
}
}
}
if(wait_seconds > 0)/// 重新置爲阻塞模式
deactivate_nonblock(fd);
return ret;
}
/////cli.c
#include "sys.h"
int main()
{
int sock;
if((sock = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0)
ERR_EXIT("socket err");
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 err");
struct sockaddr_in localaddr;
socklen_t addrlen = sizeof(localaddr);
if( getsockname(sock,(struct sockaddr *)&localaddr,&addrlen) <0)
ERR_EXIT("getsockname err");
printf("ip=%s,port=%d\n",inet_ntoa(localaddr.sin_addr),ntohs(localaddr.sin_port));
return 0;
}
Makefile:
.PHONY:clean all
CC=gcc
CFLAGS=-Wall -g
BIN=srv cli 1 #echoser echocli echocli2 echoser2
all:$(BIN)
%.o:%.c
$(cc) $(CFLAGS) -c $< -o $@ #$< 當前依賴的文件名,$@當前目標的文件名
clean:
rm -f *.o $(BIN)