從TCP的“三次握手”和“四次分手”講起

說起TCP中最常見最重要的問題當然就是“三次握手”、“四次分手”了。在此之前,我們先來預熱一下TCP的基本知識。

TCP報文段結構

TCP-Header

Source PortDestination Port:即源端口號和目的端口號,被用于多路復用/多路分解來自或送到上層應用的數據。
Sequence Number(32 bit):是包的序號,用來解決網絡包亂序(reordering)問題。
Acknowledgment Number(32 bit):是確認號,用于確認收到,用來解決不丟包的問題。
Window(16 bit):也稱接收窗口,該字段用于流量控制,指示接收方愿意接收的字節數量。以后會談到。
Offset(4 bit):即首部長度字段,指示了以32比特的字為單位的TCP首部長度,由于TCP選項字段的原因,TCP首部的長度是可變的。
TCP Options:選項字段,用于發送方和接收方協商最大報文段長度(MSS)時,或在高速網絡環境下用作窗口調節因子時使用。
TCP Flags(6 bit):即TCP的標志字段,就是包的類型,主要是用于操控TCP的狀態機。


關于TCP報文頭部的標志位

  • URG 緊急指針
  • ACK 確認序號
  • PUSH 接收方應當盡快將這個報文段交給應用層
  • RST 重置。重建鏈接
  • SYN 同步序號用來發起一個連接
  • FIN 完成發送

正確理解序號與確認號

首先要明白TCP將數據看成是一個無結構、有序的字節流。發送端的TCP會隱式地對數據流中的每一個字節編號。一個報文段的序號指的是該報文段首字節的字節流編號。
舉個例子,假設主機A上的一個進程向通過一條TCP連接向主機B上的一個進程發送一個數據流。假定數據流由一個包含500000字節的文件組成,其MSS(TCP數據包每次能夠傳輸的最大數據分段)為1000字節,數據流的首字節編號是0,該TCP將為該數據流構建500個報文段,給第一個報文段分配序號0,第二個報文段分配序號1000,第三個分配為2000......每一個序號被填入相應的TCP報文段首部的序號字段中。
注意的是,每一個報文段的大小不一定是MSS,而且下一個要發送的報文段大小還取決于接收方的窗口大小,這在之后的流量控制會談到。

文件數據劃分成TCP報文段

由此我們試想一下,當已知一個TCP連接中正發送一個包的序號為1500,我們可以知道,該端已經發送了1500字節的數據。所以,序列號又可以記憶為:當前端已經發送的數據位數,是否確認送達這還不一定。
確認號的增加是和傳輸的字節數相關的,除了數據占用字節數之外,每發送一次有效標志位SYN或FIN也算1位數據。

現在談一下確認號。由于TCP是全雙工通信的,同一條TCP連接的兩端會相互接收和發送數據。當主機B收到來自于主機A的一個報文段,需要給主機A回個信息說明已成功收到,這個信息就是確認號。
主機B填充進報文段的確認號是主機B期望從主機A收到的下一個報文段的序號,或者也可以記憶成:當前端成功接收的數據位數。下面舉兩個例子,一個是正常情況下的數據傳輸,一個是出現差錯的數據傳輸。
情況一,假設主機B收到了來自主機A的編號為0~535的所有字節(此報文段序號為0),同時打算發送一個報文段給主機A,其中的確認號字段填充為536。說明主機B等待主機A的數據流中字節編號為536及之后的所有字節,或者也可以理解為,主機B已經成功接收到了536位數據。
情況二,假設主機A向主機B發送了字節編號為0-535,536-899,900-1000的三個報文段(報文段序號為0,536,900)。由于某種原因(報文段丟失),主機B沒有收到字節編號536-899的報文段。那么問題來了,由于第三個報文段失序到達,該怎么處理呢?如果回復的確認號為1001,會使得主機A認為三個報文段均安全到達,顯然這么做事不合理的。然而對于這種TCP連接中收到的失序報文段,TCPRFC并未明確規定任何規則,故需人為實現。現有兩個選擇:
1、立即丟棄失序報文段;
2、保留報文段,并采取合理的方法等待缺失的報文段補齊。
顯然,后者對網絡帶寬而言更為有效,具體采取什么樣的方法等待補齊,這個問題在后面的TCP重傳機制上會有深刻討論。

