我们知道,ping 命令是通过 ICMP(Internet Control Message Protocol,互联网控制消息协议)来检测网络连通性和延迟的。执行 ping 命令的主机(源主机)会向目标主机发送 ICMP Echo Request 报文,目标主机收到该报文后,应响应 ICMP Echo Reply 报文。
如果源主机能够收到目标主机返回的 ICMP Echo Reply 报文,就说明目标主机可达。再根据当前时间戳与发送时间戳(存储在 ICMP 报文中)之间的差值,即可统计出网络延迟。
下面来梳理一下 ping 第 1 版的主流程,看看 ICMP 报文的收发是如何实现的。
main(argc, char *argv[]) {
// ①
struct protoent *proto;
proto = getprotobyname("icmp");
s = socket(AF_INET, SOCK_RAW, proto->p_proto);
// ②
signal( SIGINT, finish );
signal(SIGALRM, catcher);
// ③
catcher(); /* start things going */
for (;;) {
// ④
int cc;
cc=recvfrom(s, packet, ...);
// ⑤
pr_pack( packet, cc, ... );
// ⑥
if (npackets && nreceived >= npackets)
finish();
}
}
首先,创建 1 个 ICMP 协议的原始套接字(raw socket)s
①。虽然使用普通套接字(也称为 面向传输层的套接字)更方便,操作系统的内核会自动处理数据包的封装与解析,我们完全不用关注 TCP/UDP、IP 等底层协议的格式,但同时也失去了修改网络各层数据包的头部和内容的机会。因此,必须使用原始套接字来收发 ICMP 报文。
接下来,分别注册了用于处理信号 SIGINT
和 SIGALRM
的函数②。当用户按下 Ctrl + C 时,信号 SIGINT
会触发 finish()
函数执行。该函数会输出如下汇总信息:
--- www.example.com PING statistics ---
5 packets transmitted, 5 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 9.674/10.968/11.726/0.748 ms
若未指定要发送的 ICMP 报文数量 npackets
,ping 命令会不断发送 ICMP 报文,直到用户按下 Ctrl + C 才会执行 finish()
函数并退出。若指定了 npackets
,则当收到的 ICMP 报文数量 nreceived
大于(须考虑没有收到响应的情况)等于 npackets
时,则调用 finish()
函数并退出⑥。
而对于 SIGALRM
信号,只要定时器一到期(超时),该信号就会发出,并触发 catcher()
函数的执行。
catcher()
函数③用于定时发送 ICMP Echo Request 报文。每发送完一个报文,再随即通过 alarm(1)
设定定时器在 1 秒后产生 SIGALRM
信号,进而使 catcher()
函数自身在 1 秒后再被调用,以继续发送下一个数据包,如此往复。
由于目标主机在收到 ICMP Echo Request 报文后,会向源主机返回 ICMP Echo Reply 报文,所以④这里要通过 recvfrom()
接收。接收到的报文存储在 packet
中,通过 pr_pack()
⑤函数格式化为如下字符串:
64 bytes from 93.184.216.34: icmp_seq=0 time=11.632 ms
以上就是 ping 命令的主流程。
另外,乍看之下,catcher()
函数和 recvfrom()
函数一发一收,要想实现反复不断收发似乎都应该写到死循环 for (;;) {
中。但 catcher()
是通过定时器 SIGALRM
信号驱动的,每隔 1 秒执行 1 次,所以可以写到死循环之外,这也就有点多线程或协程 go catcher();
的风格了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。