Linux connect函數非阻塞實現

1、阻塞和非阻塞connect

對於面向連接的socket類型(SOCK_STREAM、SOCK_SEQPACKET、SOCK_RDM),在通信雙方讀寫數據之前都需要先建立連接,connect()函數用於完成面向連接的socket的連接建立過程。而connect建立連接的模式可以分爲阻塞和非阻塞兩種,默認情況是阻塞模式。

多數實現中,connect的超時時間在75秒到幾分鐘之間。有時程序希望在等待一定時間內結束,使用非阻塞connect可以防止阻塞75秒。在多線程網絡編程中,非阻塞就顯得尤其必要。 例如,有一個通過建立線程與其他主機進行socket通信的應用程序,如果建立的線程使用阻塞connect與遠程進行通信,當有幾百個線程併發的時候,由於網絡延遲而全部阻塞,阻塞的線程不會釋放系統的資源,同一時刻阻塞線程超過一定數量時候,系統就不再允許建立新的線程(每個進程由於進程空間的原因能產生的線程有限),如果使用非阻塞的connect,連接失敗時,使用select只需等待很短的時間,如果還沒有連接成功,線程立刻結束釋放資源,防止大量線程阻塞而使程序崩潰。

2、connect()函數

CONNECT(2)                 Linux Programmer’s Manual                CONNECT(2)
NAME
       connect - initiate a connection on a socket
SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>
       int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);
ERRORS
       The following are general socket errors only.  There may be other domain-specific error codes.
EINPROGRESS
              The socket is non-blocking and the connection cannot be completed immediately.  It is possible to select(2) or poll(2) for completion  by  selecting
              the  socket  for  writing.   After  select(2)  indicates writability, use getsockopt(2) to read the SO_ERROR option at level SOL_SOCKET to determine
              whether connect() completed successfully (SO_ERROR is zero) or unsuccessfully (SO_ERROR is one of the usual error codes listed here, explaining  the
              reason for the failure).
EISCONN
              The socket is already connected.

通過man命令,可以看到connect返回的錯誤碼中有一個是EINPROGRESS,對應的描述大致是:在非阻塞模式下,如果連接不能馬上建立成功就會返回該錯誤碼。如果返回該錯誤碼,可以通過使用select或者poll來查看套接字是否可寫,如果可寫,再調用getsockopt來獲取套接字層的錯誤碼來確定連接是否真的建立成功,如果getsockopt返回的錯誤碼是0則表示連接真的來建立成功,否則返回對應失敗的錯誤碼。

3、非阻塞的connect

默認情況下connect是阻塞模式的,這裏不細說,重點說一下非阻塞模式的實現。套接字執行I/O操作也有阻塞和非阻塞兩種模式,在阻塞模式下,在I/O操作完成之前,執行操作的函數會一直阻塞在這裏。而在非阻塞模式下,套接字函數不管/IO操作是否完成都會理解返回並繼續運行該線程。

客戶端調用connect發起對服務端socket的連接,如果客戶端是默認的阻塞模式,則connect()函數會一直阻塞,直到與服務端成功建立連接或者超時(75秒到幾分鐘)。如果是非阻塞模式,則調用connect()函數後會立即返回EINPROGRESS,此時與服務端的連接仍在進行,後續再通過man命令中介紹的辦法來檢測連接是否建立成功。這裏說一下man命令中描述的調用select()檢測非阻塞connect是否完成,因爲select()指定的超時時間可以比connect()的超時時間短並且可以配置,而connect()超時時間在linux內核中配置,無法修改。因此可以縮短阻塞時間。

select()檢測的判斷原則:

  • A)如果select()返回0,表示在select()超時,超時時間內未能成功建立連接,此時,可以再次執行select()進行檢測,如若多次超時,需返回超時錯誤給用戶。
  • B)如果select()返回大於0的值,則說明檢測到可讀或可寫的套接字描述符。源自 伯克利(Berkeley) 的實現有兩條與 select 和非阻塞 I/O 相關的規則:
  1. 當連接建立成功時,套接口描述符變成可寫(連接建立時,寫緩衝區空閒,所以可寫)
  2. 當連接建立出錯時,套接口描述符變成既可讀又可寫(由於有未決的錯誤,從而可讀又可寫)

