【问题标题】:Winsock sendto returns error 10049 (WSAEADDRNOTAVAIL) for broadcast address after network adapter is disabled or physically disconnectedWinsock sendto 在网络适配器被禁用或物理断开后返回广播地址的错误 10049 (WSAEADDRNOTAVAIL)
【发布时间】:2022-03-02 00:10:26
【问题描述】:

我正在开发一个 p2p 应用程序,为了简化测试,我目前正在使用 udp 广播在本地网络中进行对等点发现。每个对等点将一个 udp 套接字绑定到每个本地网络接口(通过GetAdaptersInfo 发现)的 IP 地址的端口 29292,并且每个套接字定期向其网络接口/本地地址的广播地址发送一个数据包。套接字设置为允许端口重用(通过setsockoptSO_REUSEADDR),这使我能够在同一台本地机器上运行多个对等点而不会发生任何冲突。但是在这种情况下,整个网络上只有一个对等点。

这一切都工作得很好(在 1 台机器上使用 2 个对等方进行测试,在 2 台机器上使用 2 个对等方进行测试)直到网络接口断开连接。在 Windows 对话框中停用我的 wifi 或 USB 转 LAN 适配器的网络适配器,或仅插入适配器的 USB 电缆时,下一次调用 sendto 将失败并返回代码 10049。不管另一个适配器是否仍然连接,或者在开始时,它都会失败。唯一不会让它失败的是通过任务栏的花哨的 win10 对话框停用 wifi,但这并不奇怪,因为这不会停用或删除适配器本身。

我最初认为这是有道理的,因为当网卡消失时,系统应该如何路由数据包。但是:数据包无法到达其目标的事实与地址本身无效(这就是错误的含义)完全无关,所以我怀疑我在这里遗漏了一些东西。我一直在寻找可以用来检测此案例并将其与简单地尝试sendto INADDR_ANY 区分开来的任何信息,但我找不到任何东西。我开始记录我怀疑可能已更改的所有信息,但在成功的sendto 和崩溃的信息(通过getsockopt 检索)上都是一样的:

250   16.24746[886] [debug|debug] local address: 192.168.178.35
251   16.24812[886] [debug|debug] no remote address
252   16.25333[886] [debug|debug] type: SOCK_DGRAM
253   16.25457[886] [debug|debug] protocol: IPPROTO_UDP
254   16.25673[886] [debug|debug] broadcast: 1, dontroute: 0, max_msg_size: 65507, rcv_buffer: 65536, rcv_timeout: 0, reuse_addr: 1, snd_buffer: 65536, sdn_timeout: 0
255   16.25806[886] [debug|debug] Last WSA error on socket was WSA Error Code 0: The operation completed successfully.

256   16.25916[886] [debug|debug] target address windows formatted: 192.168.178.255
257   16.25976[886] [debug|debug] target address 192.168.178.255:29292
258   16.26138[886] [debug|assert] ASSERT FAILED at D:\Workspaces\spaced\source\platform\win32_platform.cpp:4141: sendto failed with (unhandled) WSA Error Code 10049: The requested address is not valid in its context.

被移除的网卡是这个:

   1.07254[0] [platform|info] Discovered Network Interface "Realtek USB GbE Family Controller" with IP 192.168.178.35 and Subnet 255.255.255.0

这是执行发送的代码(dlog_socket_information_and_last_wsaerror 生成使用getsockopt 收集的所有输出):

void send_slice_over_udp_socket(Socket_Handle handle, Slice<d_byte> buffer, u32 remote_ip, u16 remote_port){
    PROFILE_FUNCTION();

    auto socket = (UDP_Socket*) sockets[handle.handle];
    ASSERT_VALID_UDP_SOCKET(socket);
    dlog_socket_information_and_last_wsaerror(socket);

    if(socket->is_dummy)
        return;

    if(buffer.size == 0)
        return;

    DASSERT(socket->state == Socket_State::created);

    u64 bytes_left = buffer.size;

    sockaddr_in target_socket_address = create_socket_address(remote_ip, remote_port);

    #pragma warning(push)
    #pragma warning(disable: 4996)
    dlog("target address windows formatted: %s", inet_ntoa(target_socket_address.sin_addr));
    #pragma warning(pop)
    unsigned char* parts = (unsigned char*)&remote_ip;
    dlog("target address %hhu.%hhu.%hhu.%hhu:%hu", parts[3], parts[2], parts[1], parts[0], remote_port);

    int sent_bytes = sendto(socket->handle, (char*) buffer.data, bytes_left > (u64) INT32_MAX ? INT32_MAX : (int) bytes_left, 0, (sockaddr*)&target_socket_address, sizeof(target_socket_address));

    if(sent_bytes == SOCKET_ERROR){
        #define LOG_WARNING(message) log_nonreproducible(message, Category::platform_network, Severity::warning, socket->handle); return;
        switch(WSAGetLastError()){
            //@TODO handle all (more? I guess many should just be asserted since they should never happen) cases
            case WSAEHOSTUNREACH: LOG_WARNING("socket %lld, send failed: The remote host can't be reached at this time.");
            case WSAECONNRESET: LOG_WARNING("socket %lld, send failed: Multiple UDP packet deliveries failed. According to documentation we should close the socket. Not sure if this makes sense, this is a UDP port after all. Closing the socket wont change anything, right?");
            case WSAENETUNREACH: LOG_WARNING("socket %lld, send failed: the network cannot be reached from this host at this time.");
            case WSAETIMEDOUT: LOG_WARNING("socket %lld, send failed: The connection has been dropped, because of a network failure or because the system on the other end went down without notice.");

            case WSAEADDRNOTAVAIL:

            case WSAENETRESET:
            case WSAEACCES:
            case WSAEWOULDBLOCK: //can this even happen on a udp port? I expect this to be fire-and-forget-style.
            case WSAEMSGSIZE:
            case WSANOTINITIALISED:
            case WSAENETDOWN:
            case WSAEINVAL:
            case WSAEINTR:
            case WSAEINPROGRESS:
            case WSAEFAULT:
            case WSAENOBUFS:
            case WSAENOTCONN:
            case WSAENOTSOCK:
            case WSAEOPNOTSUPP:
            case WSAESHUTDOWN:
            case WSAECONNABORTED:
            case WSAEAFNOSUPPORT:
            case WSAEDESTADDRREQ:
                ASSERT(false, tprint_last_wsa_error_as_formatted_message("sendto failed with (unhandled) ")); break;
            default: ASSERT(false, tprint_last_wsa_error_as_formatted_message("sendto failed with (undocumented) ")); //The switch case above should have been exhaustive. This is a bug. We either forgot a case, or maybe the docs were lying? (That happened to me on android. Fun times. Well. Not really.)
        }
        #undef LOG_WARNING
    }

    DASSERT(sent_bytes >= 0);
    total_bytes_sent += (u64) sent_bytes;
    bytes_left -= (u64) sent_bytes;
    DASSERT(bytes_left == 0);
}

