在最開始介紹TCP的時候,我們就介紹了TCP的三個特點,分別是面向連接、可靠、字節流式。前面內容我們已經介紹過了TCP的連接管理,接下來的這部分內容將會介紹與TCP可靠性強關聯的TCP重傳。
很多網絡協議都提供了checksum或者CRC手段來檢測收到的數據包是否發生錯誤,但是檢測到數據包錯誤后很多協議都不會進行重傳等操作來可靠的修復錯誤。例如常見的IP和UDP協議完全沒有重傳,對於鏈路層的以太網協議,雖然有重傳操作但是嘗試若干次重傳還沒有成功會也會放棄(CSMA/CD)
經過N多專家前撲后繼的研究,在目前的信息論(information theory)和編碼理論(coding theory)中,主要有兩種方式用來保證可靠的傳輸
1、通過傳輸的數據包中增加冗余的error-correcting codes來修復傳出錯誤的報文,這種方式中接受端在接收到報文的時候,如果報文中有少量的bit傳輸錯誤,接收端可以通過冗余數據恢復出正確的數據包。
2、使用ARQ(Automatic Repeat Request)機制來提高數據傳輸的可靠性,ARQ機制需要發送端重復發送傳輸錯誤的數據包直到接收端接收到正確的數據包為止
當前也有把這兩種方式結合起來一起使用的協議,比如在LTE通信中,RLC層使用ARQ,MAC層使用HARQ(HARQ就是上面兩種方式的綜合體,先通過error-correcting codes來嘗試修復傳輸錯誤的報文,如果修復失敗則進行ARQ過程)。我們接下來要講到的TCP協議則使用ARQ的方式。
來解決丟包和比特錯誤兩類問題最簡單的方式就是重新發送出錯的數據包,這就需要知道
接收端是否已經接收到對應的數據包。這個可以通過ACK(acknowledgment)來反映接收端接收到數據包的情況。但是這個又帶來其他小問題比如發送端應該等待ACK確認包多長時間?如果超過這個時間發送端就認為數據包丟失而重新發送這個數據包。這個時間就叫做RTO(Retransmission Timeout),RTO應該根據環回時間RTT(round-trip-time)來估計。環回時間應該包括三部分:數據包傳送過的時間,接收端處理這個數據包並產生ACK的時間,ACK確認包返回的時間。但是RTT這個時間是隨着網絡狀況動態變化的,網絡負載較重產生擁塞的時候,RTT就會變大,因此發送端就需要一種方式來動態估計這個RTT時間,這個過程就叫做round-trip-time estimation。這個估計過程是一個統計過程,真實的RTT應該比較接近這個統計平均值。另外一個問題是如果ACK報文丟失怎么辦?如果接收端回復的ACK報文丟失,這又可以分為兩種場景,一是后面的ACK報文在發送端RTO超時前到達發送端,發送端通過這個ACK報文可以得知之前的報文接收端已經收到。另外一種情況就是RTO超時前,沒有收到后續的ACK報文,發送端則可以直接重傳沒有收到ACK的報文,這樣接收端會接收到重復的TCP報文,接收端可以丟棄重復的報文。
接收端接收到的數據包和發送端發送的數據包是否一致。一般來說有兩種方式,一種是CRC,另外一種是checksum,在TCP協議中通過checksum機制檢查比特錯誤。當TCP的checksum校驗失敗的時候,接收端並不會發送ACK給發送端。對於數據完整性要求較高的應用,應該在應用層添加更可靠的校驗方式。
當TCP發送端每次發送一個數據包然后等待ACK的時候(即停等式 stop and wait),這種場景下TCP對網絡帶寬的利用率非常低,因此為了提高帶寬利用率,允許TCP在沒有收到ACK報文的情況下發送其他數據包。當多個數據包同時在網絡中傳輸的時候,問題會變得更加復雜,比如發送端必須緩存還沒有被接收端ACK的報文,當發送端速度低於接收端速度時候發送端需要降低TCP發送速度等等。
TCP主要有兩種重傳方式,上面我們介紹的是基於定時器的重傳(timeout or timer-based retransmission),這種重傳方式是發出去的數據在RTO超時后還沒有收到對應的ACK就會進行超時重傳。另外TCP還有一種基於ACK報文結構順序的重傳,這種重傳叫做快速重傳(fast retransmission或者fast retransmit),當TCP注意到累計ack(即TCP頭中的ack number)不再推進或者接收端通過SACK信息指示發送端接收端存在洞(hole)時候就會觸發發送端的重傳,通常來說快速重傳比超時重傳更高效。另外谷歌還對快速重傳提出了一種改進的重傳機制,即早期重傳(ER,Early Retransmit),還記得之前TFO也是谷歌提出來的吧。在這重傳子系列內容中我們重點關注TCP如果判斷丟包以及重傳對應的數據包。至於發送多少數據包則等到我們后面的擁塞控制時候在來講解。
補充說明:
1、從本章起,wireshark示例中server端的端口為9877,client端的端口為10000。其中client端使用raw socket編程來精確控制TCP的每個報文的,因此不要按照通常的協議要求來看待client的行為,我在/proc下添加了參數tcp_discard_on_port,設置后內核模塊可以丟棄指定端口的tcp數據,當把tcp_discard_on_port設置為9877后,server發過來的TCP報文遞交給raw socket后,內核TCP模塊會直接丟棄這個tcp報文,而不會因為對應的端口沒打開而回復RST消息。server端則是linux內核的原始實現,因為server的行為是與linux的實現一致的。我們在查看wireshark抓包圖示的時候重點觀察server的行為,不要糾結client的行為。