所謂 ICMP,不過將軍與士卒而已

什麼是 ICMP 協議

關於這點我們在 IP 協議那篇文章中提過一嘴,IP 協議作為一種提供不可靠數據交付的網路層協議,在傳輸的過程中,其 IP 數據報可能會發生丟失、重複、延遲和亂序等各種情況, 但是 IP 協議對這些糟糕的情況並不擁有有效的檢測和彌補措施,當然更不會將這些結果通知收發雙方。

為此,鑒於上述原因,我們在構建 IP 網路時,就需要特別注意兩點:

  • 確認網路是否能夠正常工作
  • 即使診斷出現異常時的原因所在

於是,網際控制報文協議(Internet Control Message Protocol,ICMP)出現了。

形象來說,IP 協議就好像一個將軍,而 ICMP 協議就是他手下的情報員。將軍運籌帷幄於千里之外,而在前線浴血奮戰的士卒們傷亡當然也在所難免。

那無法親臨前線的將軍最起碼要知道兩件事情:第一點,我的士卒們是不是按照我指引的方向在前進著,別一陣猛衝發現沖錯了地方;第二點,我的士卒們傷亡多少,被什麼所傷,了解了己方傷亡的原因才好做下一步的戰略部署,總不能死了個不明白。不必多說,這就是情報員 ICMP 該做的事情了。

當然了,上述只是打個比方,可能不是很嚴謹,各位知道什麼意思就行,不必過於吹毛求疵。

這裡我們再用學術點的語言來總結下,ICMP 的主要功能有如下兩點:

1)確認 IP 數據報是否成功送達目標地址

2)如果某個 IP 數據報因為某種原因未能正常到達目的地,則由 ICMP 負責通知具體的原因

ICMP 報文初探

具體的出錯原因是 ICMP 協議負責通知的,這個通知的學名就是 ICMP 報文,那麼 ICMP 報文是由發送方發送方發出的還是由接收方發出的呢?

都不是。

ICMP 報文是由路由器發出來的

舉個例子:主機 A 在不知情的情況下向主機 B 發送了數據包,而主機 B 正在呼呼大睡。主機 A 和主機 B 不在同一個局部網內,假設它倆之間會經過兩個路由器,看下圖:

眾所周知,除了 IP 地址我們還需要 MAC 地址才能確保數據包精準的找到傳送方向,因此,路由器 2 為了知道主機 B 的 MAC 地址,它會廣播一個 ARP 請求報文,希望獲取到主機 B 的 MAC 地址,而主機 B 都關機了自然也就無法應答這個請求報文了。

為此,路由器 2 會一遍又一遍的重新發送著 ARP 請求報文,在多次無果後,路由器 2 就會返回一個 ICMP Destination Unreachable 的包給主機 A(關於 ICMP 報文類型下文會講),通知主機 A,非常遺憾,您發往主機 B 的包未能成功抵達。

那麼,ICMP 報文具體是怎麼傳輸給主機 A 的呢

這個很簡單,TCP/UDP 報文是怎麼傳輸的,ICMP 報文就怎麼傳輸。

也就是說,真正的數據首先會被加上 ICMP 首部,封裝成 ICMP 報文,然後被 IP 協議封裝成 IP 數據報進行明文傳輸,由 IP 協議指定源 IP 地址和目的地址。主機 A 收到數據後會一層一層解封裝,從而獲得真正的數據得知發生異常的原因,遂大怒一聲:蠢貨主機 B。

ICMP 報文格式

至此,各位已經知道了,ICMP 報文是被封裝在 IP 數據報裡面的,我們來看看下圖:

額,這裡好像沒啥好說的,上圖畫的很 Nice ,是我之前考研的時候看 B 站上的王道影片截下來的,各位看明白上圖,了解 ICMP 報頭有哪些東西,知道類型程式碼這兩個欄位很重要就好了,尤其是類型這個,接下來我們先重點講它。

ICMP 報文類型

上文提到了 ICMP Destination Unreachable,也就是目標不可達的 ICMP 報文。

ICMP 報文類型大體上可以分為兩種,差錯報文和詢問報文,解釋一下:

所謂查詢報文就是,用於主機進行診斷的查詢消息

這麼學術性的文字可能不是很好理解,這樣,咱形象來說,查詢報文其實和通訊異常沒啥關係,查詢報文就好比將軍率領著千軍萬馬來到了一片寂靜的峽谷,正是一個容易被埋伏的地方,將軍不敢貿然前進,於是派遣幾個情報員前去探明敵情,一有動靜立馬回報。

常見的 ICMP 查詢報文類型有以下幾種:

  • 回送應答(Echo Reply),對應 ICMP 報文首部類型欄位的值:0
  • 回送請求(Echo Request),對應 ICMP 報文首部類型欄位的值:8

