一次 HTTP 請求到底經(jīng)歷了什么?

今天這篇文章我們用抓包分析工具來分析 HTTP 請求是怎么樣的?
環(huán)境準(zhǔn)備
本來是想找個網(wǎng)站進行抓包分析的,但是正式環(huán)境的網(wǎng)站 HTTP 請求太多,干擾太多,對分析不太友好,所以簡單些了一個 demo,對 HTTP 請求返回字符串。
環(huán)境:
1.響應(yīng)http請求的服務(wù)demo
2.客戶端ip:192.168.2.135
3.服務(wù)端:45.76.105.92
4.抓包工具:Wireshark
把 demo部署到服務(wù)器,啟動成功訪問如下:

打開抓包工具 Wireshark 進行抓包,抓包結(jié)果如下:

從上圖我們已經(jīng)看到成功抓包到一次 HTTP 請求和響應(yīng)了,但是我們看到卻有很多 TCP請求,接下來我們來分析下這些 TCP 請求是做什么的?
抓包分析
A) 三次握手
最開始是本地發(fā)送了2次請求到服務(wù)器,這里為什么會有兩次請求,稍后再說,我們先主要看 HTTP 對應(yīng)的端口請求,如下:
192.168.2.135:60738---->45.76.105.92:8081
看上面的截圖我們知道這是 TCP 協(xié)議的第一次握手,熟悉 TCP 協(xié)議的同學(xué)肯定知道 TCP 建立連接有三次握手,斷開連接有四次揮手。
我們先看第一次請求:
60738 -> 8081 [SYN] Seq=0Win=64240Len=0Mss=1460Ws=256 SACK_PERM=1
我們來解析下這段包請求信息:
60783->8081端口號:源端口--->目標(biāo)端口[SYN]:同步握手信號Seq: 消息編號Win: TCP 窗口大小Len: 消息長度Mss: 最大報文段長度Ws: 窗口縮放調(diào)整因子SACK_PERM: SACK選項,這里等于1表示開啟 SACK。
對于上面的概念,這里簡單解釋下,再介紹之前我們先看 TCPHeader 的數(shù)據(jù)結(jié)構(gòu)圖,對 TCP 頭部數(shù)據(jù)結(jié)構(gòu)有個直觀的了解

1. Win: TCP 窗口大小,是指 TCP傳輸能接受的最大字節(jié)數(shù),這個可以進行動態(tài)調(diào)節(jié),也就是 TCP的滑動窗口,通過動態(tài)調(diào)整窗口大小,來控制發(fā)送數(shù)據(jù)的速率。上圖中占用 2個字節(jié),也就是 16位,那么可以支持的最大數(shù)就是 2^16=65536,所以默認(rèn)情況下 TCP頭部標(biāo)記能支持的最大窗口數(shù)是 65536字節(jié),也就是 64KB。
2. Len: 消息長度 就是指數(shù)據(jù)報文段,因為整個 TCP報文 = Header + packSize,所以這個消息長度就是指要傳送的數(shù)據(jù)包總共長度,在本次分析中也就是 HTTP報文的大小。
3. Mss: 最大報文段長度:這個就是規(guī)定最大的能傳輸報文的長度,為了達到最佳的傳輸效能, TCP 協(xié)議在建立連接的時候通常要協(xié)商雙方的 MSS 值,這個值 TCP 協(xié)議在實現(xiàn)的時候往往用 MTU 值代替(需要減去 IP數(shù)據(jù)包包頭的大小 20Bytes和 TCP數(shù)據(jù)段的包頭 20Bytes)所以一般 MSS 值 1460,這也和我們抓包圖中的值一致。
4. Ws: 窗口縮放調(diào)整因子:在前面說 TCP 窗口大小中我們說到,默認(rèn)情況下, TCP 窗口大小最大只能支持 64KB的緩沖數(shù)據(jù),在今天這個高速上網(wǎng)時代,這個大小肯定不滿足條件了,所以,為了能夠支持更多的緩沖數(shù)據(jù) RFC 1323中就規(guī)定了 TCP 的擴展選項,其中窗口縮放調(diào)整因子就是其中之一,這個是如何起作用的呢?首先說明,這個參數(shù)是在 [SYN] 同步階段進行協(xié)商的,我們結(jié)合上面抓包數(shù)據(jù)分析下。我們看到第一次請求協(xié)商的結(jié)果是 WS=256,然后再 ACK 階段擴展因子生效,調(diào)整了窗口大小。生效的抓包如下:
60738 ->8081 [ACK] Seq=1 ACK=1Win=66560Len=0
我們發(fā)現(xiàn)這個窗口變成了 66560,比默認(rèn)的窗口要大,我們查看報文詳情:

