HTTP協(xié)議以及基于UDP實(shí)現(xiàn)可靠的協(xié)議QUIC
前言
HTTP協(xié)議
請(qǐng)求準(zhǔn)備:
請(qǐng)求構(gòu)建:
請(qǐng)求的發(fā)送:
HTTP返回的構(gòu)建:
如何實(shí)現(xiàn)一個(gè)靠譜的協(xié)議?
QUIC協(xié)議
自定義連接機(jī)制:
自定義重傳機(jī)制:
無(wú)阻塞的多路復(fù)用:
自定義流量控制:
往期推薦:
前言
有三個(gè)月沒(méi)更筆記文了,似乎忘了這是一個(gè)技術(shù)類公眾號(hào)。
在這段時(shí)間內(nèi)花了兩個(gè)月重學(xué)了一遍數(shù)據(jù)結(jié)構(gòu),然后在leetcode上刷了一百多道題。
還看了一本《現(xiàn)代操作系統(tǒng)》,看的有點(diǎn)懵,又花錢買了一個(gè)大牛專欄看
后續(xù)將這些筆記都整理一下,發(fā)到這個(gè)公眾號(hào)。
還花了一周空閑時(shí)間用python寫(xiě)了一個(gè)小程序,每天定時(shí)獲取熱點(diǎn)信息、自動(dòng)編排、上傳、發(fā)布文章。熱點(diǎn)在文章中支持點(diǎn)擊跳轉(zhuǎn)。
之前寫(xiě)的網(wǎng)絡(luò)協(xié)議其實(shí)只寫(xiě)了一小部分,后面還包括了HTTP協(xié)議、HTTPS協(xié)議、DNS、CDN、網(wǎng)絡(luò)模式以及容器網(wǎng)絡(luò)等。
那么今天就來(lái)記錄一下HTTP協(xié)議相關(guān)內(nèi)容。
HTTP協(xié)議
瀏覽器上輸入url,比如http://www.xxx.com,這個(gè)叫做統(tǒng)一資源定位符
其中www.xxx.com是一個(gè)域名,表示互聯(lián)網(wǎng)的一個(gè)位置。
請(qǐng)求準(zhǔn)備:
瀏覽器會(huì)將域名發(fā)送給DNS服務(wù)器,解析成IP地址。
HTTP是基于TCP協(xié)議的,先要建立TCP連接。
目前使用的HTTP協(xié)議大部分都是1.1,在1.1的協(xié)議里面,默認(rèn)開(kāi)啟了Keep-Alive的,這樣建立的TCP連接,可以在多次請(qǐng)求中復(fù)用。
請(qǐng)求構(gòu)建:

HTTP報(bào)文分為三大部分,分別是請(qǐng)求行,首部和請(qǐng)求的正文實(shí)體
請(qǐng)求行
URL就是http://www.xxx.com
版本就是HTTP1.1
方法有GET、POST、PUT、DELETE
首部字段
首部是Key-Value格式,通過(guò)冒號(hào)分隔,保存一些header信息
例如:Accept-Charset,表示客戶端可接受的字符集。
Content-Type指的是正文的格式,例如是JSON
Cache-control 用來(lái)控制緩存
請(qǐng)求的發(fā)送:

