我需要在客户端程序中绑定UDP套接字才能接收数据吗?(我总是得到WSAEINVAL错误)

40
我正在使用Winsock创建一个UDP套接字(AF_INETSOCK_DGRAMIPPROTO_UDP),并尝试在此套接字上进行recvfrom,但它始终返回-1,并出现WSAEINVAL(10022)错误。为什么会这样?
当我使用bind()绑定端口时,这种情况不会发生,但我已经读过绑定客户端套接字是非常糟糕的做法。
我正在将数据发送到我的服务器,服务器会尝试回答。
Inc::STATS CConnection::_RecvData(sockaddr* addr, std::string &strData)
{
    int ret;            // return code
    int len;            // length of the data
    int fromlen;        // sizeof(sockaddr)
    char *buffer;       // will hold the data
    char c;

    //recv length of the message
    fromlen = sizeof(sockaddr);
    ret = recvfrom(m_InSock, &c, 1, 0, addr, &fromlen);
    if(ret != 1)
    {
#ifdef __MYDEBUG__
        std::stringstream ss;
        ss << WSAGetLastError();
        MessageBox(NULL, ss.str().c_str(), "", MB_ICONERROR | MB_OK);
#endif
        return Inc::ERECV;
    }
    ...

这是我在几分钟前写的一个工作示例,客户端不需要调用 bind() 方法即可正常运行:

#pragma comment(lib, "Ws2_32.lib")

#define WIN32_LEAN_AND_MEAN

#include <WS2tcpip.h>
#include <Windows.h>
#include <iostream>

using namespace std;

int main()
{
    SOCKET sock;
    addrinfo* pAddr;
    addrinfo hints;
    sockaddr sAddr;
    int fromlen;
    const char czPort[] = "12345";
    const char czAddy[] = "some ip";

    WSADATA wsa;
    unsigned short usWSAVersion = MAKEWORD(2,2);

    char Buffer[22] = "TESTTESTTESTTESTTEST5";
    int ret;

    //Start WSA
    WSAStartup(usWSAVersion, &wsa);

    //Create Socket
    sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

    //Resolve host address
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_INET;
    hints.ai_protocol = IPPROTO_UDP;
    hints.ai_socktype = SOCK_DGRAM;

    if(getaddrinfo(czAddy, czPort, &hints, &pAddr))
    {
        std::cerr << "Could not resolve address...\n";
        std::cin.get();
        return 1;
    }

    //Start Transmission
    while(1)
    {
        ret = sendto(sock, Buffer, sizeof(Buffer), 0, pAddr->ai_addr,
                     pAddr->ai_addrlen);
        if(ret != sizeof(Buffer))
        {
            std::cerr << "Could not send data\n";
            std::cin.get();
            return 1;
        }

        fromlen = sizeof(SOCKADDR);
        ret = recvfrom(sock, Buffer, sizeof(Buffer), 0, &sAddr, &fromlen);
        if(ret != sizeof(Buffer))
        {
            std::cout << "Could not receive data -  error: " << 
                          WSAGetLastError() << std::endl;
            std::cin.get();
            return 1;
        }

        Buffer[ret-1] = '\0';
        std::cout << "Received: " << Buffer << std::endl;
    }
    return 0;
}
5个回答

60

使用UDP时,在客户端必须要调用bind()函数绑定socket,因为UDP是无连接的,所以操作系统无法知道将数据报文发送给哪个程序处理。

如果你没有调用bind(),而直接调用recvfrom(),那么你实际上是在请求操作系统将所有发送到该计算机的UDP数据报文都交给你的程序进行处理。由于操作系统只会将数据报传递给一个程序,这将会破坏DNS、Windows网络邻居、网络时间同步等功能。

你可能在网上看到过一些建议在客户端不需要绑定,但这只适用于TCP连接。


3
没有看到你的代码,我只能猜测,但我认为你正在将端口号放在你传递给“from”参数的结构体中。这是没有用的。该参数不被堆栈读取,它只是被写入,以便您的程序知道谁发送了数据报。请注意,它是可选的,因此您可以在这里传递0,因此您又回到了之前相同的情况,即堆栈必须有某种方式仅基于端口号来知道要将数据报递送给哪个程序。这就是bind()的作用:它将一个程序与一个端口关联起来。 - Warren Young
在那段代码片段中,你想要堆栈为你提供数据报的UDP端口号是不存在的。你需要使用bind()调用来建立UDP端口号和你的程序之间的关联。 - Warren Young
嗯...但是为什么不绑定套接字也可以工作呢?...我编写了一个小型测试客户端和测试服务器,以证明客户端可以在不绑定套接字的情况下发送和接收...我已经在我的原始帖子中发布了上面的代码。我现在完全困惑了 :/... - Juarrow
2
我不会说sendto()会绑定套接字,但确实不需要bind()来使用sendto()。由于UDP的无连接特性,recvfrom()是不同的。从本质上讲,使用recvfrom()的程序就像一个“服务器”,即使您将其视为客户端,因为与TCP服务器一样,调用recvfrom()的程序必须等待来自任何可能发送者的数据。 - Warren Young
1
@WarrenYoung,你的评论“堆栈只向一个程序传递数据报”是不正确的。数据报将被传递到与相应端口绑定的所有程序。默认情况下,只有一个程序可以绑定给定端口。但是,在绑定之前调用setsockopt(SOL_SOCKET,SO_REUSEADDR,1)允许多个绑定器,因此单个UDP数据报可以传递到许多正在侦听它的程序。 - user1775138
显示剩余3条评论

54

你的另一个代码示例之所以有效,是因为你在使用 recvfrom 之前使用了 sendto。如果UDP套接字未绑定并调用 sendtoconnect,系统会自动为其绑定,因此稍后调用 recvfrom 的操作将成功。而 recvfrom 不会绑定套接字,因为该调用期望套接字已经被绑定,否则将抛出错误。


你确定 connect 函数也会尝试绑定未绑定的 UDP 套接字吗?另一个 SO 线程 https://dev59.com/5FLTa4cB1Zd3GeqPdcnb#19368670 指出,UDP 套接字需要显式绑定才能接收数据。 - FaceBro
1
@FaceBro 是的,我确定。当系统向特定目标IP地址发送数据时,找出它将使用哪个本地IP地址的典型方法是:创建一个新的UDP套接字,将其连接到目标IP,查询其本地IP。而且只有在套接字绑定时才能查询本地IP,因为绑定是使套接字具有本地IP的操作。 - Mecki

6

几个星期前我也遇到了同样的问题,下面这些说明帮助我理解是否需要显式调用bind()方法:

recvfrom函数(MSDN)

不建议客户端应用程序使用显式绑定。对于使用此函数的客户端应用程序,套接字可以通过sendtoWSASendToWSAJoinLeaf隐式地绑定到本地地址。

sendto函数(MSDN)

注意:如果打开了一个套接字,进行了setsockopt调用,然后进行了sendto调用,Windows Sockets将执行隐式的bind函数调用。如果套接字未绑定,则系统会为其分配唯一的本地关联值,并将套接字标记为已绑定。


第一个备注中说“不建议客户端应用程序使用显式绑定”。为什么?在这种情况下正确的解决方案是什么?如果我有一个需要连接并开始接收数据而不发送任何内容的客户端,我该怎么办? - Magallo
已经有一段时间了,但这里是我当时找到的一个类似问题的答案 - user925861

1

这里写着:

参数

s [in]:标识绑定套接字的描述符。

...

返回值

WSAEINVAL:套接字未绑定bind,或指定了未知标志,或对启用SO_OOBINLINE的套接字指定了MSG_OOB,或(仅适用于字节流式套接字)len为零或负数。

据我所记,对于UDP套接字,不需要绑定bind,因为堆栈会为您调用绑定。我猜这是Windows要求在recvfrom调用中使用绑定的套接字的事情。


是的,我同意你的看法...我也在想,为什么会这样...在我之前编写的 Windows 程序中从来没有这样做过... - Juarrow
你能贴一些代码吗?也许是recvfrom的另一个参数导致了这个问题。还有,你使用的Windows版本是什么? - Eugen Constantin Dinca
Inc :: STATS CConnection :: _RecvData(sockaddr * addr,std :: string strData) { int ret,len,fromlen; //返回代码/数据长度/sizeof(sockaddr) char * buffer; //将保存数据 字符c; //接收消息的长度 fromlen = sizeof(sockaddr); ret = recvfrom(m_InSock,& c,1,0,addr,& fromlen); 如果(ret!= 1) { - Juarrow
请将代码添加到您的问题中,以便能够正确格式化它。 - Eugen Constantin Dinca
已添加上述代码,并添加了我不久前编写的另一个可行示例... - Juarrow
recvfrom不会绑定一个套接字,它必须已经被绑定才能成功调用。如果您将UDP套接字连接到目标地址或使用sendto发送任何数据,则系统会自动绑定它。 - Mecki

-2
如果您希望套接字绑定到系统自动选择的本地端口,可以使用一个零化的sockaddr调用bind(sock, &sa, sa_len)。只需设置所需的协议族即可。这将默认将套接字绑定到系统选择的某个端口。
sockaddr_in sa = {};
sa.sin_family = AF_INET;
const int res = ::bind(socket_id, (sockaddr*) &sa, sizeof sa);
if (res < 0)
{
    std::cerr << "UDP binding has failed\n";
    return;
}

1
这不是对非常古老的原始问题的答案,@WarrenYoung已经提到过了。 - Juarrow

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