Linux下Socket通信中非阻塞connect的注意事项

       最近在弄Linux下的网络编程,用到了socket通信。在网上查了一些资料,自己也看了一下《Unix网络编程》相关章节。对于编程过程中遇到的一些问题,希望通过本文表达出来。我觉得最让我印象深刻的就是非阻塞的connect的一些使用注意事项了,鉴于自己的文采不是很好,就在网上查找相关的资料,怎料nphyez博主的一篇文章http://blog.csdn.net/nphyez/article/details/10268723正中我心,所以摘抄过来,稍作修改。

        对于面向连接的socket类型(SOCK_STREAM, SOCK_SEQPACKET),在读写数据之前必须建立连接,connect()函数用于完成面向连接的socket的建链过程,对于TCP,也就是三次握手过程。

connect()函数

connect头文件:

        #include

        #include

connect声明:

        int connect (int sockfd, struct sockaddr * serv_addr, int addrlen);
connect功能:

        使用套接字sockfd建立到指定网络地址serv_addr的socket连接,参数addrlen为serv_addr指向的内存空间大小,即sizeof(struct sockaddr_in)。

connect返回值:

        1)成功返回0,表示连接建立成功(如服务器和客户端是同一台机器上的两个进程时,会发生这种情况)

        2)失败返回SOCKET_ERROR,相应的设置errno,通过errno获取错误信息。常见的错误有对方主机不可达或者超时错误,也可能是对方主机没有进程监听对应的端口。


非阻塞connect(non-block mode connect)

        套接字执行I/O操作有阻塞非阻塞两种模式:

        1)在阻塞模式下,在I/O操作完成前,执行操作的函数一直等候而不会立即返回,该函数所在的线程会阻塞在这里。

        2)相反,在非阻塞模式下,套接字函数会立即返回,而不管I/O是否完成,该函数所在的线程会继续运行。

        客户端调用connect()发起对服务端的socket连接,如果客户端的socket描述符为阻塞模式,则connect()会阻塞到连接建立成功或连接建立超时(linux内核中对connect的超时时间限制是75s, Soliris 9是几分钟,因此通常认为是75s到几分钟不等)。如果为非阻塞模式,则调用connect()后函数立即返回,如果连接不能马上建立成功(返回-1),则errno设置为EINPROGRESS,此时TCP三次握手仍在继续。此时可以调用select()检测非阻塞connect是否完成。select指定的超时时间可以比connect的超时时间短,因此可以防止连接线程长时间阻塞在connect处。


Select()函数

select头文件:

         #include

         #include

         #include

select声明:

         int select(int maxfdp, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval* timeout);

select功能:

         本函数用于确定一个或多个套接口的状态。对每一个套接口,调用者可查询它的可读性、可写性及错误状态信息。

select参数:

         先说明两个结构体:
         第一,struct fd_set
可以理解为一个集合,这个集合中存放的是文件描述符(filedescriptor),即文件句柄,这可以是我们所说的普通意义的文件,当然Unix下任何设备、管道、FIFO等都是文件形式,全部包括在内,所以毫无疑问一个socket就是一个文件,socket句柄就是一个文件描述符。fd_set集合可以通过一些宏由人为来操作:

                    FD_ZERO(fd_set *) 清空集合

                    FD_SET(int ,fd_set*) 将一个给定的文件描述符加入集合之中

                    FD_CLR(int,fd_set*) 将一个给定的文件描述符从集合中删除

                    FD_ISSET(int ,fd_set* ) 检查集合中指定的文件描述符是否可以读写

         第二,struct timeval是一个大家常用的结构,用来代表时间值,有两个成员,一个是秒数,另一个是毫秒数。

         1) int maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!

         2)fd_set * readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。

         3) fd_set * writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。

         4)fd_set * errorfds同上面两个参数的意图,用来监视文件错误异常。

         5) struct timeval * timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。


