一、概覽
上一篇文章《對於Ping的過程,你真的瞭解嗎?》我們通過抓包工具來分析了一次 Ping 的過程,我們知道了 ping 是依託於 ICMP 協議,然後再區域網中還會涉及到 ARP 請求,今天這篇文章我們同樣用抓包分析工具來分析我們熟悉的 HTTP 請求是怎麼樣的?
二、環境準備
本來我是想找個網站進行抓包分析的,但是正式環境的網站 HTTP 請求太多,干擾太多,對分析不太友好,所以我簡單些了一個demo,對 HTTP 請求返回字串。
環境:
1.響應http請求的服務demo2.客戶端ip:192.168.2.1353.服務端:45.76.105.924.抓包工具:Wireshark複製程式碼
我把demo部署到伺服器,啟動成功訪問如下:
開啟抓包工具 Wireshark 進行抓包,抓包結果如下:
三、抓包分析
A) 三次握手
1.最開始是本地傳送了2次請求到伺服器,這裡為什麼會有兩次請求,稍後再說,我們先主要看 HTTP 對應的埠請求,如下:
192.168.2.135:60738---->
45.76.105.92:8081複製程式碼
看上面的截圖我們知道這是 TCP 協議的第一次握手,熟悉 TCP 協議的同學肯定知道 TCP 建立連線有三次握手,斷開連線有四次揮手。(對 TCP 協議不太瞭解的同學可以檢視這篇文章《跟著動畫來學習 TCP 三次握手和四次揮手》)
我們先看第一次請求:
60738 ->
8081 [SYN] Seq=0 Win=64240 Len=0 Mss=1460 Ws=256 SACK_PERM=1複製程式碼
我們來解析下這段包請求資訊:
- 60783 ->
8081 埠號:源埠—>
目標埠 - [SYN] :同步握手訊號
- Seq : 訊息編號
- Win: TCP 視窗大小
- Len: 訊息長度
- Mss: 最大報文段長度
- Ws: 視窗縮放調整因子
- SACK_PERM : SACK選項,這裡等於1表示開啟 SACK。
對於上面的概念,這裡簡單解釋下,再介紹之前我們先看 TCP Header 的資料結構圖,對 TCP 頭部資料結構有個直觀的瞭解
1. Win: TCP 視窗大小,是指TCP傳輸能接受的最大位元組數,這個可以進行動態調節,也就是TCP的滑動視窗,通過動態調整視窗大小,來控制傳送資料的速率。上圖中佔用2個位元組,也就是16位,那麼可以支援的最大數就是2^16=65536,所以預設情況下TCP頭部標記能支援的最大視窗數是65536位元組,也就是64KB。
2. Len: 訊息長度 就是指資料包文段,因為整個TCP報文=Header+packSize,所以這個訊息長度就是指要傳送的資料包總共長度,在本次分析中也就是HTTP報文的大小。
3. Mss: 最大報文段長度:這個就是規定最大的能傳輸報文的長度,為了達到最佳的傳輸效能,TCP 協議在建立連線的時候通常要協商雙方的 MSS 值,這個值 TCP 協議在實現的時候往往用 MTU 值代替(需要減去IP資料包包頭的大小20Bytes和TCP資料段的包頭20Bytes)所以一般 MSS 值1460,這也和我們抓包圖中的值一致。
4. Ws: 視窗縮放調整因子:在前面說 TCP 視窗大小中我們說到,預設情況下,TCP 視窗大小最大隻能支援64KB的緩衝資料,在今天這個高速上網時代,這個大小肯定不滿足條件了,所以,為了能夠支援更多的緩衝資料 RFC 1323 中就規定了 TCP 的擴充套件選項,其中視窗縮放調整因子就是其中之一,這個是如何起作用的呢?首先說明,這個引數是在 [SYN] 同步階段進行協商的,我們結合上面抓包資料分析下。我們看到第一次請求協商的結果是WS=256,然後再 ACK 階段擴充套件因子生效,調整了視窗大小。生效的抓包如下:
60738 ->
8081 [ACK] Seq=1 ACK=1 Win=66560 Len=0複製程式碼
我們發現這個視窗變成了66560,比預設的視窗要大,我們檢視報文詳情:
我們發現,實際請求宣告的視窗是260,WS擴充套件因子是256,最終計算的視窗大小是66560,所以我們知道了,這個擴充套件因子的作用就是,用原視窗大小乘以擴充套件因子,得到最終的視窗大小,也就是260*256=66560.
5. SACK_PERM:SACK選項 ,我們知道 TCP 傳輸有包的確認機制,預設情況下,接受端接受到一個包後,傳送 ACK 確認,但是,預設只支援順序的確認,也就是說,傳送 A,B,C 個包,如果我收到了A,C的包,B沒有收到,那麼對於C,這個包我是不會確認的,需要等B這個包收到後再確認,那麼TCP有超時重傳機制,如果一個包很久沒有確認,就會當它丟失了,進行重傳,這樣會造成很多多餘的包重傳,浪費傳輸空間。為了解決這個問題,SACK就提出了選擇性確認機制,啟用 SACK 後,接受端會確認所有收到的包,這樣傳送端就只用重傳真正丟失的包了。
簡單介紹了上面的基礎概念後,我們來根據抓包梳理下 HTTP 請求的過程,根據 HTTP 請求本地埠是 60378,我梳理的流程如下:
------------------------請求連線--------------------------1) 60738 ->
8081 [SYN] Seq=0 Win=64240 Len=0 Mss=1460 Ws=256 SACK_PERM=12) 8081 ->
60738 [SYN,ACK] Seq=0 ACK =1 Win=29200 Len=0 MSS=1420 SACK_PERM=1 WS=1283) 60738 ->
8081 [ACK] Seq=1 ACK=1 Win=66560 Len=04) Get /test HTTP/1.15) 8081 ->
60738 [ACK] Seq=1 ACK=396 Win=30336 Len=06) HTTP/1.1 200 (text/html)7) 60738 ->
8081 [ACK] Seq=396 ACK=120 Win=66560 Len=0------------------斷開連線-----------------------------8) 60738 ->
8081 [FIN ACK] Seq=396 Ack=120 Win=66560 Len=09) 8081 ->
60738 [FIN ACK] Seq=120 Ack=397 Win=30336 Len=010) 60738 ->
8081 [ACK] Seq=397 Ack=121 Win=66560 Len=0複製程式碼
我們根據上面的流程梳理,可以知道,序號1-序號3是明顯的三次握手,然後序號4進行了一次 HTTP 請求,接著序號5是對 HTTP 請求的一次接收確認,序號6是響應 HTTP 請求,序號7是對響應請求的確認。
B) 四次揮手
上述序號 8,9,10 是我關閉瀏覽器後抓到的包,既然是關閉瀏覽器,我們肯定知道就是 TCP 連線的斷開了。這裡有同學應該已經發現了問題了,我們的斷開是4次揮手,你這抓的包只有三條記錄,是你寫錯了吧?我要告訴你的是,我沒有寫錯,這是真實的抓包抓的,至於為什麼是三次,我們來分析一下:
正常情況下,連線斷開是4次揮手的,4次揮手過程如下圖:
我們分析這圖,揮手流程是這樣的:
1.客戶端發起一個斷開請求,進入 FIN-WAIT 狀態2.服務端確認斷開請求3.服務端立即傳送一個斷開請求,進入 CLOSE-WAIT 狀態4.客戶端確認服務端斷開請求,進入 TIME-WAIT 狀態複製程式碼
我們發現上面的流程2和流程3都是由服務端發起的,那麼有沒有可能合併這兩個請求,一次傳送給客戶端?答案是 可以。在 RFC 2581中的4.2 節有提到,ack可以延遲確認,只要求保證在500ms之內保證確認包到達即可。在這樣的標準下,TCP確認是有可能進行合併延遲確認的,所以,根據這一點,我們推斷下面這個包:
9) 8081 ->
60738 [FIN ACK] Seq=120 Ack=397 Win=30336 Len=0複製程式碼
合併了對客戶端的ack確認以及服務端傳送的FIN斷開訊號包。我們點選該包詳情如下:這裡紅框中體現了,這個9號包是對 Frame 500 的 ACK 確認,我們根據最開始的截圖可以知道,這個包就是8號包
8) 60738 ->
8081 [FIN ACK] Seq=396 Ack=120 Win=66560 Len=0複製程式碼
並且 9號包 本身自己是傳送的 FIN 訊號包,所以,我們可以認為 9號包合併了ACK 和 FIN 的內容,所以通常的4次揮手,經過合併後變成了3次揮手。
以上就是一個 HTTP 完整的請求,整個流程用圖表示如下:
C) Keep-Alive
這裡肯定有同學會問,既然這是一次完整的 HTTP 請求,那麼是不是每次請求都會有三次握手嗎?
答案是:目前的協議是不用的
在 HTTP 0.9 版本和 HTTP 1.0 版本中,每次請求響應都是要三次握手的, 但是 HTTP 1.0 開始嘗試持續連線,也就是 Keep-Alive 引數,但是官方還沒有正式支援,在 HTTP 1.1協議中,官方預設就是支援 Keep-Alive 引數的,預設是持續連線。Keep-Alive 的作用主要有兩點:
1.檢查死節點2.防止連線由於不活躍而斷開複製程式碼
檢查死節點
主要是為了讓連線快速失敗被發現,可以進行重新連線,比如A 和 B 兩端已經建立了連線,B節點因為 異常原因掛掉了,同時 A 節點並不知道,這時候有兩種情況:
1.假設 B 節點還沒有恢復,那麼 B 節點不會回覆 ACK,A節點就會一直重試,重試到一定次數才能知道 B 節點是死節點。
2.B節點在A傳送資料之前重啟成功了,這個時候A節點傳送資料,B節點並不會接受,而是會傳送一個 RST 訊號(在一個已關閉的 socket 上收到資料時,將傳送RST資料包,要求對端關閉異常連線且對端不需要回復ACK),然後 A 才知道 B 節點需要重連了。
以上兩種情況,都會導致只有到傳送資料的時候才知道對方已經出異常了。而Keep-Alive 每隔一段時間就會傳送心跳,就可以很快的知道服務端節點的情況。
防止連線由於不活躍而斷開
我們知道,網路連線的建立和維持是消耗資源的,一個伺服器上能建立的連線是有限的,所以像防火牆或者作業系統中會為了節省資源會釋放掉不活躍的連線,而 Keep-Alive 每隔一段時間傳送一個心跳包,就是告訴防火牆或者作業系統,我這個連線是活躍的,不要殺我。
我重新抓了一次帶有 Keep-Alive 的包,截圖如下:
說完 Keep-Alive,我們回到最開始的問題,為啥一次 HTTP 請求會有進行兩個埠的握手呢?其實,這個和協議本身沒有任何關係,第一個抓包的截圖(圖 Http-Request )是我用谷歌瀏覽器訪問的,最後一個抓包圖(圖Keep-Alive)是我用火狐瀏覽器訪問的,仔細對比我們發現,火狐瀏覽器只有一個埠三次握手。所以這種情況的發生就是瀏覽器自身的實現,谷歌瀏覽器為什麼會這麼實現,我的猜測是:儘可能的保證HTTP訪問的可用性,當某個埠不可用,可以立即切換到另外一個埠,完成HTTP的請求和響應。(個人猜測,如果有權威解答,麻煩告知交流)
四、總結
- HTTP 請求是依託於 TCP 連線的,第一次連線的時候會進行 TCP 的三次握手。
- HTTP 通過 Keep-Alive 來進行持久連線,通過定時傳送一個心跳包,來告訴服務端自己還活躍。
- HTTP 連線的斷開也會導致TCP的四次揮手,但是如果伺服器判斷滿足條件,會合並 ACK 和 FIN 訊號,進而轉化為三次揮手。
五、參考
[1] RFC 1323
[2] RFC 2581