淺析一次HTTP請求
一、概覽
上一篇文章《對於Ping的過程,你真的了解嗎?》我們通過抓包工具來分析了一次 Ping 的過程,我們知道了 ping 是依託於 ICMP 協議,然後再區域網中還會涉及到 ARP 請求,今天這篇文章我們同樣用抓包分析工具來分析我們熟悉的 HTTP 請求是怎麼樣的?
二、環境準備
本來我是想找個網站進行抓包分析的,但是正式環境的網站 HTTP 請求太多,干擾太多,對分析不太友好,所以我簡單些了一個demo,對 HTTP 請求返回字元串。
環境:
1.響應http請求的服務demo
2.客戶端ip:192.168.2.135
3.服務端:45.76.105.92
4.抓包工具:Wireshark
我把demo部署到伺服器,啟動成功訪問如下:
打開抓包工具 Wireshark 進行抓包,抓包結果如下:
圖 Http-Request
從上圖我們已經看到成功抓包到一次 HTTP 請求和響應了,但是我們看到卻有很多TCP請求,接下來我們來分析下這些 TCP 請求是做什麼的?
三、抓包分析
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=1
2) 8081 -> 60738 [SYN,ACK] Seq=0 ACK =1 Win=29200 Len=0 MSS=1420 SACK_PERM=1 WS=128
3) 60738 -> 8081 [ACK] Seq=1 ACK=1 Win=66560 Len=0
4) Get /test HTTP/1.1
5) 8081 -> 60738 [ACK] Seq=1 ACK=396 Win=30336 Len=0
6) 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=0
9) 8081 -> 60738 [FIN ACK] Seq=120 Ack=397 Win=30336 Len=0
10) 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
在上圖中最後兩個包就是發的 Keep-Alive 包,然後服務端進行 ACK 確認,我們看到 keep-alive 包,實際上是會髮帶有一個位元組的包,這就是 keep-alive 的實現。
說完 Keep-Alive,我們回到最開始的問題,為啥一次 HTTP 請求會有進行兩個埠的握手呢?其實,這個和協議本身沒有任何關係,第一個抓包的截圖(圖 Http-Alive)是我用谷歌瀏覽器訪問的,最後一個抓包圖(圖Keep-Alive)是我用火狐瀏覽器訪問的,仔細對比我們發現,火狐瀏覽器只有一個埠三次握手。所以這種情況的發生就是瀏覽器自身的實現,谷歌瀏覽器為什麼會這麼實現,我的猜測是:儘可能的保證HTTP訪問的可用性,當某個埠不可用,可以立即切換到另外一個埠,完成HTTP的請求和響應。(個人猜測,如果有權威解答,麻煩告知交流)
四、總結
- HTTP 請求是依託於 TCP 連接的,第一次連接的時候會進行 TCP 的三次握手。
- HTTP 通過 Keep-Alive 來進行持久連接,通過定時發送一個心跳包,來告訴服務端自己還活躍。
- HTTP 連接的斷開也會導致TCP的四次揮手,但是如果伺服器判斷滿足條件,會合併 ACK 和 FIN 信號,進而轉化為三次揮手。
作者:木木匠
原文:https://my.oschina.net/kinglaw007/blog/3003053
※CSS選擇器權重計算規則
※5款面向Linux的簡單Web瀏覽器
TAG:程序員小新人學習 |