Linux下Socket通信中非阻塞connect、select、recv 和 recvfrom、send和sendto大致讲解,附带非租塞connect代码、MSG_NOSIGNAL

linux中send函数MSG_NOSIGNAL异常消息

在服务器端用ctrl+c 来结束服务器接收进程来模拟服务器宕机的情况,结束服务 socket 进程之后,服务端自然关闭进程,可是 client 端也竟然出乎意料的关闭掉。

更改发送函数 write 为 send 并添加 MSG_NOSIGNAL 标志,重新编译,运行,中断 server,这个问题被很潇洒的解决

Linux 下当网络连接断开,还发送数据的时候,不仅 send() 的返回值会有反映,而且还会向系统发送一个异常消息,如果不作处理,系统会出 BrokePipe,程序会退出,这对于服务器提供稳定的服务将造成巨大的灾难。为此,send() 函数的最后一个参数可以设置为 MSG_NOSIGNAL,禁止 send() 函数向系统发送常消息。

其实第一次send的时候应该会收到RST返回的错误码,当再次执行send的时候会发送管道破裂信号(ESPIPE)错误码,同时系统会发送信号(SIGPIPE)给进程,然后进程收到后执行退出操作。其实MSG_NOSIGNAL的作用应该是告诉进程忽略信号(SIGPIPE)

connect()函数

connect头文件:

        #include<sys/types.h>

        #include<sys/socket.h>

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<sys/time.h> 

         #include<sys/types.h> 

         #include<unistd.h>

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成功。

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(连接超时)

将一个socket 设置成阻塞模式和非阻塞模式-使用fcntl方法

设置成非阻塞模式:

先用fcntl的F_GETFL获取flags,用F_SETFL设置flags|O_NONBLOCK;        

      flags = fcntl(sockfd, F_GETFL, 0);                        //获取文件的flags值。

      fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);   //设置成非阻塞模式;

同时在接收和发送数据时,需要使用MSG_DONTWAIT标志

      在recv,recvfrom和send,sendto数据时,将flag设置为MSG_DONTWAIT

设置成阻塞模式:

先用fcntl的F_GETFL获取flags,用F_SETFL设置flags&~O_NONBLOCK;     

     flags  = fcntl(sockfd,F_GETFL,0);                          //获取文件的flags值。

     fcntl(sockfd,F_SETFL,flags&~O_NONBLOCK);    //设置成阻塞模式;            

同时在接收和发送数据时,需要使用阻塞标志

        在recv,recvfrom和send,sendto数据时,将flag设置为0,默认是阻塞。  

send函数-MSG_NOSIGNAL

UINT flag = MSG_NOSIGNAL;//禁止 send() 函数向系统发送常消息。

在将socket设置成非阻塞模式后,每次的对于sockfd 的操作都是非阻塞的;

非阻塞模式下:

connect   

       =0   当返回0时,表示立即创建了socket链接,

       <0   当返回-1时,需要判断errno是否是EINPROGRESS(表示当前进程正在处理),否则失败。

       例如:下面会有select或epoll监听fd是否建立链接,

        select监听connect是否成功的例子,注意getsockopt验证,因为三次握手的第三个ACK有可能会丢失,但是客户端认为链接已经建立:


