天天看點

Socket程式設計實踐(9) --套接字IO逾時設定方法

引:逾時設定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的變化在一定程度上反映了網絡擁塞程度的變化。簡單來說就是發送方從發送資料開始,到收到來自接受方的确認資訊所經曆的時間。

繼續閱讀