因此,當發現套接字描述符是可讀或可寫時,需要進一步判斷是連接是成功還是出錯。這裏必須將上述B和另外一種連接正常的情況區分開,就是連接建立好了之後,服務器端發送了數據給客戶端,此時select()返回的非阻塞socket描述符同樣是既可讀又可寫的。此時可通過調用getsockopt()來檢測描述符集合是連接成功還是出錯:
a. 如果連接建立是成功的,則通過getsockopt(sockfd,SOL_SOCKET,SO_ERROR,(char *)&error,&len) 獲取的error 值將是0。
b. 如果建立連接時遇到錯誤,則errno 的值是連接錯誤所對應的errno值,比如ECONNREFUSED,ETIMEDOUT 等。

還可以使用另外一種比較簡單的方法來進行判斷。我們注意到man命令中列出的connect返回錯誤碼中還有一個是EISCONN,表示連接已經建立。所以我們可以再一次調用connect()函數,如果返回的錯誤碼是EISCONN,則表示連接建立成功,否則認爲連接建立失敗。

4、非阻塞connect的實現過程

  1. 創建套接字sock
    int sock = socket(AF_INET, SOCK_STREAM, 0);
  1. 設置套接字爲非阻塞模式(默認是阻塞模式)
    int flags = fcntl(sock , F_GETFL, 0);
    fcntl(sock , F_SETFL, flags|O_NONBLOCK);
    int imode = 1;
    ioctl(sock , FIONBIO, &imode);
    /* 最後我會介紹一下fcntl和ioctl系統調用 */
  1. 調用connect()進行連接
    struct sockaddr_in server_socket;
    memset(&server_socket, 0, sizeof(server_socket));
    server_socket.sin_family = AF_INET;
    inet_pton(AF_INET, SERVER_IP, &server_socket.sin_addr);
    server_socket.sin_port = htons(SERVER_PORT);
    int conn = connect(sock, (struct sockaddr *)&server_socket, sizeof(server_socket));
    if (conn == 0) {
        printf("Socket Connect Success Immediately.\n");
    }
    else {
        printf("Get The Connect Result by select().\n");
        if (errno == EINPROGRESS)
        {
            ....
        }
    }
    //connect會立即返回,可能返回成功,也可能返回失敗。如果連接的服務器在同一臺主機上,那麼在調用connect 建立連接時,連接通常會立即建立成功。

4.調用select(),通過FD_ISSET()檢查套接口是否可寫,確定連接請求是否完成

    int times = 0;
    while (times++ < 5){
        fd_set rfds, wfds;
        struct timeval tv;
              
        printf("errno = %d\n", errno);
        FD_ZERO(&rfds);
        FD_ZERO(&wfds);
        FD_SET(sock_fd, &rfds);
        FD_SET(sock_fd, &wfds);
                
        /* set select() time out */
        tv.tv_sec = 10; 
        tv.tv_usec = 0;
        int selres = select(sock_fd + 1, &rfds, &wfds, NULL, &tv);
        switch (selres){
            case -1:
                printf("select error\n");
                ret = -1;
                break;
            case 0:
                printf("select time out\n");
                ret = -1;
                break;
            default:
                if (FD_ISSET(sock_fd, &rfds) || FD_ISSET(sock_fd, &wfds)){
                    int errinfo, errlen;
                    if (-1 == getsockopt(sock_fd, SOL_SOCKET, SO_ERROR, &errinfo, &errlen)){
                        printf("getsockopt return -1.\n");
                        ret = -1;
                        break;
                    }else if (0 != errinfo){
                        printf("getsockopt return errinfo = %d.\n", errinfo);
                        ret = -1;
                        break;
                    }
                            
                    ret = 0;
                    printf("connect ok?\n");
                    /*
                    connect(sock_fd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in));
                    int err = errno;
                    if  (err == EISCONN){
                        printf("connect finished 111.\n");
                        ret = 0;
                    }else{
                        printf("connect failed. errno = %d\n", errno);
                        printf("FD_ISSET(sock_fd, &rfds): %d\n FD_ISSET(sock_fd, &wfds): %d\n", FD_ISSET(sock_fd, &rfds) , FD_ISSET(sock_fd, &wfds));
                        ret = errno;
                    }
                    */
                        
                    char buff[2];
                    if (read(sock_fd, buff, 0) < 0){
                        printf("connect failed. errno = %d\n", errno);
                        ret = errno;
                    }else{
                        printf("connect finished.\n");
                        ret = 0;
                    }
                }else{
                    printf("haha\n");
                }
            }
                
            if (-1 != selres && (ret != 0)){
                printf("check connect result again... %d\n", times);
                continue;
            }else{
                break;
            }
        }