我們發(fā)現(xiàn),實際請求聲明的窗口是 260, WS擴展因子是 256,最終計算的窗口大小是 66560,所以我們知道了,這個擴展因子的作用就是,用原窗口大小乘以擴展因子,得到最終的窗口大小,也就是 260*256=66560.
5. SACK_PERM:SACK選項 ,我們知道 TCP 傳輸有包的確認(rèn)機制,默認(rèn)情況下,接受端接受到一個包后,發(fā)送 ACK 確認(rèn),但是,默認(rèn)只支持順序的確認(rèn),也就是說,發(fā)送 A, B, C 個包,如果我收到了 A, C的包, B沒有收到,那么對于 C,這個包我是不會確認(rèn)的,需要等 B這個包收到后再確認(rèn),那么 TCP有超時重傳機制,如果一個包很久沒有確認(rèn),就會當(dāng)它丟失了,進行重傳,這樣會造成很多多余的包重傳,浪費傳輸空間。為了解決這個問題, SACK就提出了選擇性確認(rèn)機制,啟用 SACK 后,接受端會確認(rèn)所有收到的包,這樣發(fā)送端就只用重傳真正丟失的包了。
簡單介紹了上面的基礎(chǔ)概念后,我們來根據(jù)抓包梳理下 HTTP 請求的過程,根據(jù) HTTP 請求本地端口是 60378,梳理的流程如下:
------------------------請求連接--------------------------
1) 60738 -> 8081 [SYN] Seq=0Win=64240Len=0Mss=1460Ws=256 SACK_PERM=1
2) 8081 -> 60738 [SYN,ACK] Seq=0 ACK =1Win=29200Len=0 MSS=1420 SACK_PERM=1 WS=128
3) 60738 -> 8081 [ACK] Seq=1 ACK=1Win=66560Len=0
4) Get /test HTTP/1.1
5) 8081 -> 60738 [ACK] Seq=1 ACK=396Win=30336Len=0
6) HTTP/1.1200 (text/html)
7) 60738 -> 8081 [ACK] Seq=396 ACK=120Win=66560Len=0
------------------斷開連接-----------------------------
8) 60738 -> 8081 [FIN ACK] Seq=396Ack=120Win=66560Len=0
9) 8081 -> 60738 [FIN ACK] Seq=120Ack=397Win=30336Len=0
10) 60738 -> 8081 [ACK] Seq=397Ack=121Win=66560Len=0
我們根據(jù)上面的流程梳理,可以知道, 序號1- 序號3是明顯的三次握手,然后 序號4進行了一次 HTTP 請求,接著 序號5是對 HTTP 請求的一次接收確認(rèn), 序號6是響應(yīng) HTTP 請求, 序號7是對響應(yīng)請求的確認(rèn)。
B) 四次揮手
上述序號 8, 9, 10 是我關(guān)閉瀏覽器后抓到的包,既然是關(guān)閉瀏覽器,我們肯定知道就是 TCP 連接的斷開了。這里有同學(xué)應(yīng)該已經(jīng)發(fā)現(xiàn)了問題了,我們的斷開是 4次揮手,你這抓的包只有三條記錄,是你寫錯了吧?我要告訴你的是,我沒有寫錯,這是真實的抓包抓的,至于為什么是三次,我們來分析一下:
正常情況下,連接斷開是 4次揮手的, 4次揮手過程如下圖:

我們分析這圖,揮手流程是這樣的:
1. 客戶端發(fā)起一個斷開請求,進入 FIN-WAIT 狀態(tài)
2. 服務(wù)端確認(rèn)斷開請求
3. 服務(wù)端立即發(fā)送一個斷開請求,進入 CLOSE-WAIT 狀態(tài)
4. 客戶端確認(rèn)服務(wù)端斷開請求,進入 TIME-WAIT 狀態(tài)
我們發(fā)現(xiàn)上面的 流程2和 流程3都是由服務(wù)端發(fā)起的,那么有沒有可能合并這兩個請求,一次發(fā)送給客戶端?答案是 可以。在 RFC 2581中的 4.2 節(jié)有提到, ack可以延遲確認(rèn),只要求保證在 500ms之內(nèi)保證確認(rèn)包到達即可。在這樣的標(biāo)準(zhǔn)下, TCP確認(rèn)是有可能進行合并延遲確認(rèn)的,所以,根據(jù)這一點,我們推斷下面這個包:
9) 8081 -> 60738 [FIN ACK] Seq=120Ack=397Win=30336Len=0
合并了對客戶端的 ack確認(rèn)以及服務(wù)端發(fā)送的 FIN斷開信號包。我們點擊該包詳情如下:這里紅框中體現(xiàn)了,這個 9號包是對 Frame500 的 ACK 確認(rèn),我們根據(jù)最開始的截圖可以知道,這個包就是 8號包
8) 60738 -> 8081 [FIN ACK] Seq=396Ack=120Win=66560Len=0

