引:超時設置3種方案
1. alarm超時設置方法
- //代碼實現: 這種方式較少用
- void sigHandlerForSigAlrm(int signo)
- {
- return ;
- }
- signal(SIGALRM, sigHandlerForSigAlrm);
- alarm(5);
- int ret = read(sockfd, buf, sizeof(buf));
- if (ret == -1 && errno == EINTR)
- {
- // 超時被時鐘打斷
- errno = ETIMEDOUT;
- }
- else if (ret >= 0)
- {
- // 正常返回(沒有超時), 則將鬧鐘關閉
- alarm(0);
- }
2. 套接字選項: SO_SNDTIMEO, SO_RCVTIMEO
調用setsockopt設置讀/寫超時時間
- //示例: read超時
- int seconds = 5;
- if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &seconds, sizeof(seconds)) == -1)
- err_exit("setsockopt error");
- int ret = read(sockfd, buf, sizeof(buf));
- if (ret == -1 && errno == EWOULDBLOCK)
- {
- // 超時,被時鐘打斷
- errno = ETIMEDOUT;
- }
3. select方式[重點]
_timeout函數封裝
1. read_timeout封裝
- /**
- *read_timeout - 讀超時檢測函數, 不包含讀操作
- *@fd: 文件描述符
- *@waitSec: 等待超時秒數, 0表示不檢測超時
- *成功(未超時)返回0, 失敗返回-1, 超時返回-1 並且 errno = ETIMEDOUT
- **/
- int read_timeout(int fd, long waitSec)
- {
- int returnValue = 0;
- if (waitSec > 0)
- {
- fd_set readSet;
- FD_ZERO(&readSet);
- FD_SET(fd,&readSet); //添加
- struct timeval waitTime;
- waitTime.tv_sec = waitSec;
- waitTime.tv_usec = 0; //將微秒設置爲0(不進行設置),如果設置了,時間會更加精確
- do
- {
- returnValue = select(fd+1,&readSet,NULL,NULL,&waitTime);
- }
- while(returnValue < 0 && errno == EINTR); //等待被(信號)打斷的情況, 重啓select
- if (returnValue == 0) //在waitTime時間段中一個事件也沒到達
- {
- returnValue = -1; //返回-1
- errno = ETIMEDOUT;
- }
- else if (returnValue == 1) //在waitTime時間段中有事件產生
- returnValue = 0; //返回0,表示成功
- // 如果(returnValue == -1) 並且 (errno != EINTR), 則直接返回-1(returnValue)
- }
- return returnValue;
- }
2. write_timeout封裝
- /**
- *write_timeout - 寫超時檢測函數, 不包含寫操作
- *@fd: 文件描述符
- *@waitSec: 等待超時秒數, 0表示不檢測超時
- *成功(未超時)返回0, 失敗返回-1, 超時返回-1 並且 errno = ETIMEDOUT
- **/
- int write_timeout(int fd, long waitSec)
- {
- int returnValue = 0;
- if (waitSec > 0)
- {
- fd_set writeSet;
- FD_ZERO(&writeSet); //清零
- FD_SET(fd,&writeSet); //添加
- struct timeval waitTime;
- waitTime.tv_sec = waitSec;
- waitTime.tv_usec = 0;
- do
- {
- returnValue = select(fd+1,NULL,&writeSet,NULL,&waitTime);
- } while(returnValue < 0 && errno == EINTR); //等待被(信號)打斷的情況
- if (returnValue == 0) //在waitTime時間段中一個事件也沒到達
- {
- returnValue = -1; //返回-1
- errno = ETIMEDOUT;
- }
- else if (returnValue == 1) //在waitTime時間段中有事件產生
- returnValue = 0; //返回0,表示成功
- }
- return returnValue;
- }
3. accept_timeout函數封裝
- /**
- *accept_timeout - 帶超時的accept
- *@fd: 文件描述符
- *@addr: 輸出參數, 返回對方地址
- *@waitSec: 等待超時秒數, 0表示不使用超時檢測, 使用正常模式的accept
- *成功(未超時)返回0, 失敗返回-1, 超時返回-1 並且 errno = ETIMEDOUT
- **/
- int accept_timeout(int fd, struct sockaddr_in *addr, long waitSec)
- {
- int returnValue = 0;
- if (waitSec > 0)
- {
- fd_set acceptSet;
- FD_ZERO(&acceptSet);
- FD_SET(fd,&acceptSet); //添加
- struct timeval waitTime;
- waitTime.tv_sec = waitSec;
- waitTime.tv_usec = 0;
- do
- {
- returnValue = select(fd+1,&acceptSet,NULL,NULL,&waitTime);
- }
- while(returnValue < 0 && errno == EINTR);
- if (returnValue == 0) //在waitTime時間段中沒有事件產生
- {
- errno = ETIMEDOUT;
- return -1;
- }
- else if (returnValue == -1) // error
- return -1;
- }
- /**select正確返回:
- 表示有select所等待的事件發生:對等方完成了三次握手,
- 客戶端有新的鏈接建立,此時再調用accept就不會阻塞了
- */
- socklen_t socklen = sizeof(struct sockaddr_in);
- if (addr != NULL)
- returnValue = accept(fd,(struct sockaddr *)addr,&socklen);
- else
- returnValue = accept(fd,NULL,NULL);
- return returnValue;
- }
4. connect_timeout函數封裝
(1)我們爲什麼需要這個函數?
TCP/IP在客戶端連接服務器時,如果發生異常,connect(如果是在默認阻塞的情況下)返回的時間是RTT(相當於客戶端阻塞了這麼長的時間,客戶需要等待這麼長的時間,顯然這樣的客戶端用戶體驗並不好(完成三次握手需要使用1.5RTT時間));會造成嚴重的軟件質量下降.
(2)怎樣實現connect_timeout?
1)sockfd首先變成非阻塞的; 然後試着進行connect,如果網絡狀況良好,則立刻建立鏈接並返回,如果網絡狀況不好,則鏈接不會馬上建立,這時需要我們的參與:調用select,設置等待時間,通過select管理者去監控sockfd,一旦能夠建立鏈接,則馬上返回,然後建立鏈接,這樣就會大大提高我們的軟件質量.
2)需要注意:select機制監控到sockfd可寫(也就是可以建立鏈接時),並不代表調用connect就一定能夠成功(造成sockfd可寫有兩種情況: a.真正的鏈接可以建立起來了; b.建立鏈接的過程中發生錯誤,然後錯誤會回寫錯誤信息,造成sockfd可寫);
通過調用getsockopt做一個容錯即可(見下例)!
(3)代碼實現:
- /**設置文件描述符fd爲非阻塞/阻塞模式**/
- bool setUnBlock(int fd, bool unBlock)
- {
- int flags = fcntl(fd,F_GETFL);
- if (flags == -1)
- return false;
- if (unBlock)
- flags |= O_NONBLOCK;
- else
- flags &= ~O_NONBLOCK;
- if (fcntl(fd,F_SETFL,flags) == -1)
- return false;
- return true;
- }
- /**
- *connect_timeout - connect
- *@fd: 文件描述符
- *@addr: 要連接的對方地址
- *@waitSec: 等待超時秒數, 0表示使用正常模式的accept
- *成功(未超時)返回0, 失敗返回-1, 超時返回-1 並且 errno = ETIMEDOUT
- **/
- int connect_timeout(int fd, struct sockaddr_in *addr, long waitSec)
- {
- if (waitSec > 0) //設置爲非阻塞模式
- setUnBlock(fd, true);
- socklen_t addrLen = sizeof(struct sockaddr_in);
- //首先嚐試着進行鏈接
- int returnValue = connect(fd,(struct sockaddr *)addr,addrLen);
- //如果首次嘗試失敗(並且errno == EINPROGRESS表示連接正在處理當中),則需要我們的介入
- if (returnValue < 0 && errno == EINPROGRESS)
- {
- fd_set connectSet;
- FD_ZERO(&connectSet);
- FD_SET(fd,&connectSet);
- struct timeval waitTime;
- waitTime.tv_sec = waitSec;
- waitTime.tv_usec = 0;
- do
- {
- /*一旦建立鏈接,則套接字可寫*/
- returnValue = select(fd+1, NULL, &connectSet, NULL, &waitTime);
- }
- while (returnValue < 0 && errno == EINTR);
- if (returnValue == -1) //error
- return -1;
- else if (returnValue == 0) //超時
- {
- returnValue = -1;
- errno = ETIMEDOUT;
- }
- else if (returnValue == 1) //正確返回,有一個套接字可寫
- {
- /**由於connectSet只有一個文件描述符, 因此FD_ISSET的測試也就省了**/
- /**注意:套接字可寫有兩種情況:
- 1.連接建立成功
- 2.套接字產生錯誤(但是此時select是正確的, 因此錯誤信息沒有保存在errno中),需要調用getsockopt獲取
- */
- int err;
- socklen_t errLen = sizeof(err);
- int sockoptret = getsockopt(fd,SOL_SOCKET,SO_ERROR,&err,&errLen);
- if (sockoptret == -1)
- return -1;
- // 測試err的值
- if (err == 0) //確實是鏈接建立成功
- returnValue = 0;
- else //連接產生了錯誤
- {
- errno = err;
- returnValue = -1;
- }
- }
- }
- if (waitSec > 0)
- setUnBlock(fd, false);
- return returnValue;
- }
- /**測試:使用connect_timeout的client端完整代碼(server端如前)**/
- int main()
- {
- int sockfd = socket(AF_INET, SOCK_STREAM, 0);
- if (sockfd == -1)
- err_exit("socket error");
- struct sockaddr_in serverAddr;
- serverAddr.sin_family = AF_INET;
- serverAddr.sin_port = htons(8001);
- serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
- int ret = connect_timeout(sockfd, &serverAddr, 5);
- if (ret == -1 && errno == ETIMEDOUT)
- {
- cerr << "timeout..." << endl;
- err_exit("connect_timeout error");
- }
- else if (ret == -1)
- err_exit("connect_timeout error");
- //獲取並打印對端信息
- struct sockaddr_in peerAddr;
- socklen_t peerLen = sizeof(peerAddr);
- if (getpeername(sockfd, (struct sockaddr *)&peerAddr, &peerLen) == -1)
- err_exit("getpeername");
- cout << "Server information: " << inet_ntoa(peerAddr.sin_addr)
- << ", " << ntohs(peerAddr.sin_port) << endl;
- close(sockfd);
- }
附-RTT(Round-Trip Time)介紹:
RTT往返時延:在計算機網絡中它是一個重要的性能指標,表示從發送端發送數據開始,到發送端收到來自接收端的確認(接收端收到數據後便立即發送確認),總共經歷的時延。
RTT由三個部分決定:即鏈路的傳播時間、末端系統的處理時間以及路由器的緩存中的排隊和處理時間。其中,前面兩個部分的值作爲一個TCP連接相對固定,路由器的緩存中的排隊和處理時間會隨着整個網絡擁塞程度的變化而變化。所以RTT的變化在一定程度上反映了網絡擁塞程度的變化。簡單來說就是發送方從發送數據開始,到收到來自接受方的確認信息所經歷的時間。