SELECT裝置逾時用法小結
目前各平台通用的設定socket connect逾時的辦法是通過select(),具體方法如下:
1.建立socket;
2.將該socket設定為非阻塞模式;
3.調用connect();
4.使用select()檢查該socket描述符是否可寫;
5.根據select()返回的結果判斷connect()結果;
6.將socket設回阻塞模式。
select,就是用來監視某個或某些控制代碼的狀態變化的,執行I/O多路轉換。
select函數原型如下:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
ndfs:select監視的檔案控制代碼數,視進程中開啟的檔案數而定,一般設為要監視各檔案中的最大檔案號加一。
readfds:select監視的可讀檔案控制代碼集合。
writefds: select監視的可寫檔案控制代碼集合。
exceptfds:select監視的異常檔案控制代碼集合。
timeout:本次select()的逾時結束時間
當readfds或writefds中映象的檔案可讀或可寫或逾時,本次select()就結束返回。程式員利用一組系統提供的宏在select()結束時便可判斷哪一檔案可讀或可寫。對Socket編程特別有用的就是readfds。幾隻相關的宏解釋如下:
FD_ZERO(fd_set *fdset):清空fdset與所有檔案控制代碼的聯絡。
FD_SET(int fd, fd_set *fdset):建立檔案控制代碼fd與fdset的聯絡。
FD_CLR(int fd, fd_set *fdset):清除檔案控制代碼fd與fdset的聯絡。
FD_ISSET(int fd, fdset *fdset):檢查fdset聯絡的檔案控制代碼fd是否可讀寫,>0表示可讀寫。
函 數的最後一個參數timeout顯然是一個逾時時間值,其類型是struct timeval *,即一個struct timeval結構的變數的指標,所以我們在程式裡要申明一個struct timeval tv;然後把變數tv的地址&tv傳遞給select函數。struct timeval結構如下:
struct timeval
{
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
第2、 3、4三個參數是一樣的類型: fd_set *,即我們在程式裡要申明幾個fd_set類型的變數,比如rdfds, wtfds, exfds,然後把這個變數的地址&rdfds, &wtfds, &exfds 傳遞給select函數。
這三個參數都是一個控制代碼的集合,第一個rdfds是用來儲存這樣的控制代碼的:當控制代碼的狀態變成可讀的時系統就會告訴select函數返回,同理第二個wtfds是指有控制代碼狀態變成可寫的時系統就會告訴select函數返回,同理第三個參數exfds是特殊情況,即控制代碼上有特殊情況發生時系統會告訴select函數返回。特殊情況比如對方通過一個socket控制代碼發來了緊急資料。如果我們程式裡只想檢測某個socket是否有資料可讀,我們可以這樣:
fd_set rdfds; /* 先申明一個 fd_set 集合來儲存我們要檢測的 socket控制代碼 */
struct timeval tv; /* 申明一個時間變數來儲存時間 */
int ret; /* 儲存傳回值 */
FD_ZERO(&rdfds); /* 用select函數之前先把集合清零 */
FD_SET(socket, &rdfds); /* 把要檢測的控制代碼socket加入到集合裡 */
tv.tv_sec = 1;
tv.tv_usec = 500; /* 設定select等待的最大時間為1秒加500微秒 */
ret = select(socket + 1, &rdfds, NULL, NULL, &tv); /* 檢測我們上面設定到集合rdfds裡的控制代碼是否有可讀資訊 */
if(ret < 0)
perror("select");/* 這說明select函數出錯 */
else if(ret == 0)
printf("逾時/n"); /* 說明在我們設定的時間值1秒加500毫秒的時間內,socket的狀態沒有發生變化 */
else
{ /* 說明等待時間還未到1秒加500毫秒,socket的狀態發生了變化 */
printf("ret=%d/n", ret); /* ret這個傳回值記錄了發生狀態變化的控制代碼的數目,由於我們只監視了socket這一個控制代碼,所以這裡一定ret=1,如果同時有多個控制代碼發生變化返回的就是控制代碼的總和了 */
/* 這裡我們就應該從socket這個控制代碼裡讀取資料了,因為select函數已經告訴我們這個控制代碼裡有資料可讀 */
if(FD_ISSET(socket, &rdfds)) { /* 先判斷一下socket這外被監視的控制代碼是否真的變成可讀的了 */
/* 讀取socket控制代碼裡的資料 */
recv(...);
}
}
注意select函數的第一個參數,是所有加入集合的控制代碼值的最大那個值還要加1。比如我們建立了3個控制代碼:
int sa, sb, sc;
sa = socket(...); /* 分別建立3個控制代碼並串連到伺服器上 */
connect(sa,...);
sb = socket(...);
connect(sb,...);
sc = socket(...);
connect(sc,...);
FD_SET(sa, &rdfds);/* 分別把3個控制代碼加入讀監視集合裡去 */
FD_SET(sb, &rdfds);
FD_SET(sc, &rdfds);
在使用select函數之前,一定要找到3個控制代碼中的最大值是哪個,我們一般定義一個變數來儲存最大值,取得最大socket值如下:
int maxfd = 0;
if(sa > maxfd)
maxfd = sa;
if(sb > maxfd)
maxfd = sb;
if(sc > maxfd)
maxfd = sc;
然後調用select函數:
ret = select(maxfd + 1, &rdfds, NULL, NULL, &tv); /* 注意是最大值還要加1 */
同樣的道理,如果我們要檢測使用者是否按了鍵盤進行輸入,我們就應該把標準輸入0這個控制代碼放到select裡來檢測,如下:
FD_ZERO(&rdfds);
FD_SET(0, &rdfds);
tv.tv_sec = 1;
tv.tv_usec = 0;
ret = select(1, &rdfds, NULL, NULL, &tv); /* 注意是最大值還要加1 */
if(ret < 0)
perror("select");/* 出錯 */
else if(ret == 0)
printf("逾時/n"); /* 在我們設定的時間tv內,使用者沒有按鍵盤 */
else { /* 使用者有按鍵盤,要讀取使用者的輸入 */
scanf("%s", buf); }
LINUX設定連線逾時方法:
在阻塞通訊端的一般情況下,connect ()直到用戶端對SYN訊息的ACK訊息到達之前才會返回。使connect()調用具有逾時機制的一個方法是讓通訊端成為非阻塞的通訊端體,然後用select()來等待它完成。
s = socket(AF_INET, SOCK_STREAM, 0);
//下面擷取通訊端的標誌
if ((flags = fcntl(s, F_GETFL, 0)) < 0) {
//錯誤處理
}
//下面設定通訊端為非阻塞
if (fcntl(s, F_SETFL, flags | O_NONBLOCK) < 0) {
//錯誤處理
}
if ((retcode = connect(s, (struct sockaddr*)&peer, sizeof(peer)) &&
errno != EINPROGRESS) {
//因為通訊端設為NONBLOCK,通常情況下,串連在connect()返回
//之前是不會建立的,因此它會返回EINPROGRESS錯誤,如果返回
//任何其他錯誤,則要進行錯誤處理
}
if (0 == retcode) { //如果connect()返回0則串連已建立
//下面恢複通訊端阻塞狀態
if (fcntl(s, F_SETFL, flags) < 0) {
//錯誤處理
}
//下面是串連成功後要執行的代碼
exit(0)
}
(要裝置send/recv逾時只需從此處開始修改相應值,前面不用)
FD_ZERO(&rdevents);
FD_SET(s, &rdevents); //把先前的通訊端加到讀集合裡面
wrevents = rdevents; //寫集合
exevents = rdevents; //異常集合
tv.tv_sec = 5; //設定時間為5秒
tv_tv_usec = 0;
retcode = select(s+1, &rdevents, &wrevents, &exevents, &tv);
if (retcode < 0) { //select返回錯誤???
//錯誤處理
}
else if (0 == retcode) { //select 逾時???
//逾時處理
}
esle {
//通訊端已經準備好
if (!FD_ISSET(s, &rdevents) && !FD_ISSET(s, &wrevents)) {
//connect()失敗,進行錯處理
}
if (getsockopt(s, SOL_SOCKET, SO_ERROR, &err, &len) < 0) {
//getsockopt()失敗,進行錯處理
}
if (err != 0) {
//connect()失敗,進行錯處理
}
(send/recv逾時到此為止,返回send()/recv()函數)
//到這裡說明connect()正確返回
//下面恢複通訊端阻塞狀態
if (fcntl(s, F_SETFL, flags) < 0) {
//錯誤處理
}
//下面是串連成功後要執行的代碼
exit(0)