通過上一篇的學習,我們了解到傳輸層中的UDP協定是一種無連接配接的協定,不在乎資料有沒有被對方收到,是以很高效快捷,但是不可靠,可靠性實際上是更加重要的,這也是TCP應用更加廣泛的原因,相應的代價就是TCP的設計要比UDP複雜得很多,唉,魚與熊掌不可兼得嗎?
不過反過來想,沒有技術壁壘的東西,大家一學就會,就不會有差距,那麼對TCP的了解或許就是計算機網絡世界中定位青銅還是王者的重要差別。還是有理由好好學習它。我們暫時先不要深究,先從經典的三次握手來個入門吧!前方高能,坐穩扶牢,開始發車!

一、交流之前先建立通信
在 TCP 的世界裡,對于發送的資料進行确認是它的最大特色,這樣才能保證你發的我真的收到了,其實這種确認從第一步連接配接就開始了!
我們之前用寫信來比喻UDP,用打電話來比喻TCP,假設 fossi 給好兄弟 stephen 打電話:
- fossi 進行撥号,嘟嘟嘟,等待 stephen 接聽,接聽成功,先喊一聲:hello
- stephen 接起電話:hello,請問哪位?
- fossi 說:hello,我是你爸爸 fossi 啊!
- stephen 說:奧,原來是兒子 fossi 啊,啥事啊?
- fossi 巴拉巴拉...
也就是說,在正式巴拉巴拉之前,有一個建立通信的過程,不然對方有沒有在聽你說話都不能确定呢!這個過程對于TCP也是一樣的道理。我們在發送正式資料之前,我們要先建立通信,就是發“hello”的過程。TCP的過程:
- 你想和我聊天嗎?
- 是的,我已經準備好了!
- 好的收到,讓我們開始聊天吧。
是以需要某些資訊來表明此資料是一個連接配接請求(對應hello)?響應/确認?(對應好的/收到),這個就是由 TCP 頭上的辨別位來做區分的。
二、三次握手
下圖展示的是 TCP 首部:
字段比較多,有顔色的是本文最需要關注的:序列号、确認号、ACK、RST、SYN、FIN。最後四個就是上面說的辨別位,這四位很重要,辨別了 TCP 請求的類型,先來看下他們分别的作用:
- ACK:該位為 1 時,「确認應答」的字段變為有效,TCP 規定除了最初建立連接配接時的 SYN 包之外該位必須設定為 1 。
- RST:該位為 1 時,表示 TCP 連接配接中出現異常必須強制斷開連接配接。
- SYC:該位為 1 時,表示希望建立連接配接,并在其「序列号」的字段進行序列号初始值的設定。
- FIN:該位為 1 時,表示今後不會再有資料發送,希望斷開連接配接。當通信結束希望斷開連接配接時,通信雙方的主機之間就可以互相交換 FIN 位置為 1 的 TCP 段。
三次握手的一般過程如圖:
可以看到,過程分為三步,用打電話的場景來了解就是:
- fossi:hello,我想找你通話,你願意不?為了確定你本次通話有效,我現在說下我的暗号是1,你小子收到請回複我2
- stephen:我收到了,我願意!我給你回複2,另外我也有暗号,我的暗号是99,你小子願意跟我聊天的話請回複100給我
- fossi:收到收到,我給你100!好兄弟,下面我們開始說點秘密事!
(這裡的暗号比喻不是很好,這裡說的暗号其實是 ISN ,下面會細說,這個字段很重要,含義比我說的暗号要深多了,作用也大多了,是以舉例跟實際闡述還是有差别的,例子隻是引子)下面來好好看下 TCP 中雙方是如何三次握手的:
①第一步,用戶端發 SYN 請求連接配接:
發送的第一個封包是一個同步請求,就是我們打電話第一句“hello”。對應的标志位就是 SYN 。那麼也就是說,如果我要給使用 TCP 的應用程式發送資料,第一步就是發送 SYN=1 的封包,表明我想與這個應用程式進行通信,相當于上述的“我想找你通話,你願意不?”這一步。
注意,這裡還涉及到 ISN ,即初始序列号。
在建立連接配接時由計算機生成的随機數作為其初始值,通過 SYN 包傳給接收端主機,每發送一次資料,就「累加」一次該「資料位元組數」的大小。用來解決網絡包亂序問題。
②第二步,服務端發 ACK 和 SYN :
服務端接收到 SYN 連接配接請求後,通常是會回複同意與用戶端應用程式進行通信,因為如果不同意就沒有後續了。同意的這個場景下,服務端會回複 ACK 标志位作為響應。這裡涉及到确認号:
指下一次「期望」收到的資料的序列号,發送端收到這個确認應答以後可以認為在這個序号以前的資料都已經被正常接收。用來解決不丢包的問題。
此外要注意,伺服器應用程式還會詢問用戶端是否要與自己通信,是以除了 ACK 标志位以外,也要設定 SYN 标志位。是以,将在其響應中設定 SYN + ACK 标志位(就是 SYN 和 ACK 這兩個标志位都設定為 1)。
這裡引申出一個重要問題:如果用戶端要求與伺服器通信,伺服器為什麼還要問它是否要與自己通信呢?這不是多此一舉嗎?
事實上,當你想使用 TCP 進行通信時,你不是建立一個連接配接,而是建立兩個連接配接!
TCP 認為在一個方向上有一個通信,在另一個方向也有一個通信。是以,它為每個通信方向建立一個連接配接。是以說 TCP 是全雙工的。
前兩步都是不帶任何應用層資料的,也不允許帶資料。下面來到最後一步:
③第三步,服務端也要知道用戶端願不願意跟他通信,是以用戶端将發送帶有 ACK 标志位的資料包。
不難想象:前面兩次握手時不能攜帶資料的,不過第三次握手時可以攜帶資料的。
三次握手簡化圖就是:
用 wireshark 實際抓了一個包看了下三次握手,觀察是否符合上述三個過程:
可以看到,還有其他的重要資訊,暫且先不管。我們要知道初始序列号是随機生成的,并不是從0開始的,是以不要被上圖所示的值誤導。ISN 不能被設定為固定值,處于安全性和避免前後連接配接互相幹擾的考慮。
三、三次握手的狀态變化
整個過程為:
- 第一步:用戶端發送連接配接請求,進入 SYN-SENT 狀态,等到服務端 ACK。
- 第二步:服務端一開始處于監聽狀态,收到用戶端連接配接請求後,回複 ACK + SYN 後進入 SYN-RCVD 等待用戶端 ACK。
- 第三步:用戶端在 SYN-SENT 狀态下收到服務端的 ACK 後,進入 ESTABLISHED 狀态,并回複 ACK 給服務端。服務端收到用戶端确認封包後,也進入 ESTABLISHED 狀态。
- 雙方互發資料。
如果用戶端發了 SYN 資料包,遲遲沒有收到服務端的 ACK ,則用戶端會進行定時重發多次 SYN 包。多次重試後還是無效,則放棄重試,如果在JAVA語言中會傳回
java.net.ConnectException: Connection timed out
異常。
若一切正常,雙方都處于 ESTABLISHED 狀态,此連接配接就已建立完成,用戶端和服務端就可以互相發送資料了。在
Linux
系統下可以通過
netstat -napt
指令檢視 TCP 連接配接狀态:
四、RST封包
RST 是 reset 的縮寫,表示 “重置”,是以 RST 标志位用于重置連接配接。在 TCP 中發送的每個資料都必須被确認。如果發送的資料和接收的資料之間存在不一緻,則該連接配接就被認為是異常的。
如果機器 A 和機器 B 建立了 TCP 連接配接,并且在進行了一些交換之後,機器 A 意識到連接配接中存在資訊的不一緻,它将發送一個包含 RST 标志位的資料包,請求機器 B 同意關閉此連接配接。這一次,連接配接的終止不是用 “四次揮手” 了。
這種異常情況,下面用打電話舉個例子模拟三次握手中的一個會導緻RST的異常情況:
- fossi:我的初始暗号是1,你小子确認的話告訴我2就行了
- stephen:我的初始暗号是99,你小子也給我确認下,到時候告訴我100就好了。對了,我給你确認值是25
- fossi:25?尼瑪。。。看來這小子跟我不是一個頻道,直接RST挂斷電話吧。。。
同樣地,如果我将 SYN 資料包(用于請求建立連接配接)發送到已關閉的機器的端口,該機器會回複 RST 資料包,以告訴我所請求的端口未在監聽。
五、資料互發階段
在
hello
完後,
fossi
和
stephen
開始巴拉巴拉了,正常情況下是這樣通話的:
- fossi:第一件事你聽清楚了嗎?
- stephen:嗯,清楚了,你繼續說
- fossi:第二件事你聽清楚了吧?
- stephen:嗯,了解了,還有沒有其他事了?有你就你繼續說
- 巴拉巴拉...
如果遇到下面這個情況可就麻煩了:
- fossi:第一件事你聽清楚了嗎?
- stephen:...
- fossi:???聽清楚了嗎?
- stephen:...
- fossi:尼瑪...
回到主題,經過 “三次握手” 的過程,雙向通信已經建立起來了,應用程式之間可以互傳資料包了。在互傳資料的過程中,發送的所有資料包上都會設定 ACK 标志,以确認收到了先前的資料包:
當然了,不是每個資料包都要回複 ACK 的,比如用戶端發了1,2,3,4,5五個資料包,如果服務端傳回的 ACK 是5,說明前面四個資料包也都已成功接收到!
五、經典問題:為什麼是三次握手?而不是兩次或四次?
①首要原因:為了防止舊的重複連接配接初始化造成混亂。
在複雜的網絡環境下,資料包不一定如願準時送到,可能會發出去很久還沒收到回複,此時用戶端會重試,萬一老的封包在曆經千辛萬苦之後,突然又送到對方那裡了呢?三次握手可以解決這個問題,如下圖:
那麼防止這個問題的關鍵就是用戶端判斷服務端傳回的 ACK 值是不是符合預期值,不是(可能是過期)則發 RST 封包中止本次連接配接。
如果隻有兩次握手的話,就不能判斷目前連接配接是否是曆史連接配接。
②原因2:同步雙方初始序列号
上面我們說了序列号,這個東西太重要了,有了它,我們才能達到以下目的:
- 接收方可以去除重複的資料;
- 接收方可以根據資料包的序列号按序接收;
- 可以辨別發送出去的資料包中, 哪些是已經被對方收到的;
可見,序列号在 TCP 連接配接中占據着非常重要的作用,是以當用戶端發送攜帶「初始序列号」的 SYN 封包的時候,需要服務端回一個 ACK 應答封包,表示用戶端的 SVN 封包已被服務端成功接收,那當服務端發送「初始序列号」給用戶端的時候,依然也要得到用戶端的應答回應,這樣一來一回,才能確定雙方的初始序列号能被可靠的同步。
③原因3:避免資源浪費
其實這個就是原因①,如果隻有兩次握手,當用戶端的 SYN 請求連接配接在網絡中阻塞,用戶端沒有接收到 ACK 封包,就會重新發送 SYN ,服務端就會建立兩個連接配接并保持,這裡就會有無效連接配接保持着,浪費了資源。
至于為什麼不是4次,原因是服務端傳回的 SYN 和 ACK 是可以合并的,不需要四次。
是以,不使用「兩次握手」和「四次握手」的原因:
- 「兩次握手」:無法防止曆史連接配接的建立,會造成雙方資源的浪費,也無法可靠的同步雙方序列号;
- 「四次握手」:三次握手就已經理論上最少可靠連接配接建立,是以不需要使用更多的通信次數。
六、總結
本文主要探讨了三次握手的細節,以下是本文重點:
- 了解 TCP 頭中的這些字段的含義:序列号、确認号、ACK、RST、SYN、FIN
- 三次握手的過程 TCP 首部字段的變化
- 三次握手的狀态變化
- 資料互發階段的确認機制初步了解
- 為什麼是三次握手?而不是兩次或四次?