【发布时间】: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