Winsock sendto returns 网络适配器禁用或物理断开后广播地址错误 10049 (WSAEADDRNOTAVAIL)

Winsock sendto returns error 10049 (WSAEADDRNOTAVAIL) for broadcast address after network adapter is disabled or physically disconnected

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

这一切都工作得很好(在 1 台机器上测试了 2 个对等点,在 2 台机器上测试了 2 个对等点)直到网络接口断开连接。在 windows 对话框中停用我的 wifi 或 USB-to-LAN 适配器的网络适配器,或者只是插入适配器的 USB 电缆时,对 sendto 的下一次调用将失败并显示 return代码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 和端口生成地址的代码如下所示:

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 来发送,但这只是一种启发式方法。我想解决实际问题。

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

如果你知道我在这里遗漏了什么,请告诉我!

这是人们已经面对了一段时间的问题,人们建议阅读 Microsoft 提供的有关以下问题的文档。 “顺便说一句,我不知道它们是否是相同的问题,但代码返回的错误是相同的,这就是为什么我附加了相同的 link!!”

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

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


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


错误是这样的:

我的期望是,当我禁用 NIC 时,遍历所有现有 NIC 会将禁用的 NIC 显示为已禁用。事实并非如此。发生的情况是 NIC 不再存在于现有 NIC 列表中,即使 windows 对话框仍会显示它(已禁用)。这让我有些吃惊,但我想并不是那么不合理。

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

  • 网卡以前是否存在,启用过,现在禁用 -> 禁用通知
  • 网卡之前是否存在,被禁用,现在启用 -> 启用通知
  • 之前网卡是否存在,未启用->启用通知

修复是添加第四个:

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

我仍然不是 100% 高兴在竞争条件下可能会出现一些模棱两可的错误,但我可能会在这里结束。