1. 程式人生 > >非阻塞connect的作用及程式碼示例

非阻塞connect的作用及程式碼示例

connect 函式的呼叫涉及到TCP連線的三次握手過程,通常阻塞的connect 函式會等待三次握手成功或失敗後返回,0成功,-1失敗。如果對方未響應,要隔6s,重發嘗試,可能要等待75s的嘗試並最終返回超時,才得知連線失敗。即使是一次嘗試成功,也會等待幾毫秒到幾秒的時間,如果此期間有其他事務要處理,則會白白浪費時間,而用非阻塞的connect 則可以做到並行,提高效率。

         而通常,非阻塞的connect 函式與 select 函式配合使用:在一個TCP套介面被設定為非阻塞之後呼叫connect,connect (函式本身返回-1)會立即返回EINPROGRESS或EWOULDBLOCK錯誤,表示連線操作正在進行中,但是仍未完成;同時TCP的三路握手操作繼續進行;在這之後,我們可以呼叫select來檢查這個連結是否建立成功。

       若建立連線成功,select的結果中該描述符可寫;若失敗,則可寫可讀,此時可以使用getsockopt獲取錯誤資訊。

正常的三次握手時序:

非阻塞connect有三種用途:
1. 我們可以在三路握手的同時做一些其它的處理。connect 操作要花一個往返時間完成,而且可以是在任何地方,從幾個毫秒的區域網到幾百毫秒或幾秒的廣域網,在這段時間內我們可能有一些其他的處理想要執行;
2. 可以用這種技術同時建立多個連線.在Web瀏覽器中很普遍;
3. 由於我們使用select 來等待連線的完,因此我們可以給select設定一個時間限制,從而縮短connect 的超時時間。在大多數實現中,connect 的超時時間在75秒到幾分鐘之間。有時候應用程式想要一個更短的超時時間,使用非阻塞connect 就是一種方法。

非阻塞connect 聽起來雖然簡單,但是仍然有一些細節問題要處理:
1. 即使套介面是非阻塞的。如果連線的伺服器在同一臺主機上,那麼在呼叫connect 建立連線時,連線通常會立即建立成功,我們必須處理這種情況。
2. 源自Berkeley的實現(和Posix.1g)有兩條與select 和非阻塞IO相關的規則:
A. 當連線建立成功時,套介面描述符變成可寫;
  B. 當連接出錯時,套介面描述符變成既可讀又可寫。

處理非阻塞connect的步驟(重點):
1. 建立socket,返回套介面描述符;
2. 呼叫fcntl 把套介面描述符設定成非阻塞;
3. 呼叫connect 開始建立連線;
4. 判斷連線是否成功建立。

判斷連線是否成功建立:
A. 如果connect 返回0,表示連線成功(伺服器和客戶端在同一臺機器上時就有可能發生這種情況);
B. 呼叫select 來等待連線建立成功完成;
     如果select 返回0,則表示建立連線超時。我們返回超時錯誤給使用者,同時關閉連線,以防止三路握手操作繼續進行下去。
     如果select 返回大於0的值,則需要檢查套介面描述符是否可寫,如果套介面描述符可寫,則我們可以通過呼叫getsockopt來得到套介面上待處理的錯誤(SO_ERROR)。如果連線建立成功,這個錯誤值將是0;如果建立連線時遇到錯誤,則這個值是連線錯誤所對應的errno值(比如:ECONNREFUSED,ETIMEDOUT等)。

程式碼示例

int conn_nonb(int sockfd, const struct sockaddr_in *saptr, socklen_t salen, int nsec)
{
    int flags, n, error, code;
    socklen_t len;
    fd_set wset;
    struct timeval tval;

    flags = fcntl(sockfd, F_GETFL, 0);
    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

    error = 0;
    if ((n == connect(sockfd, saptr, salen)) == 0) {
        goto done;
    } else if (n < 0 && errno != EINPROGRESS){
        return (-1);
    }

    /* Do whatever we want while the connect is taking place */

    FD_ZERO(&wset);
    FD_SET(sockfd, &wset);
    tval.tv_sec = nsec;
    tval.tv_usec = 0;

    if ((n = select(sockfd+1, NULL, &wset, 
                    NULL, nsec ? &tval : NULL)) == 0) {
        close(sockfd);  /* timeout */
        errno = ETIMEDOUT;
        return (-1);
    }

    if (FD_ISSET(sockfd, &wset)) {
        len = sizeof(error);
        code = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len);
        /* 如果發生錯誤,Solaris實現的getsockopt返回-1,
         * 把pending error設定給errno. Berkeley實現的
         * getsockopt返回0, pending error返回給error. 
         * 我們需要處理這兩種情況 */
        if (code < 0 || error) {
            close(sockfd);
            if (error) 
                errno = error;
            return (-1);
        }
    } else {
        fprintf(stderr, "select error: sockfd not set");
        exit(0);
    }

done:
    fcntl(sockfd, F_SETFL, flags);  /* restore file status flags */
    return (0);
}