為了更好理解序號和確認號,可以用WireShark創建一個TCP流的圖形摘要。下面這個摘要圖是從網上copy來的,不過不影響,我們只是用來解讀的。

TCP流摘要圖

首先,每行代表一個單獨的TCP報文段,左邊列顯示時間,中間列顯示包的方向、TCP端口、段長度和設置的標志位,右邊列以10進制的方式顯示相關序號/確認號,這里的序號/確認號用的是相對序號/確認號。
這里我們做個約定:箭頭左側代表客戶端(端口號54841),箭頭右側代表服務器(端口號80)。
報文段1:客戶端發送一個特殊的報文段給服務器端,該報文段叫SYN報文段(報文段首部僅SYN標志位被置1),不含任何應用層數據。另外,客戶端會隨機選擇一個初始序號,由于是初始序號,其相對序號為0,也就是 Seq=0。
報文段2:包含TCP SYN報文段的IP數據報到達服務器主機,服務器會從該數據報中提取SYN報文段,并為該TCP連接分配TCP緩存和變量,同時向客戶端發送允許連接的報文段,這個報文段稱為SYNACK報文段,也不包含應用層數據,但其首部包含3個重要的信息。
首先SYN標志位被置1,其次,確認號Ack被置為1,最后服務器選擇自己的初始序號,由于是初始序號,其相對序號為0,也就是 Seq=0。
需要注意的是,盡管客戶端沒有發送任何有效數據,確認號還是被加1,這是因為接收的包中包含SYN或FIN標志位(并不會對有效數據的計數產生影響,因為含有SYN或FIN標志位的包并不攜帶有效數據)。
報文段3:客戶端在收到SYNACK報文段之后,客戶端也要給該TCP連接分配緩存和變量。客戶端向服務器發送另外一個報文段,表示對服務器的允許連接進行了確認,由于已經建立了連接,所以SYN置0(此時可以攜帶有效數據了)。序號Seq為1,表示已經發送了1位數據(即報文段1的SYN),確認號Ack為1,表示已經接收到了1位數據(即報文段2的SYN)。
報文段4:這是流中第一個攜帶有效數據的包(比如說是客戶端發送的HTTP請求),序號依然為1,因為到上個報文段為止,還沒有發送任何數據,確認號也保持1不變,因為客戶端沒有從服務端接收到任何數據。這里有效數據的長度為725字節。
報文段5:服務器端收到了來自客戶端發送的請求數據(共725字節)后,發送報文段確認收到,確認號的值增加了725(725是包4中有效數據長度),變為726,簡單來說,服務端以此來告知客戶端端,目前為止,我總共收到了726字節的數據,服務端的序列號保持為1不變(因為到報文段5為止,服務器還未發送過數據)。
報文段6:這個報文段標志著服務端對客戶端請求響應的開始,序列號依然為1,因為服務端在該報文段之前返回的報文段中都不帶有有效數據,該報文段帶有1448字節的有效數據。
報文段7:此報文段用于對報文段6中1448字節數據的確認。由于上個數據報文段(報文段4)的發送,TCP客戶端的序列號增長至726,從服務端接收了1448字節的數據,客戶端的確認號由1增長至1449。

接下來就是重復類似的過程。

小Tip
從上面的圖可以看到,除了第一次握手的報文段SYN之外,其他所有報文段都必須有Ack。為什么呢?
TCP作為一個可靠的傳輸協議,其可靠性是依賴于收到對方的消息并回復確認給對方,所以任何方發送的TCP報文段都要捎帶著ACK狀態位。ACK狀態位要有Acknowledge Number配合才行。

下面再來補充一下有關初始化序號的知識點(縮寫為ISN:Inital Sequence Number)

關于ISN