HTTP 協(xié)議是基于 TCP 協(xié)議的,所以它使用面向連接的方式發(fā)送請(qǐng)求。
到了 TCP 層,它會(huì)轉(zhuǎn)換成一個(gè)個(gè)報(bào)文段發(fā)送給服務(wù)器。
在發(fā)送給每個(gè)報(bào)文段的時(shí)候,都需要對(duì)方有一個(gè)回應(yīng) ACK,來(lái)保證報(bào)文可靠地到達(dá)了對(duì)方。
如果沒(méi)有回應(yīng),那么 TCP 這一層會(huì)進(jìn)行重傳,直到可以到達(dá)。
TCP 層發(fā)送每一個(gè)報(bào)文的時(shí)候,都需要加上自己的地址(即源地址)和它想要去的地方(即目標(biāo)地址),將這兩個(gè)信息放到 IP 頭里面,交給 IP 層進(jìn)行傳輸。
IP 層需要查看目標(biāo)地址和自己是否是在同一個(gè)局域網(wǎng)。如果是,就發(fā)送 ARP 協(xié)議來(lái)請(qǐng)求這個(gè)目標(biāo)地址對(duì)應(yīng)的 MAC 地址,然后將源 MAC 和目標(biāo) MAC 放入 MAC 頭,發(fā)送出去即可;
如果不在同一個(gè)局域網(wǎng),就需要發(fā)送到網(wǎng)關(guān),還要需要發(fā)送 ARP 協(xié)議,來(lái)獲取網(wǎng)關(guān)的 MAC 地址,然后將源 MAC 和網(wǎng)關(guān) MAC 放入 MAC 頭,發(fā)送出去。
網(wǎng)關(guān)收到包發(fā)現(xiàn) MAC 符合,取出目標(biāo) IP 地址,根據(jù)路由協(xié)議找到下一跳的路由器,獲取下一跳路由器的 MAC 地址,將包發(fā)給下一跳路由器。
這樣路由器一跳一跳終于到達(dá)目標(biāo)的局域網(wǎng)。
這個(gè)時(shí)候,最后一跳的路由器能夠發(fā)現(xiàn),目標(biāo)地址就在自己的某一個(gè)出口的局域網(wǎng)上。于是,在這個(gè)局域網(wǎng)上發(fā)送 ARP,獲得這個(gè)目標(biāo)地址的 MAC 地址,將包發(fā)出去。
目標(biāo)的機(jī)器發(fā)現(xiàn) MAC 地址符合,就將包收起來(lái);發(fā)現(xiàn) IP 地址符合,根據(jù) IP 頭中協(xié)議項(xiàng),知道上一層是 TCP 協(xié)議,于是解析 TCP 的頭,里面有序列號(hào),需要看一看這個(gè)序列包是不是我要的,如果是就放入緩存中然后返回一個(gè) ACK,如果不是就丟棄。
TCP 頭里面還有端口號(hào),HTTP 的服務(wù)器正在監(jiān)聽(tīng)這個(gè)端口號(hào)。于是,目標(biāo)機(jī)器自然知道是 HTTP 服務(wù)器這個(gè)進(jìn)程想要這個(gè)包,于是將包發(fā)給 HTTP 服務(wù)器。HTTP 服務(wù)器的進(jìn)程看到,原來(lái)這個(gè)請(qǐng)求是要訪問(wèn)一個(gè)網(wǎng)頁(yè),于是就把這個(gè)網(wǎng)頁(yè)發(fā)給客戶端。
HTTP返回的構(gòu)建:

狀態(tài)碼有幾種,1xx、2xx、3xx、4xx、5xx
首部中,Retry-After 表示,告訴客戶端應(yīng)該在多長(zhǎng)時(shí)間以后再次嘗試一下。
Content-Type,表示返回的是 HTML,還是 JSON。
構(gòu)造好了返回的 HTTP 報(bào)文,接下來(lái)就是把這個(gè)報(bào)文發(fā)送出去。還是交給 Socket 去發(fā)送,還是交給 TCP 層,讓 TCP 層將返回的 HTML,也分成一個(gè)個(gè)小的段,并且保證每個(gè)段都可靠到達(dá)。
這些段加上 TCP 頭后會(huì)交給 IP 層,然后把剛才的發(fā)送過(guò)程反向走一遍。雖然兩次不一定走相同的路徑,但是邏輯過(guò)程是一樣的,一直到達(dá)客戶端。
客戶端發(fā)現(xiàn) MAC 地址符合、IP 地址符合,于是就會(huì)交給 TCP 層。根據(jù)序列號(hào)看是不是自己要的報(bào)文段,如果是,則會(huì)根據(jù) TCP 頭中的端口號(hào),發(fā)給相應(yīng)的進(jìn)程。這個(gè)進(jìn)程就是瀏覽器,瀏覽器作為客戶端也在監(jiān)聽(tīng)某個(gè)端口。
當(dāng)瀏覽器拿到了 HTTP 的報(bào)文。發(fā)現(xiàn)返回“200”,一切正常,于是就從正文中將 HTML 拿出來(lái)。HTML 是一個(gè)標(biāo)準(zhǔn)的網(wǎng)頁(yè)格式。瀏覽器只要根據(jù)這個(gè)格式,渲染網(wǎng)頁(yè)。
這就是一個(gè)正常的 HTTP 請(qǐng)求和返回的完整過(guò)程。
如何實(shí)現(xiàn)一個(gè)靠譜的協(xié)議?
為了保證順序性,每一個(gè)包都有一個(gè)ID,在建立連接的時(shí)候,會(huì)商定起始的ID是什么,然后按照ID一個(gè)個(gè)發(fā)送,為了保證不丟包,對(duì)于發(fā)送的包都要進(jìn)行應(yīng)答,但這個(gè)應(yīng)答也不是一個(gè)一個(gè)來(lái),而是會(huì)應(yīng)答某個(gè)之前的ID,表示都收到了,這種模式稱為累計(jì)確認(rèn)或者累計(jì)應(yīng)答
QUIC協(xié)議
QUIC協(xié)議,是Google內(nèi)部的一個(gè)基于UDP的可靠傳輸協(xié)議。
自定義連接機(jī)制:
TCP由一個(gè)四元組確認(rèn)一個(gè)連接,發(fā)生變化就得重連。移動(dòng)互聯(lián)網(wǎng)下,網(wǎng)絡(luò)不穩(wěn)定會(huì)再次重連,導(dǎo)致時(shí)延。
基于UDP,不再以四元組為標(biāo)識(shí),而是以一個(gè)64位的隨機(jī)數(shù)作為ID來(lái)標(biāo)識(shí),而且UDP是無(wú)連接的,所以當(dāng)IP或者端口變化的時(shí)候,只要ID不變,就不需要重新建立連接。
自定義重傳機(jī)制:
TCP超時(shí)重傳是通過(guò)自適應(yīng)重傳算法,通過(guò)采用往返時(shí)間RTT不斷調(diào)整。
QUIC 也有個(gè)序列號(hào),是遞增的。任何一個(gè)序列號(hào)的包只發(fā)送一次,下次就要加一了。
例如,發(fā)送一個(gè)包,序號(hào)是 100,發(fā)現(xiàn)沒(méi)有返回;再次發(fā)送的時(shí)候,序號(hào)就是 101 了;如果返回的 ACK 100,就是對(duì)第一個(gè)包的響應(yīng)。如果返回 ACK 101 就是對(duì)第二個(gè)包的響應(yīng),RTT 計(jì)算相對(duì)準(zhǔn)確。
QUIC 定義了一個(gè) offset 概念。QUIC 既然是面向連接的,也就像 TCP 一樣,是一個(gè)數(shù)據(jù)流,發(fā)送的數(shù)據(jù)在這個(gè)數(shù)據(jù)流里面有個(gè)偏移量 offset,可以通過(guò) offset 查看數(shù)據(jù)發(fā)送到了哪里,這樣只要這個(gè) offset 的包沒(méi)有來(lái),就要重發(fā);如果來(lái)了,按照 offset 拼接,還是能夠拼成一個(gè)流。