而差錯報文就是,用於通知主機出錯的原因。顯然,ICMP 差錯報告報文是伴隨著出錯數據產生的。一旦 IP 協議發現某個 IP 數據報出錯了,首先就會毅然地丟棄出錯的這個 IP 數據報,然後發送 ICMP 差錯報文

常見的 ICMP 差錯報文類型有以下幾種:

  • 目標不可達(Destination Unreachable),對應 ICMP 報文首部類型欄位的值:3
  • 原點抑制(Source Quench),對應 ICMP 報文首部類型欄位的值:4
  • 重定向或改變路由(Redirect),對應 ICMP 報文首部類型欄位的值:5
  • 超時(Time Exceeded),對應 ICMP 報文首部類型欄位的值:11

下面詳細解釋一下這幾個常見的 ICMP 報文類型。

ICMP 回送消息(類型 0、8)

用於進行通訊的主機或路由器之間,判斷所發送的數據包是否已經成功到達對端的一種消息。

可以向對端主機發送 ICMP 回送請求的消息(Echo Request,類型 8),也可以接收對端主機發回來的 ICMP 回送應答消息(Echo Reply,類型 0)

我們常用的 ping 命令就是基於 ICMP 回送消息實現的。

ping 這個單詞源自聲納定位,而這個命令的作用也確實如此,它發送類型為 0 的 ICMP Echo Request 消息,收到請求的主機則用類型為 8 的 ICMP Echo Reply 消息進行回應。ping 就會計算髮送 Requenst 和接收到 Reply 的消息間隔時間,並計算有多少個包被送達,丟失了多少個包等。用戶就可以據此判斷網路大致的情況。

如下圖我們來 ping 一下 Github:

ping 也並不是啥事也沒做,它在 ICMP 報文格式中又添加了兩個欄位:標識符和序號。這倆其實很好理解:

1)標識符用來區分是哪個應用程式發 ICMP 包。

形象來說,將軍派出了兩個情報員,一個用來是了解戰況的,一個是用來搬救兵的,那總得有個標識區分這倆情報員吧。標識符就是干這事的。最容易想到的能作為標識符的東西,想來也不用我多嘴吧,就是進程的 PID。

2)序號用來確認網路包是否有丟失。

形象來說,將軍派出了 10 個情報員,給每個情報員都編個號。這樣,如果派出去 10 個,回來 10 個,就說明前方戰況不錯;如果派出去 10 個,一個也沒回來或者就回來 1 個,說明情況不妙啊。

ICMP 目標不可達消息(類型 3)

路由器無法將 IP 數據報發送給目標地址時,會給發送端主機返回一個目標不可達(Destination Unreachable Message)的 ICMP 消息。

那目標不可達有多種可能的原因,比如說網路問題、目標主機問題等等,所以這個目標不可達消息還需要指明不可達的具體原因,這個具體原因就記錄在 ICMP 報頭的程式碼欄位。

那麼這裡我們仍然以行軍打戰的例子來看看常見的目標不可達類型的程式碼有哪些:

1)前方戰事吃緊,將軍(主機 A)派了一隊士兵回京城找皇上(主機 B)搬救兵,中途情報員快馬加鞭趕到彙報:將軍,我們在途中迷失了方向,找不到京城在哪。這就是網路不可達,其程式碼為 0

2)假設士兵們成功回到京城,但是皇上出城了,不在京城,朝廷百官也不敢私自同意出兵救援。這就是主機不可達,到了地方卻沒找到人,其程式碼為 1

3)假設士兵成功找到了京城,但是由於將士們常年在外征戰,守城的年輕護衛們已經不認識這些威名赫赫的將士們了,所以需要進城口令證明身份,但是久經沙場的將士們一時半會想不起來這些東西,遂無法進城。這就是協議不可達,其程式碼為 2

4)假設士兵們成功進了城,也成功面見了聖上,但是皇上卻說密偵司告訴他的情報和你們說的不一樣,你們說你們需要的是救兵,而我得到的消息是你們只需要糧草。這就是埠不可達,其程式碼為 3

5)假設士兵們成功求得了救兵,並且獲得了火器十餘箱,但是中途山路狹窄,裝火器的馬車太大過不去,為此需要換小一點的馬車,每個馬車裝一點,但是由於火器技術尚不成熟,考慮安全問題,將軍早就嚴令禁止把火器分裝。於是乎,浩浩蕩蕩的援兵阻塞在了狹窄的山路。這就是需要進行分片但設置了不分片位,其程式碼為 4

ICMP 重定向消息(類型 5)

說到這個,我們先要明白 IP 協議或者網路層的職責是什麼,就是選擇合適的網間路由和交換結點, 確保數據的及時傳送。

為此,我們總是傾向於基於最短最優的路徑進行傳輸。