為什么ISN要動態隨機?
事實上,一條TCP連接的雙方均可以隨機地選擇初始序號,這樣可以減少將那些仍在網絡中存在的來自兩臺主機之間先前已經終止的連接報文段,誤認為是后來這兩臺主機之間新連接所產生的有效報文段的可能性(碰巧與舊連接使用了相同端口號)。
這句話聽起來有點繞口,舉個例子,比如:如果連接建好后始終用1來做ISN,如果client發了30個segment過去,但是網絡斷了,于是 client重連,又用了1做ISN,但是之前連接的那些包到了,于是就被當成了新連接的包,此時,client的Sequence Number 可能是3,而Server端認為client端的這個號是30了。全亂了。
再有,ISN動態隨機使得每個tcp session的字節序列號沒有重疊,如果出現tcp五元組沖突這種績效概率情況的發生,一個session的數據也不會被誤認為是另一個session的。

ISN會和一個假的時鐘綁在一起,這個時鐘會在每4微秒對ISN做加一操作,直到超過2^32,又從0開始。這樣,一個ISN的周期大約是4.55個小時。因為,我們假設我們的TCP Segment在網絡上的存活時間不會超過Maximum Segment Lifetime(縮寫為MSL),所以,只要MSL的值小于4.55小時,那么,我們就不會重用到ISN。


TCP的連接管理

所謂TCP的連接管理也就是“三次握手”、“四次分手”的過程。在這個部分中我們會仔細的分析如何建立和拆除一條TCP連接。


tcp_open_close

建立連接

其實在前面的序號和確認號分析中我們已經走了一遍建立連接的三次握手流程,這里做個總結:

  • 第一步:客戶端發送一個特殊的報文段給服務器端,該報文段叫SYN報文段(報文段首部僅SYN標志位被置1),不含任何應用層數據。另外,客戶端會隨機選擇一個初始序號x,并將此編號放置于該起始的TCP SYN報文段的序號段中,然后被封裝在一個IP數據報中,并發送給服務器。
  • 第二步:包含TCP SYN報文段的IP數據報到達服務器主機,服務器會從該數據報中提取SYN報文段,并為該TCP連接分配TCP緩存和變量,同時向客戶端發送允許連接的報文段,這個報文段稱為SYNACK報文段,也不包含應用層數據,但其首部包含3個重要的信息。首先SYN標志位被置1,其次,確認號Ack被置為x+1,最后服務器選擇自己的初始序號y,并將其放置到TCP報文段首部的序號字段中。
    這個允許連接的報文段實際上表明了:“我收到了你發起建立連接的SYN分組,該分組帶有初始序號x。我同意建立該連接,我自己的序號是y”。
  • 第三步:客戶端在收到SYNACK報文段之后,客戶端也要給該TCP連接分配緩存和變量。客戶端向服務器發送另外一個報文段,表示對服務器的允許連接進行了確認,由于已經建立了連接,所以SYN置0(此時可以攜帶有效數據了)。三次握手的第三個階段可以在報文段負載中攜帶客戶到服務器的數據。

關于TCP3次握手的幾個討論
1、為什么需要初始化序號?
首先要明白,對于建鏈接的3次握手,實際上在握什么?握的是數據原點的序號。通信的雙方要互相通知對方自己的初始化的序號(上面的x和y)。這個號要作為以后的數據通信的序號,以保證應用層接收到的數據不會因為網絡上的傳輸的問題而亂序(TCP會用這個序號來拼接數據)。

2、三次握手的第一次為什么不可以攜帶數據?
因為握手還沒成功。
那難道不可以將數據緩存下來等握手成功再提交嗎?
不行,這樣容易受到SYN FLOOD攻擊。如果第一次可攜帶數據,攻擊者會偽造成千上萬的握手報文,每個報文攜帶大量數據,那么接收端要開辟大量內存來緩存這些巨大的數據,內存容易耗盡,從而拒絕服務。

