理清 HTTP 之下的 TCP 流程,讓你的 HTTP 水平更上一層
平時(shí)我們用 chrome devtools 的 Network 工具也只是能分析 HTTP 請(qǐng)求:

TCP 層的東西看不見(jiàn)摸不著的,所以對(duì)它的理解也模模糊糊。
那怎么能看到 TCP 層的數(shù)據(jù)包來(lái)理清 TCP 和 HTTP 的關(guān)系呢?
這里推薦一個(gè)抓包工具 WireShark,它能抓取 TCP 層的包:

今天我們就用它來(lái)抓包分析下 TCP 和 HTTP 吧!
首先,我們準(zhǔn)備這樣一段服務(wù)端代碼:
const?express?=?require('express')
const?app?=?express()
app.get('/',?function?(req,?res)?{
??res.setHeader('Connection',?'close')
??res.end('hello?world');
})
app.listen(4000)
用 express 起了一個(gè)服務(wù),監(jiān)聽(tīng) 4000 端口,處理路徑為 / 的 get 請(qǐng)求,返回 hello world 的響應(yīng)體,并設(shè)置 Connection: close 的 header。
瀏覽器訪問(wèn)下:

header 和 body 都符合預(yù)期。
那 TCP 層都做了什么呢?
我們用 WireShark 抓包分析下:
打開(kāi) WireShark 后會(huì)看到有個(gè)設(shè)置按鈕:

因?yàn)槲覀冊(cè)L問(wèn)的是 localhost: 4000,所以這里選擇本地回環(huán)地址那個(gè)虛擬網(wǎng)卡,并輸入抓包過(guò)濾條件為 port 4000:

點(diǎn)擊 start 開(kāi)始錄制,然后刷新一下瀏覽器:
這樣就能看到抓到的 TCP 數(shù)據(jù)包:

我們一一分析下。
在分析之前需要了解一些 TCP 基礎(chǔ)知識(shí):
TCP 的頭部是這樣的:

TCP 是從端口到端口的傳輸協(xié)議,所以開(kāi)始是源端口和目的端口。
接下來(lái)是序列號(hào)(sequence number),表示當(dāng)前包的序號(hào),后面是確認(rèn)的序列號(hào)(acknowledgment number),表示我收到了序號(hào)為 xxx 的包。
然后紅框標(biāo)出的部分是 flags 標(biāo)識(shí)位,通過(guò) 0、1 表示有沒(méi)有:
這里我們只會(huì)用到其中的 SYN、ACK、FIN:
SYN:請(qǐng)求建立一個(gè)連接(說(shuō)明這是鏈接的開(kāi)始) ACK:表示 ack number 是否是有效的 FIN:表示本端要斷開(kāi)鏈接了(說(shuō)明這是鏈接的結(jié)束)
有了這些,我們就知道怎么區(qū)分 TCP 鏈接的開(kāi)始和結(jié)束了。
再看一下抓到的包:

有 SYN 標(biāo)志位的是連接的開(kāi)始,有 FIN 標(biāo)志位的是連接的結(jié)束,所以我們分為 3 段來(lái)看:
首先是連接開(kāi)始的部分:

大家聽(tīng)過(guò) TCP 的三次握手么?說(shuō)的就是這個(gè)。
其中有一個(gè)端口是 4000,這個(gè)是服務(wù)的端口,那另一個(gè)端口 57454 明顯就是瀏覽器的端口。
首先是瀏覽器向服務(wù)器發(fā)送了一個(gè) SYN 的 TCP 請(qǐng)求,表示希望建立連接,序列號(hào) Seq 是 0。
嚴(yán)格來(lái)說(shuō),序列號(hào)的相對(duì)值是 0,絕對(duì)值是 2454579144。

然后服務(wù)器向?yàn)g覽器發(fā)送了一個(gè) SYN 的 TCP 請(qǐng)求,表示希望建立連接,ACK 是 1,代表現(xiàn)在的 ack number 是有效的:

這里 ack number 的相對(duì)值是 1,絕對(duì)值是 2454579145,不就是上個(gè) TCP 數(shù)據(jù)包的 seq 加 1 么?
TCP 連接中就是通過(guò)返回 seq number + 1 作為 ack number 來(lái)確認(rèn)收到的。
然后又返回了一個(gè) seq number 給瀏覽器,相對(duì)值是 0, 絕對(duì)值是 2765691269。
瀏覽器收到后返回了一個(gè) TCP 數(shù)據(jù)包給服務(wù)器,ack number 自然是 2765691270,代表收到了連接請(qǐng)求。

這樣瀏覽器和服務(wù)器各自向?qū)Ψ桨l(fā)送了 SYN 的建立連接請(qǐng)求,并且都收到了對(duì)方的確認(rèn),那么 TCP 連接就建立成功了。
這就是 TCP 三次握手的原理!

趁熱打鐵來(lái)看下四次揮手的部分:

瀏覽器向服務(wù)器發(fā)送了有 FIN 標(biāo)志位的數(shù)據(jù)包,表示要斷開(kāi)連接,然后服務(wù)端返回了 ACK 的包表示確認(rèn)。
之后服務(wù)端發(fā)送了 FIN 標(biāo)志位的數(shù)據(jù)包給瀏覽器,表示要斷開(kāi)連接,瀏覽器也返回了 ACK 的包表示確認(rèn)。
這樣就完成了四次揮手的過(guò)程。
當(dāng)然,具體確認(rèn)的還是靠 ack number = seq number + 1 來(lái)實(shí)現(xiàn)的,和上面的一樣,就不展開(kāi)了:


我們通過(guò)抓包理清了 TCP 連接建立和連接的過(guò)程。

