TCP 串流沒有訊息邊界(a TCP stream has no message boundaries)
想像把好幾個杯子的水倒進一根長管。從遠端流出時,水是一道連續的流——你分不出哪裡是一個杯子的結束、下一個的開始。TCP 就像那根管子:它忠實且有序地搬運一道位元組串流,但它完全不記得你各別的訊息從哪裡開始、到哪裡停止。這是通訊端程式設計中最重要、也最令人意外的真相之一。
具體來說,若你用 send() 三次分別送出「AB」、「CD」、「EF」,對端可能一次就 recv() 到「ABCDEF」,或先收到「A」再收到「BCDE」再收到「F」,或任何其他切法。TCP 保證位元組完整且有序地抵達;它對於這些位元組如何被分組進各次 recv() 呼叫,則毫無保證。所以若你想送出一個個分離的訊息——「這是一個請求」、「這是下一個」——你必須自己加上結構。這叫做封幀(framing),兩種標準做法是:在每則訊息前面加上它的長度(先送一個 4 位元組的計數,再送那麼多位元組),或使用分隔符(每則訊息以換行結尾,讀到它為止)。接收端再從原始位元組串流中重組出完整的訊息。
為何重要:假設「一次 send() 等於一次 recv()」大概是最常見的單一網路程式錯誤,而它之所以陰險,是因為它在測試時往往會動——在 localhost 上用小訊息時,位元組通常剛好一起抵達,到了正式環境的負載下、或跨越真實網路時才壞掉。對自己誠實:TCP 給你的是位元組串流,不是訊息串流,而把位元組重新變回訊息,每一次都是你應用程式的責任。UDP 則不同——每個資料包是一個單位——但 UDP 是以放棄可靠性換來這一點的。
長度前綴封幀:要送出 5 位元組的訊息「hello」,先送 4 位元組的長度 5(採網路位元組序),再送那 5 個位元組。接收端正好讀 4 個位元組得知長度,再迴圈 recv() 直到它再收齊正好那麼多位元組——無論 TCP 如何切割串流,都重組出一則完整的訊息。
長度前綴或分隔符封幀,從一道沒有內建邊界的串流中重建訊息。
那種「在 localhost 上能動」卻在真實網路上失敗的程式碼,往往正是這個錯誤——它依賴了位元組剛好在一次 recv() 中抵達。TCP 從未承諾這點;唯有封幀才能讓訊息邊界成真。