并且 9號包 本身自己是發(fā)送的 FIN 信號包,所以,我們可以認(rèn)為 9號包合并了 ACK 和 FIN 的內(nèi)容,所以通常的 4次揮手,經(jīng)過合并后變成了 3次揮手。
以上就是一個 HTTP 完整的請求,整個流程用圖表示如下:

C) Keep-Alive
這里肯定有同學(xué)會問,既然這是一次完整的 HTTP 請求,那么是不是每次請求都會有三次握手嗎?
答案是:目前的協(xié)議是不用的
在 HTTP0.9 版本和 HTTP1.0 版本中,每次請求響應(yīng)都是要三次握手的, 但是 HTTP1.0 開始嘗試持續(xù)連接,也就是 Keep-Alive 參數(shù),但是官方還沒有正式支持,在 HTTP1.1協(xié)議中,官方默認(rèn)就是支持 Keep-Alive 參數(shù)的,默認(rèn)是持續(xù)連接。 Keep-Alive 的作用主要有兩點:
1.檢查死節(jié)點
2.防止連接由于不活躍而斷開
檢查死節(jié)點
主要是為了讓連接快速失敗被發(fā)現(xiàn),可以進行重新連接,比如 A 和 B 兩端已經(jīng)建立了連接, B節(jié)點因為 異常原因掛掉了,同時 A 節(jié)點并不知道,這時候有兩種情況:
1.假設(shè) B 節(jié)點還沒有恢復(fù),那么 B 節(jié)點不會回復(fù) ACK, A節(jié)點就會一直重試,重試到一定次數(shù)才能知道 B 節(jié)點是死節(jié)點。
2. B節(jié)點在 A發(fā)送數(shù)據(jù)之前重啟成功了,這個時候 A節(jié)點發(fā)送數(shù)據(jù), B節(jié)點并不會接受,而是會發(fā)送一個 RST 信號(在一個已關(guān)閉的 socket 上收到數(shù)據(jù)時,將發(fā)送 RST數(shù)據(jù)包,要求對端關(guān)閉異常連接且對端不需要回復(fù) ACK),然后 A 才知道 B 節(jié)點需要重連了。
以上兩種情況,都會導(dǎo)致只有到發(fā)送數(shù)據(jù)的時候才知道對方已經(jīng)出異常了。而 Keep-Alive 每隔一段時間就會發(fā)送心跳,就可以很快的知道服務(wù)端節(jié)點的情況。
防止連接由于不活躍而斷開
我們知道,網(wǎng)絡(luò)連接的建立和維持是消耗資源的,一個服務(wù)器上能建立的連接是有限的,所以像防火墻或者操作系統(tǒng)中會為了節(jié)省資源會釋放掉不活躍的連接,而 Keep-Alive 每隔一段時間發(fā)送一個心跳包,就是告訴防火墻或者操作系統(tǒng),我這個連接是活躍的,不要殺我。
后來重新抓了一次帶有 Keep-Alive 的包,截圖如下:

在上圖中最后兩個包就是發(fā)的 Keep-Alive 包,然后服務(wù)端進行 ACK 確認(rèn),我們看到 keep-alive 包,實際上是會發(fā)帶有一個字節(jié)的包,這就是 keep-alive 的實現(xiàn)。
說完 Keep-Alive,我們回到最開始的問題,為啥一次 HTTP 請求會有進行兩個端口的握手呢?其實,這個和協(xié)議本身沒有任何關(guān)系,第一個抓包的截圖是用谷歌瀏覽器訪問的,最后一個抓包圖是用火狐瀏覽器訪問的,仔細(xì)對比我們發(fā)現(xiàn),火狐瀏覽器只有一個端口三次握手。所以這種情況的發(fā)生就是瀏覽器自身的實現(xiàn),谷歌瀏覽器為什么會這么實現(xiàn),猜測是:盡可能的保證HTTP訪問的可用性,當(dāng)某個端口不可用,可以立即切換到另外一個端口,完成HTTP的請求和響應(yīng)。(個人猜測,如果有權(quán)威解答,可以評論區(qū)交流)
總結(jié)
HTTP請求是依托于TCP連接的,第一次連接的時候會進行TCP的三次握手。HTTP通過Keep-Alive來進行持久連接,通過定時發(fā)送一個心跳包,來告訴服務(wù)端自己還活躍。HTTP連接的斷開也會導(dǎo)致TCP的四次揮手,但是如果服務(wù)器判斷滿足條件,會合并ACK和FIN信號,進而轉(zhuǎn)化為三次揮手。
作者:木木匠
鏈接:https://url.cn/5ER9kt2
文章轉(zhuǎn)自:DevOps技術(shù)棧
![]()