//////////////////////////////////////////////////////////////////////
//
// func description	:	建立与TCP服务器的连接
//
//-----input------
// Parameters
//     	s 	 		: 	由socket函数返回的套接字描述符
//	   	servaddr 	:   指向套接字地址结构的指针
//     	addrlen		:  结构的长度
//------output------
// Return 
//     BOOL 		:	成功返回0,若出错则返回-1
//     不要把错误(EINTR/EINPROGRESS/EAGAIN)当成Fatal
//
BOOL SocketAPI::connect_ex(SOCKET s, const struct sockaddr* servaddr, UINT addrlen)
{
	DEBUG_TRY
    if (connect(s, servaddr, addrlen) == -1)
    {
        //LOGDEBUG("[SocketAPI::connect_ex] Error errno[%d] discription[%s]", errno, strerror(errno));
#if defined(__LINUX__)	
        switch(errno)
        {
            case EALREADY:      //#define EALREADY    114 /* Operation already in progress */
            case EINPROGRESS:   //#define EINPROGRESS 115 /* Operation now in progress */
            case EINTR:         //#define EINTR        4  /* Interrupted system call */
            case EAGAIN:        //#define EAGAIN      11  /* Try again */          
            {
                    //!alter by huyf:修改非阻塞connet处理流程
                    //建立connect连接,此时socket设置为非阻塞,connect调用后,无论连接是否建立立即返回-1,同时将errno(包含errno.h就可以直接使用)设置为EINPROGRESS, 
                    //表示此时tcp三次握手仍就进行,如果errno不是EINPROGRESS,则说明连接错误,程序结束。
                    return reconnect_ex(s, servaddr, addrlen) == 0 ? TRUE : FALSE;
                    //return TRUE;
                    //!alter end:修改非阻塞connet处理流程
            }            
            //!alter end:修改非阻塞connet处理流程
            //增加已经连接的处理,此处可以直接返回告之
            case EISCONN:   //#define EISCONN     106 /* Transport endpoint is already connected */
            {
                return TRUE;
            }
            //!alter end:修改非阻塞connet处理流程
            case ECONNREFUSED:
            case ETIMEDOUT:
            case ENETUNREACH:
            case EADDRINUSE:
            case EBADF:
            case EFAULT:
            case ENOTSOCK:
            default:
            {
                //LOGERROR("[SocketAPI::connect_ex] Is Error errno[%d] discription[%s]", errno, strerror(errno));
                break;
            }
        }//end of switch
#elif defined(__WINDOWS__)
        INT iErr = WSAGetLastError();
        switch(iErr)
        {
        case WSANOTINITIALISED: 
            {
                strncpy(Error, "WSANOTINITIALISED", ERROR_SIZE);
                break;
            }

        case WSAENETDOWN:
            { 
                strncpy(Error, "WSAENETDOWN", ERROR_SIZE);
                break;
            }
        case WSAEADDRINUSE: 
            { 
                strncpy(Error, "WSAEADDRINUSE", ERROR_SIZE);
                break;
            }
        case WSAEINTR: 
            { 
                strncpy(Error, "WSAEINTR", ERROR_SIZE);
                break;
            }
        case WSAEINPROGRESS: 
            { 
                strncpy(Error, "WSAEINPROGRESS", ERROR_SIZE);
                break;
            }
        case WSAEALREADY: 
            { 
                strncpy(Error, "WSAEALREADY", ERROR_SIZE);
                break;
            }
        case WSAEADDRNOTAVAIL: 
            { 
                strncpy(Error, "WSAEADDRNOTAVAIL", ERROR_SIZE);
                break;
            }
        case WSAEAFNOSUPPORT: 
            { 
                strncpy(Error, "WSAEAFNOSUPPORT", ERROR_SIZE);
                break;
            }
        case WSAECONNREFUSED: 
            { 
                strncpy(Error, "WSAECONNREFUSED", ERROR_SIZE);
                break;
            }
        case WSAEFAULT: 
            { 
                strncpy(Error, "WSAEFAULT", ERROR_SIZE);
                break;
            }
        case WSAEINVAL: 
            { 
                strncpy(Error, "WSAEINVAL", ERROR_SIZE);
                break;
            }
        case WSAEISCONN: 
            { 
                strncpy(Error, "WSAEISCONN", ERROR_SIZE);
                break;
            }
        case WSAENETUNREACH: 
            { 
                strncpy(Error, "WSAENETUNREACH", ERROR_SIZE);
                break;
            }
        case WSAENOBUFS: 
            { 
                strncpy(Error, "WSAENOBUFS", ERROR_SIZE);
                break;
            }
        case WSAENOTSOCK: 
            { 
                strncpy(Error, "WSAENOTSOCK", ERROR_SIZE);
                break;
            }
        case WSAETIMEDOUT: 
            { 
                strncpy(Error, "WSAETIMEDOUT", ERROR_SIZE);
                break;
            }
        case WSAEWOULDBLOCK: 
            { 
                strncpy(Error, "WSAEWOULDBLOCK", ERROR_SIZE);
                break;
            }
        default:
            {
                strncpy(Error, "UNKNOWN", ERROR_SIZE);
                break;
            }
        }//end of switch		
#endif
        return FALSE;
    }
    return TRUE;	
	DEBUG_CATCHF("SocketAPI::connect_ex");	
}


