引言
TCP三次握手和四次揮手不管是在開發還是面試中都是一個非常重要的知識點,它是我們優化web程式效能的基礎。但是大部分教材都對這部分解釋的比較抽象,本文我們就利用wireshark來抓包以真正體會整個流程的細節。
三次握手
根據下面這幅圖我們來看一下TCP三次握手。p.s: 每個箭頭代表一次握手。
第一次握手
client傳送一個SYN(J)包給server,然後等待server的ACK回覆,進入SYN-SENT
狀態。p.s: SYN為synchronize的縮寫,ACK為acknowledgment的縮寫。
第二次握手
server接收到SYN(seq=J)包後就返回一個ACK(J+1)包以及一個自己的**SYN(K)**包,然後等待client的ACK回覆,server進入SYN-RECIVED
狀態。
第三次握手
client接收到server發回的ACK(J+1)包後,進入ESTABLISHED
狀態。然後根據server發來的SYN(K)包,返回給等待中的server一個ACK(K+1)包。等待中的server收到ACK回覆,也把自己的狀態設定為ESTABLISHED
。到此TCP三次握手完成,client與server可以正常進行通訊了。
為什麼要進行三次握手
我們來看一下為什麼需要進行三次握手,兩次握手難道不行麼?這裡我們用一個生活中的具體例子來解釋就很好理解了。我們可以將三次握手中的客戶端和伺服器之間的握手過程比喻成A和B通訊的過程:
- 在第一次通訊過程中,A向B傳送資訊之後,B收到資訊後可以確認自己的收信能力和A的發信能力沒有問題。
- 在第二次通訊中,B向A傳送資訊之後,A可以確認自己的發信能力和B的收信能力沒有問題,但是B不知道自己的發信能力到底如何,所以就需要第三次通訊。
- 在第三次通訊中,A向B傳送資訊之後,B就可以確認自己的發信能力沒有問題。
wireshark
上面分析還不夠形象,很容易忘記,下面我們利用wireshark來證明一下上面的分析過程。從下面的的輸出就可以很容易看出來,必須要經過前面的三次tcp請求才會有起一次http請求。
第一次請求客戶端傳送一個SYN包,序列號是0。
第二次請求伺服器會傳送一個SYN和一個ACK包,序列號是0,ack號是1。
第三次本地客戶端請求會傳送一個ACK包,序列號是1,ack號是1來回復伺服器。
四次揮手
以下面這張圖為例,我們來分析一下TCP四次揮手的過程。
第一次揮手
client傳送一個FIN(M)包,此時client進入FIN-WAIT-1
狀態,這表明client已經沒有資料要傳送了。
第二次揮手
server收到了client發來的FIN(M)包後,向client發回一個ACK(M+1)包,此時server進入CLOSE-WAIT
狀態,client進入FIN-WAIT-2
狀態。
第三次揮手
server向client傳送FIN(N)包,請求關閉連線,同時server進入LAST-ACK
狀態。
第四次揮手
client收到server傳送的FIN(N)包,進入TIME-WAIT
狀態。向server傳送**ACK(N+1)**包,server收到client的ACK(N+1)包以後,進入CLOSE
狀態;client等待一段時間還沒有得到回覆後判斷server已正式關閉,進入CLOSE
狀態。
TCP滑動視窗
一般在提到TCP三次握手的時候,同樣會涉及到TCP滑窗,下面我們補充一下什麼是TCP滑窗。如果採用PAR的形式來傳遞的話,每一次傳送方傳送完包後必須得到接收方的確認回覆,改進一下這個流程,傳送端一次可以傳送多個包,不必每次等到接收方的ack回覆,同時接收端也要告訴傳送端自己能接收多少。當然還需要保證順序性,對於亂序的狀況,我們可以允許等待一定時間的亂序,比如先快取提前到達的資料,然後等待需要的資料,如果一定時間沒有達到就drop掉。
TCP滑動視窗可以解決我們上面提到的概念,滑動視窗中的資料主要分為下面幾類:
- Sent and Acknowledged: 這些資料表示已經傳送成功並已被確認的資料。
- Sent But Not Yet Acknowledged: 這部分資料稱為傳送但還沒有被確認,資料被髮送出去,沒有收到接收端的ACK,認為並沒有完全傳送,這個屬於視窗內的資料。
- Not Sent, Recipient Ready to Receive: 這部分是儘快傳送的資料,這部分資料已經被載入到快取中,也就是視窗中了,等待傳送,接受端已經告訴傳送端自己能夠完全接受這些包,所以傳送方需要儘快傳送這些包。
- Not Sent, Recipient Not Ready to Reccive: 這些資料屬於未傳送,因為這些資料已經超出了接收端所能接受的範圍。
對於接收端也是有一個接收視窗,類似傳送端,接收端的資料有三個分類(注意接收端並不要等待ACK):
- Received and ACK Not Send to Process: 這部分資料屬於接收了資料但是還沒有被上層的應用程式接收,也是被快取在視窗中。
- Received Not ACK: 已經接收,但是還沒有ACK。
- Not Received: 有空位但是還沒有接收資料。
TCP重傳機制
下面這部分內容參考自coolshell,我認為寫的比較好,所以轉過來分享一下,感興趣的朋友可以閱讀一下 :)
TCP要保證所有的資料包都可以到達,所以,必需要有重傳機制。
注意,接收端給傳送端的Ack確認只會確認最後一個連續的包,比如,傳送端發了1,2,3,4,5一共五份資料,接收端收到了1,2,於是回ack 3,然後收到了4(注意此時3沒收到),此時的TCP會怎麼辦?我們要知道,因為正如前面所說的,SeqNum和Ack是以位元組數為單位,所以ack的時候,不能跳著確認,只能確認最大的連續收到的包,不然,傳送端就以為之前的都收到了。
超時重傳機制
一種是不回ack,死等3,當傳送方發現收不到3的ack超時後,會重傳3。一旦接收方收到3後,會ack 回 4——意味著3和4都收到了。
但是,這種方式會有比較嚴重的問題,那就是因為要死等3,所以會導致4和5即便已經收到了,而傳送方也完全不知道發生了什麼事,因為沒有收到Ack,所以,傳送方可能會悲觀地認為也丟了,所以有可能也會導致4和5的重傳。
對此有兩種選擇:
- 一種是僅重傳timeout的包。也就是第3份資料。
- 另一種是重傳timeout後所有的資料,也就是第3,4,5這三份資料。
這兩種方式有好也有不好。第一種會節省頻寬,但是慢,第二種會快一點,但是會浪費頻寬,也可能會有無用功。但總體來說都不好。因為都在等timeout,timeout可能會很長。
快速重傳機制
於是,TCP引入了一種叫Fast Retransmit 的演算法,不以時間驅動,而以資料驅動重傳。也就是說,如果,包沒有連續到達,就ack最後那個可能被丟了的包,如果傳送方連續收到3次相同的ack,就重傳。Fast Retransmit的好處是不用等timeout了再重傳。
比如:如果傳送方發出了1,2,3,4,5份資料,第一份先到送了,於是就ack回2,結果2因為某些原因沒收到,3到達了,於是還是ack回2,後面的4和5都到了,但是還是ack回2,因為2還是沒有收到,於是傳送端收到了三個ack=2的確認,知道了2還沒有到,於是就馬上重轉2。然後,接收端收到了2,此時因為3,4,5都收到了,於是ack回6。
Fast Retransmit只解決了一個問題,就是timeout的問題,它依然面臨一個艱難的選擇,就是,是重傳之前的一個還是重傳所有的問題。對於上面的示例來說,是重傳#2呢還是重傳#2,#3,#4,#5呢?因為傳送端並不清楚這連續的3個ack(2)是誰傳回來的?也許傳送端發了20份資料,是#6,#10,#20傳來的呢。這樣,傳送端很有可能要重傳從2到20的這堆資料(這就是某些TCP的實際的實現)。可見,這是一把雙刃劍。