Linux C++ Socket Select循环

5

我在使用sockets时遇到了一些问题,循环中除了第一次循环外,我都没有接收到数据,每次都超时了。但是如果我在每次循环中关闭并重新打开socket,似乎就可以正确地获取数据。你们有什么想法吗?

不关闭socket的循环示例:

int socketHandle = socket(AF_INET,SOCK_DGRAM,0);

sockaddr_in serverAddr;

serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr(/*UDP IP ADDRESS*/);
serverAddr.sin_port = htons(/*UDP PORT*/);

struct timeval tv;
fd_set rfds;

FD_ZERO(&rfds);
FD_SET(socketHandle, &rfds);

tv.tv_usec = 0.0;
int recVal = 0;
int sockLen = sizeof(serverAddr);
bind(socketHandle, (struct sockaddr*)&serverAddr, (socklen_t)sockLen);

bool timePassed = false;
time_t startListenTime = time(NULL);

tv.tv_sec = maxUpdateTime;

while(true)
{
    recVal = select(socketHandle + 1, &rfds, NULL, NULL, &tv);
    switch(recVal)
    {
        case(0):
        {
            //Timeout
            break;
        }
        case(-1):
        {
            //Error
             break;
        }
        default:
        {
            /*Packet Data Type*/ pkt;
            if(recvfrom(socketHandle, &pkt, sizeof(/*Packet Data Type*/), 0, (sockaddr*)&serverAddr, (socklen_t*)&sockLen) < 0)
            {
                //Failed to Recieve Data
                break;
            }
            else
            {
                //Recieved Data!!
            }
            break;
        }
    }
}

循环结束的示例:

while(true)
{
    int socketHandle = socket(AF_INET,SOCK_DGRAM,0);

    sockaddr_in serverAddr;

    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = inet_addr(/*UDP IP ADDRESS*/);
    serverAddr.sin_port = htons(/*UDP PORT*/);

    struct timeval tv;
    fd_set rfds;

    FD_ZERO(&rfds);
    FD_SET(socketHandle, &rfds);

    tv.tv_usec = 0.0;
    int recVal = 0;
    int sockLen = sizeof(serverAddr);
    bind(socketHandle, (struct sockaddr*)&serverAddr, (socklen_t)sockLen);


    bool timePassed = false;
    time_t startListenTime = time(NULL);

    tv.tv_sec = maxUpdateTime;

    recVal = select(socketHandle + 1, &rfds, NULL, NULL, &tv);
    switch(recVal)
    {
        case(0):
        {
            //Timeout
            break;
        }
        case(-1):
        {
            //Error
             break;
        }
        default:
        {
            /*Packet Datastructure*/ pkt;
            if(recvfrom(socketHandle, &pkt, sizeof(/*Packet Datastructure*/), 0, (sockaddr*)&serverAddr, (socklen_t*)&sockLen) < 0)
            {
                //Failed to read packet
                break;
            }
            else
            {
                //Read Packet!!
            }
            break;
        }
    }
    close(socketHandle);
}

3
将FD_ZERO()和FD_SET()移动到select()的循环内部的正确位置。 - Richard Chambers
3
请参阅http://www.mkssoftware.com/docs/man3/select.3.asp,该文档说明文件描述符已修改以显示哪些描述符准备好进行I/O。 - Richard Chambers
@RichardChambers,你应该把那个变成答案而不仅仅是评论,这样我就可以给它点赞了 :) - Jonathan Wakely
@JonathanWakely,感谢您的点赞。我想快速检查一下文档,确保我的记忆是正确的!哈哈 - Richard Chambers
2个回答

11
select()函数使用指定的文件描述符掩码来确定要监视哪些文件描述符以进行事件(读取、写入等)操作。当文件描述符可用于I/O活动(读取、写入)时,select()函数会修改描述符,以指示哪些文件已准备好进行给定的I/O操作。
请参阅选择功能和与文件描述符一起使用的宏/函数的文章
旧式Unix类型程序通常将文件描述符视为位掩码并仅检查位。然而,文件描述符的实际实现可能因编译器而异,因此最好使用标准文件描述符宏/函数来设置、清除和测试各个文件描述符。
因此,在使用select()函数时,您需要使用FD_ZERO()FD_SET(),以便为此特定调用select()函数设置特定的文件描述符。当select()返回时,它将指示指定的哪些文件描述符实际上已准备好用于I/O操作(读取、写入等)。
因此,您的代码实际上将是这样的:
while(true)
{
    fd_set rfds;

    FD_ZERO(&rfds);
    FD_SET(socketHandle, &rfds);
    recVal = select(socketHandle + 1, &rfds, NULL, NULL, &tv);
    switch(recVal)
    {
        case(0):
        {
            //Timeout
            break;
        }
        case(-1):
        {
            //Error
             break;
        }
        default:
        {
            /*Packet Data Type*/ pkt;
            if(recvfrom(socketHandle, &pkt, sizeof(/*Packet Data Type*/), 0, (sockaddr*)&serverAddr, (socklen_t*)&sockLen) < 0)
            {
                //Failed to Recieve Data
                break;
            }
            else
            {
                //Recieved Data!!
            }
            break;
        }
    }

然而,你真正应该做的是使用FD_ISSET()函数来检查哪些特定的文件描述符已准备好使用。在你的情况下只有一个,但如果存在多个描述符的情况下,你也需要使用FD_ISSET()函数。


啊哈!非常感谢及时的回复和解释。 - APoster

1
select返回除了-1以外的任何值时,都被视为成功,并且fd集合被修改以指示哪些文件描述符已准备好或存在错误。来自POSIX规范的内容如下:

成功完成后,pselect()或select()函数将修改由readfdswritefdserrorfds参数指向的对象,以指示哪些文件描述符已准备好进行读取、写入或存在待处理的错误条件,并返回所有输出集中准备就绪的描述符总数。

如果超时,则返回零,表示没有描述符已准备好,并且fd集合中不会设置任何位。
因此,当您的调用超时时,rfds中没有设置任何位,因此在下一个循环中调用select时,您要求它等待一个空集,这将永远不会返回正值,因为如果您等待零个FD,则永远不会获得非零数量的准备好的FD!

你需要记住,rfds 同时是输入参数和输出参数,因此在每次调用 select 之前,请确保它被正确设置。


超时的好处是会返回所有关闭的文件描述符。 - Richard Chambers

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接