select判断规则:

        1)如果select()返回0,表示在select()超时,超时时间内未能成功建立连接,也可以再次执行select()进行检测,如若多次超时,需返回超时错误给用户。

        2)如果select()返回大于0的值,则说明检测到可读或可写的套接字描述符。源自 Berkeley 的实现有两条与 select 和非阻塞 I/O 相关的规则:

        A) 当连接建立成功时,套接口描述符变成 可写(连接建立时,写缓冲区空闲,所以可写)

        B) 当连接建立出错时,套接口描述符变成 既可读又可写(由于有未决的错误,从而可读又可写)

        因此,当发现套接口描述符可读或可写时,可进一步判断是连接成功还是出错。这里必须将B)和另外一种连接正常的情况区分开,就是连接建立好了之后,服务器端发送了数据给客户端,此时select同样会返回非阻塞socket描述符既可读又可写。

        □对于Unix环境,可通过调用getsockopt来检测描述符集合是连接成功还是出错(此为《Unix Network Programming》一书中提供的方法,该方法在Linux环境上测试,发现是无效的):在linux下,无论网络是否发生错误,getsockopt始终返回0,不返回-1。

               A)如果连接建立是成功的,则通过getsockopt(sockfd,SOL_SOCKET,SO_ERROR,(char *)&error,&len) 获取的error 值将是0

               B)如果建立连接时遇到错误,则errno 的值是连接错误所对应的errno值,比如ECONNREFUSED,ETIMEDOUT 等

        □一种更有效的判断方法,经测试验证,在Linux环境下是有效的

        再次调用connect,相应返回失败,如果错误errno是EISCONN,表示socket连接已经建立,否则认为连接失败。

        方法尝试:一次select之后,发现此时套接口描述字可读或可写,再次执行connect,此时errno始终不变,仍为EINPROGRESS,增加select的超时时间结果也一样。之后尝试在select返回值为0,或返回值为1,且connect后errno仍为EINPROGRESS(115)时,再次执行select+connect,即再次检测连接状态。此时errno被置为EISCONN(106),connect成功。

 

综上所述,这里总结一下非阻塞connect的实现过程。 

非阻塞connect的实现过程     

1. 创建套接字sockfd

   /* 1. Creat a socket */  
   int sock_fd;  
   sock_fd = socket(AF_INET, SOCK_STREAM, 0);

2. 设置套接字为非阻塞模式

   /* 2. set non-blocking mode no socket */  
   int flags = fcntl(sock_fd, F_GETFL, 0);  
   fcntl(sock_fd, F_SETFL, flags|O_NONBLOCK);

3. 调用connect进行连接

    struct sockaddr_in addr;  
    addr.sin_family = AF_INET;  
    addr.sin_port   = htons(PEER_PORT);  
    addr.sin_addr.s_addr = inet_addr(PEER_IP);  
    int ret = connect(sock_fd, (struct sockaddr *)&addr, sizeof(struct sockaddr));  
    if (0 == res)  
    {  
        printf("socket connect succeed immediately.\n");  
        ret = 0;  
    }  
    else  
    {  
        printf("get the connect result by select().\n");  
        if (errno == EINPROGRESS)  
        {  
           ....  
        }  
    }  

connect会立即返回,可能返回成功,也可能返回失败。如果连接的服务器在同一台主机上,那么在调用connect 建立连接时,连接通常会立即建立成功(我们必须处理这种情况)。

4.调用select(),通过FD_ISSET()检查套接口是否可写,确定连接请求是否完成

    fd_set rfds, wfds;  
    struct timeval tv;  
      
    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))  
           {  
               .....  
           }  
    }  
对于无连接的socket类型(SOCK_DGRAM),客户端也可以调用connect进行连接,此连接实际上并不建立类似SOCK_STREAM的连接,而仅仅是在本地保存了对端的地址,这样后续的读写操作可以默认以连接的对端为操作对象。


Linux下常见的socket错误码:

EACCES, EPERM:用户试图在套接字广播标志没有设置的情况下连接广播地址或由于防火墙策略导致连接失败。

