问题描述
原生socket使用ICMP协议实现ping功能,网上代码很多了,我参考的是这本:王艳平,张越.Windows网络与通信程序设计[M].北京人民邮电出版社,2006。
代码逻辑也很清晰,先构造ICMP包,把当前时间填入timestamp字段,调用sendto发到指定IP,再调用recvfrom接收,然后用当前时间减去接收到的包的timestamp字段,得到ping的时间。
例子里只发了4个包,没有问题。网络通畅时,也没有问题。
我改了下代码,循环向多个ip轮流发包,然后接收。问题就出现了。
ping的耗时越来越长,开始是1000ms,后面2000ms,3000ms。但从返回包的速度来看,又明显没那么高。
原因分析
反复分析,我又在sendto和recvfrom中间加了一段代码:
if (rand() % 2)
{
pr.second.ret = TIMEOUT;
continue;
}
随机地让程序发出后不收,直接报超时。问题就复现了。在网络好的情况下也能复现问题。
原因其实就是recvfrom这步漏包了。假设我们发送一组序号为0 1 2 3的ICMP包,发出0之后,如果此时网络较差,recvfrom没能在超时限制内拿到包,就会返回SOCKET_ERROR,此时调用WSAGetLastError()会返回WSAETIMEDOUT。
之后我们进入下一轮循环,发送1号包,再调用一次recvfrom取包,而此时收到的包是0号包。
之后我们又进入一轮循环,发送2号包,而此时网络又差了,recvfrom超时一次。
再一轮循环,发送3号包,再调用一次recvfrom取包,这时取出了1号包,假设此时2号,3号包其实都已经到了,但因为recvfrom中间超时2次,现在还在取1号包,所以:
计算出的耗时 = (当前时间-发1号包的时间) > (2次超时时间)
若超时设置为1秒,则计算出的时间一定大于2秒。
解决方法
所以,在网络较差的环境中,这种sendto一次,再recvfrom一次的做法是不正确的。sendto时的ICMP包应该填入sequence字段,然后把recvfrom包进循环,每次recvfrom得到数据,就进行解析,只有取出的sequence和当前sequence相符,才证明中间积压的包已经处理掉了。取最后一次recvfrom的时间减去最后一次收到的包的timestamp,得到正确的耗时。
顺带一提,这个问题还有其他解决方法:
- 多进程:用
CreateProcess调用系统的ping程序,用pipe取得结果。这样开销很大。 - 多线程:开多个线程,每个线程读取自己管理的ip ping的结果,这样情况会好一些,但不能从根本上解决窜包的问题,我没有实验过,不过我猜想如果逻辑还是
sento一次,recvfrom一次的话,窜包问题会再现。 - 用第三方dll,比如ICMP.dll,再开多线程调用。这样应该是可以的,系统自带的ping似乎也是调用的ICMP.dll,我没有考证。但系统的ping没有出现窜包的问题,应该是有所规避。这个方法的缺点在于不能跨平台。而我的方法是平台无关的,linux上一样可以用,并且不需要一个ip开一个线程,这样开销太高了。

优化ping功能:避免ICMP包丢失与耗时增长
本文探讨了使用原生socket实现ping功能时遇到的包丢失问题,通过分析发现recvfrom可能漏包导致计算出的ping时间错误。作者提供了改进方案,强调正确处理sequence号并连续解析,避免网络不稳定时的延时累积。还介绍了其他解决方案,如多进程、多线程和第三方库应用。
1279

被折叠的 条评论
为什么被折叠?



