套接字编程,检查输入的UDP缓冲区是否为空?

3
我正在编写一个UDP客户端,向服务器发送一个字符串,当服务器回复多个数据包时,程序的行为与我的期望不同。我想通过 process() 逐个处理任何传入的数据包,直到输入缓冲区为空,但我认为与 recv 的阻塞行为有关的问题存在。
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <winsock.h>

using namespace std;

void process(const char *in, int size)
{
    fprintf(stdout, "%s\n", in);
}

int main()
{
    char quack_addr[] = "127.0.0.1";
    unsigned short quack_port = 9091;

    WSAData data;
    WSAStartup(MAKEWORD(2, 2), &data);

    sockaddr_in qserver;
    qserver.sin_family = AF_INET;
    qserver.sin_addr.s_addr = inet_addr(quack_addr);
    qserver.sin_port = htons(quack_port);
    SOCKET client = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

    if (client <= 0)
    {
        fprintf(stderr, "Error - Can not create socket.\n");
        exit(1);
    }

    while (true)
    {
        const int MAX = 1024;
        char sbuf[MAX];
        char rbuf[MAX];

        fprintf(stdout, ": ");
        fgets(sbuf, MAX, stdin);
        int slen = strlen(sbuf);

        int r = sendto(client,sbuf,slen,0,(sockaddr*)&qserver,sizeof(qserver));

        // Current code:
        // int rlen = recv(client, rbuf, MAX, 0);
        // if (rlen > 0)
        // {
        //     rbuf[rlen] = 0;
        //     process(rbuf, rlen);
        // }

        // Question starts here:
        //
        // While there is data in queue do:
        // {
        //    (break if there is no data)
        //    int rlen = recv(client, rbuf, MAX, 0);
        //    rbuf[rlen] = 0;
        //    process(rbuf, rlen);
        // }   
    }

    return 0;
}

在调用recv(...)之前,如何检查缓冲区是否为空?

这种情况发生在以下场景中:

  1. 用户在客户端程序中输入命令(cmd1)。
  2. 同时,服务器向客户端发送3个数据包(pkt1pkt2pkt3)。
  3. 在客户端按下Enter后,我希望接收到这3个数据包,可能还有与cmd1相对应的结果,并逐个process()它们。
  4. 但是,在第3个阶段按下Enter后,我只收到了pkt1!并且在向服务器发送另一个命令后,我将接收到pkt2等等...!

我知道我的代码无法处理这个问题,所以我的问题是如何处理它?

注意:我正在使用netcat -L -p 9091 -u作为UDP服务器。


1
你知道服务器应该为你的请求发送多少个包裹吗? - Zuljin
我认为你想要的一个版本应该按照以下方式工作: - gnometorule
(1)取消注释,问题从这里开始 - gnometorule
(4)添加一行代码检查缓冲区是否为空,即如果您使用(3)读取了任何内容。 - gnometorule
我忘记了是否有一个套接字命令可以做到这一点(可能有)。但是,您可以将其调整为0大小,然后再调整为所需的大小。我在某个地方提到了调整缓冲区大小的命令。 - gnometorule
显示剩余3条评论
3个回答

2

在调用recv()之前,使用适当的超时时间使用select()检查传入数据。

类似以下非便携式代码:

#include <winsock2.h>

...

/* somewhere after your sendto, or your first recv */
fd_set recv_set;
timeval tv = {1, 0}; /* one second */
FD_ZERO(&recv_set);
FD_SET(client, &recv_set);
while (select(0, &recv_set, NULL, NULL, &tv) > 0)
{
    /* recv... */
    FD_SET(client, &recv_set); /* actually redundant, since it is already set */
}

我更新了我的标签,我正在使用mingw,并且在我的头文件中找不到O_NONBLOCK,因此出现了问题。而且我对select()也不熟悉,你能给我展示一下它的最佳实践吗? - masoud
我错过了这是Winsock,正在更新答案以匹配。 - Hasturkun
+1:我现在正在使用它,谢谢。但是它有一些问题。在从服务器接收结果后,它必须等待超时时间到期,这很糟糕。而且我不想在process()之后breakwhile - masoud
1
@MasoudM:这只是一个例子。您需要根据自己的标准进行修改,例如在接收到一些数据后缩短超时时间。 - Hasturkun
在Windows上,非阻塞模式不使用O_NONBLOCK,而是使用ioctlsocketFIONBIO进行设置。一些函数,如WSAAsyncSelect会自动设置非阻塞模式。 - Ben Voigt