//////////////////////////////////////////////////////////////////////
//
// func description :   非阻塞套接字建立连接时未立即完成的检查(tcp三次握手阶段)
//
//-----input------
// Parameters
//      s         :   由socket函数返回的套接字描述符
//------output------
// Return 
//     BOOL         :   成功返回0,若出错则返回-1
//     不要把错误(EINTR/EINPROGRESS/EAGAIN)当成Fatal
//
int SocketAPI::reconnect_ex(SOCKET s, const struct sockaddr* servaddr, UINT addrlen)
{   
    //LOGDEBUG("[SocketAPI::reconnect_ex] Get The Connect Result By Select() Errno=[%d] Discription=[%s]", errno, strerror(errno));    
    //if (errno == EINPROGRESS)    
    //{    
        //int nTimes = 0; 
        int nRet = -1;   
        //while (nTimes++ < 5)    
        {    
            fd_set rfds, wfds;    
            struct timeval tv;
            FD_ZERO(&rfds);    
            FD_ZERO(&wfds);    
            FD_SET(s, &rfds);    
            FD_SET(s, &wfds);    
                
            /* set select() time out */    
            tv.tv_sec = 1;     
            tv.tv_usec = 0;
            /*
            2.源自Berkeley的实现(和Posix.1g)有两条与select和非阻塞IO相关的规则:
            A:当连接建立成功时,套接口描述符变成可写;
            B:当连接出错时,套接口描述符变成既可读又可写;
            注意:当一个套接口出错时,它会被select调用标记为既可读又可写;
            一种更有效的判断方法,经测试验证,在Linux环境下是有效的:
                再次调用connect,相应返回失败,如果错误errno是EISCONN,表示socket连接已经建立,否则认为连接失败。
            */    
            int nSelRet = select(s+1, &rfds, &wfds, NULL, &tv);    
            switch (nSelRet)    
            {    
                case -1:    //出错
                {
                    //LOGERROR("[SocketAPI::reconnect_ex] Select Is Error... nSelRet=[%d] Errno=[%d] Discription=[%s]", nSelRet, errno, strerror(errno)); 
                    nRet = -1; 
                }   
                break;    
                case 0:    //超时
                {
                    //LOGWARNING("[SocketAPI::reconnect_ex] Select Is Time Out... nSelRet=[%d] Errno=[%d] Discription=[%s]", nSelRet, errno, strerror(errno));     
                    nRet = -1; 
                }   
                break;    
                default:    //有数据过来
                {
                    //LOGDEBUG("[SocketAPI::reconnect_ex] nSelRet=[%d] Errno=[%d] Discription=[%s]", nSelRet, errno, strerror(errno));   
                    if (FD_ISSET(s, &rfds) || FD_ISSET(s, &wfds))    //判断可读或者可写
                    {    
                        #if 0 // not useable in linux environment, suggested in <<Unix network programming>>  SO_ERROR no used 
                            int errinfo, errlen;    
                            if (-1 == getsockopt(s, SOL_SOCKET, SO_ERROR, &errinfo, &errlen))    
                            {    
                                nRet = -1;  
                                LOGERROR("getsockopt return -1.\n");  
                                break;    
                            }    
                            else if (0 != errinfo)    
                            {      
                                nRet = -1;   
                                LOGERROR("getsockopt return errinfo = %d.\n", errinfo); 
                                break;    
                            }                                       
                            nRet = 0;  
                            LOGDEBUG("connect ok?\n");     
                        #else    
                            #if 1    
                                connect(s, servaddr, addrlen);      //再次连接来判断套接字状态    
                                if (errno == EISCONN)    
                                {    
                                    //LOGDEBUG("[SocketAPI::reconnect_ex] Reconnect Finished...nSelRet=[%d]", nSelRet);    
                                    nRet = 0;    
                                }    
                                else    
                                {      
                                    //LOGWARNING("[SocketAPI::reconnect_ex] Reconnect Failed...FD_ISSET(s, &rfds)=[%d] FD_ISSET(s, &wfds)=[%d] nSelRet=[%d] Errno=[%d] Discription=[%s]", FD_ISSET(s, &rfds) , FD_ISSET(s, &wfds), nSelRet, errno, strerror(errno));     
                                    nRet = -1;    
                                }    
                            #else    //test
                                char buff[2];    
                                if (read(s, buff, 0) < 0)    
                                {    
                                    LOGERROR("connect failed. errno = %d\n", errno);    
                                    nRet = errno;    
                                }    
                                else    
                                {    
                                    LOGDEBUG("connect finished.\n");    
                                    nRet = 0;    
                                }    
                            #endif    
                        #endif    
                    } 
                }
                break;   
            } 
        }   
    //}  
    return nRet;
}