5、fcntl和ioctl

fcntl定義函數:

  int fcntl(int fd, int cmd);  
  int fcntl(int fd, int cmd, long arg);  
  int fcntl(int fd, int cmd, struct flock *lock);
  //fcntl()針對(文件)描述符提供控制.參數fd 是被參數cmd操作(如下面的描述)的描述符。
  //針對cmd的值,fcntl能夠接受第三個參數int arg
  • F_DUPFD:與dup函數功能一樣,複製由fd指向的文件描述符,調用成功後返回新的文件描述符,與舊的文件描述符共同指向同一個文件。
  • F_GETFD:讀取文件描述符close-on-exec標誌。
  • F_SETFD:將文件描述符close-on-exec標誌設置爲第三個參數arg的最後一位。
  • F_GETFL:獲取文件打開方式的標誌,標誌值含義與open調用一致。
  • F_SETFL:取得文件描述符狀態旗標,此旗標爲open()的參數flags。
  • F_GETLK:取得文件鎖定的狀態。
  • F_SETLK:設置文件鎖定的狀態。此時flcok 結構的l_type 值必須是F_RDLCK、F_WRLCK或F_UNLCK。如果無法建立鎖定,則返回-1,錯誤代碼爲EACCES 或EAGAIN。
  • F_SETLKW:也是給文件上鎖,不同於F_SETLK的是,該上鎖是阻塞方式。當希望設置的鎖因爲其他鎖而被阻止設置時,該命令會等待相沖突的鎖被釋放

ioctl函數影響由fd 參數引用的一個打開的文件。
函數定義:

NAME
       ioctl - control device
SYNOPSIS
       #include <sys/ioctl.h>
       int ioctl(int d, int request, ...);
ERRORS
       EBADF  d is not a valid descriptor.
       EFAULT argp references an inaccessible memory area.
       EINVAL Request or argp is not valid.
       ENOTTY d is not associated with a character special device.
       ENOTTY The specified request does not apply to the kind of object that the descriptor d references.

第三個參數總是一個指針,但指針的類型依賴於request 參數。我們可以把和網絡相關的請求劃分爲6 類:

  • 套接口操作
  • 文件操作
  • 接口操作
  • ARP 高速緩存操作
  • 路由表操作
  • 流系統

下表列出了網絡相關ioctl 請求的request 參數以及arg 地址必須指向的數據類型:
img.jpg

套接口操作:
明確用於套接口操作的ioctl 請求有三個, 它們都要求ioctl 的第三個參數是指向某個整數的一個指針。

  • SIOCATMARK: 如果本套接口的的度指針當前位於帶外標記,那就通過由第三個參數指向的整數返回一個非0 值;否則返回一個0 值。POSIX 以函數sockatmark 替換本請求。
  • SIOCGPGRP : 通過第三個參數指向的整數返回本套接口的進程ID 或進程組ID ,該ID 指定針對本套接口的SIGIO 或SIGURG 信號的接收進程。本請求和fcntl 的F_GETOWN 命令等效,POSIX 標準化的是fcntl 函數。
  • SIOCSPGRP : 把本套接口的進程ID 或者進程組ID 設置成第三個參數指向的整數,該ID 指定針對本套接口的SIGIO 或SIGURG 信號的接收進程,本請求和fcntl 的F_SETOWN 命令等效,POSIX 標準化的是fcntl 操作。

其他類型請參考:ioctl函數詳細說明(網絡)

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