那麼如果路由器發現發送端主機使用了某個不是最優的路徑發送數據,他就會返回一個 ICMP 重定向消息(ICMP Redirect Message)給這個主機,並且,在這個消息中包含了最優的路由資訊和源數據。

寫上癮了哈哈,舉個例子:將軍得知 ICMP 的情報後震怒,派出去搬救兵的領隊竟然帶著十萬救兵在繞彎子,亂臣賊子,將軍趕緊下令誅殺這個領隊並立即走最近的路趕回來。

ICMP 超時消息(類型 11)

IP 包中有一個欄位叫做 TTLTime To Live,生存周期),它的值隨著每經過一次路由器就會減 1,直到減到 0 時該 IP 包會被丟棄

此時,IP 路由器將會發送一個 ICMP 超時消息(ICMP Time Exceeded Message)給發送端主機,並通知該包已被丟棄。

形象來說,就是將軍派出去的搬救兵的那隊人馬苦於找不到京城的方向,路途開始帶上的只夠三天的糧草斷盡,全隊飲恨而死。

設置 IP 包生存周期的主要目的,是為了在路由控制遇到問題發生循環狀況時,避免 IP 包無休止地在網路上被轉發。

ICMP 的應用

其中一個應用 ping 命令我們已經說過了,它是基於 ICMP 查詢報文的。

還有一個命令,是基於 ICMP 差錯報文的,在 Linux 下這條命令是 traceroute,在 Windows 是 tracert

大家可能會覺得 ICMP 差錯報文是只有在通訊異常的時候才會生成,其實不然,traceroute 命令就是一個例外,它會使用 ICMP 的規則,故意製造一些能夠產生異常的場景。

traceroute 命令有兩大作用:

1)故意設置特殊的 TTL,來追蹤去往目的主機上沿途經過的路由器

具體來說,就是發送端主機會不斷的向接收端主機發送 UDP 報文,UDP 報文被封裝成 IP 數據報,同時將 TTL 從 1 開始按照順序遞增。

比如說,將 TTL 設置 為 1,那麼遇到第一個路由器的時候,這個 IP 數據報就會被丟棄,接著返回 ICMP 差錯報文,類型是 ICMP 超時消息

接下來將 TTL 設置為 2,第一個路由器過了,遇到第二個路由器時這個 IP 數據報就會被丟棄,接著返回ICMP 差錯報文。

……

這樣,traceroute 就拿到了所有路由器 IP。

那到這裡其實還有一個問題,怎麼知道數據到底有沒有到達目的主機呢

traceroute 是基於 UDP 傳輸的,那自然是需要指定一個埠號的,traceroute 會選擇一個不可能的值作為 UDP 的埠號。

這樣,當數據到達目的主機時,就會發現埠對不上,於是路由器會產生一份 ICMP 目標不可達消息,其程式碼是 3,即埠不可達

當發送端主機接收這份埠不可達的 ICMP 報文時,就知道目的主機成功收到了數據。

2)故意設置不分片,從而確定路徑的最大傳輸單元 MTU

某些情況下我們並不知道路徑的 MTU 大小,所以我們需要某種手段去獲取 MTU,才能控制發送的數據包的大小。

發送端主機要做的工作很簡單,就是像往常一樣發送 IP 數據報,但是將 IP 首部的分片禁止標誌位置為 1。

這樣,如果 IP 數據報的長度超過了 MTU,該數據報會被路由器直接丟棄,並且給發送端主機發送 ICMP 目標不可達消息,其程式碼為 4,即需要進行分片但設置了不分片位

這樣,發送端主機每次收到 ICMP 需要進行分片但設置了不分片位消息時就減小 IP 數據報的長度,直到順利到達目標主機。

🎉 關注公眾號 | 飛天小牛肉,即時獲取更新

  • 部落客東南大學碩士在讀,攜程 Java 後台開發暑期實習生,利用課餘時間運營一個公眾號『 飛天小牛肉 』,2020/12/29 日開通,專註分享電腦基礎(數據結構 + 演算法 + 電腦網路 + 資料庫 + 作業系統 + Linux)、Java 技術棧等相關原創技術好文。本公眾號的目的就是讓大家可以快速掌握重點知識,有的放矢。關注公眾號第一時間獲取文章更新,成長的路上我們一起進步
  • 並推薦個人維護的開源教程類項目: CS-Wiki(Gitee 推薦項目,現已累計 1.6k+ star), 致力打造完善的後端知識體系,在技術的路上少走彎路,歡迎各位小夥伴前來交流學習 ~ 😊
  • 如果各位小夥伴春招秋招沒有拿得出手的項目的話,可以參考我寫的一個項目「開源社區系統 Echo」Gitee 官方推薦項目,目前已累計 700+ star,基於 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + … 並提供詳細的開發文檔和配套教程。公眾號後台回復 Echo 可以獲取配套教程,目前尚在更新中。