背景
通过ping来检测网络延迟和网络质量是一个很常用的方法。很多时候,网络应用也希望通过ping来检测到对端的网络状况。除了通过调用外部进程ping之外,Windows提供了一组ICMP接口来支持有关的操作。最核心的接口是IcmpSendEcho(以及 IcmpSendEcho2,IcmpSendEcho2Ex)
,详细接口说明可以参考 https://learn.microsoft.com/zh-cn/windows/win32/api/icmpapi/
问题
IcmpSendEcho()的声明如下,其中Timeout
是等待回复的时间(以毫秒为单位)。
IPHLPAPI_DLL_LINKAGE DWORD IcmpSendEcho(
[in] HANDLE IcmpHandle,
[in] IPAddr DestinationAddress,
[in] LPVOID RequestData,
[in] WORD RequestSize,
[in, optional] PIP_OPTION_INFORMATION RequestOptions,
[out] LPVOID ReplyBuffer,
[in] DWORD ReplySize,
[in] DWORD Timeout
);
直觉上,Timeout设成几百或者几十毫秒应该可以正常工作。实际测试,却会遇到丢包率或者超时比例,远远超过系统ping命令的现象。
只有Timeout设成1000或者以上才能获得正常的数据统计。最后也没有找到Microsoft的官方说法,但是从网络上找到了一些前人的分析的测试。
测试分析
1. 模拟测试和分析
https://stackoverflow.com/questions/45528336/winapi-why-does-icmpsendecho2ex-report-false-timeouts-when-timeout-is-set-belo
这篇文章对这个问题做了详细的测试,文章的要点:
- .Net的
System.Net.NetworkInformation.Ping
接口是基于IcmpSendEcho*
实现的。 - 利用
System.Net.NetworkInformation.Ping
进行测试,用一个比较稳定的往返时间几百ms的IP作为目标,很容易可以发现,Timeout=999是丢包率明显高,Timeout=1000结果就是正常的。
PowerShell / .Net, 使用999ms Timeout,丢包和超时比例异常:
$Pinger = New-Object -TypeName System.Net.NetworkInformation.Ping
while($true) {
$Pinger.Send('20.231.239.246', 999)
start-sleep -Seconds 1
}
命令行用999ms 超时,结果要可靠得多。
ping '20.231.239.246' -t -w 999
PowerShell / .Net, 使用1000ms timeout参数, 结果看起来正常:
$Pinger = New-Object -TypeName System.Net.NetworkInformation.Ping
while($true) {
$Pinger.Send('20.231.239.246', 1000)
start-sleep -Seconds 1
}
2. 第三方代码库解决方案
查找这个问题过程中,发现openjdk论坛讨论过类似的问题,所以,去查看了一下openjdk的代码。作者当年应该遇到了同样的问题,通过代码逻辑回避的这个问题。
java.net.InetAddress.isReachable()
在widows平台使用IcmpSendEcho*
来实现。openjdk V11的代码位置 Inet4AddressImpl.c
其中重点代码如下。
如果调用参数timeout<1000,调用IcmpSendEcho*
会强制使用1000。收到正确返回包,再比较返回包的RoundTripTime和timeout,看看是否超时。
static jboolean
ping4(JNIEnv *env, HANDLE hIcmpFile, SOCKETADDRESS *sa,
SOCKETADDRESS *netif, jint timeout)
{
...
if (netif == NULL) {
dwRetVal = IcmpSendEcho(hIcmpFile, // HANDLE IcmpHandle,
sa->sa4.sin_addr.s_addr, // IPAddr DestinationAddress,
SendData, // LPVOID RequestData,
sizeof(SendData), // WORD RequestSize,
NULL, // PIP_OPTION_INFORMATION RequestOptions,
ReplyBuffer,// LPVOID ReplyBuffer,
ReplySize, // DWORD ReplySize,
// Note: IcmpSendEcho and its derivatives
// seem to have an undocumented minimum
// timeout of 1000ms below which the
// api behaves inconsistently.
(timeout < 1000) ? 1000 : timeout); // DWORD Timeout
} else {
dwRetVal = IcmpSendEcho2Ex(hIcmpFile, // HANDLE IcmpHandle,
NULL, // HANDLE Event
NULL, // PIO_APC_ROUTINE ApcRoutine
NULL, // ApcContext
netif->sa4.sin_addr.s_addr, // IPAddr SourceAddress,
sa->sa4.sin_addr.s_addr, // IPAddr DestinationAddress,
SendData, // LPVOID RequestData,
sizeof(SendData), // WORD RequestSize,
NULL, // PIP_OPTION_INFORMATION RequestOptions,
ReplyBuffer,// LPVOID ReplyBuffer,
ReplySize, // DWORD ReplySize,
(timeout < 1000) ? 1000 : timeout); // DWORD Timeout
}
...
} else {
PICMP_ECHO_REPLY pEchoReply = (PICMP_ECHO_REPLY)ReplyBuffer;
// This is to take into account the undocumented minimum
// timeout mentioned in the IcmpSendEcho call above.
// We perform an extra check to make sure that our
// roundtrip time was less than our desired timeout
// for cases where that timeout is < 1000ms.
if (pEchoReply->Status == IP_SUCCESS &&
(int)pEchoReply->RoundTripTime <= timeout)
{
ret = JNI_TRUE;
}
}
...
}
总结
这个问题确实存在,我们自己用C API直接写的代码也可以重现这个问题,至少到Windows 10仍然没有解决。OpenJDK的解决思路是目前能采取的最好的方案。