从 ip 和 port 生成地址的代码如下所示:

sockaddr_in create_socket_address(u32 ip, u16 port){
    sockaddr_in address_info;
    address_info.sin_family = AF_INET;
    address_info.sin_port = htons(port);
    address_info.sin_addr.s_addr = htonl(ip);
    memset(address_info.sin_zero, 0, 8);
    return address_info;
}

错误似乎有点不稳定。它会 100% 地复制,直到它决定不再复制。重启后通常会恢复。

我正在寻找正确处理此案的解决方案。当错误发生时,我当然可以重新进行网络接口发现,因为我“知道”我不会将任何损坏的 IP 提供给 sendto,但这只是一种启发式方法。我想解决实际问题。

我也不太明白错误 10049 什么时候应该完全触发。是否只是将 ipv6 地址传递给 ipv4 套接字,或发送到 0.0.0.0?毕竟没有完全“非法”的 ipv4 地址,只有从上下文中看不出来的地址。

如果你知道我在这里缺少什么,请告诉我!

【问题讨论】:

  • 我认为您在考虑错误代码,尽管它确实有些模棱两可。关于网络接口发现,Network List Manager API 可能会更好。
  • 我查看了您建议的 API,但我没有看到任何检索与网络关联的 IP 地址的方法,这是必需的,因为我需要根据此信息打开/关闭套接字。我错过了什么吗?
  • 应该已经澄清了。我的意思是您可以使用 Network List Manager API 来获取添加/删除网络的通知。我认为这会做你想要的,但我自己没有测试过。我认为 API 为您提供了一个 AdapterId,您可以将其与 GetAdaptersInfo() 返回的数据同步。
  • 与您的错误无关,但是在函数create_socket_address() 中,当它在sockaddr_in address_info = {0}; 中定义时,我将初始化address_info,而不是填写数据字段然后使用memset()。这样您就知道在开始设置数据字段之前所有内容都已初始化为零。
  • stackoverflow.com/questions/6594523/…。它声称当 NIC 断开连接时,它不再具有分配的 IP 地址。

标签: c++ windows sockets udp winsock


【解决方案1】:

这是一个人们一直在面对的问题,人们建议阅读Microsoft提供的关于以下问题的文档。 “顺便说一句,我不知道它们是否是相同的问题,但返回代码的错误是相同的,这就是为什么我附上了相同的链接!!”

https://docs.microsoft.com/en-us/answers/questions/537493/binding-winsock-shortly-after-boot-results-in-erro.html

【讨论】:

    【解决方案2】:

    我找到了解决方案(解决方法?)


    我使用NotifyAddrChange 接收对 NIC 的更改,并认为由于某种原因在我禁用 NIC 时它没有触发。事实证明确实如此,我只是愚蠢并且过早停止调试:代码中存在一个错误,它将结果从GetAdaptersInfo 与最后一个已知状态进行比较以找出差异,因此应用程序错过了 NIC 断开连接。现在它观察到断开连接,它可以在套接字尝试在禁用的 NIC 上发送之前终止套接字,从而防止错误发生。但这并不是真正的解决方案,因为这里存在竞争条件(NIC 在发送之前和检查更改之后被禁用),所以我仍然必须处理错误 10049。


    错误是这样的:

    我的期望是,当我禁用 NIC 时,遍历所有现有 NIC 会将禁用的 NIC 显示为已禁用。事实并非如此。发生的情况是 NIC 不再位于现有 NIC 列表中,即使 Windows 对话框仍将显示它(已禁用)。这对我来说有点令人惊讶,但我猜并不是那么不合理。

    在我进行这些检查以检测 NIC 的变化之前:

    • NIC 之前是否存在、已启用但现在已禁用 -> 禁用通知
    • NIC 之前是否存在、已禁用但现在已启用 -> 启用通知
    • 之前网卡是否不存在,未启用->启用通知

    修复方法是添加第四个:

    • 是否存在不在 NIC 列表中的现有 NIC -> 禁用通知

    我仍然不是 100% 高兴,因为在比赛条件下可能会出现有点模棱两可的错误,但我可能会在这里收工。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-03-05
      • 1970-01-01
      • 2013-09-04
      • 2014-09-04
      • 1970-01-01
      • 2011-08-27
      • 1970-01-01
      相关资源
      最近更新 更多