我在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;
}
......
}
初步怀疑:
(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。否则就会出现本文的现象。
代码对比: