JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

封框:在位元四周畫出界線

實體層交給你的是一條沒有標點符號、無止盡滴落的位元流。這篇要談資料連結層如何把這條河切成接收端找得到的訊框,以及一件出乎意料棘手的工作:標出一個訊框在哪裡結束、下一個從哪裡開始。

為什麼光有位元還不夠

你剛從實體層爬上來,那一層的全部工作就是把原始的 1 與 0 送過導線或穿過空氣。但實體層對一件事很坦白:它交付的是一條連續、不分段的位元流,就像一個沒關緊、一直滴的水龍頭。它從不告訴你「一則訊息從這裡開始」或「這則訊息結束了」。如果你的朋友把兩張短紙條的位元一張接一張地送來,導線只會把一條又長又不間斷的細流交給另一端。第一張紙條到哪裡才算結束?

這是資料連結層的第一件實際工作:把那條毫無特徵的位元流,在一段段資料四周畫上界線,好讓接收端能挑出一個個自成單位、稱為訊框的東西。訊框就是資料連結層的信封。它是那張明信片:前面有標頭(位址與幾個控制欄位),中間是真正的貨物,後面通常還有一段檢查序列。上層交下來的所有東西,一整個網路封包,都當作貨物坐在裡面。

一個訊框的解剖

在我們煩惱怎麼找出訊框的邊界之前,先看看一個訊框實際裝了什麼。幾乎每一種封框格式都依循同一套食譜:一個說「訊框從這裡開始」的標記、一個帶有位址與控制位元的標頭、一段承載上層封包、長度可變的酬載(payload),以及一個帶有錯誤檢查欄位的結尾。經典的例子是乙太網路訊框,它幾十年來承載了全世界有線流量的絕大部分。

把一個乙太網路訊框從前往後讀一遍,你會依序遇上整套食譜。先是一小段前導碼,一個固定的喚醒圖樣(位元交替),它唯一的工作是讓接收端的時脈鎖上訊號;它並不真正算訊框的資料。接著是標頭:6 個位元組的目的 MAC、6 個位元組的來源 MAC,再加 2 個位元組的類型,告訴接收端裡面裝的是哪種封包。然後是酬載,介於 46 到 1500 個位元組之間,網路層的封包就當作貨物坐在這裡。最後是 4 個位元組的結尾——FCS,裝著錯誤檢查。標頭、酬載、結尾:每一次都是同一個信封形狀。

這裡有兩個細節物有所值。第一,標頭裡的 MAC 位址指名這一條鏈上的來源與目的硬體,這個主題我們會在後面的指南完整拆解;現在只要知道每個網路介面都有自己的一個。第二,結尾的 FCS(訊框檢查序列)是一個循環冗餘檢查,也就是本段下一篇要談的那枚偵錯指紋。訊框格式早就替「抓出損壞」保留了一個欄位,因為一條鏈是被允許會掉資料、會有雜訊的。

難的地方:訊框在哪裡結束?

現在來到真正棘手的問題。接收端拿到的是一串赤裸裸的位元,必須在沒有任何外部幫助的情況下,自行判斷每個訊框從哪裡開始、到哪裡停止。這就是封框問題,跟你從訊息封框中遇到的是同一道謎題:當你從通訊端讀進位元組、需要知道一則訊息在哪裡結束時。對此有三種誠實的策略,真實的通訊協定會把它們混著用。

第一個點子是長度欄位:在訊框前面放一個數字,說「我恰好有 N 個位元組長」。接收端讀到 N,數出 N 個位元組,就知道下一個位元組是新訊框的開頭。乾淨又簡單,但脆弱:只要那個長度欄位裡有一個位元被弄壞,接收端就會數錯,並且從此之後的每個訊框都跟著迷失,就像讀一欄密密麻麻的數字時跳掉了一行。所以長度欄位通常需要一個哨兵標記來協助復原。

第二個點子是旗標(flag):選一個特殊的位元圖樣,比方說 01111110,蓋在每個訊框的開頭與結尾,就像句子末尾的句號。接收端只要掃描這個旗標就好。這很穩健,因為就算發生錯誤,接收端也只需等到下一個旗標就能重新對齊。但它引出一個尷尬的問題,而這正是整個主題的味道所在:當貨物資料自己恰好含有旗標圖樣時,會發生什麼事?

填塞:別讓資料被誤認成旗標

假設你的旗標是位元組 01111110,而在某人照片的深處,這個一模一樣的位元組恰好出現在酬載裡。如果你什麼都不做,接收端看到它就會錯誤地宣告訊框結束,把照片切成兩半。解法是填塞(stuffing):一條規則,保證旗標圖樣絕不會意外出現在酬載中,同時又仍讓任何資料都能通過。

位元填塞是最聰明的版本,HDLC 之類的協定就用它。旗標 01111110 連續有六個 1。規則是:每當發送端在資料中正要送出連續五個 1 時,它就自動在後面立刻插入一個 0。於是資料永遠生不出六個 1。接收端把規則反過來執行:看到五個 1 之後跟著一個填塞的 0,就直接把它刪掉,還原出原本的位元。連續六個 1 從此只可能代表一個真正的旗標。

Bit stuffing (flag = 01111110, i.e. six 1s)

  Original data:    0 1 1 1 1 1 1 0 0 1
                            ^ five 1s here -> sender inserts a 0
  Sent on wire:     0 1 1 1 1 1 0 1 0 0 1
                              (stuffed 0, never confused with a flag)

  Receiver: after five 1s, delete the next 0  ->  back to 0 1 1 1 1 1 1 0 0 1
發送端在任何連續五個 1 之後偷塞一個 0,讓資料永遠無法偽裝成那個六個 1 的旗標;接收端再把這個 0 拆掉。

還有一個位元組層級的表親,叫位元組(或字元)填塞,PPP 與許多序列協定的封框都用它。它不數位元,而是替任何與旗標相撞的位元組加上跳脫,就像程式語言讓你在字串裡放一個引號的辦法是在它前面寫一個反斜線。原理一模一樣:保留一個特殊標記當邊界,然後仔細地把任何會被誤認成那個標記的資料重新編碼。

讓一個訊框走過一條鏈

讓我們追蹤一個訊框,從發送端的網路卡到接收端,好讓各個零件咬合起來。注意封框、定址與錯誤檢查是並排坐著的,全都發生在這一跳上。接收端先做它的各項檢查,之後才願意去看裡面裝了什麼。

  1. 網路層交下一個封包。發送端的網路卡把它包起來:加上一個標頭(目的與來源 MAC,再加一個類型欄位),並對全部內容算出 CRC,放進結尾的 FCS。
  2. 它對這些位元組執行填塞,讓酬載絕不可能模仿邊界標記,再用起始與結束的定界符把整個東西封起來。
  3. 實體層把封好框的位元變成訊號送出。在線上它再度只是一條平直的位元流,但這次它帶著定界符,讓另一端找得到邊界。
  4. 接收端的網路卡掃描起始定界符,一路讀位元直到結束定界符,再把填塞拆掉,精確還原出原本的酬載。
  5. 它對收到的訊框重新計算 CRC,並與 FCS 比對。若吻合,訊框完好,酬載(也就是原本的封包)就往上交給網路層;若不吻合,訊框就被悄悄丟棄,要由更高的層級去察覺並重送。