linux網絡編程之套接字:套接字I/O超時設置方法和用select實現超時

一、使用alarm 函數設置超時

void handler(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);
.................
程序大概框架如上所示,如果read在5s內被SIGALRM信號中斷而返回,則表示超時,否則未超時已讀取到數據,取消鬧鐘。但這種方法不常用,因爲有時可能在其他地方使用了alarm會造成混亂。

二、使用套接字選項SO_SNDTIMEO、SO_RCVTIMEO

struct timeval timeout = {3,0}; 
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(struct timeval));

int ret = read(sock, buf, sizeof(buf));
if (ret == -1 && errno == EWOULDBLOCK)
    errno = ETIMEOUT;
..........
即使用setsockopt 函數進行設置,但這種方法可移植性比較差,不是每種系統實現都有這些選項。

三、使用select 實現超時

下面程序包含read_timeout、write_timeout、accept_timeout、connect_timeout 四個函數封裝

#include "sysutil.h"

/* read_timeout - 讀超時檢測函數,不含讀操作
 * fd:文件描述符
 * wait_seconds:等待超時秒數, 如果爲0表示不檢測超時;
 * 成功(未超時)返回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會阻塞直到檢測到事件或者超時
            // 如果select檢測到可讀事件發送,則此時調用read不會阻塞
        }
        while (ret < 0 && errno == EINTR);//如果select返回-1或者errno爲EINTR,說明是被信號中斷,需要重啓select

        if (ret == 0)//select返回0表示超時
        {
            ret = -1;
            errno = ETIMEDOUT;
        }
        else if (ret == 1)//select返回1表示檢測到可讀時間,則此函數最後返回0,即沒有超時
            return 0;

    }

    return ret;
}

/* write_timeout - 寫超時檢測函數,不含寫操作
 * fd:文件描述符
 * wait_seconds:等待超時秒數, 如果爲0表示不檢測超時;
 * 成功(未超時)返回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_timeout - 帶超時的accept
 * fd: 套接字
 * addr: 輸出參數,返回對方地址
 * 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;
        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);

        if (ret == -1)
            return -1;
        else if (ret == 0)
        {
            errno = ETIMEDOUT;
            return -1;
        }
    }//第一個if執行完說明select返回1,檢測到已連接隊列不爲空,此時調用accept則不會阻塞

    if (addr != NULL)
        ret = accept(fd, (struct sockaddr *)addr, &addrlen);//accept阻塞等待,返回已連接的套接字
    else
        ret = accept(fd, NULL, NULL);
    if (ret == -1)
        ERR_EXIT("accpet error");

    return ret;
}

/* activate_nonblock - 設置IO爲非阻塞模式
 * fd: 文件描述符
 */
void activate_nonblock(int fd)
{
    int ret;
    int flags = fcntl(fd, F_GETFL);
    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爲阻塞模式
 * fd: 文件描述符
 */
void deactivate_nonblock(int fd)
{
    int ret;
    int flags = fcntl(fd, F_GETFL);
    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
 * 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;
    socklen_t addrlen = sizeof(struct sockaddr_in);

    if (wait_seconds > 0)//在調用connect前需要使用fcntl 函數將套接字標誌設置爲非阻塞
        activate_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);
        }
        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來獲取 */
            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)
                ret = 0;
            else
            {
                errno = err;
                ret = -1;
            }
        }
    }

    if (wait_seconds > 0)//退出之前還需重新將套接字標誌設置爲阻塞
        deactivate_nonblock(fd);


    return ret;
}
1.如果 read_timeout(fd, 0); 則表示不檢測超時,函數直接返回爲0,此時再調用read 將會阻塞。

2.當wait_seconds 參數大於0,則進入if 括號執行,將超時時間設置爲select函數的超時時間結構體,select會阻塞直到檢測到事件發生或者超時。如果select返回-1且errno 爲EINTR,說明是被信號中斷,需要重啓select;如果select返回0表示超時;如果select返回1表示檢測到可讀事件;否則select返回-1 表示出錯。

3.write_timeout :此函數跟read_timeout 函數類似,只是select 關心的是可寫事件,不再贅述。

4.accept_timeout :此函數是帶超時的accept 函數,如果能從if (wait_seconds > 0) 括號執行後向下執行,說明select 返回爲1,檢測到已連接隊列不爲空,此時再調用accept 不再阻塞,當然如果wait_seconds == 0 則像正常模式一樣,accept 阻塞等待,注意,accept 返回的是已連接套接字。

5.connect_timeout :在調用connect前需要使用fcntl 函數將套接字標誌設置爲非阻塞,如果網絡環境很好,則connect立即返回0,不進入if 大括號執行;如果網絡環境擁塞,則connect返回-1且errno == EINPROGRESS,表示正在處理。此後調用select與前面3個函數類似,但這裏關注的是可寫事件,因爲一旦連接建立,套接字就可寫。還需要注意的是當select 返回1,可能有兩種情況,一種是連接成功,一種是套接字產生錯誤,由這裏可知,這兩種情況都會產生可寫事件,所以需要使用getsockopt來獲取一下。退出之前還需重新將套接字設置爲阻塞。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章