"select一直返回0"的问题解决和总结

场景:一个简单的TCP 服务器,以实现UPNP的事件体系结构

我在linux平台下,创建一个TCP套接字,绑定到某个端口,向UPNP SERVER发一个subscribe订阅请求,超时时间设置为5minutes.

然后开启一个Thread_Main主接收线程。该线程完成以下工作:

(1)调用select监听是否有数据可读,设置4s的超时;

(2)如果select返回值正常(>0),则调用accept,接收客户端请求;

(3)调用recv接收客户端数据;

(4)解析收到的TCP裸数据;

其软件架构如下图所示::


技术背景介绍:


      select是Linux/Unix环境下的高级网络I/O编程接口,它使我们能够进行基于I/O多路转接。I/0多路转接(multiplexing)的核心思想是:先构造一张有关描述符的列表,然后调用一个函数,直到这些描述符中的一个已经准备好进行I/O时,该函数才返回。在返回时,它告诉进程哪些描述符已准备好可以进行I/O操作。

Linux中,我们可以使用select函数实现I/O端口的复用(多路转接),传递给select函数的参数会告诉内核:

      •我们所关心的描述符,可能为文件描述符或网络套接字描述符。

      •对每个描述符,我们所关心的状态。(我们是要想从一个文件描述符中读或者写,还是关注一个描述符中是否出现异常)

      •我们愿意等待多长时间。(可以无限等待,等待固定的一段时间,或者完全不等待)

    select函数返回后,内核告诉我们一下信息:

      •对我们的要求已经做好准备的描述符的个数

      •对于三种状态(读,写或异常)中的每一个,哪些描述符已经做好准备.

   有了这些返回信息,我们可以调用合适的I/O函数(通常是 read write),并且这些函数不会再阻塞.


#include    

int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);

          返回值:做好准备的文件描述符的个数,超时为0,错误为 -1.

struct timeval{      

        long tv_sec;   /* */

        long tv_usec;  /*微秒 */   

    }

   首先我们先看一下最后一个参数。它指明我们要等待的时间,有如下三种情况:

    timeout == NULL  等待无限长的时间。等待可以被一个信号中断。当有一个描述符做好准备或者是捕获到一个信号时函数会返回。如果捕获到一个信号, select函数将返回 -1,并将变量 erro设为 EINTR

    timeout->tv_sec == 0 &&timeout->tv_usec == 0不等待,直接返回。加入描述符集的描述符都会被测试,并且返回满足要求的描述符的个数。这种方法通过轮询,无阻塞地获得了多个文件描述符状态。

    timeout->tv_sec !=0 ||timeout->tv_usec!= 0 等待指定的时间。当有描述符符合条件或者超过超时时间的话,函数返回。在超时时间即将用完但又没有描述符合条件的话,返回 0。对于第一种情况,等待也会被信号所中断。

   接着,我们看看中间的三个参数 readset, writset, exceptset,指向描述符集。这些参数指明了我们关心哪些描述符,和需要满足什么条件(可写,可读,异常)。一个文件描述集保存在 fd_set 类型中。fd_set类型变量每一位代表了一个描述符。我们也可以认为它只是一个由很多二进制位构成的数组。

   理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set;
 (2) FD_ZERO(&set);则set用位表示是0000,0000。
(3)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
(4)若再加入fd=2,fd=1,则set变为0001,0011
(5)执行select(6,&set,0,0,0)阻塞等待
(6)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。

   由于我是服务器端主程序,只关心是否收到对端发来的消息或通知事件,因此我只需要监听某个端口,采用select检查相应的套接字描述符是否有数据可读。调用FD_ZERO(&readfds)将一个指定的fd_set变量(read_fds)所有位设置为0,调用FD_SET(m_server_sock, &readfds)将read_fds变量的第m_server_sock个位置1。

    如果select返回-1,说明有错误;如果为0, 说明超时了;否者说明我们关心的描述符准备好了。对于本文,我关心的是只有一个读文件描述符,当有数据可读时,内核(I/O)根据状态修改文件描述符集,select返回一个大于0的数,该数值表示已经准备好的描述符个数(本文是1,由于我只关心一个描述符)。准备好是什么意思呢?意思是,我关心的读集readfds中的其中一个描述符m_sock_fd描述符,有数据可读了,对其read操作不会阻塞。


我的代码实现大体如下:

void Thread_Main(int m_server_sock)
{
 fd_set readfds;
 FD_ZERO(&readfds);
 FD_SET(m_server_sock, &readfds);
 
 while(1)
 {
  int ret = select(m_server_sock +1, &readfds, NULL, NULL, NULL);

  if(ret == -1)
  {
   char err[1024];
   TRACE_DEBUG("Call to select() for GENA  domain socket returns errno %d, %s", errno, strerror_r(errno, err, sizeof(err)));
   usleep(500*1000); // half a second
   continue;
  }

  if(ret == 0)
  {
   TRACE_DEBUG(" timeout, nothing ready to accept().  continue");
   continue;
  }

......

}


问题描述:数据接收开始是正常的,过一阵子就接收不到数据了,select总是返回0

初步怀疑:

(1)由于发送的subscribe订阅请求是有超时限制的,因此必须在超时前向upnp server发送续订请求。

代码初步改动如下:

按这种思路,添加续订请求后,问题依然存在,百思不得其解。

通过wireshark抓包分析,发现这种情况下,我的机器是收到了upnp server的notify消息的。

那为什么我的TCP程序却解析不到呢?

通过添加打印发现,异常情况下, select函数始终返回0.也就是说,我的TCP服务器程序始终认为没有数据可读或超时。这是一个很奇怪的现象。既然wireshark已经收到消息了,而我的服务器却无法读取数据,因此可以判定这段TCP服务器程序存在bug。跟踪发现,select调用是在while 循环loop里,而FD的设置却在while loop之外,即:

fd_set readfds;
 FD_ZERO(&readfds);
 FD_SET(m_server_sock, &readfds);
 while(1)
 {
  int sockfd = -1;
  int ret = select(m_server_sock +1, &readfds, NULL, NULL, NULL);

......

}

是不是这个逻辑有问题呢?于是想到试试看:把FD_SET操作都放到select之前,即统一放到while Loop循环里。没想到,这么一改问题直接就解决了。

初步分析认为:

select返回后, 会把以前加入的但并无事件发生的fd从fd_set清除,因此需要重新调用select 前再次把关心的fd添加到FD_SET。否则就会出现本文的现象。


问题解决:每次调用select之前,调用FD_ZERO清空可读文件句柄集,并调用FD_SET把TCP套接字添加到该fd_set类型的集合中。

代码对比:










你可能感兴趣的:(network,socket)