recv 和 recvfrom

       =0  当返回值为0时,表示对端已经关闭了这个链接,我们应该自己关闭这个链接,即close(sockfd)。

另外因为异步操作会用select或epoll做事件触发,所以:

       1、如果使用select,应该使用FD_CLR(sockfd,fd_set)将sockfd清除掉,不再监听。

       2、如果使用epoll,系统会自己将sockfd清除掉,不再进行监听。

       >0 当返回值大于0 且 小于sizeof(buffer)时,表示数据肯定读完。

(如果等于sizeof(buffer),可能有数据还没读,应该继续读,不可能有大于)

       <0 当返回值小于0,即等于-1时,分情况判断:

        1、如果   errno   为  EAGAINE  或 EWOULDBLOCK-继续读                                  

                表示暂时无数据可读,可以继续读,或者等待epoll或select的后续通知。(EAGAINE,EWOULDBLOCK产生的

         原因:可能是多进程读同一个sockfd,可能一个进程读到数据,其他进程就读取不到数据(类似惊群效应),当然

         单个进程也可能出现这种情况。对于这种错误,不需用close(sockfd)。可以等待select或epoll的下一次触发,

         继续读。)

         2、如果   errno   为  EINTR-继续读

                表示被中断了,可以继续读,或者等待epoll或select后续的通知。

                否则,真的是读取数据失败。(此时应该close(sockfd))

send和sendto      

        返回值是实际发送的字符数,因为我们知道要发送的总长度,所以,如果没有发送完,我们可以继续发送。

          <0 当返回值为 -1   时 

我们需要判断  errno:

                1、如果errno为  EAGAINE   或 EWOULDBLOCK ,表示当前缓冲区写满,可以继续写,

                      或者等待epoll或select的后续通知,一旦有缓冲区,就会触发写操作,这个也是经常利用的一个特性。  

                 2、如果errno为EINTR  ,表示被中断了,可以继续写,或者等待epoll或select的后续通知。

                       否则真的出错了,即errno不为EAGAINE或EWOULDBLOCK或EINTR,此时应该close(sockfd)

          >=0时

 >=0且不等于要求发送的长度,应该继续send,如果等于要求发送的长度,发送完毕。

猜你喜欢

转载自blog.csdn.net/Windgs_YF/article/details/94589497