那么為什么握手是三次,揮手是四次呢?
因?yàn)閾]手是一個(gè) FIN,一個(gè) ACK,一個(gè) FIN + ACK,一個(gè) ACK:

而握手是一個(gè) SYN,一個(gè) ACK + SYN,一個(gè) ACK:

不過(guò)是因?yàn)槲帐謺r(shí)把 ACK 和 SYN 合并到一個(gè)數(shù)據(jù)包了而已。
那揮手時(shí)能合并成三次么?
不能!因?yàn)橛袃蓚€(gè) ack number,怎么合并,沖突了,而握手時(shí)只有一個(gè) ack number,自然可以合并。

接下來(lái)再來(lái)看下連接建立后的 http 請(qǐng)求和響應(yīng)吧:

其實(shí)一次 HTTP 請(qǐng)求響應(yīng)會(huì)有四個(gè) TCP 數(shù)據(jù)包,其中兩個(gè)數(shù)據(jù)包與滑動(dòng)窗口有關(guān),這里先不展開(kāi)了。
我們就看下 HTTP 的那兩個(gè)包吧:
請(qǐng)求的 seq 是這樣的:

而響應(yīng)的 ack 是這樣的:

相對(duì)值是 ack number = seq number + 1 沒(méi)錯(cuò),但是絕對(duì)值不是:
絕對(duì)值 2454579855 = 2454579145 + 710,也就是 ack number = seq number + segment len。
這些細(xì)節(jié)暫時(shí)不用深究。
總之,我們知道了HTTP 的請(qǐng)求和響應(yīng)是通過(guò)序列號(hào)關(guān)聯(lián)在一起的。
就算同一個(gè) TCP 鏈接并行發(fā)送多個(gè) HTTP 的請(qǐng)求和響應(yīng),它們也能找到各自對(duì)應(yīng)的那個(gè)。就是通過(guò)這個(gè) seq number 和 ack number。
這里為啥鏈接建立了發(fā)送了一個(gè)請(qǐng)求就斷掉了呢?
我刷新瀏覽器,請(qǐng)求了兩次,發(fā)現(xiàn)經(jīng)歷了兩次連接的建立、http 請(qǐng)求響應(yīng)、連接斷開(kāi):

這是因?yàn)槲以O(shè)置了 Connection:close 的 header,它的作用就是一次 http 請(qǐng)求響應(yīng)結(jié)束就斷開(kāi) TCP 鏈接。

我們改成 HTTP 1.1 支持的 keep-alive 試試:

設(shè)置 Connection 為 keep-alive,然后設(shè)置 keep-alive 的細(xì)節(jié)為 timeout 10 ,也就是 10s 后斷開(kāi)。
重啟服務(wù)器,再刷新下瀏覽器試試:

可以看到在一個(gè) TCP 連接內(nèi)發(fā)送了多次 http 請(qǐng)求響應(yīng)。(通過(guò) SYN 開(kāi)始,F(xiàn)IN 結(jié)束)
這就是 keep-alive 的作用。
細(xì)心的同學(xué)會(huì)發(fā)現(xiàn)只是瀏覽器向服務(wù)器發(fā)送了 FIN 數(shù)據(jù)包,服務(wù)器沒(méi)有發(fā)給瀏覽器 FIN 數(shù)據(jù)包。
這是因?yàn)?keep-alive 的 header 只是控制的瀏覽器的斷開(kāi)連接的行為,服務(wù)器的斷開(kāi)連接邏輯是獨(dú)立的。
這樣,我們就理清了 HTTP 在 TCP 層面的流程,連接的建立、斷開(kāi),請(qǐng)求響應(yīng),還有 keep-alive。
總結(jié)
我們平時(shí)都是分析 HTTP 請(qǐng)求響應(yīng),TCP 對(duì)我們來(lái)說(shuō)看不見(jiàn)摸不著的,理解的模模糊糊。
所以今天我們用 WireShark 抓了下 TCP 的包,來(lái)理清了 TCP 和 HTTP 的關(guān)系。
TCP 是從一個(gè)端口到另一個(gè)端口的傳輸控制協(xié)議,TCP header 中有序列號(hào) seq number、確認(rèn)序列號(hào) ack number,還有幾個(gè)標(biāo)志位:
SYN 標(biāo)志位代表請(qǐng)求建立連接 ACK 標(biāo)志位代表當(dāng)前確認(rèn)序列號(hào)是有效的。 FIN 標(biāo)志位代表請(qǐng)求斷開(kāi)連接
然后我們抓了 localhost:4000 的包分析了下 HTTP 請(qǐng)求的 TCP 流程,理清了三次握手(SYN、SYN + ACK、ACK),四次揮手(FIN、ACK、FIN + ACK、ACK)的連接建立、斷開(kāi)的流程。知道了為什么不能三次揮手(因?yàn)閮蓚€(gè) ACK 沖突了)
然后還理清了同一個(gè) TCP 連接傳輸?shù)亩鄠€(gè) HTTP 請(qǐng)求響應(yīng)是通過(guò) seq number 和 ack number 來(lái)關(guān)聯(lián)的。
之后我們分別測(cè)試了 Connection:close 和 Connection:keep-alive 的情況,發(fā)現(xiàn)確實(shí) keep-alive 能減少頻繁的連接建立和斷開(kāi),能復(fù)用同一個(gè) TCP 鏈接。
HTTP 是通過(guò) TCP 完成端口到端口的數(shù)據(jù)傳輸?shù)摹R粋€(gè) TCP 連接可以傳輸多個(gè) HTTP 請(qǐng)求、響應(yīng)。請(qǐng)求和響應(yīng)的關(guān)聯(lián)是通過(guò) TCP 包的序列號(hào) seq。
理清了 TCP 和 HTTP 的關(guān)系,你是否對(duì) HTTP 的理解更深了呢?
