連線 — TCP 的三向交握與斷線
連線的真面目是兩端合意的號碼起算點。追蹤 SYN → SYN/ACK → ACK 為什麼需要 3 次,以及 FIN 與 TIME_WAIT 守護的是什麼。
「建立連線」的真面目是什麼
平常只要 connect() 有回來就當作連上了,不會有什麼困擾。但故障時,如果無法區分「連不上」的內容 — 是被拒絕、沒有回應,還是建立之後才斷掉 — 就無法前進。為此,需要知道「建立連線」在物理上到底是什麼。
答案出乎意料地樸素。路徑上不會保留專用線路,途中的路由器什麼都不記得。所謂連線,只不過是兩端 2 台機器各自在記憶體中持有的「與這個四元組的對象,號碼互通到哪裡了」這對帳本。所以建立連線的儀式要做的也只有一件事 — 互相傳達彼此的「起算號碼」,並互相確認確實聽到了。
這個起算號碼稱為初始序號(ISN)。不從 1 開始數的理由,與第 5 題的 TIME_WAIT 也是相通的 — 為了不和不久前還在用同一個四元組通訊的舊封包混在一起,每次都從難以預測的值開始。
三向交握。範例中用戶端的 ISN 是 5000、伺服器是 9000。請確認 ack 號碼永遠是「對方的號碼 + 1 = 下一個想要的號碼」。
把 3 個封包一個一個讀
以用戶端 ISN 為 5000、伺服器 ISN 為 9000 的例子來追蹤。
seq = 5000。「我想連線。我要從 5000 開始數」。SYN 不攜帶資料,但會消耗 1 個號碼seq = 9000, ack = 5001。「聽到 5001 之前了(= 收到了你的 SYN)。我要從 9000 開始數」— 確認與宣告的共乘seq = 5001, ack = 9001。「你的起算點我也聽到了」。此時雙方都進入 ESTABLISHED,可以傳送資料為什麼是 3 次?TCP 持有雙向各自獨立的 2 條位元組串流,所以每個方向都需要「起算點的宣告(SYN)」與「聽到了的確認(ACK)」。角色合計 4 個,但中間的 2 個(伺服器的 ACK 與伺服器的 SYN)可以共乘在 1 個封包裡,所以最少 3 個封包。2 次的話沒有人確認過「伺服器的宣告送達了」,4 次則多出 1 個浪費,就是這麼回事。
練習 2-1 — 逐一封包看三向交握
確認 3 個封包各自「在說什麼」。號碼的題目請寫在紙上計算。
Q1. 三向交握的第 2 個封包(伺服器回傳的 SYN/ACK)在說的事情,正確的是哪一個?
SYN/ACK 是 2 個角色的共乘。ACK 的部分是對用戶端 SYN 的確認,SYN 的部分是伺服器自身起算點(伺服器側 ISN)的宣告。TCP 在雙向各持有一條獨立的位元組串流,所以兩個方向各需要一次 SYN 與一次 ACK。把它們塞進 3 個封包,就是這個形狀。
Q2. 用戶端以初始序號 ISN = 5000 送出 SYN。伺服器回傳的 SYN/ACK 的 ack 號碼是多少?(SYN 不攜帶資料,但會消耗 1 個序號)
ack 號碼是「下一個想要的號碼」。SYN 依約定會消耗 1 個序號,所以收到 seq = 5000 的 SYN 的伺服器,下一個想要的是 5001。因此 ack = 5001。這條「回覆下一個想要的號碼」規則,在第 3 章的資料傳輸中也會以完全相同的形式使用。
Q3. 交握只有 2 次(SYN → SYN/ACK 就結束)無法成立的理由,正確的是哪一個?
如果 2 次就結束,伺服器只能在不知道自己的 SYN/ACK 是否送達的情況下當作「已建立」。連遲到的舊 SYN 也會立刻建起連線,抱著一堆沒有對象的幽靈連線。第 3 次的 ACK 是「伺服器的起算點我也確實聽到了」的確認。把雙向 2 條串流各自的 SYN + ACK(共 4 個角色)以共乘壓縮成 3 個封包,這就是最少次數。加密金鑰的交換是 TLS 的工作,不包含在 TCP 中。
斷線是 4 個角色 — 一個方向一個方向地打烊
斷線比建立多 1 手,是因為不一定能共乘。「已經沒有東西要送了」的宣告 FIN,只關閉從自己到對方的方向。對方可能還有東西沒送完,所以送出 FIN 之後仍可繼續接收(半關閉)。雙向各需要 FIN 與 ACK,所以基本形是 4 個封包。
而先送出 FIN 的一方,在一切結束之後仍會以 TIME_WAIT 狀態留存一段時間(多數實作為 30 秒〜數分鐘)。理由有 2 個。
還有一種不客氣的結束方式,就是 RST(重置)。這是「我不知道那條連線/現在立刻毀掉」的單方面通告,會回應給送往沒有等待程式的連接埠的 SYN、送給已當掉行程的資料等。FIN 是合意的打烊,RST 是不由分說的廢棄 — 光是能在封包擷取中分辨這兩者,故障的風景就會大不相同。
練習 2-2 — 斷線與 TIME_WAIT,還有 RST
TCP 的結尾比開頭更深奧。實務上遇到的頻率也是斷線這邊比較高。
Q4. TCP 的斷線需要雙向各 1 次 FIN(合計 2 次,各自附帶 ACK)的理由是哪一個?
FIN 是「從我到你的這個方向,已經沒有東西要送了」的單向宣告。對方可能還有東西沒送完,所以自己送出 FIN 之後,仍然可以繼續接收對方的資料(半關閉)。雙向各自的 FIN + ACK 合計 4 個封包(有時會共乘變成 3 個)— 建立只要 3 次而斷線要 4 次,正是因為這個「一次關一個方向」的設計。
Q5. 先送出 FIN 斷線的一方不會立刻消失,而是在 TIME_WAIT 狀態等待一段時間。這段等待時間的目的,正確的是哪一個?
TIME_WAIT 的角色有 2 個。(1) 自己回覆的最後一個 ACK 若遺失,對方會重送 FIN,所以要留下來以便回應。(2) 等待仍漂流在網路上的舊區段因壽命耗盡而消失,防止舊資料混入重複使用同一 (來源 IP, 來源連接埠, 目的 IP, 目的連接埠) 的新連線。也就是說,它不是故障,而是把承諾守到最後的正常狀態。第 7 章會處理「TIME_WAIT 大量出現」的案例。
Q6. 對沒有任何程式在等待的連接埠送出 SYN,與防火牆默默丟棄封包這兩種情況,從用戶端看到的差異,正確的是哪一個?
SYN 送達沒有等待程式的連接埠時,主機的 OS 會立刻回傳 RST(重置),用戶端馬上以 Connection refused 失敗。另一方面,默默丟棄(drop)的防火牆不會有任何回應,用戶端會邊重送 SYN 邊等待,最後逾時。「立即的拒絕 = 至少已送達對方主機」「逾時 = 沒有任何回應回來」— 這個切分在第 7 章的案例演習中會是主角。
本章帶走的重點
- 連線的真面目是兩端持有的帳本對。所謂建立,就是「起算號碼(ISN)」的合意
- 交握是把雙向份的 SYN + ACK 共 4 個角色共乘進 3 個封包的最小形。ack 永遠是「下一個想要的號碼」
- 斷線是 FIN 一次關一個方向。TIME_WAIT 不是故障,而是最後一個 ACK 的保險與舊區段的排毒
- 立刻回 RST = 有送達但被拒絕,無回應逾時 = 根本沒有回應。切分的第一步
下一章將以手算追蹤建立之後的主角 — 序號與 ACK 號碼在資料傳輸中如何運作。