無(wú)阻塞的多路復(fù)用:
同一條 QUIC 連接上可以創(chuàng)建多個(gè) stream,來(lái)發(fā)送多個(gè) HTTP 請(qǐng)求。但是,QUIC 是基于 UDP 的,一個(gè)連接上的多個(gè) stream 之間沒(méi)有依賴。這樣,假如 stream2 丟了一個(gè) UDP 包,后面跟著 stream3 的一個(gè) UDP 包,雖然 stream2 的那個(gè)包需要重傳,但是 stream3 的包無(wú)需等待,就可以發(fā)給用戶。
自定義流量控制:
TCP 的流量控制是通過(guò)滑動(dòng)窗口協(xié)議。QUIC 的流量控制也是通過(guò) window_update,來(lái)告訴對(duì)端它可以接受的字節(jié)數(shù)。但是 QUIC 的窗口是適應(yīng)自己的多路復(fù)用機(jī)制的,不但在一個(gè)連接上控制窗口,還在一個(gè)連接中的每個(gè) stream 控制窗口。
在 TCP 協(xié)議中,接收端的窗口的起始點(diǎn)是下一個(gè)要接收并且 ACK 的包,即便后來(lái)的包都到了,放在緩存里面,窗口也不能右移,因?yàn)?TCP 的 ACK 機(jī)制是基于序列號(hào)的累計(jì)應(yīng)答,一旦 ACK 了一個(gè)序列號(hào),就說(shuō)明前面的都到了,所以只要前面的沒(méi)到,后面的到了也不能 ACK,就會(huì)導(dǎo)致后面的到了,也有可能超時(shí)重傳,浪費(fèi)帶寬。
QUIC 的 ACK 是基于 offset 的,每個(gè) offset 的包來(lái)了,進(jìn)了緩存,就可以應(yīng)答,應(yīng)答后就不會(huì)重發(fā),中間的空檔會(huì)等待到來(lái)或者重發(fā)即可,而窗口的起始位置為當(dāng)前收到的最大 offset,從這個(gè) offset 到當(dāng)前的 stream 所能容納的最大緩存,是真正的窗口大小。顯然,這樣更加準(zhǔn)確。

往期推薦:
瀏覽器發(fā)起HTTP請(qǐng)求后經(jīng)歷了什么?
