问题描述
原生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开一个线程,这样开销太高了。