一跳不等於一趟旅程
上一段你學到了資料連結層如何讓一個訊框跨過單獨一條線——從這個盒子到同一條纜線或無線電上的下一個盒子。但房間另一頭的那個盒子並不是網際網路。要抵達另一個大陸上的伺服器,你的資料得跨過幾十條各自獨立的鏈,由不同公司擁有、用不同材料做成、首尾相接。每一條鏈只知道怎麼把一個訊框交給它緊鄰的鄰居。那麼,是誰在決定一跳又一跳地交給哪個鄰居,好讓資料真的能抵達?這正是 網路層 的工作。
網路層在 TCP/IP 模型中坐在資料連結層的上面一層,而它只有一個志向:跨越許多條鏈的端到端傳遞。它的做法,是給地球上每個介面一個全域位址,並把那個目的位址蓋在每一塊資料上,好讓沿途任何一個盒子都能讀到它、把這塊資料往前推近一步。把資料連結層想成一輛只開一段路程的快遞車;網路層則是包裹上的定址系統,它讓一連串各自都不知道整條路線的快遞車,仍然能把包裹送到門口。
轉送對上路由:兩座時鐘
網路層其實偷偷在做兩件非常不同的工作,而把它們分開來看,是整個這一段裡最能讓人豁然開朗的一個觀念。這個區分叫做轉送對上路由。轉送是本地、發生在一瞬間的反射動作:一個封包落在路由器某個輸入埠上,路由器瞄一眼目的位址,在一張表裡查一下,就把這個封包推出正確的輸出埠。就這樣。它一秒鐘要發生幾百萬次,而且必須在奈秒之內完成。
路由則是緩慢、全域、需要動腦的工作:那張表一開始到底是怎麼被填好的?路由器之間必須彼此交談,分享自己能抵達哪些地方,並算出橫越整張網路的好路徑。那場對話在背景以秒與分為單位進行,並更新那張之後轉送會去查閱的表。這兩者活在完全不同的時鐘上。工程師把它們分別叫做 資料平面(轉送,那套快速、逐封包運轉的機械)與 控制平面(路由,那個緩慢、決定路徑的大腦)。
資料包模型:每個封包都自己旅行
網路層該怎麼搬移資料?歷史提供了兩種宏大的設計。其一是虛擬電路模型,在送出任何東西之前先建立一條固定路徑,就像舊式電話網路為你整通通話保留一條線路。網際網路選了另一種:資料包模型。它沒有建立階段,也沒有保留路徑。每一個封包(稱為資料包)都帶著完整的目的位址,獨自被射進網路裡,而每一台路由器都各自、重新、當場決定它的下一跳。
日常的比喻是明信片。你把一張只寫了地址的明信片投進信箱,沒有事先打電話,也沒有保留卡車。每一個經手它的郵局只做一個本地決定(大致朝那個地址轉送出去)就放它走。你前後寄出的兩張明信片,可能走不同的路線,甚至以顛倒的順序抵達。網際網路做的正是這樣的約定:簡單、強韌,而且中段美妙地不保留狀態。路由器不持有任何「逐對話」的記憶,這正是為什麼網路核心能在一條鏈飛行途中失效時存活下來——下一個封包只是被導著繞過那個缺口而已。
這裡有個誠實的玄機,值得大聲說出來。正因為路由器不保留狀態、不做任何承諾,資料包服務是盡力而為(best-effort):一個封包可能遺失、被複製、被延遲,或以顛倒的順序送達,而網路層既不會察覺,也不會道歉。這聽起來嚇人,直到你想起它的設計哲學:讓網路核心又笨又快,把聰明才智推到邊緣去。如果你需要每個位元組都照順序送達,那份可靠性是在上面的傳輸層疊加出來的(這就是 TCP 在做的事),而不是在這下面。網路層那謙遜而刻意的工作,只是把資料包大致送到它們要去的地方。
打開信封:IP 資料包
撐起這整場表演的協定,就是 網際網路協定(Internet Protocol,IP),而它定義的封包格式,就是 IP 資料包。這一個格式是偉大的統一者:著名的「網際網路的窄腰」。在它下面,一整座狂野的鏈技術動物園(乙太網路、Wi-Fi、光纖、行動網路)一旦它們的訊框裡裝著 IP,看起來就都一個樣;在它上面,每一個傳輸與應用協定都騎在 IP 之上。地球上幾乎每台裝置在幾乎所有事情上都意見不合,唯獨這件事例外:它們全都同意要講 IP。
就像你見過的每一個封包,一個資料包就是一個標頭後面跟著一段酬載。回想封裝:酬載是上面交下來的一整個傳輸層區段(一塊 TCP 或 UDP 的資料),而標頭是網路層自己包在它外面的那組控制欄位。標頭正是所有網路層機械居住的地方。讓我們讀一讀其中最要緊的幾個欄位,用的是至今仍承載大部分流量的版本:IPv4。
An IPv4 datagram (the header, then the cargo) +-----------------------------------------------------+ | version | header len | total length (bytes) | +-----------------------------------------------------+ | identification | flags | fragment offset | <- fragmentation +-----------------------------------------------------+ | TTL | protocol | header checksum | +-----------------------------------------------------+ | source IP address (32 bits) | +-----------------------------------------------------+ | destination IP address (32 bits) | <- forwarding uses this +-----------------------------------------------------+ | payload: the transport-layer segment (TCP / UDP) | +-----------------------------------------------------+
有四個欄位特別值得注意。目的 IP 位址是主角:它是每台路由器拿來決定下一跳的依據,而接下來兩篇指南講的全都是這些位址是怎麼被結構化的。TTL(存活時間)是一個小計數器,每台路由器都把它減一;只要它歸零,封包就被丟掉。這是那個安全閥,阻止一個被誤導的封包永遠繞著網際網路打轉,就像一個蓋了「轉手 64 次後丟棄」的包裹。protocol 欄位是個標籤,說明酬載裡坐的是什麼(例如代表 TCP 或 UDP 的編號),好讓接收端知道該去敲樓上哪一扇門。至於 identification 欄位加上 flags,存在是為了我們接下來要碰到的一個特定頭痛:分段。
當資料包太胖:MTU 與分段
這裡有一個 IP 無法假裝它不存在的問題。每一條鏈都有一個它願意承載的最大訊框尺寸,叫做 MTU,也就是最大傳輸單元。乙太網路的 MTU 出了名的是 1500 個位元組;其他鏈更小。一個資料包必須塞得進它所跨越的每一跳的資料連結層訊框裡。那麼,當一個在粗鏈上塞得舒舒服服的資料包,抵達一台下一條鏈的 MTU 比較小、而這個封包根本大到送不出去的路由器時,會怎麼樣?
IP 的答案是分段:把那個過大的資料包切成好幾個比較小的資料包,每一個都是一個完整的小封包、有自己的標頭,而且全都塞得下。聰明的地方在於重組。每個片段都帶著相同的 identification 編號(好讓目的端知道它們是一夥的)、一個片段位移(fragment offset,說明這一片在原本封包裡的位置),以及一個意思是「後面還有片段」的旗標位元。關鍵在於:重組只發生在最終目的端,絕不發生在中途的路由器,因為一個不保留狀態的核心沒有地方擱半個資料包。接收端把所有 id 相同的片段收齊,再縫回原本的那個。
當出了差錯:ICMP,以及前方的路
IP 是盡力而為的,所以事情確實會出差錯,網路需要一個回報的辦法。這是 ICMP(網際網路控制訊息協定)的工作:IP 自己那個負責回報錯誤與診斷的小信差。當一台路由器因為某封包的 TTL 歸零、或因為目的端無法抵達、或因為某資料包需要分段卻又被禁止分段而丟掉它時,它會送一則 ICMP 訊息回到來源端,說明發生了什麼事。ICMP 不是用來載你的資料的;它是網路在談論它自己。
你幾乎肯定在不知情的情況下用過 ICMP。ping 指令送出 ICMP 回音請求,並對回覆計時,藉此問「你在嗎?來回一趟要多久?」。traceroute 工具則是對 TTL 的一個漂亮的取巧:它送出 TTL 設為 1、再來 2、再來 3、依此類推的封包,於是每個封包都在前進一跳之後死掉,而殺掉它的那台路由器會透過 ICMP 自報身分,一跳一跳地揭露出路徑。當網路出毛病時,這些會是你最先伸手去拿的工具。
這就是網路層的資料平面,一口氣說完:一個盡力而為、不保留狀態的資料包服務,建立在一個通用的封包格式之上,其中每台路由器依目的位址轉送,而路由則在幕後安靜地把表填好。這一段接下來會聚焦在最要緊的幾個部分。第 2 篇拆解 IP 位址與 CIDR 的斜線記法;第 3 篇談 NAT 以及我們為何位址不夠用;第 4 篇是 IPv6 那段漫長的故事;第 5 篇則走進路由器內部,看最長前綴匹配如何在奈秒之內挑出輸出埠。你現在拿到了地圖;讓我們去把這片土地填滿。