头图

我们知道,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 sockets①。虽然使用普通套接字(也称为 面向传输层的套接字)更方便,操作系统的内核会自动处理数据包的封装与解析,我们完全不用关注 TCP/UDP、IP 等底层协议的格式,但同时也失去了修改网络各层数据包的头部和内容的机会。因此,必须使用原始套接字来收发 ICMP 报文。

接下来,分别注册了用于处理信号 SIGINTSIGALRM 的函数②。当用户按下 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(); 的风格了。


da_miao_zi
1 声望0 粉丝

软件工程师、技术图书译者。译有《图解云计算架构》《图解量子计算机》《计算机是怎样跑起来的》《自制搜索引擎》等。