我們知道,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(); 的風格了。