2
我认为问题(你没有描述不令人满意的行为)来自于不同的来源。让我列出一些想法和评论,关于之前说过的内容:
(1) recvfrom()也会被阻塞。但是,您希望使用它。您的通信目前从loopback发送和接收数据,这对于您的玩具程序而言很好(但是:请参见以下内容)。通过recv()接收UDP数据时,由于套接字从未连接(),因此您不知道谁发送了数据。在更严格的程序中使用recvfrom()进行一些最小错误检查。
(2) 因为select()暂停程序以等待I/O可用性,所以它只会将任何与您的套接字阻塞有关的问题放置到不同的级别。但这不是问题所在。
(3) 要检查接收缓冲区是否为空,请在适当的位置使用recvfrom()中的MSG_PEEK标志。通常它仅用于处理稀缺内存,但它应该能够胜任。
(4) 我认为您看到了更多细节问题的原因之一: UDP数据包保留消息边界。这意味着recvfrom()将读取组成任何发送消息的整个数据块。然而,如果您读取此数据的缓冲区比实际数据小,则任何剩余的部分都将被静默丢弃。因此,请确保您有一个大的缓冲区(最好是65k)。
(5) 第二个原因: 您将接收发送到loopback的任何数据。如果您目前也连接到某些网络(例如互联网),则捕获到的内容可能来自您所期望的来源不同。因此,在休息阶段至少要断开连接。
阻塞不应该是一个问题。您的基本逻辑,编写清晰时,是这样的: Recvfrom() (阻塞/等待直到准备就绪) 处理 Peek if buffer empty 如果是,则退出 如果不是,则回到接收更多内容,
您似乎希望目前做到这一点。由于您没有多线程,也没有优化性能,或类似的事情,因此您不需要关心阻塞。如果您发现接收缓冲区太小,请使用
Setsockopt()的optName SO_RCVBUF增加其大小。

+1:谢谢。我更新了问题并在场景中描述了问题。它描述了我的真实问题。但是你的答案对我非常有用。 - masoud
你能同时更新你的实际代码吗?例如,处理循环只是草图。 - gnometorule
请更精确地描述顺序:“同时”可能不行,因为您可能手动操作服务器和客户端。我不熟悉您使用的服务器,但假设它旨在回显您的消息。但是您首先要做什么,其次是什么 - 例如,告诉服务器发送三条消息,然后启动客户端程序并键入/输入回车? - gnometorule
客户端的实际代码:删除第一个注释块。服务器端的代码:我注意到,我正在使用netcat - masoud

1

iPhone有时会出现错误,不让我发表评论。谢谢,史蒂夫。这只是继续对话。

我猜这意味着“取消注释以开始问题”。部分答案,因为这仍然取决于我的第二条评论;这或多或少是可以预期的。假设服务器已经排队发送了三条消息,在您第一次按下回车键后,您的数据包被发送(由于sendto()不会阻塞UDP,因此永远不会被阻止),被服务器接收并(我假设,参见上文)回显并添加到FIFO接收缓冲区中,其中您已经排队了三条消息。然后,您的程序中有一个recv(),它接收第一个排队的消息,并将其打印出来。您当前的逻辑返回到循环顶部,期望另一个输入并等待它(因此在套接字级别上没有被阻止,但由于您的程序请求输入,例如只需“回车”),然后来到第二个最初发送的消息(由服务器发送)并处理该消息。再循环一次,所有三个都完成了。再次按回车键,假设服务器回显您发送的内容,您应该开始接收您键入的消息(如果您只按回车键,则可能为空)。除非您杀死它,否则该循环当前不会退出。


再次强调,这假设您未连接到互联网或其他UDP数据报可能会出现;并且您按照我刚才写的顺序操作。 - gnometorule
如果它与Android应用程序相似,那么发表一条评论后,添加评论链接/按钮将被隐藏。因此,您无法将长消息拆分为多个评论 :( - Ben Voigt

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