當使用connect()完成雙方的通訊連接後,整個控制流程就會回到應用程式中,這時我們可以使用write()來發送消息,read()來讀取消息。不過write()操作會由應用程式決定要寫入多長的數據,而這些數據會透過協議棧存放在作業系統分配好的緩衝區當中,由TCP模塊決定一次要發布多長的數據。同理read()操作也是向作業系統分配的特定緩衝區拿取消息
由於TCP主打可靠性與面向連接的通訊,在封包收發途中,雙方需要對消息發送方回應一個ACK,代表我收到你發送的封包了。另外假設TCP真的針對應用程式下放的資料進行分段,發送方必須藉由序號功能告知接收方封包占整個資料的具體位置,以防止消息漏掉
儘管調用write()操作會指定寫入長度,但協議棧會先將指定長度的數據存入緩衝中,並不會完全依照指定長度發送封包,而每個操作系統對於發布封包的長度限制都不太相同,不過它們一致的目標是要避免太短資料長度的頻繁發送,以及太長資料長度的壅塞與時延
為了達到更高的效率,協議棧也允許應用程式在執行發送操作時也可以透過設置flag告訴協議棧要如何處理緩衝區消息,例如允許緩衝區在沒有全滿的狀態下發布消息
緩衝區有額外配置計時器,用來防止太長時間沒發布的問題產生
上圖顯示一種發送組封包的功能,藉由創建一個專門組封包長度的緩衝區,它會在單位時間或者在作業系統緩衝到某個長度時將先進入的資料抓進組封包緩衝內,待達到指定長度後就會把組裝好的封包發送出去(這裡是以三個封包為例子),這種功能多用於對網路連線功能有限制的設備上
一般來說應用程式下放的資料長度並不會超過MSS(最大數據包長度),所以一般應用數據不會涉及到數據的切分,也就是分段。但有些應用像是提交一份表單或是裝置發布了一串非常長的資料,就有可能超出MSS的範圍,因此TCP會需要對這些數據包進行處理,把它們切成小塊
TCP的分段功能就像將剛出爐的蛋糕切成剛好的等分,並將分出來的蛋糕包裝並且掛上名牌販售給消費者,畢竟真的會有人一次買整塊沒切的大蛋糕嗎?恐怕沒有吧
TCP也是一樣,它會將超過MSS的應用數據切成等分,需要特別注意應用層協定頭部也被包含在被裁切的範圍內
那麼問題來了,傳輸雙方到底是怎麼決定裁切的長度呢?其實在通訊雙方進行連接的三次握手時雙方會在TCP頭部的選項部分填入自己允許的MSS長度,兩相比較後選擇最小的那個做為TCP分段的裁切大小,在往後資料發送中若是資料長度超過MSS(這裡假設途中的500),就以500來切分數據
為什麼要使用TCP協定?其中一個目的便是使雙方之間的通訊具備可靠性,所以理所當然,消息封包不能發了就不理它,TCP要求接收方要回應是否正確收到消息,以判斷是否啟動重傳機制。此外,假如數據被分段了,接收方要使用收到的序號消息去判斷有沒有漏包,以及接收完成後要怎麼拼裝
ACK就好像是聊天中的"嗯、喔"等語助詞,它告訴對方我收到你的消息了,還記得我在使用網路視訊的時候,若是對話中發生延遲,整個對話會變得很奇怪,當我說完一句話,對方會有一段時間沒有反應,這時就在想我該重新說一遍嗎?是對方不懂我的意思嗎?不過對方通常會需要過個1、2秒才會反應過來,看來我對時間延遲的標準比電腦還高很多阿...
接收方可以透過資料總長減去TCP頭部的資料偏移算出下一次需要從第幾個bytes開始,這種機制可以避免封包的遺失,例如下一次從序號501開始,卻收到序號1000的封包,這代表在傳輸過程中有遺失。另外作業系統也配置了Timer(時間計時器)來判斷指定時間內是否接收到返回響應,進而決定是否啟動重傳
對傳送方而言,序號就是他告訴接收方我要發送的資料是從哪裡開始,有多長。對接收方而言,就是回應發送方我成功接收了,下次封包要從第幾個bytes開始
上圖具體化了雙方的序號溝通以及封包遺失的處理方法
ACK號與控制位ACK號差別
確認回應編號(Acknowledgment Number): 顯示的是下次要接收的資料包起始位置序號,該序號-1就是目前接收到的資料總長
控制位ACK(Acknowledgment Flag): 布林值,在TCP三次握手與封包接收時會設成1表示我有收到你的資料
關於起始序號
一般來說起始的封包序號不會是1,而是在建立連接時藉由客戶端與服務端互相發送的序號所產生的一個共用隨機亂數,這麼做的目的是為了防止網路攻擊者預測封包序號,提高安全性,不過後續的確認回復依然都是+1就對了
當客戶端發送的封包在指定時間內沒有獲得確認消息時會啟動重傳機制,不過問題來了,這個時間到底是怎麼判斷的?
其實TCP會動態監測包傳遞的收發狀況,計算每次封包發送的時間並以此推測出逾時時間的大概值,這個逾時時間會依照作業系統的不同而擁有不同的時間區間(例如UNIX系統和Window使以0.5秒為單位),但在一開始因為不知道封包的連線狀況所以預設逾時時間會設定為6秒
不過重傳機制也不是沒有任何限制的,通常作業系統會設定一個retry上限的重傳限制,當發生逾時時作業系統會將逾時時間調整為2、4倍等指數倍增,但若客戶端重傳太多次把retry的quota耗完時就會觸發斷開socket連線或者重新建立連線操作
TCP對逾時時間的設定要求是不會輕易觸發逾時,發送多於的封包,同時取大於平均值的0.5秒倍數的最小值(以UNIX舉例)。不過有時候會遇到封包收發時間的波動區間過於陡峭,例如上圖的第二張圖,這種狀況通常是網路核心發生壅塞等狀況,這個時候作業系統會設定一個比一般收發狀態更高的逾時時間上限
從TCP的一些操作中有沒有體會到一點trade off的味道呢?畢竟現實中魚與熊掌很難兼得的,效率、可靠性、速率的取捨會根據實際應用場景來做調整,並沒有絕對的優劣之分
斷開連線的時間點發生在資料完全傳送完成後,或是某些特定的操作。當瀏覽器向服務器請求結束會後主動斷開連線,表示溝通結束。另一種狀況可能是服務器判斷客戶端發送的資料封包不符合格式,主動斷開連線。所以發起斷開連線的操作可以是客戶端或服務端雙方,端看當前的溝通狀況決定
我們以客戶端完成所有請求發送後主動斷開連線為例,因為不管發送方或是接收方都需要發送兩次FIN消息封包以及兩次ACK消息封包,所以斷開連線又被稱為四次握手
首先要注意的是只有起初發起斷開請求的一方才會進入TIME_WAIT階段。我們先假設客戶端一接收到服務器發送的FIN消息馬上進入CLOSE階段,但好死不死,網路狀況不穩或任何通訊延遲導致客戶端ACK響應超過逾時時間,服務器會進行FIN的重傳,如果又這麼剛好,客戶端又剛創建一個端口號相同的應用程式socket,這時重傳回來的FIN消息封包就有可能錯誤的關閉連線。所以一般來說會等待一段時間後才會進入CLOSE階段,此處的MSL是 Maximum Segment Lifetime,封包最大生存時間,一般可以解釋成一組FIN消息的往返需要的處理時間,Linux默認是60秒。若確定四次握手成功後客戶端就會在2MSL時間過後刪除相對應的套接字
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT state, about 60 seconds */