3、第三次為什么可以攜帶數據?
能夠發出第三次握手報文段的主機肯定接收到了第二次的握手報文段,因為偽造IP的主機是不會收到第二次報文的(為啥呢?)。所以能夠發出第三次握手報文的主機應該是合法用戶。
再次,經過三次握手,通信雙方已確認初始序列,也為對方開辟了臨時內存與變量。客戶端發出第三次報文段的瞬間進入“ESTABLISHED”狀態,單方面宣告連接建立,可發送數據,盡管服務器側狀態還沒“建立”,但接收到第三次握手的瞬間就會切換到“ESTABLISHED”,里面攜帶的數據就會按正常流程走好。
4、為什么是三次握手而不是兩次或者四次?
試想一下兩次握手的過程:
Client:SYN+Client ISN
Server:SYN+Server ISN+Server Ack
這里有一個問題,Client與Server就Client的初始序列號達成了一致,但是Server無法知道Client是否收到自己的初始序號和確認號,如果丟失,Client與Server就Server的初始序列號無法達成一致。

于是TCP設計者將標志位SYN(FIN)設計成占用一個字節的編號,既然是一個字節的數據,按照TCP對有數據的TCP報文段必須確認的原則,這里Client必須給Server一個確認,以確認Client已經收到Server的同步信號SYN。

這里還有一個問題,就是如果Client發給Server的確認中途丟失了怎么辦,Client會重傳這個ACK嗎?不會的,TCP不會為不帶數據的ACK執行重傳!解決的辦法是,Server會重傳自己的SYN同步信號,直至收到Client的ACK信號。

再想想四次握手的過程:
Client:SYN+Client ISN
Server:SYN+Server ISN+Server ACK
Client:SYN+Client ISN+Client ACK
Server:SYN+Server ISN+Server ACK
......
然后沒完沒了了,其實四次和五次六次七次八次是一個道理。
只有三次剛剛好保證數據的可靠傳輸和傳輸的效率。
下面是一個繪聲繪色的段子供大家理解(來自某乎):


某乎截圖

斷開連接

對建立連接有了理解之后,斷開連接的道理也就很好懂了。舉個例子,假設某客戶端打算關閉連接:

  • 第一步:客戶應用進程發出一個關閉連接命令,這會引起客戶TCP想服務器進程發送一個特殊的TCP報文段,稱為FIN報文段(首部只有一個標志位FIN置1)。
  • 第二步:服務器收到FIN報文段后就向客戶端會送一個確認報文段;
  • 第三步:服務器發送自己的終止報文段,FIN置為1;
  • 第四步:最后,客戶端對這個服務器的終止報文段進行確認,此時,兩臺主機上用于該連接的所有資源都被釋放了。

我們再來分析一下這四次的過程:
Client:FIN , Server:ACK
Client屬于主動關閉方,當Client收到來自Server的ACK之后,進入半關閉狀態,這時候,Client不能再發送數據了。這時Server還可以單向發送數據,Server數據發送完了,也做關閉動作。

Server:FIN , Client:ACK
Client收到Server發送的FIN后,馬上回復確認關閉ACK,等待2MSL時間后,連接正式關閉,客戶端所有資源(包括端口號)將被釋放。(為什么需要等待2MSL?下面接著分析)

有個小問題:為什么第二次和第三次分手不合在一起呢?也就是Server的ACK和FIN不一起發送?
其實,當客戶主動請求關閉連接,說明客戶端已經不再請求數據了,得到服務器端的確認后,服務器并沒有馬上發起自己的關閉,是因為服務器端還有一些剩余的數據未發完,直到數據發送完畢后再發起關閉動作。


客戶端經歷的TCP狀態序列

其實,網絡上的傳輸是沒有連接的,包括TCP也是一樣的。而TCP所謂的“連接”,其實只不過是在通訊的雙方維護一個“連接狀態”,讓它看上去好像有連接一樣。所以,TCP的狀態變換是非常重要的。
在一個TCP連接的生命周期內,運行在每臺主機中的TCP協議在各種TCP狀態之間變遷。TCP的狀態是由TCP報文段的標志位控制的。下圖說明了客戶TCP會經歷的一系列典型TCP狀態。


