9.10. TCP -- 可靠的位元組串流¶
TCP(傳輸控制協定,Transmission Control Protocol)是建構在 IP 之上的另一種傳輸層服務。如果說 UDP 最貼切的描述是「送出封包後祈禱它能到達」,那麼 TCP 就是「在兩個端點之間開啟一條連線,並把它視為一條雙向的位元組管道,對方一定會依序、且只接收到一次完整的資料」。網際網路上大多數的流量都使用它,而相機在網路上所做的大部分工作也都使用它。
9.10.1. TCP 在 IP 之上增加了什麼¶
TCP 所做的遠比 UDP 多。它增加了:
連線。 在任何資料傳輸之前,兩個端點會先交換一段簡短的交握程序,以確認彼此正在通訊。連線具有狀態——「開啟」、「半關閉」、「關閉」——而雙方都會追蹤這個狀態。
可靠的傳遞。 傳送端送出的每一個位元組都會由接收端確認(acknowledge)。任何在逾時時間內未被確認的資料都會重新傳送。應用程式永遠不會看到遺失的位元組;它只會在協定重送資料時感受到一段延遲。
依序傳遞。 位元組會以送出的相同順序抵達。即使封包在接收端以亂序到達,TCP 也會在應用程式讀取之前重新排序。
流量控制。 如果接收端速度較慢,傳送端會被告知放慢速度;連線會自動適應較慢一端的速率。
壅塞控制。 如果中間的網路開始丟棄封包,傳送端會退讓,直到情況恢復。這可避免任何單一連線把已飽和的連結拖垮。
這一切都是自動的。應用程式所使用的 Python API 不過就是 send(bytes) 和 recv(n);其餘的一切都由 TCP 在底層處理。
9.10.2. 交握¶
TCP 連線會在允許任何資料通過之前,先進行一次三向交換以開啟連線:
三向交握。一旦雙方都完成確認,連線即告開啟,資料便可開始流動。¶
client 送出一個 SYN(synchronise,同步)封包以請求開啟連線。server 以 SYN-ACK(synchronise + acknowledge,同步+確認)回覆,表示接受。client 再送出最後一個 ACK 以確認。經過這一輪往返之後,雙方都同意連線已開啟,並已同步它們用來追蹤哪些位元組已送出與已接收的計數器。
交握在第一個有用的位元組通過之前,需付出一個往返時間(round-trip-time)的延遲成本。對區域網路而言這只是一毫秒;對跨洲連線而言則大約是 100 ms。這是 TCP 的主要成本,也是為什麼簡短的一次性訊息有時改用 UDP 反而更划算的原因。
9.10.2.1. 關閉交握¶
TCP 連線在關閉時也會進行一次交換(每一方各送出一個 FIN)。任一端都可以獨立關閉自己這一半的連線;一端已結束傳送但另一端仍在通訊的 半關閉 狀態是合法的,雖然並不常見。應用程式通常只需呼叫 close(),並讓協定自行處理關閉序列。
9.10.3. TCP 不 保證什麼¶
有幾件事有時會被誤以為是 TCP 會做的,但其實它並不會做:
訊息邊界。 應用程式送出的是 位元組串流,而非訊息串流。兩次
send(b"hello")呼叫可能會以一次recv()的形式抵達成為b"hellohello",也可能成為兩次大小不一的recv()。如果應用程式需要訊息框架(framing),就必須自行加上框架(換行字元、長度前綴,或任何標記皆可)。舉例來說,透過 TCP 傳送 JSON 文件時,需要以換行字元或其他標記區隔每一份文件。加密。 TCP 會把應用程式交給它的位元組以明文形式一路載送。如果內容需要保密,應用程式必須將連線包裝在 TLS 之中(請參閱 加密 socket 與 TLS)。
身分驗證。 TCP 只確保位元組完整抵達。它對 是誰 送出這些位元組一概不管。身分驗證同樣是更高層次才需處理的事。
閒置連線上的存活偵測。 如果雙方長時間都沒有傳送資料,這條連線在技術上仍然開啟,但無法偵測到對端已當機或消失。當這一點很重要時,可在 socket 上啟用 Keepalive 探測來解決。
9.10.4. 何時該使用 TCP¶
對於幾乎任何符合「client 開啟一條通往 server 的連線,雙方交換一些位元組,結束後關閉連線」這種形態的通訊,TCP 都是正確的選擇。HTTP 與 HTTPS 請求、SSH 連線、資料庫查詢、檔案傳輸、影像上傳——全都透過 TCP。
只有當通訊不符合那種形態時才該採用 UDP:彼此獨立、自成一體且可接受遺失的訊息、多播(multicast)流量、時間同步、名稱查詢,或是交握成本高到難以接受的極度延遲敏感情境。
在埠號、UDP 與 TCP 都到位之後,傳輸層的故事就講完了。揭露這兩者的 Python API 將在下一頁說明:Socket 物件。