所讀版本:機械工業出版社

這一篇在做什麼

前幾篇已經在程式層面開始使用 Socket,這一篇往下補的是 TCP 本身的工作機制。目標不是把協議規格背完,而是理解幾個會直接影響實作判斷的問題:

  • TCP 在整個網絡模型中的位置
  • 連接建立、數據傳輸與連接關閉的基本流程
  • 幾個常見 Socket 參數的用途
  • 為什麼實戰裡常常還要加上心跳機制

先把 4 層網絡模型放回腦中

在日常開發裡,最常接觸的是 Socket.SendSocket.Receive 這類 API,但它們其實只是整個網絡模型中的一小段。

這裡可以把常見的 4 層模型簡單整理成:

應用層

最接近遊戲或業務邏輯的一層。程式在這裡決定要發什麼內容,再把資料轉成字節流交給下面的傳輸層。

傳輸層

這一層就是 TCP 所在的位置。它不是單純把資料往下丟,而是會處理:

  • 分段
  • 順序控制
  • 確認回應
  • 重傳
  • 流量控制

也就是說,TCP 的價值在於它把「可靠傳輸」這件事包進了協議本身。

網絡層

網絡層主要負責把資料送到正確的地址,也就是 IP 處理的範圍。它會在資料上附加來源與目的地地址資訊。

網絡接口層

這一層才真正和實際的物理傳輸介質打交道。

TCP 連接建立

TCP 是面向連接的協議,所以在真正傳數據之前,雙方必須先建立連接。這就是常說的三次握手。

三次握手在做什麼

它不只是「打個招呼」,而是在確認幾件事情:

  • 雙方都在線
  • 雙方都能收也能發
  • 初始序列號與確認號已同步
  • 一些窗口大小等控制信息已交換

三次握手流程

  1. 連接方呼叫 Connect,送出 SYN
  2. 監聽方收到後,回送 SYN/ACK
  3. 連接方收到後,再送出 ACK,雙方把狀態切到已建立連接。

如果 Connect 很久不返回,通常代表底層還在重試 SYN 或等待 SYN/ACK,直到成功或超出重試上限。

TCP 傳輸數據時在做什麼

連接建立之後,TCP 的傳輸過程也不是「送了就算」,而是持續做可靠性控制。

流量控制

發送方不會無限制地把數據灌給接收方。它會考慮對方接收緩衝區的容量,如果對端快滿了,就會暫停或放慢發送,避免對方溢出。

確認與重傳

發送方送出一段數據後,不會直接假設成功,而是會等待接收方確認。如果太久收不到確認,就會觸發重傳。

擁塞控制

TCP 還會根據回應時間與丟包狀況判斷當前網絡是否擁堵,必要時自動減慢傳輸速度。

這也是為什麼 TCP 在可靠性上好用,但在即時性上有時不如 UDP。

TCP 連接終止

關閉連接時,TCP 用的是四次揮手,而不是三次握手那種簡化流程。

為什麼是四次

因為 TCP 是全雙工的。也就是說,一方說「我這邊不再發了」,不代表另一方也同時沒有資料要送,所以雙方的關閉必須分開確認。

四次揮手流程

  1. 主機 1 發送 FIN,表示自己沒資料要發了。
  2. 主機 2 回送 ACK,確認收到這個關閉請求。
  3. 主機 2 把剩餘資料發完後,再送一個自己的 FIN
  4. 主機 1 回送 ACK,然後進入 TIME_WAIT,等待一段時間再真正釋放。

TIME_WAIT 的作用,是確保最後一個確認包如果丟失,對方重發 FIN 時仍有機會被正確處理。

幾個常見 TCP / Socket 參數

ReceiveBufferSize

指定作業系統接收緩衝區大小,默認通常是 8192 字節。它影響的是本地一次可暫存多少收到但尚未被應用層取走的數據。

SendBufferSize

指定作業系統發送緩衝區大小,也通常默認是 8192 字節。它會影響本地可先排隊多少待發送數據。

NoDelay

這個參數和 Nagle 演算法有關。

如果啟用 Nagle,系統會傾向先把很多小包累積起來,再合併成較大的數據包送出,優點是降低封包碎片與總開銷;缺點則是增加延遲。

所以:

  • 想減少封包數量,可以接受延遲時,Nagle 有價值
  • 很在意即時性的互動,例如某些遊戲同步場景,常會關掉它

TTL

TTL 是 IP 數據包的存活跳數。每經過一個路由器都會減少,歸零後就被丟棄。

它的主要用途是避免一個錯誤路由的數據包在網絡中無限循環。

ReuseAddress

ReuseAddress 允許端口複用。這個參數在服務端重啟場景裡很常見,因為程序退出和 OS 真正釋放端口並不是完全同步的。

如果不處理,常見情況就是:

  • 程序剛關掉
  • 你立刻重啟
  • 結果 Bind 失敗,因為端口還沒完全釋放

LingerState

LingerState 用來控制 Socket 在關閉時要不要等,以及等多久。

它實際上對應的是「關閉前是否還要保證把剩餘資料處理完」這個問題。

簡化理解:

  • 時間為 0:傾向一直等到處理完成
  • 時間大於 0:只等指定時間,之後強制結束

為什麼還需要心跳機制

理論上,TCP 有自己的保活檢測能力,但在實際遊戲服務中,通常還是會再做應用層心跳。

原因很簡單:如果某一方異常斷線,卻沒有機會正常送出 FIN,另一端可能還會暫時誤以為連接有效。

TCP KeepAlive

系統層面可以打開 KeepAlive:

Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true)

它的作用是:長時間無通信時,底層會主動探測對端是否還活著;如果多次探測都失敗,才關閉連接。

但這個機制往往:

  • 觸發週期較長
  • 粒度不一定符合遊戲需求

應用層心跳的常見做法

所以很多遊戲會自己再做一層:

  1. 客戶端定時送 Ping
  2. 服務端收到後回 Pong
  3. 服務端記錄最近一次收到 Ping 的時間
  4. 超過一定時間沒再收到,就判定該連接失效並主動清理資源

這一套做法比單純依賴 TCP KeepAlive 更可控,也更適合實時服務。

這一篇先記住的重點

如果只收斂成最關鍵的幾點,可以記成:

  • TCP 提供的是可靠傳輸,不只是單純發包
  • 三次握手負責建立連接,四次揮手負責安全關閉
  • 傳輸中會同時受到流量控制、重傳與擁塞控制影響
  • NoDelay、BufferSize、ReuseAddressLingerState 都會直接影響實際行為
  • 遊戲實戰裡通常還要在 TCP 之上再補一層應用層心跳