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函数详细说明(网络)

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