前後端開發必會的HTTP 協議“十全大補丸”
前後端開發必會的HTTP 協議“十全大補丸”
HTTP 協議全稱為HyperText Transfer Protocol,即超文本傳輸協議。
- 超文本:指文字、圖片、音頻、視頻、文件等的混合體,比如最常見的HTML。
- 傳輸:指數據從一方轉移到另一方,二者之間可能相距數千里。
- 協議:指通信雙方所做的一些約定,比如怎麼開始通信、信息的格式與順序、怎麼結束通信等。
HTTP 協議是乾啥的呢?答案是用於客戶端與服務器端之間的通信。我們日常上網過程中最常見的就是HTTP 協議了,瀏覽器是最常見的HTTP 客戶端。
比如我們使用瀏覽器訪問淘寶時,瀏覽器就會發送一個遵循HTTP 協議的請求報文到淘寶服務器,告訴淘寶服務器自己想要獲取淘寶首頁信息。
淘寶服務器收到此報文後,則會發送一個同樣遵循HTTP 協議的響應報文到瀏覽器,此響應報文中包含淘寶首頁的內容。
瀏覽器收到響應報文後解析其內容並展示在界面上。
1. HTTP 請求
客戶端向服務器端發送的信息稱為請求報文,一般結構如下:
(1)請求行
請求行用於說明要做些什麼,包含三部分內容,中間用空格分割。
- 方法,指定要對請求資源做什麼樣的操作(比如查詢、修改等)。常見的方法有:GET、POST、PUT、DELETE、HEAD 等。在前後端分離開發中,經常會遵循RESTful 設計風格,其使用POST、DELETE、PUT、GET 分別表示對數據的增、刪、改、查。
- 資源路徑,指定所要請求資源在服務器中的位置。比如/index.html,表示訪問服務器根目錄下名字為index 的html 文件。
- HTTP 版本,指定所使用的HTTP 版本。目前使用最多的版本為HTTP/1.1。
舉個栗子:
面試中常見的一個問題: GET 和POST 的區別是什麼?,在這裡做一下解答。
- 首先,一般GET 請求參數存放在URL 中,而POST 請求參數存儲在請求體中。因為參數放在URL 中可以直接被看到,則GET 請求相對POST 請求更不安全。但並不是說POST 請求安全,因為參數放在請求體中,如果不採取加密手段的話,技術人員抓包就能看到明文。同時各個瀏覽器對URL 長度做了限制,比如IE 瀏覽器限制URL 的長度最大為2KB,這就使得了GET 請求傳輸的數據長度有了限制,而POST 請求傳輸數據長度無限制。
- 其次,一般GET 請求用於獲取數據,POST 請求用於新增數據。這裡需要提一下冪等性的概念。冪等性是指對於同一個系統,在同樣條件下,一次請求和重複多次請求對資源的影響是一致的,不會因為多次請求而產生了副作用。GET 請求用於獲取資源,不會對系統資源進行改變,因此是冪等的。POST 用於新增資源,這意味著多次請求將創建多個資源,因此不是冪等的。基於這個特點,GET 請求可被緩存、可保留在瀏覽器歷史記錄中、瀏覽器回退不會產生副作用,而POST 請求反之。
- 最後,GET 和POST 在本質上並無區別。HTTP 的底層是TCP,所以無論是GET 還是POST 底層都是通過TCP 進行連接通信。我們可以給GET 加請求體,給POST 帶上URL 參數,可以用GET 請求新增數據,POST 請求查詢數據,實際上也是完全可行的。
(2)請求頭
請求頭用於向服務器傳遞一些額外的重要信息,比如所能接收的語言等。
請求頭由字段名和字段值構成,二者之間用冒號進行分隔。常見的一些請求頭有:
請求頭 | 含義 |
Host | 接收請求的域名 |
User-Agent | 客戶端軟件的名稱和版本號等相關信息 |
Connection | 設置發送響應之後TCP 連接是否繼續保持的通信選項 |
Cache-Control | 控制緩存的相關信息 |
Referer | 記錄請求的來源(當通過點擊超級鏈接進入下一個頁面時,會記錄上一個頁面的URI) |
Accept | 客戶端可支持的數據類型, 以MIME 類型來表示 |
Accept-Encoding | 客戶端可支持的編碼格式 |
Accept-Language | 客戶端可支持的語言 |
If-Modified-Since | 用於判斷資源的緩存是否有效(客戶端通知服務器,本地緩存的最後變更時間) |
If-None-Match | 用於判斷資源的緩存是否有效 |
Range | 用於斷點續傳,指定第一個字節的位置和最後一個字節的位置。 |
Cookie | 表示請求者的身份,用於保存狀態信息 |
(3)請求空行
請求空行用於表明請求頭已經結束。
(4)請求體
請求體用於傳送客戶端要發給服務器的數據,比如請求參數,通常出現在POST 請求方法中,而GET 方法無請求體,它的請求參數直接會顯示在網址上面。
請求行和請求頭的數據都是文本形式且格式化的,而請求體不同,其可以包含任意的二進制數據,比如文本、圖片、視頻等等。
2. HTTP 響應
服務器向客戶端發送的信息稱為響應報文,響應報文的結構一般如下:
(1)響應行
響應行用於說明對請求的處理情況,包含三部分內容,中間用空格分割。
- HTTP 版本,指定所使用的HTTP 版本。比如HTTP/1.1 表示使用的HTTP 版本是1.1。
- 狀態碼,以三位數字形式描述服務器對請求的處理結果。比如200 表示成功。
- 消息短語,以文本形式描述服務器對請求的處理結果。比如OK 表示成功。
舉個栗子:
面試中常見的一個問題: HTTP 有哪些常見狀態碼?,在這裡做一下解答。
- 200 OK:表示請求被正常處理,這是最常見的狀態碼。
- 204 No Content:表示請求被正常處理,但在返回的響應報文中不含響應體內容。
- 301 Moved Permanently :永久重定向,表示請求的資源已經被永久轉移了,新的URL 定義在響應報文的Location 字段中,瀏覽器將自動獲取新的URL 發出新的請求。場景:比如建設一個網站後,將網站的url 變換了,重新申請一個域名,但是希望之前url 仍然可以訪問到,就可以做一個重定向到新的url 下面。比如京東最早網址http://www.360buy.com 重定向到http://www.jd.com。
- 302 Found :臨時重定向(即以後還可能有變化),表示請求的資源已被臨時分配了新的URL,新的URL 會在響應報文中的Location 字段中返回,瀏覽器將會自動使用新的URL 發出新的請求。比如用戶在未登錄時訪問個人中心頁面,這時可以臨時重定向到登錄的url;或者協議發生變化,比如京東http://www.jd.com 重定向到https://www.jd.com;再比如,今天夜裡網站後台要係統維護,服務暫時不可用,這就屬於『臨時』的,可以配置成302 跳轉,把流量臨時切換到一個靜態通知頁面,瀏覽器看到這個302 就知道這只是暫時的情況,不會做緩存優化,第二天還會訪問原來的地址。
- 304 Not Modified:代表上次的文檔已經被緩存了,還可以繼續使用,即訪問緩存。
- 400 Bad Request:一個通用差錯狀態碼,表示請求報文中存在語法錯誤,客戶端發生的錯誤。
- 401 Unauthorized :用戶未認證。
- 403 Forbidden:表示服務器雖然收到了請求,但是拒絕提供服務,常見的原因是為沒有訪問權限(即用戶未授權)。
- 404 Not Found :表示請求資源不存在。
- 500 Internal Server Error:表示服務器出現錯誤,可能是出現了一些Bug 或故障。
- 502 Bad Gateway:通常是服務器作為網關或代理時返回的錯誤碼,表示服務器自身工作正常,訪問後端服務器發生了錯誤(可能後端服務器宕機了)。
- 503 Service Unavailable:表示服務器暫時處於超負載或者正在進行停機維護,暫時無法處理請求,可以稍後再試。Web 服務器如果限流,就可以給超載的流量直接響應503 狀態碼。
(2)響應頭
響應頭用於向客戶端傳遞一些額外的重要信息,比如響應內容的長度等。
響應頭由字段名和字段值構成,二者之間用冒號進行分隔。常見的一些響應頭有:
響應頭 | 含義 |
Date | 日期時間信息,表示服務器產生並發送響應報文的日期和時間。 |
Server | 表示HTTP服務器應用程序的信息,類似於請求報文中的User-Agent |
Location | 此字段會配合重定向使用,用於提供重定向後新的URI。 |
Connection | 設置發送響應之後TCP 連接是否繼續保持的通信選項 |
Cache-Control | 控制緩存的相關信息 |
Content-Type | 服務器返回的響應類型 |
Content-length | 服務器返回的響應長度 |
Content-Encoding | 服務器返回的響應編碼 |
Content-Language | 服務器返回的響應語言 |
Last-Modified | 指定響應內容最後的修改時間 |
Expires | 表示資源失效的時間,瀏覽器會在指定過期時間內使用本地緩存 |
Etag | 用於協商緩存,返回一個摘要值 |
Accept-Ranges | 用於斷點續傳,指定服務器所支持的內容範圍 |
Set-Cookie | 設置狀態信息 |
(3)響應空行
響應空行用於表明響應頭已經結束。
(4)響應體
響應體用於傳送服務器要發給瀏覽器的正文。
同請求報文的請求體一樣,響應體可包含任意的二進制數據。瀏覽器收到響應報文後,則會將正文加載到內存,然後解析渲染,最後顯示頁面內容。
3. HTTP 持久連接
客戶端發送一系列請求給服務器,如果服務器與客戶端對每個請求/響應對都經過一個單獨的TCP 連接發送,則稱為非持續連接,也稱為短連接;如果經過相同的TCP 連接發送,則稱為持續連接,也稱為長連接。
比如打開一個Web 頁面時,假設該頁面含有一個HTML 基礎文件和2 張圖片,如果客戶端與服務器通過同一個TCP 連接來獲取這3 個數據,則為持續連接,如果通過建立3 次不同的TCP 連接,則為非持續連接。
非持續連接的缺點:
- 每次建立連接需要三次握手過程,導致總的請求響應時間變長。當然也不是絕對的,如果多個連接可以並行請求,總響應時間可能變短,比如Chrome 瀏覽器為了提升加載速度,可以同時打開6 個並行連接,但多個並行連接會加重Web 服務器負擔。
- 必須為每一個請求的對象建立和維護一個全新的連接,而每一個連接都需要客戶和服務器分配TCP 的緩衝區和保持TCP 變量,使得Web 服務器存在嚴重的負擔,因為一台Web 服務器可能同時服務於數以百計不同的客戶的請求。
HTTP(1.1 及之後) 默認採用持續連接方式,但也可配置成非持續連接方式。在報文中使用Connection 字段來表示是否使用持久連接。
- 如果Connection 字段的值為keep-alive,則表明此連接為持久連接,HTTP1.1 及以後可默認不寫。
- 如果Connection 字段的值為close,則表明要關閉連接。
注意:持久連接不是永久連接,一般在一個可配置的超時間隔後,如果此連接仍未被使用,HTTP 服務器就會關閉該連接。
4. HTTP 緩存
對於一些短時間內不會產生變化的資源,客戶端(瀏覽器)可以在一次請求後將服務器響應的資源緩存在本地,之後直接讀取本地的數據,而不必再重新發送請求。
我們經常會接觸到『緩存』這一概念,比如由於內存和CPU 之間速度差距較大,為了進一步提升電腦性能,於是設計了L1 緩存、L2 緩存等,讓CPU 先從緩存中取數據,如果取不到,再去內存取。
又比如在後端開發中,由於數據庫一般存儲在硬盤上,讀取速度較慢,於是可能會採用Redis 等內存數據庫作為緩存,先去Redis 中取數據,如果取不到,再去數據庫中取。
再比如在操作系統中,由於頁表進行地址轉換的速度較慢,於是有了TLB 快表,當需要進行邏輯地址到物理地址的轉換時,先去查詢速度更快的TLB 快表,如果查不到,再去查詢頁表,此時TLB 快表就是一種緩存。
緩存的主要目的在於提升查詢速度,一般邏輯如圖所示。
同樣,在HTTP 設計中也有緩存的概念,主要是為了加快響應速度,HTTP 緩存的實現依賴於請求報文和響應報文中的一些字段,分為強緩存和協商緩存。
(1)強緩存
強緩存指的是在緩存數據未失效的情況下,那麼就會直接使用瀏覽器的緩存數據,不會再向服務器發送任何請求,邏輯類似於前面舉的L1 緩存、Redis、TLB 快表。
具體實現主要是通過Cache-Control字段和Expires字段。
Cache-Control 是一個相對時間(即多長時間後過期,http1.1 規範),Expires 是一個絕對時間(即在某個時間點過期,http1.0 規範),如果兩個字段同時存在,Cache-Control 的優先級更高。
由於服務器端時間和客戶端時間可能不同步,存在偏差,這也就是導致了使用Expires 可能會存在時間誤差,因此一般更推薦使用Cache-Control 來實現強緩存。
以Cache-Control 為例,強緩存的具體的實現流程如下:
- 當瀏覽器第一次請求訪問服務器資源時,服務器會在響應頭中加上Cache-Control。Cache-Control 中可以設置以下內容。
max-age=秒,表示緩存將於指定毫秒值後過期。比如:cache-control: max-age=31536000,表示緩存將於365 天后過期。
no-store,表示不允許緩存(包括強緩存和協商緩存)。
no-cache,表示不使用強緩存,而是使用協商緩存,即使用之前必須要先去服務器端驗證是否失效,如果沒失效,則再使用緩存,如果失效了,則返回最新數據。等價於max-age=0, must-revalidate。
must-revalidate,表示允許緩存,並且如果緩存不過期的話,先使用緩存,如果緩存過期的話,再去服務器端進行驗證緩存是否還有效。這裡很多小伙伴可能會有疑問,即使沒有加上must-revalidate,有了max-age 後,緩存過期了不也會去服務器驗證嗎,加不加must-revalidate 有什麼區別呢?在HTTP 協議規範中,允許客戶端在某些特殊情況下直接使用過期緩存,比如校驗請求錯誤時(如無法再次連通服務器),而加上了must-revalidate 後,在校驗請求錯誤時,會返回504 錯誤碼,而不是使用過期緩存。
- 瀏覽器再次請求訪問服務器中的該資源時,根據請求資源的時間與Cache-Control 中設置的過期時間大小,計算出該資源是否過期,
如果沒有過期(且Cache-Control 沒有設置no-cache 屬性和no-store 屬性),則使用該緩存,結束;
否則重新請求服務器;
(2)協商緩存
協商緩存指的是當第一次請求後,服務器響應頭Cache-Control 字段屬性設置為no-cache 或者緩存時間過期了,那麼瀏覽器再次請求時就會與服務器進行協商,判斷緩存資源是否有效,即資源是否進行了修改更新。
- 如果資源沒有更新,那麼服務器返回304 狀態碼,表明緩存仍然可用,而不需要再次發送資源,減少了服務器的數據傳輸壓力,並更新緩存時間。
- 如果數據有更新,服務器返回200 狀態碼,新資源存放在請求體中。
協商緩存可以基於以下兩種方式來實現:
第一種(HTTP/1.0 規範):請求頭部中的If-Modified-Since 字段與響應頭部中的Last-Modified 字段:
- Last-Modified:標示這個響應資源的最後修改時間。第一次請求資源後,服務器將在響應頭中帶上此信息。
- If-Modified-Since:當資源過期了,瀏覽器再次發起請求的時候帶上Last-Modified 的時間(放在請求頭If-Modified-Since 中),服務器將此時間與被請求資源的最後修改時間進行對比,
如果最後修改時間較大,說明資源有被修改過,則返回最新資源和200 狀態碼;
否則說明資源無新修改,返回304 狀態碼。
- 此種方式存在以下問題:
基於時間實現,可能會由於時間誤差而出現不可靠問題,並且只能精確到秒級,在同一秒內,Last-Modified 無感知。
如果某些文件被修改了,但是內容並沒有任何變化(比如只是修改時間發生了變化),而Last-Modified 卻改變了,導致文件沒法使用緩存。
第二種(HTTP/1.1 規範):請求頭部中的If-None-Match 字段與響應頭部中的ETag 字段:
- Etag:唯一標識響應資源,是一個hash 值;第一次請求資源後,服務器將在響應頭中帶上此信息。
- If-None-Match:當資源過期了,瀏覽器再次向服務器發起請求時,會將請求頭If-None-Match 值設置為Etag 中的值。服務器將此值與資源的hash 值進行比對,
如果二者相等,則資源沒有變化,則返回304 狀態碼。
如果資源變化了,則返回新資源和200 狀態碼。
- 此種方式存在的問題在於計算Etag 會消耗系統性能,但可以解決第一種方式所存在的問題,推薦使用。
注意:
- 如果HTTP 響應頭部同時有Etag 和Last-Modified 字段的時候,Etag 的優先級更高,也就是先會判斷Etag 是否變化了,如果Etag 沒有變化,然後再看Last-Modified。
- Ctrl + F5 強制刷新,會直接向服務器提取數據。
- 按F5 刷新或瀏覽器的刷新按鈕,默認加上Cache-Control:max-age=0,即會走協商緩存。
5. Cookie
HTTP 是一種無狀態協議,即其本身不會記憶請求和響應之間的通信狀態,那麼Web 服務器就無法判斷此請求到底來自於哪個用戶,HTTP 協議中並不會保存關於用戶的任何信息。這樣設計的好處是不需要額外資源保存用戶狀態信息,減少了服務器的CPU 及內存資源的消耗。
但是隨著Web 的發展,很多業務需要保存用戶狀態。
- 比如電商網站需要在用戶跳轉到其他商品頁面時,仍然可以保存用戶的登錄狀態。不然用戶每訪問一次網站都要重新登錄一下,過於繁瑣,體驗效果就很差。
- 比如短視頻網站希望記錄用戶以前看過的視頻,以便之後向其精準化推薦感興趣的視頻。
為了實現保持狀態的功能,這就出現了Cookie。Cookie (服務器給的憑證)類似於我們逛商場時的會員卡(商家給的憑證),記錄著我們的身份信息,只要出示了會員卡,商場工作人員就能確定我們的身份。同樣的,只要給服務器發送報文時帶上了Cookie,他就知道我們是誰了。
Cookie 中可以包含任意信息,最常見的是包含一個服務器為了進行跟踪而產生的獨特的識別碼。
舉個栗子:
張三在發出第一次請求後,服務器將其狀態信息記錄下來,比如他的名字、年齡、地址、購物歷史等,並通過響應頭Set-Cookie字段,給予其一個id=12345 的獨特識別碼作為Cookie,那麼其再次向服務器發出請求時,瀏覽器會自動在請求報文中的Cookie 字段中帶上id=12345,服務器就可以通過這個查詢到張三的具體信息,從而實現了保持狀態的功能。
Cookie 屬性:
- max-age:過期時間有多長(絕對時間,單位:秒)。
負數,表示瀏覽器關閉即失效。默認即為-1。
正數:失效時刻= 創建時刻+ max-age。
0:表示Cookie 立即刪除,即Cookie 直接過期(從而實現使cookie 失效)。
- expires:過期時間(相對時間)。
- secure:表示這個Cookie 只會在https 的時候才會發送。
- HttpOnly:設置後無法通過使用JavaScript 腳本訪問,可以保障安全,防止攻擊者盜用用戶Cookie。
- domain:表示該Cookie 對於哪個域是有效的。(Cookie 默認是不能直接跨域訪問的,但是二級域名是可以共享cookie 的)
Cookie 的缺點是如果傳遞的狀態信息較多,使得包過大,將會降低網絡傳輸效率。
一般瀏覽器限制Cookie 大小為4KB。
6. HTTP 版本
隨著互聯網的發展,HTTP 也在不斷升級打怪,下面分別介紹一下HTTP/1.1、HTTP/2 以及HTTP/3 在前一版本基礎上的改進之處。
(1)HTTP/1.1 相比HTTP/1.0 性能上的改進
HTTP/1.1 是目前最常見的HTTP 版本,其相對於HTTP/1.0 有以下改進。
① 持久連接
這個在前文中已經提到過,HTTP/1.0 中一個TCP 連接只能發送一個請求和響應,而HTTP/1.1 進行了優化,同一個TCP 連接可以發送多次HTTP 請求,減少了建立和關閉連接的性能開銷。
Web 服務軟件一般都會提供keepalive_timeout 參數,用來指定HTTP 持久連接的超時時間。比如設置了HTTP 持久連接的超時時間是60 秒,Web 服務軟件就會啟動一個定時器,如果完成某個HTTP 請求後,在60 秒內都沒有再發起新的請求,就會觸發回調函數來釋放該連接。
② 管道機制
持久連接雖然可以多個請求復用同一個連接,但是每次都需要等到上一個請求響應完成後,才能發送下一個請求。
管道機制中,只要第一個請求發出去了,不必等其回來,就可以發第二個請求出去,即相當於同時發出多個請求,因而可以減少整體的響應時間。
雖然客戶端可以同時發出多個HTTP 請求,不用⼀個個等待響應,但是服務器必須按照接收請求的順序依次發送對這些管道化請求的響應,以保證客戶端能夠區分出每次請求的響應內容。這存在下面問題:
如果服務端在處理一個請求時耗時比較長,那麼後續請求的處理都會被阻塞住,會導致客戶端遲遲收不到數據,這稱為「隊頭堵塞」。
實際上,雖然管道機制的想法很好,但實現卻非常困難,因而很多瀏覽器根本不支持它。一般為了提升性能,採用並行多個TCP 連接的形式來實現請求的同時發送。
③ 緩存控制
前文已經提到過,HTTP/1.1 在HTTP/1.0 基礎之上,增加了一些請求響應頭,以更好的實現對緩存的控制。比如
- 新增Cache-Control 代替原先的Expires;
- 新增If-None-Match 和Etag 代替原先的 If-Modified-Since和Last-Modified 。
④ 斷點續傳
利⽤ HTTP 消息頭使⽤分塊傳輸編碼,將實體主體分塊傳輸。
(2)HTTP/2 相比HTTP/1.1 性能上的改進
HTTP/2 協議本身是基於HTTPS 的,因此更加安全,其相對於HTTP/1.1 有以下改進。
① 頭部壓縮
HTTP/1.1 中的請求頭攜帶大量信息,而且每次都要重複發送,即使是同樣的內容,每次請求都需要附帶,這會造成性能的損耗。HTTP/2 進行了優化,引入了頭信息壓縮機制。
客戶端和服務器同時維護一張頭信息表,高頻出現的字段會存入這個表,生成一個索引號。發送報文時直接使用索引號替代字段。另外,索引表中不存在的字段使用哈夫曼編碼壓縮。
同時,多個請求中,如果請求頭相同,則後續請求只需要發送差異的部分,重複的部分無需再發送。
② 二進制幀
HTTP/1.1 的報文為純文本格式,而HTTP/2 的報文全面採用二進制格式,並將原始的報文拆分為頭信息幀(Headers Frame)和數據幀(Data Frame)。採用二進制格式有利於提升數據傳輸效率。
③ 多路復用
在HTTP/2 中定義了流(Stream)的概念,它是二進制幀的雙向傳輸序列,一個數據流對應著一個完整的請求-響應過程,在同一個請求響應過程中,往返的幀會分配一個唯一的流編號(Stream ID)。
在流的支持下,HTTP/2 可以在一個TCP 連接中傳輸多個請求或響應,而不用按照順序一一對應(即實現多路復用),因為它們屬於不同的流,所發送的幀頭部都會攜帶Stream ID,可以通過此Stream ID 有效區分不同的請求-響應。
因而HTTP/2 解決了HTTP/1.1 的『隊頭阻塞』問題,多個請求- 響應之間沒有了順序關係,不需要排隊等待,降低了延遲,大幅度提高了連接的利用率。
舉個栗子:
在一個TCP 連接裡面,服務器同時收到了A 請求和B 請求,於是先回應A 請求,結果發現處理過程非常耗時,於是就發送A 請求已經處理好的部分,接著回應B 請求,完成後,再發送A 請求剩下的部分。
④ 服務端推送
在HTTP/1.1 中,只能客戶端發起請求,服務器對請求進行響應。
而在HTTP/2 中,服務端可以主動給客戶端推送必要的資源,以減少請求延遲時間。
比如當客戶端向服務器請求一個HTML 文件後,服務器除了將此HTML 文件響應給客戶端外,還可以提前主動將此HTML 中所依賴的JS 和CSS 文件推送給客戶端,這樣客戶端在解析HTML 時,無需耗費額外的請求去得到相應的JS 和CSS 文件。
(3)HTTP/3 相比HTTP/2 性能上的改進
Google 公司為了解決HTTP/2 存在的一些問題,提出了QUIC 協議,而HTTP-over-QUIC 就是HTTP/3,其相對於HTTP/2 有以下改進。
① 無隊頭阻塞
前面提到,HTTP/2 通過多路復用解決了HTTP1.1 的『隊頭阻塞』問題,但其只是解決了HTTP 這一層面的『隊頭阻塞』問題,底層仍然採用的TCP 連接,HTTP/2 並沒有解決TCP 的『隊頭阻塞』問題。
TCP 是可靠的、面向字節流的協議。HTTP/2 的多個請求雖然可以跑在同一個TCP 連接中,但如果出現丟包現象,TCP 就需要進行重傳,這可能就會導致整個TCP 連接上的所有流阻塞,直到丟的包重傳成功,這就是TCP 的『隊頭阻塞』問題。
為了解決此問題,HTTP/3 底層不再使用TCP,而是採用UDP!而UDP 是無連接的,多個流互相獨立,之間不再有依賴,因而即使某個流發生了丟包,只會對該流產生影響,並不會使得其他流阻塞!
這時候有的小伙伴可能會問了,HTTP/3 底層不採用TCP,那怎麼保證可靠傳輸呢?答案就是HTTP/3 在應用層自己重新實現了可靠性機制。也就是說,HTTP/3 將原先TCP 協議提供的部分功能上移至QUIC,而且進行了改進。
② 優化重傳機制
TCP 採用序號+確認號+超時重傳機制來保證消息的可靠性,即如果某條消息超過一定時間還沒有得到確認,則重新發送此消息。
由於網絡擁堵情況不斷變化,因而消息的超時時間並不是固定的,而是通過不斷採樣消息的往返時間不斷調整的,但TCP 超時採樣存在不准確的問題。
舉個栗子:
客戶端發送一個序號為N 的包,然後超時了(可能丟了,也可能網絡堵塞了),於是重新發送一個序號為N 的包,之後服務器收到後返回一個確認號ACK 為N+1 的包。但此時客戶端並無法判斷這個確定包是對原始報文的確認還是重傳報文的確認,那麼此時往返時間應該如何計算呢?
- 如果認為確認包是對原始報文的確認,則可能把時間算長了;
- 如果認為確認包是對重傳報文的確認,則可能包時間算長了。
因而TCP 的重傳超時時間計算不准確,如果計算偏大,則效率慢,很久才會重傳,而如果計算偏小,則可能確認報文已經在路上了,但卻重傳了!
QUIC 是如何解決此問題呢?其定義了一個遞增的序列號(不再叫Seq,而是Packet Number),每個序列號的包只發送一次,即使重傳相同的包,其序列號也不一樣。
舉個栗子:
客戶端發送一個序號為N 的包,然後超時了,於是重新發送一個相同的包,但序號不再是N,而是N+1;那麼如果返回的確認包ACK 為N+1,就是對原始報文的響應,如果ACK 為N+2,就是對重傳報文的響應,因而採樣時間計算相對更加準確!
那此時怎麼知道包N 和包N+1 是同一個包呢?QUIC 定義了一個Offset 概念。發送的數據有個偏移量Offset,可以通過Offset 知道數據目前發送到了哪裡,因而如果某個Offset 的包沒有收到確認,就重發。
③ 連接遷移
眾所周知,一條TCP 連接是由四元組標識的,分別是源IP、源端口、目的IP、目的端口。一旦其中一個元素發生了變化,就需要斷開重連。
當手機信號不穩定或者在WIFI 與移動網絡切換時,都將會導致重連,而重連就意味著需要重新進行三次握手,將產生一定的時延,用戶感到卡頓,體驗不友好。
而QUIC 不採用四元組的方式標識連接,而是以一個64 位的隨機數作為ID 來標識,通過此連接ID 標記通信的兩端,之後即使網絡發生變化,IP 或端口變了,但只要ID 不變,則無需重連,只需要復用原先連接即可,時延低,減少了用戶的卡頓感,實現連接遷移。
(4)總結
本文轉載自微信公眾號「 一楓說碼」,作者「一楓說碼」,可以通過以下二維碼關注。
轉載本文請聯繫「 一楓說碼」公眾號。