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

連接埠與通訊端:從主機到行程

網路層負責把封包送到正確的機器,但一台機器同時跑著數十支程式。這篇談的就是最後那一小段路:傳輸層如何用連接埠號與通訊端,把每一個抵達的位元組交給恰好正確的那場對話。

位址只帶你到大樓,不到人

到目前為止,你已能追蹤一個封包跨越全世界。網際網路協定讀取目的地的 IP 位址,一跳接一跳地把一個 IP 資料報送到正確的機器。但這裡有個還沒人提過的缺口:一台筆電同時跑著瀏覽器、聊天程式、音樂串流,還有三個背景更新程式。IP 位址只把封包送到大樓的大門口,卻完全沒說它是要給哪一戶的。

回想前幾階的「門牌號碼」比喻。街道地址(IP 位址)標出網路上的一個位置;公寓門牌號則標出那棟樓裡這封信是給誰的。連接埠號就是那個門牌號。傳輸層的工作,就是把抵達這台機器的一串通用位元組,交給那個正在等待它的特定程式;送出去時則做相反的事。

這裡也正是著名的端到端原則所在。中間的網路(路由器、交換器、光纖)被刻意保持「笨」:它只負責轉送資料報,不做任何承諾。所有聰明、與端點相關的工作——例如判斷哪個程式該拿到資料、有沒有東西遺失——都被推到兩端的主機上。端到端原則正是網際網路能如此快速成長的原因:核心永遠不必為了理解任何新應用而升級。

多工:多場對話,一條線

在任一瞬間,你的機器可能同時開著許多場對話。讓它們能共用單一網路連線的訣竅,就是多工與解多工。送出時,傳輸層從好幾支程式取得資料,在每一塊資料上蓋上來源連接埠與目的連接埠,再把它們全部匯集成交給 IP 的資料報串流。這個匯集的動作就是多工。

送入時,機器收到的是一道沒有區分的資料報水柱。解多工就是把這道水柱重新分類:讀取每個區段裡的連接埠號,把每一塊丟進正確程式的桶子裡。連接埠號就是讓這件事成為可能的欄位。它是一個 16 位元的數字,所以每個協定、每個 IP 位址有 2^16 = 65536 個可能的連接埠。這就是一台主機能提供的不同「公寓」總額。

通訊端:程式握住網路的把手

連接埠號只是標頭裡的一個標籤。程式真正握在手裡的,是一個通訊端。通訊端是應用程式與傳輸層之間的那道門:程式從自己這一端寫入位元組、再讀回位元組,而作業系統負責把這些位元組包進傳輸層標頭並送出去。對程式而言,它幾乎就像一個可讀可寫的檔案,只不過它的另一頭是地球上某處的另一台機器。

這裡有個微妙之處,值得放慢腳步。一個 UDP 通訊端只用兩件事來辨識:目的地 IP 與目的地連接埠。瞄準這一對的每個資料報都會落進同一個通訊端,不管是誰送的。而一個 TCP 通訊端則用全部四件事辨識:來源 IP、來源連接埠、目的地 IP、目的地連接埠,也就是所謂的四元組。這就是為什麼一台繁忙的網頁伺服器能在 TCP 連接埠 443 上同時維持數千條連線:每個用戶端獨一無二的來源 IP 與來源連接埠構成不同的四元組,所以即使伺服器這一側的每條連線都共用同一個埠,每條連線仍各有自己的通訊端。

Server side (pseudocode)            Client side
--------------------------          ---------------------------
s = socket()                        c = socket()
bind(s, port = 443)                 connect(c, host="203.0.113.7",
listen(s)                                        port = 443)
conn = accept(s)   <----[ SYN ]----  send(c, request_bytes)
read(conn, ...)    ----[ data ]----> recv(c, reply_bytes)

The accepted 'conn' is a NEW socket, distinct per client,
keyed by the four-tuple (srcIP,srcPort,dstIP,dstPort).
一個最精簡的 TCP 伺服器/用戶端骨架。listen() 在某個連接埠上開門;每次 accept() 都回傳一個專屬於單一用戶端的全新通訊端。

兩道門,兩種承諾:UDP 與 TCP

傳輸層讓你的程式選擇要走哪道門,而這個選擇是真正的設計決策,不是品質高低的排名。UDP是輕薄的那道:它幾乎只加上來源與目的連接埠,再加一個用來察覺毀損的檢查碼,就把你的訊息交給 IP,然後忘了它。它是非連線導向且盡力而為的,意思是封包可能亂序抵達、重複,或根本沒到,而 UDP 不會動一根手指去修正。本階的下一篇將深入探討,為什麼這種極簡有時正是對的工具。

TCP是厚實的那道。它先用一次三向交握建立連線,再給你的程式一條可靠的位元組串流:位元組在另一端依序而出,不遺失也不重複,彷彿你有一條乾淨的管子直通對方的行程。為了在 UDP 所騎乘的同一條會掉封包的 IP 之上做到這點,TCP 疊加了可靠資料傳輸流量控制與壅塞控制。這些機制是本階其餘篇章的主題,所以這裡我們只先點名。

端到端追蹤一個抵達的封包

讓我們把這一切串起來,追蹤一個封包在抵達 IP 為 203.0.113.7 的伺服器那一刻發生的事。注意每一層如何剝開屬於自己的信封、讀取屬於自己的欄位——這正是基礎階學到的封裝觀念反向運作。

  1. 連結層往上交出框架的酬載,網路層看到一個目的地為 203.0.113.7 的 IP 資料報。那就是我們,於是它剝掉 IP 標頭,把酬載往上交給傳輸層。
  2. 傳輸層讀取協定欄位:這是 TCP。它接著查看四元組——來源 198.51.100.5 連接埠 51000、目的地 203.0.113.7 連接埠 443——以進行解多工。
  3. 它在自己開啟中的通訊端表裡尋找這個確切的四元組,找到屬於這個用戶端的那一條連線。(若沒有任何通訊端相符,TCP 會以一個重設回絕;若是 UDP,則會回送一個 ICMP「連接埠不可達」。)
  4. 位元組酬載依序被附加到該通訊端的接收緩衝區,而正在等待的程式那個 read() 呼叫便帶著新資料醒來。封包不只抵達了正確的主機,更抵達了正確的行程。

注意連接埠與通訊端是如何協同運作的。連接埠號是那個小標籤,告訴傳輸層該把位元組分類到哪裡;通訊端則是那個由某支程式擁有、真正收下這些位元組的活物件。這個交接——從網路上的一個位置,向下交到單一個執行中的行程——正是傳輸層存在的全部理由。本階的其餘一切,可靠性、流量控制等等,都建立在這個乾淨的觀念之上。