EADDRINUSE 98:Address already in use(本地地址处于使用状态)

EAFNOSUPPORT 97:Address family not supported by protocol(参数serv_add中的地址非合法地址)

EAGAIN:没有足够空闲的本地端口。

EALREADY 114:Operation already in progress(套接字为非阻塞套接字,并且原来的连接请求还未完成)

EBADF 77:File descriptor in bad state(非法的文件描述符)

ECONNREFUSED 111:Connection refused(远程地址并没有处于监听状态)

EFAULT:指向套接字结构体的地址非法。

EINPROGRESS 115:Operation now in progress(套接字为非阻塞套接字,且连接请求没有立即完成)

EINTR:系统调用的执行由于捕获中断而中止。

EISCONN 106:Transport endpoint is already connected(已经连接到该套接字)

ENETUNREACH 101:Network is unreachable(网络不可到达)

ENOTSOCK 88:Socket operation on non-socket(文件描述符不与套接字相关)

ETIMEDOUT 110:Connection timed out(连接超时)


测试代码:

#include   
#include   
#include 

#include #include

#include #include #include #include

//inet_addr() #include #include #include

#define PEER_IP "192.254.1.1" #define PEER_PORT 7008

int main(int argc, char **argv) { int ret = 0; int sock_fd; int flags; struct sockaddr_in addr; /* obtain a socket */ sock_fd = socket(AF_INET, SOCK_STREAM, 0); /* set non-blocking mode on socket*/ #if 1 flags = fcntl(sock_fd, F_GETFL, 0); fcntl(sock_fd, F_SETFL, flags|O_NONBLOCK); #else int imode = 1; ioctl(sock_fd, FIONBIO, &imode); #endif /* connect to server */ addr.sin_family = AF_INET; addr.sin_port = htons(PEER_PORT); addr.sin_addr.s_addr = inet_addr(PEER_IP); int res = connect(sock_fd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)); if (0 == res) { printf("socket connect succeed immediately.\n"); ret = 0; } else { printf("get the connect result by select().\n"); if (errno == EINPROGRESS) { 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)) { #if 0 // not useable in linux environment, suggested in <> 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"); #else #if 1 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; } #else 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; } #endif #endif } else { printf("haha\n"); } } if (-1 != selres && (ret != 0)) { printf("check connect result again... %d\n", times); continue; } else { break; } } } else { printf("connect to host %s:%d failed.\n", PEER_IP, PEER_PORT); ret = errno; } } if (0 == ret) { send(sock_fd, "12345", sizeof("12345"), 0); } else { printf("connect to host %s:%d failed.\n", PEER_IP, PEER_PORT); } close(sock_fd); return ret; }


问题:

1. 如何有效判断连接状态:

    1)getsockopt方法在linux环境下无效

    2)再次执行connect,检查errno值,该方法在Linux环境下有效。测试中发现的问题:

    一次select之后,发现此时套接口描述字可读或可写,再次执行connect,此时errno始终不变,仍未EINPROGRESS,增加select的超时时间结果也一样。

    之后尝试在select返回值为0,或返回值为1,且connect后errno仍为EINPROGRESS(115)时,再次执行select+connect,即再次检测连接状态。此时errno被置为EISCONN(106),connect成功。


2. socket连接成功后是否重置为阻塞模式

    这要看连接需要什么样的效果。

    如果连接建立成功后,重置socket为阻塞模式。在给服务器发送信息等待接收数据时,如果服务器很忙,而服务器也没设计好,每到来一个客户端就服务,那么后来的要排队,客户端多的话,会导致后来的请求长时间得不到应答,线程一直被阻塞。
    然而,在非阻塞模式下,send和recv也会立即返回。测试发现,非阻塞模式下,recv常常未能接收到数据,返回错误。而在建链之后将socket重置为阻塞,recv的接收正常。


你可能感兴趣的:(Linux网络编程)