Window ICMPAPI timeout陷阱

23 篇文章 1 订阅

背景

通过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

这篇文章对这个问题做了详细的测试,文章的要点:

  1. .Net的System.Net.NetworkInformation.Ping接口是基于IcmpSendEcho*实现的。
  2. 利用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的解决思路是目前能采取的最好的方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值