客戶TCP經歷的典型的TCP狀態

現在我們來走一下這個狀態流程:

  • CLOSED-->ESTABLISHED
    客戶TCP開始處于CLOSED(關閉狀態),客戶端的應用進程發起一個新的TCP連接,這引起客戶中的TCP向服務器中的TCP發送一個SYN報文段,在發送SYN報文段過后,客戶TCP進入SYN_SENT狀態。此時客戶TCP等待來自服務器TCP的對客戶所發送報文段進行確認且SYN被置1的一個報文段。收到這個報文段之后,客戶TCP單方面進入ESTABLISHED(已建立)狀態。當處在ESTABLISHED狀態時,TCP客戶就能發送和接收包含有效載荷數據(即應用層產生的數據)的TCP報文段了。
  • ESTABLISHED-->CLOSED
    假設客戶應用進程決定關閉該連接(注意到服務器也能選擇關閉該連接)。這引起客戶TCP發送一個FIN特殊報文段,并進入FIN_WAIT_1狀態,等待一個來自服務器帶有確認的TCP報文段。當收到該報文段時,客戶TCP進入FIN_WAIT_2狀態,此時客戶TCP處于半關閉狀態,不能再發送數據了。處于FIN_WAIT_2狀態的客戶TCP等待來自服務器的FIN特殊報文段,當收到該報文段后,客戶TCP對服務器進行確認,并進入TIME_WAIT狀態。經過2MSL等待之后,連接正式關閉,客戶端釋放所有資源。


    關閉一條TCP連接

現在來解釋一下為什么主動關閉方最后為什么要等待2MSL之后才關閉連接?
主要有兩點:
(1)可靠的實現TCP全雙工連接的終止;
(2)語序來到重復的分節在網絡中消逝;
先說第一點,當客戶端發送ACK確認報文段給服務器之后,客戶端無法得知服務器是否收到ACK,所以我們定一個規則:在客戶端發送ACK確認報文段之后的2MSL的期間內未收到服務器發送的FIN報文段,則認為服務器已成功接收到了該ACK確認報文段,所以TCP連接可完全關閉。
為什么是2MSL呢?這只是一個保守的估計時間。所謂MSL是Maximum Segment Life,這是TCP 對TCP Segment 生存時間的限制,任何報文段在丟棄之前在網絡中內的最大生存時間。假如ACK沒有到達服務端,服務端會為FIN這個消息超時重傳 timeout retransmit ,那如果客戶端等待時間足夠,又收到FIN消息,說明ACK沒有到達服務端,于是再發送ACK,直到在足夠的時間內沒有收到FIN,說明ACK成功到達。這個等待時間至少是:服務端的timeout + FIN的傳輸時間,為了保證可靠,采用更加保守的等待時間2MSL。

再說第二點,如果Client直接CLOSED,然后又再向Server發起一個新連接,我們不能保證這個新連接與剛關閉的連接的端口號是不同的。也就是說有可能新連接和老連接的端口號是相同的。一般來說不會發生什么問題,但是還是有特殊情況出現:假設新連接和已經關閉的老連接端口號是一樣的,如果前一次連接的某些數據仍然滯留在網絡中,這些延遲數據在建立新連接之后才到達Server,由于新連接和老連接的端口號是一樣的,又因為TCP協議判斷不同連接的依據是socket pair,于是,TCP協議就認為那個延遲的數據是屬于新連接的,這樣就和真正的新連接的數據包發生混淆了。所以TCP連接還要在TIME_WAIT狀態等待2倍MSL,這樣可以保證本次連接的所有數據都從網絡中消失。

服務器經歷的TCP狀態序列

服務器TCP經歷的典型TCP狀態

服務器的TCP狀態比較好解釋,大家可以對著圖自行理解。


至此,關于TCP的一點點點點知識算是講完了,不過后續的還有流量控制,擁塞控制等等,敬請關注!
推薦公眾號:車小胖談網絡

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容