HTTP
為什麼會出現 HTTP 協定,從 HTTP1.0 到 HTTP3 經曆了什麼?HTTPS 又是怎麼回事?
HTTP 是一種用于擷取類似于 HTML 這樣的資源的 應用層通信協定, 他是網際網路的基礎,是一種 CS 架構的協定,通常來說,HTTP 協定一般由浏覽器等 “用戶端” 發起,發起的這個請求被稱為 Request, 服務端接受到用戶端的請求後,會傳回給用戶端所請求的資源,這一過程被稱為 Response,在大部分情況下,用戶端和伺服器之間還可能存在許多 proxies,他們的作用可能各不相同,有些可能作為網關存在,有些可能作為緩存存在。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5yY0YmYyUjN4I2N2QmYiFzYjJTO5QjY2QmNkJGMhJ2N18CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
HTTP 協定有三個基本的特性:
- 簡單:HTTP 的協定和封包是簡單,易于了解和閱讀的(HTTP/2 已經改用二進制傳輸資料,但 HTTP 整體還是簡單的)
- 可拓展的:請求和響應都包括 “Header” 和 “Body” 兩部分,我們可以通過添加頭部字段輕松的拓展 HTTP 的功能
- 無狀态的:服務端不儲存用戶端狀态,也就是說每一次請求的服務端來說都是唯一無差别的,我們隻能通過 Cookie 等技術建立有狀态的會話。
HTTP 的曆史
HTTP 的曆史可以追溯到網際網路剛被發明的時候,1989年, Tim Berners-Lee 博士寫了一份關于建立一個通過網絡傳輸超文本系統的報告。該系統起初被命名為 Mesh,在随後的1990年項目實施期間被更名為網際網路(*World Wide Web)。他以現有的 TCP IP 協定為基礎建造, 由四個部分組成:
- 用來表示超文本文檔的文本格式,即超文本标記語言(HTML)
- 一個用來傳輸超文本的簡單應用層協定,即超文本傳輸協定(HTTP)
- 一個用來顯示或編輯超文本文檔的用戶端,即網絡浏覽器,而第一個浏覽器則被稱為 WorldWideWeb
- 一個用于提供可通路文檔的服務,httpd 的前身.
這四部分在 1990 年底完成,這時候的 HTTP 協定還很簡單,後來為了于其他版本的協定區分,最初的 HTTP 協定被記為 HTTP/0.9,
後來,随着計算機技術的發展,HTTP 協定也随着 HTTP/1.0, HTTP/1.1, HTTP/2 等關鍵版本更疊變得更加高效實用。
HTTP/0.9 on-line
最初的 0.9 版本也被稱為單行協定(on-line), 基于 TCP 協定,該版本下隻有一個可用的請求方法:GET, 請求格式也相當簡單:
GET /index.html
它表示用戶端請求
index.html
的内容,0.9 版本的 HTTP 響應也同樣簡單,他隻允許響應 HTML 格式的字元串,如:
<html>
<h1> ..... </h1>
</html>
這一階段的響應甚至沒有響應頭,也沒有響應碼或錯誤代碼,一旦出現問題,服務端會響應一段特殊的 HTML 字元串以便用戶端檢視。 服務端在發送完資料後,就會立刻關閉 TCP 連接配接。
HTTP/1.0
0.9 版本的 HTTP 協定太過于簡單甚至是簡陋,而随着浏覽器和伺服器的應用被擴充到越來越多的領域,0.9 版本的協定已經不能适應,直到 1996年11月,RFC 1945 定義了 HTTP/1.0, 但他并不是官方标準,該版本的 HTTP 協定較 0.9 版本有了一下改變:
- 版本号被添加到了請求頭上,像下面這樣:
GET /mypage.html HTTP/1.0
- 引入了 HTTP頭的概念,無論是請求還是響應,允許傳輸中繼資料,這使得協定更加靈活和具有拓展性。
- 請求方法拓展到了 GET,HEAD,POST
- 在新 HTTP 頭(
)的幫助下,可以傳輸不止 HTML 的任意格式的資料。Content-Type
- 響應時帶上了狀态碼,使得浏覽器能夠知道響應的狀态并作出響應的處理。
- …
HTTP/1.1
同 0.9 版本一樣,1.0 版本下,TCP 連接配接是不能複用的,資料發送完後服務端會立刻關閉連接配接,但由于建立 TCP 連接配接的代價較大,是以 1.0 版本的 HTTP 協定并不是足夠高效,加上 HTTP/1.0 多種不同的實作方式在實際運用中顯得有些混亂,自1995年就開始了 HTTP 的第一個标準化版本的修訂工作,到1997年初,HTTP1.1 标準釋出。
1.1 版本的改進包括:
- 支援長連接配接:在 HTTP1.1 中預設開啟 Connection: keep-alive,允許在一個 TCP 連接配接上傳輸多個 HTTP 請求和響應,減少了建立和關閉連接配接造成的性能消耗。
- 支援
: HTTP/1.1 還支援流水線(pipline)工作,流水線是指在同一條長連接配接上發出連續的請求,而不用等待應答傳回。這樣可以避免連接配接延遲。pipline
- 支援響應分塊:對于比較大的響應,HTTP/1.2 通過
首部支援将其分割成多個任意大小的分塊,每個資料塊在發送時都會附上塊的長 度,最後用一個零長度的塊作為消息結束的标志。Transfer-Encoding
- 新的緩存控制機制:HTTP/1.1定義的
頭用來區分對緩存機制的支援情況,同時,還提供Cache-Control
,If-None-Match
,ETag
,Last-Modified
等實作緩存的驗證等工作。If-Modified-Since
- 允許不同域名配置到同一IP的伺服器上:在 HTTP/1.0 時,認為每台伺服器綁定一個唯一的 IP,但随着技術的進步,一台伺服器的多個虛拟主機會共享一個IP,為了區分同一伺服器上的不同服務,HTTP/1.1 在請求頭中加入了
字段,它指明了請求将要發送到的伺服器主機名和端口号,這是一個必須字段,請求缺少該字段服務端将會傳回 400.HOST
- 引入内容協商機制,包括語言,編碼,類型等,并允許用戶端和伺服器之間約定以最合适的内容進行交換。
- 使用了 100 狀态碼:HTTP/1.0 中,定義:
在 2.0 版本時,使用了這個保留的狀态碼,用來表示臨時響應。o 1xx: Informational - Not used, but reserved for future use
HTTPS
HTTP/1.1 之後,對 HTTP 協定的拓展變得更加簡單,但 HTTP 依然存在一個天然的缺陷就是明文傳輸資料,直到 1994 年底,網景公司在 TCP/IP 協定棧的基礎上添加了 SSL 層用來加密傳輸,後來,在标準化的過程中, SSL 成了 TLS (Transport Layer Security 傳輸層安全協定),基于 HTTPS 通信的用戶端和伺服器在建立完 TCP 連接配接之後會協商通信密鑰,在之後的通信過程中, 用戶端和伺服器會使用該密鑰對資料進行對稱加密,以防資料被竊取或篡改。(密鑰協商階段會使用非對稱加密)。
HTTP/2
HTTP/1.1 雖然允許連接配接複用和以流水線方式運作,但在一個 TCP 連接配接裡面,所有資料依然還是按序發送的,伺服器隻能處理完一個請求再去處理另一個請求,如果第一個請求非常慢,就會造成後面的請求長時間阻塞,這被稱為 隊頭阻塞(Head-of-line blocking),2009 年,谷歌公開了自行研發的 SPDY 協定,它基于 HTTPS,并采用多路複用解決了隊頭阻塞的問題,同時,它還使用了 Header 壓縮等技術大大降低了延時并提高了帶寬使用率,在之後的 2015 到 2019 年間,谷歌在自家浏覽器上實踐和證明了這個協定,SPDY 也成了 HTTP/2 的基石。
2015 年 5 月, HTTP/2 正式标準化,他與 1.x 版本 不同在于:
- 1.x 版的 HTTP 協定傳輸的是文本資訊,這對開發者很友好,但卻浪費了計算機的性能,HTTP/2 改成了基于二進制而不再是基于文本的協定,
- 這是一個複用協定。并行的請求能在同一個連結中處理,移除了HTTP/1.x中順序和阻塞的限制。
- 壓縮了headers。因為headers在一系列請求中常常是相似的,其移除了重複和傳輸重複資料的成本。
- 其允許伺服器在用戶端緩存中填充資料,通過一個叫伺服器推送的機制來提前請求。
雖然 HTTP/2 2015 年就被标準化,在到目前為止,HTTP/1.1 任然被廣泛使用,據 MySSL 的最新統計,截至 2020 年 12 月,已有 65.84% 的站點支援了 HTTP/2.
HTTP/3
HTTP/3 是即将到來的第三個主要版本的 HTTP 協定,在 HTTP/3 中,将棄用 TCP 協定,改為使用基于 UDP 的 QUIC 協定實作。QUIC(快速UDP網絡連接配接)是一種實驗性的網絡傳輸協定,由Google開發,該協定旨在使網頁傳輸更快。
在2018年10月28日的郵件清單讨論中,IETF(網際網路工程任務組) HTTP和QUIC工作組主席 Mark Nottingham 提出了将 HTTP-over-QUIC 更名為 HTTP/3 的正式請求,以“明确地将其辨別為HTTP語義的另一個綁定……使人們了解它與 QUIC 的不同”,并在最終确定并釋出草案後,将 QUIC 工作組繼承到 HTTP 工作組, 在随後的幾天讨論中,Mark Nottingham 的提議得到了 IETF 成員的接受,他們在2018年11月給出了官方準許,認可 HTTP-over-QUIC 成為 HTTP/3。
2019年9月,HTTP/3支援已添加到 CloudFlare 和 Chrome 上。Firefox Nightly 也将在2019年秋季之後添加支援。
HTTP/1.1 細節
HTTP封包
HTTP 的封包都由消息頭和消息體兩部分組成,兩者之間以
CRLF(回車換行)
分割。
請求頭格式
請求頭第一行為請求行,其餘為請求頭字段:如下:
POST /api/article/list HTTP/1.1
Host: junebao.top:8888
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0
Accept: application/json, text/plain, */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Content-Type: application/json;charset=utf-8
Content-Length: 32
Origin: https://junebao.top
Connection: keep-alive
Referer: https://junebao.top/
Cache-Control: max-age=0
請求行由三部分組成:
- 請求方法
- 請求資源的 url
- 協定版本
他們以空格分隔,RFC2068 定義了其中不同的請求方法,他們分别為 OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE,除此之外,後來還添加了一個 PATCH 方法。
方法 | 基本用法 | 請求 | 響應 | 幂等性 | 緩存 | 安全性 |
---|---|---|---|---|---|---|
OPTIONS | 擷取目的資源所支援的通信選項, 如檢測伺服器所支援的請求方法或CORS預檢請求 | 不能攜帶請求體或資料 | 可以攜帶響應體,但一般有效資料被放在頭部如 Allow 等字段 | 幂等 | 不可緩存 | 安全 |
GET | 用于擷取某個資源 | 參數一般攜帶在 URL 後面,沒有請求體 | 有響應體 | 幂等 | 可緩存 | 安全 |
HEAD | 用于請求資源的頭部資訊,如下載下傳前擷取大檔案的大小 | 沒有請求體 | 沒有響應體,響應頭應該與使用 GET 請求時的一樣 | 幂等 | 可緩存 | 安全 |
POST | 将資料發送給伺服器 | 資料放在請求體中 | 有響應體 | 不幂等 | 可緩存(包含新鮮資訊時) | 不安全 |
PUT | 使用請求中的負載建立或替換目标資源 | 資料放在請求體中 | 有響應體 | 幂等 | 不可緩存 | 不安全 |
DELETE | 删除指定資源 | 可以由請求體 | 可以由響應體 | 幂等 | 不可緩存 | 不安全 |
TRACE | 回顯伺服器收到的請求,主要用于測試或診斷。 | 無請求體 | 無響應體 | 幂等 | 不可緩存 | 不安全 |
PATCH | 作為 PUT 的補充,用于修改已知資源的部分 | 有請求體 | 無響應體 | 非幂等 | 不可緩存 | 不安全 |
請求頭字段
RFC 2068 提供了 17 種請求頭字段,但 HTTP 協定是易于拓展的,我們可以根據自己的需要添加自己的請求頭,常見的請求頭字段包括:
字段 | 作用 | 示例 |
---|---|---|
HOST | 指明了要發送到的伺服器的主機号和端口号,這是一個必須字段,缺失伺服器一般會傳回 400, 端口号預設 80 和 443 | Host: www.baidu.com |
ACCEPT | 告知伺服器用戶端可以處理的内容類型,用MIME類型來表示。 | Accept: text/html |
User-Agent | 使用者代理辨別 | |
Cookies | 用于維持會話 | |
… | … | … |
響應頭格式
Response = Status-Line
*( general-header
| response-header
| entity-header )
CRLF
[ message-body ]
類似于請求頭,響應頭包括狀态行和響應頭字段兩部分組成。
狀态行包括協定版本,狀态碼,狀态描述三部分組成,類似:
http/2 200 ok
目前 http 使用的狀态碼分為 5 類:
- 1xx: 資訊響應類
- 2xx: 正常響應類
- 3xx: 重定向類
- 4xx: 用戶端錯誤類
- 5xx: 服務端錯誤類
常見狀态碼
狀态碼 | 描述 | 作用 |
---|---|---|
100 | Continue | 迄今為止的所有内容都是可行的,用戶端應該繼續請求 |
200 | Ok | 請求成功 |
201 | Created | 該請求已成功,并是以建立了一個新的資源。這通常是在POST請求,或是某些PUT請求之後傳回的響應。 |
301 | Moved Permanently | 永久重定向 |
302 | Found | 臨時重定向 |
400 | Bad Request | 請求參數錯誤或語義錯誤 |
401 | Unauthorized | 請求未認證 |
403 | Forbidden | 拒絕服務 |
404 | Not Found | 資源不存在 |
429 | Too Many Requests | 超過請求速率限制(節流) |
500 | Internal Server Error | 服務端未知異常 |
501 | Not Implemented | 此請求方法不被服務端支援 |
502 | Bad Gateway | 網關錯誤 |
503 | Service Unavailable | 服務不可用 |
504 | Gateway Timeout | 網關逾時 |
505 | HTTP Version Not Supported | HTTP 版本不被支援 |
無狀态的 HTTP
HTTP 是一個無狀态的協定,為了維持會話,每用戶端請求時,都應該攜帶一個 “憑證”,證明 who am i, 目前維持會話常用的技術有:cookie, session, token, 等
cookie
RFC 6265 定義了 Cookie 的工作方式, Cookie 是伺服器發送給用戶端并存儲在本地的一小段資料,在使用者第一次登入時,伺服器生成 Cookie 并在響應頭裡添加
Set-Cookie
字段,用戶端收到響應後,将
Set-Cookie
字段的值(Cookie)存儲在本地,以後每次請求時,用戶端會自動通過
Cookie
字段攜帶 Cookie。
Cookie 以鍵值的形式儲存,除了必須的 Name 和 Value,還可以為 Cookie 設定以下屬性:
- Domain:指定了哪寫主機可以接收該 Cookie,預設為 Origin, 不包含子域名。
- Path:規定了請求主機下的哪些路徑時要攜帶該 Cookie。
- Expires/Max-Age: 規定該 Cookie 過期時間或最大生存時間,該時間隻與用戶端有關。
- HttpOnly: JavaScript
API 無法通路帶有 HttpOnly 屬性的cookie,用于預防 XSS 攻擊;用于持久化會話的 Cookie 一般應該設定 HttpOnly 。Document.cookie
- Secure:标記為 Secure 的 Cookie 隻能使用 HTTPS 加密傳輸給伺服器,是以可以防止中間人攻擊,但 Cookie 天生具有不安全性,任何敏感資料都不應該使用 Cookie 傳輸,哪怕标記了 secure.
- Priority:
- SameSite:要求該 Cookie 在跨站請求時不會被發送,用來阻止 CSRF 攻擊,它有三種可選的值:
- None:在同站請求和跨站請求時都會攜帶上 Cookie
- Strict:隻會在通路同站請求時帶上 Cookie
- Lex:與 Strict 類似,但使用者從外部站點導航至URL時(例如通過連結)除外,新版浏覽器一般以 Lex 為預設選項。
Cookie 被完全儲存在用戶端,對用戶端使用者來說是透明的,使用者可以自己建立和修改 Cookie,是以将敏感資訊(如用于持久化會話的使用者身份資訊等)存放在 Cookie 中是十分危險的,如果不得已需要使用 Cookie 來存儲和傳遞這類資訊,應該考慮使用 JWT 等類似機制。
由于 Cookie 的不安全性,絕大部分 Web 站點已經開始停止使用 Cookie 持久化會話,但 Cookie 在一些對安全性要求不高的場景下依然被廣泛使用,如:
- 個性化設定
- 浏覽器使用者行為跟蹤。
了解更多:
超級 Cookie 和僵屍 Cookie
決戰僵屍 Cookie
SESSION
Cookie 不安全的根源在于它将會話資訊儲存在了用戶端,為此,就有了使用 Session 持久化會話的方案,使用者在第一次登入時,伺服器會将使用者會話狀态資訊儲存在伺服器記憶體中,同時會為這段資訊生成一串唯一索引,将這個索引作為 Cookie (Name 一般為 SESSION_IDSESSION_ID)傳回給用戶端,用戶端下一次請求時,會自動攜帶這個 SESSION_ID, 伺服器隻需要根據 SESSION_ID 的值找到對應的狀态資訊就可以知道這次請求是誰發起的。
SESSION 很大程度上還是依賴于 Cookie,但這時 Cookie 中儲存的已經是一段對用戶端來說無意義的字元串了,是以使用 Session 能安全的實作會話持久化,但 Session 資訊被儲存在伺服器記憶體中,可能造成伺服器壓力過大,并且在分布式和前後端分離的環境下,Session 并不容易拓展。
TOKEN
Cookie 和 Session 都是開箱即用的 API,是以,他們不可避免地缺少靈活性,在一般開發中,往往采用更靈活地 Token,Token 與 Session 原理一緻,都是将會話資訊儲存到伺服器,然後向用戶端傳回一個該資訊的索引(token),但 Token 完全由開發者實作,可以根據需要将會話資訊存儲在記憶體,資料庫,檔案等地方,而對于該資訊的索引,也可以根據具體需要選擇使用請求頭,請求體或者 Cookie 傳遞,也不必拘束于隻 Cookie 傳遞。
JWT
全稱 json web token, 是一種用戶端存儲會話狀态的技術,它使用數字簽名技術防止了負荷資訊被篡改,jwt 包含三部分資訊:
- Header:包含 token 類型和算法名稱
- Payload:存儲的負載資訊(敏感資訊不應該明文存放在此)
- Signature:服務端使用私鑰對 Header 和 Payload 的簽名,防止資訊被篡改。
這三部分原本都是 json 字元串,最終他們會經過 Base64 編碼後拼接到一起,使用
.
分割。
分布式解決方案
在分布式場景下,同一使用者的不同次請求可能會被打到不同的伺服器上,這時如果還像單機時那樣存儲,就會出問題,一般的解決方案包括:
- 粘性 session:将使用者綁定到一台伺服器上,如 Nginx 負載均衡政策使用 ip_hash, 但這樣如果目前伺服器發生故障,可能導緻配置設定到這台伺服器上的使用者登入資訊失效,容錯度低。
- session 複制:一台伺服器的 session 改變,就廣播給所有伺服器,但會影響伺服器性能
- session 共享:把所有伺服器的 session 放在一起,如使用 redis 等分布式緩存做 session 叢集。
- 用戶端記錄狀态:使用諸如 JWT 之類的方法。
連接配接管理
連接配接管理是一個 HTTP 的關鍵話題:打開和保持連接配接在很大程度上影響着網站和 Web 應用程式的性能。在 HTTP/1.x 裡有多種模型:短連接配接 ,長連接配接和HTTP 流水線
短連接配接
HTTP 最早期的模型,也是 HTTP/1.x 的預設模型,是短連接配接。每發起一個HTTP請求都會通過三次握手建立一個TCP連接配接,在接受到資料之後再通過四次揮手釋放連接配接,因為TCP連接配接的建立和釋放都是一個耗時操作,加之現代網頁可能需要多次連續請求才能渲染完成,這就顯得這種簡單的模型效率低下。
TCP 協定握手本身就是耗費時間的,是以 TCP 可以保持更多的熱連接配接來适應負載。短連接配接破壞了 TCP 具備的能力,新的冷連接配接降低了其性能。
為此,HTTP/1.1 時新增加了兩種連接配接管理模式,分别是長連接配接和流水線,在HTTP/2 中,又基于資料流采用了新的連接配接管理模式。
長連接配接
長連接配接是指在用戶端接受完資料後,不立刻關閉這個 TCP 連接配接,這個連接配接還可以用來發送和接收其他 HTTP 資料,這樣一來可以減少部分連接配接建立和釋放的耗時,但這個連接配接也并不會一直保持,服務端可以設定
Keep-alive
标頭來指定一個最小的連接配接保持時間(機關秒)和最大請求數:
HTTP/1.1 200 OK
Connection: Keep-Alive
Keep-Alive: timeout=5, max=1000
HTTP/1.0 裡預設并不使用長連接配接。把設定成
Connection
以外的其它參數都可以讓其保持長連接配接,通常會設定為
close
在 HTTP/1.1 裡,預設就是長連接配接的,協定頭都不用再去聲明它(但我們還是會把它加上,萬一某個時候因為某種原因要退回到 HTTP/1.0 呢)。
retry-after。
長連接配接并不總是好的,比如,他在空閑狀态下仍會消耗伺服器資源,而在網絡重負載時,還有可能遭受 DoS 攻擊。這種場景下,可以使用非長連接配接,即盡快關閉那些空閑的連接配接,也能對性能有所提升。
流水線
預設情況下,HTTP 請求是按順序發出的。下一個請求隻有在目前請求收到應答過後才會被發出。由于會受到網絡延遲和帶寬的限制,在下一個請求被發送到伺服器之前,可能需要等待很長時間。
流水線是在同一條長連接配接上發出連續的請求,而不用等待應答傳回。這樣可以避免連接配接延遲。理論上講,性能還會因為兩個 HTTP 請求有可能被打包到一個 TCP 消息包中而得到提升,就算 HTTP 請求不斷的繼續導緻 TCP 包的尺寸增加,通過設定 TCP 的 MSS(Maximum Segment Size) 選項,流水線方式仍然足夠包含一系列簡單的請求。
使用流水線的另一個需要注意的問題是錯誤重傳,是以,隻有幂等的方法,如 GET,HEAD,PUT, DELETE 等方法能夠安全地使用流水線。
流水線隻是針對用戶端來說的,伺服器依然和非流水線方式那樣工作,這就導緻如果第一個請求非常耗時,那流水線上後面的請求就會被阻塞住,這種現象被稱為Head-of-line blocking(隊頭阻塞),除此之外,複雜的網絡環境和代理伺服器也可能會導緻流水線不能像預期的那樣高效工作,是以,現代浏覽器都沒有預設啟用流水線,在 HTTP/2 裡,有更高效的算法代替了流水線。
三者比較
CORS
在前後端分離開發時,你也許遇到過類似這樣的報錯:
Access to XMLHttpRequest at '*' from origin '*' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
這就是 CORS 的問題了,所謂 CORS (Cross-Origin Resource Sharing,跨域資源共享),它首先是一個系統,由一系列 HTTP 頭組成,這些 HTTP 頭決定了浏覽器是否阻止前端 JavaScript 代碼擷取跨域請求的響應。
之是以需要 CORS,是由于浏覽器的同源安全政策:
同源安全政策
同源安全政策用來限制一個源(origin)的文檔或者它加載的腳本如何能與另一個源的資源進行互動。它能幫助阻隔惡意文檔,減少可能被攻擊的媒介。
隻有兩個 URL 的協定,主機,端口都相同時,他們才被認為是“同源的”,反之,如:
http://www.a.com
和
https://www.a.com
則會被認為是不同源的(協定不同),在預設情況下,同源政策會阻止通過不同源的URL擷取資源,而 CORS 就是提供了一種機制,以允許不同源的資源進行共享。
原理
CORS 的原理很簡單,它通過添加一組 HTTP 頭,允許伺服器聲明哪些源站通過浏覽器有權限通路哪些資源。另外,規範要求,對那些可能對伺服器資料産生副作用的 HTTP 請求方法(非簡單請求),浏覽器必須首先使用 OPTIONS 方法發起一個預檢請求,進而獲知服務端是否允許該跨源請求。伺服器确認允許之後,才發起實際的 HTTP 請求。在預檢請求的傳回中,伺服器端也可以通知用戶端,是否需要攜帶身份憑證(包括 Cookies 或 HTTP 認證相關資料)。
上面說到的 “可能對伺服器資料産生副作用的 HTTP 請求” 就是非簡單請求(not-so-simple request),與之對應的是簡單請求(simple request),同時滿足以下幾個條件的,屬于簡單請求。
- 請求方法是以下三種方法之一:
- HEAD
- GET
- POST
- 首部字段隻包含被使用者代理自動設定的首部字段(例如 Connection ,User-Agent)和允許人為設定的字段為 Fetch 規範定義的 對 CORS 安全的首部字段集合。該集合為:
- Accept
- Accept-Language
- Content-Language
- Content-Type, 隻限于三個值
、application/x-www-form-urlencoded
、multipart/form-data
text/plain
- DPR
- Downlink
- Save-Data
- Width
- Viewport-Width
- 請求中的任意
對象均沒有注冊任何事件監聽器;XMLHttpRequestUpload
對象可以使用XMLHttpRequestUpload
屬性通路。XMLHttpRequest.upload
- 請求中沒有使用
對象。ReadableStream
隻要有其一不滿足,就是費簡單請求,非簡單請求在正式請求之前會先使用
OPTION
方法像伺服器發起一個 預檢請求,如下面這個請求:
var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/post-here/';
var body = '<?xml version="1.0"?><person><name>Arun</name></person>';
function callOtherDomain(){
if(invocation)
{
invocation.open('POST', url, true);
invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
invocation.setRequestHeader('Content-Type', 'application/xml');
invocation.onreadystatechange = handler;
invocation.send(body);
}
}
目前域為
foo.example.com
,請求
bar.other
, 屬于跨域請求,并且請求時自己添加了一個請求頭
X-PINGOTHER
,并且
Content-Type
類型為
application/xml
, 是以它屬于一個非簡單請求,在實際請求之前需要使用
OPTION
方法發一個預檢請求:
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
預檢請求頭頭中最重要的部分有下面幾個:
-
: 要請求的域Host
-
: 發起請求的域,Origin
和Host
不一樣,說明是跨域請求Origin
-
: 正式的請求将要使用的方法Access-Control-Request-Method
-
: 正式請求将攜帶的自定義字段Access-Control-Request-Headers
伺服器在收到這樣的預檢請求後就可以根據請求頭決定是否允許即将發送的實際請求,在伺服器的響應中,最重要的字段有以下幾個:
-
: 伺服器允許的域,允許所有域該值設定為Access-Control-Allow-Origin
*
-
: 伺服器允許的請求方法,允許所有方法設定為Access-Control-Allow-Methods
*
-
: 伺服器允許的請求頭Access-Control-Allow-Headers
-
: 該響應的有效時間為 86400 秒,也就是 24 小時。在有效時間内,浏覽器無須為同一請求再次發起預檢請求。Access-Control-Max-Age
接受到響應後,浏覽器會自動判斷實際請求是否被允許,如果不被允許,将會報上面的錯誤。
對于簡單請求,通過請求中的
Origin
和響應中的
Access-Control-Allow-Origin
就可以實作簡單的通路控制,如果請求的
Origin
不在許可範圍内,伺服器會傳回一個正常的響應,浏覽器發現這個響應的頭資訊沒有包含
Access-Control-Allow-Origin
字段,就知道出錯了,進而抛出一個錯誤,被
XMLHttpRequest
的
onerror
回調函數捕獲。注意,這種錯誤無法通過狀态碼識别,因為HTTP回應的狀态碼有可能是200。
HTTP 緩存
緩存是一種儲存資源副本并在下次請求時直接使用該副本的技術。當 web 緩存發現請求的資源已經被存儲,它會攔截請求,傳回該資源的拷貝,而不會去源伺服器重新下載下傳。這樣帶來的好處有:緩解伺服器端壓力,提升性能(擷取資源的耗時更短了)。對于網站來說,緩存是達到高性能的重要組成部分。緩存需要合理配置,因為并不是所有資源都是永久不變的:重要的是對一個資源的緩存應截止到其下一次發生改變(即不能緩存過期的資源)。
緩存有很多種,以服務對象分類,緩存可以分為私有緩存和共享緩存,以行為分類,又可以把它分為強制緩存和對比緩存。
- 私有緩存:又叫浏覽器緩存,隻能用于單獨使用者。浏覽器緩存擁有使用者通過HTTP下載下傳的所有文檔。這些緩存為浏覽過的文檔提供向後/向前導航,儲存網頁,檢視源碼等功能,可以避免再次向伺服器發起多餘的請求。它同樣可以提供緩存内容的離線浏覽。
- 共享緩存:又叫代理緩存,共享緩存可以被多個使用者使用。例如,ISP 或你所在的公司可能會架設一個 web 代理來作為本地網絡基礎的一部分提供給使用者。這樣熱門的資源就會被重複使用,減少網絡擁堵與延遲。
- 強制緩存:緩存資料未失效時,都可以使用緩存。
- 對比緩存:使用資料前需要請求伺服器驗證緩存是否失效。
緩存控制
緩存的原理很簡單:用戶端在從伺服器擷取到資料後,可以選擇将這些資料存儲下來,下一次請求同樣的資料時,就可以不請求伺服器直接傳回先前存儲的資料了,正确使用緩存可以提高響應速度,降低伺服器壓力;
這裡的用戶端可以是浏覽器(如私有緩存),也可以是請求鍊路上的中間代理(如共享緩存),但對伺服器來說,他們都是一樣的,而伺服器并沒有辦法主動向用戶端推送資料,這就導緻必須有一種機制去保證緩存是“新鮮”的,HTTP 協定通過一些列的頭字段實作了緩存控制,其中最重要的字段是
Cache-Control
,他有以下幾種值:
-
: 禁用緩存,緩存不會存儲任何響應資料,每次請求都從伺服器擷取最新的資料。Cache-Control: no-store
-
: 使用緩存,但需要伺服器重新驗證,此方式下,每次有請求發出時,緩存會将此請求發到伺服器,伺服器端會驗證請求中所描述的緩存是否過期,若未過期(傳回304),則緩存才使用本地緩存副本。Cache-Control: no-cache
-
: 私有緩存,表示該響應是專用于某單個使用者的,中間人不能緩存此響應,該響應隻能應用于浏覽器私有緩存中。Cache-Control: private
-
:表示該響應可以被任何中間人(比如中間代理、CDN等)緩存。若指定了"public",則一些通常不被中間人緩存的頁面(預設是private)(比如 帶有HTTP驗證資訊(帳号密碼)的頁面 或 某些特定狀态碼的頁面),将會被其緩存。Cache-Control: public
-
: 表示資源能被緩存的最大時間,機關秒。Cache-Control: max-age=31536000
-
: 緩存在考慮使用一個陳舊的資源時,必須先驗證它的狀态,已過期的緩存将不被使用。Cache-Control: must-revalidate
強制緩存
在某個資源的響應中,如果
Cache-Control:max-age=31536000
, 則表明這個資源在未來一年内再次請求可以直接從緩存中拿,如第一次請求 avatar.png 時,響應裡标明最大有效時間為 600s (10 分鐘),第二次再次請求該資源時,從 size 列就可以看倒該資源直接從緩存傳回。
第一次,未使用緩存 | 第二次,使用強制緩存 |
---|---|
| |
這裡的緩存就是強制緩存,隻要在10分鐘内,都可以使用緩存的資源。
如果過了10分鐘,緩存中的這個資源就可能是過期了的,這時就需要詢問伺服器這個資源是不是“新鮮”的,具體用戶端會向伺服器發起一個攜帶
[If-None-Match](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match)
頭的請求:
如果這個資源是“新鮮”的,伺服器會傳回
304
(Not Modified)(該響應不會有帶有實體資訊),如果伺服器發現這個資源已經過期了,則會傳回新的資源。
對比緩存
與強制緩存不同,對比緩存每次使用緩存資料前都會向伺服器查詢該資源是否有效,但由于查詢和響應大部分情況下都隻包含頭部,是以比起不使用緩存,對比緩存也可以大大提高響應速度和降低伺服器壓力。它依賴于下面幾個頭部字段:
-
: 響應頭字段,告訴用戶端這個資源最後更新的時間Last-Modified
-
: 請求頭字段,如果請求頭中攜帶了這個字段,伺服器會将該字段的值和資源最後修改的時間做對比,如果最後修改的時間大于字段值,說明資料已經被修改,則響應 200, 傳回最新的資源,否則,響應 304 告訴用戶端資源未修改,可以使用緩存。If-Modified-Since
上面兩個頭部字段是根據修改時間判斷資源是否是新鮮的,這樣做的準确度不是很高,還有一組頭部字段
ETag
和
If-None-Match
使用資源的唯一辨別來判斷資源是否被修改:
-
: 響應頭字段,用于伺服器告訴用戶端資源的唯一辨別(辨別的生成規則由服務端确定)ETag
-
: 請求頭字段,如果請求頭中包含此字段,服務端會對比該字段的值與最新的資源的辨別,如果不相同,說明資源被修改,響應 200, 傳回最新的資源,否則,響應 304.If-None-Match
ETag
和
If-None-Match
的優先級高于
Last-Modified
和
If-Modified-Since
除此之外,與緩存相關的還有一個請求頭:
Vary
, 用來決定用戶端使用新資源還是緩存資源,使用vary頭有利于内容服務的動态多樣性。例如,使用Vary: User-Agent頭,緩存伺服器需要通過UA判斷是否使用緩存的頁面。如果需要區分移動端和桌面端的展示内容,利用這種方式就能避免在不同的終端展示錯誤的布局。另外,它可以幫助 Google 或者其他搜尋引擎更好地發現頁面的移動版本,并且告訴搜尋引擎沒有引入Cloaking。
使用緩存
說了這麼多,我們應該怎麼通過使用緩存來提高站點的性能呢?
首先,對于私有緩存,開發者一般是不需要關注的,浏覽器會自動緩存請求成功的 GET 資料,用來支援後退等功能。我們一般關注的是共有緩存,也就是在代理伺服器上緩存資料,用戶端請求到代理伺服器上後,就可以直接傳回了,下面以 Nginx 為例,簡單說明如何使用緩存。
http {
# 緩存配置
proxy_cache_path /usr/share/nginx/cache levels=1:2 keys_zone=server_cache:10m max_size=5g inactive=60m use_temp_path=off;
# 部落格後端反代
server {
listen 8888 ssl http2;
access_log /var/log/nginx/admin/access_fd.log smail;
error_log /var/log/nginx/admin/error_fd.log;
location ~ /api/ {
proxy_cache server_cache;
proxy_cache_valid 200 304 302 1h;
proxy_cache_methods GET HEAD POST;
add_header X-Proxy-Cache $upstream_cache_status;
proxy_pass http://39.106.168.39:8888;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
}
如上,
proxy_cache_path
用來配置緩存資料儲存的路徑,裡面的主要字段含義如下:
-
: 在單個目錄中包含大量檔案會降低檔案通路速度,是以我們建議對大多數部署使用兩級目錄層次結構。如果未包含levels
Nginx會将所有檔案放在同一目錄中。levels
-
: 設定共享記憶體區域,用于存儲緩存鍵和中繼資料,後面的參數表示該區域的大小,一般來說,1 MB區域可以存儲大約8,000個 key 資料。keys_zone
-
: 緩存能占的最大記憶體。max-size
-
:指定項目在未被通路的情況下可以保留在緩存中的時間長度。在此示例中,緩存管理器程序會自動從緩存中删除1分鐘未請求的檔案,無論其是否已過期。預設值為10分鐘(inactive
)。非活動内容與過期内容不同。Nginx 不會自動删除緩存header定義為已過期内容(例如10m
)。過期(陳舊)内容僅在指定時間内未被通路時被删除。通路過期内容時,Nginx 會從原始伺服器重新整理它并重置Cache-Control:max-age=120
計時器。inactive
其次,我們在 Location 塊中配置了幾個值:
-
:定義用于緩存的共享記憶體區域。proxy_cache
-
: 指定哪些狀态的響應可以被緩存。proxy_cache_valid
-
: 哪些方法的請求可以被緩存。proxy_cache_methods
除此之外,我們添加了一個響應頭部字段
X-Proxy-Cache
用來檢視緩存是否生效。
這裡隻是簡單對資料進行了緩存,服務端沒有提供緩存驗證的功能,是以可能出現服務端資料已經改變但緩存沒更新的情況。
HTTPS 細節
HTTPS 的原理
密碼學基礎
對稱加密和非對稱加密
- 對稱加密: 加密和解密時使用的密鑰是一樣的,比如 DES, 優點是速度快,缺點是在協商密鑰時,可能會洩露密鑰
- 非對稱加密:有兩個密鑰,公鑰和私鑰,使用公鑰加密,私鑰解密,公鑰是公開的,比如 RSA。
非對稱加密有一個形象的比喻:
A有一份機密檔案,想要發給B,發之前先向B要一個打開的保險櫃,把檔案裝進保險櫃鎖住後再發給B,整個過程中保險櫃密碼隻有B知道,這裡的保險櫃相當于公鑰,保險櫃密碼相當于私鑰。
CA機構和證書
公鑰是公開的,那怎麼證明這個公鑰是屬于你的呢?接上面的比喻,如果有一個中間人 C,在 B 向 A 發保險櫃的時候将 B 的保險櫃換成自己的發給 A,這樣 C 就可以竊取到檔案,A 如果想要驗證這個保險櫃是不是 B 的,就需要一個 A、B 都信任的第三方機構,B 在發給 A 之前請求第三方機構在保險箱上蓋一個戳,A 收到後再請求第三方機構檢查戳是不是真的就可以了,這裡的第三方機構就是 CA 機構,這個戳就是證書,具體來說:
每個CA機構都會有自己的一組密鑰對(CA 的公鑰是通信雙方都信任的),現在 B 有一個公鑰,他要證明這個公鑰是他的,就需要向 CA 機構請求一份該公鑰的證書,請求時,B 需要向 CA 機構提供自己的資訊以及要認證的公鑰(這些資訊會組成CSR檔案),CA 機構收到請求後,會檢查 CSR 的真實性,檢查無誤後,CA 會将 CSR 的内容哈希後用自己的私鑰簽名,然後将 CSR 中的資訊和簽名組合成證書發給 B。
是以一份證書中包含的典型内容包括:
- 明文的證書持有者資訊
- 明文的公鑰
- CA 的簽名
- 證書用途,使用的算法,證書過期時間,頒發者資訊,CRL分發點等
B 有了 CA 機構的證書,在向 A 發送公鑰時就隻需要發送證書了,A 收到證書,用 CA 機構的公鑰解密簽名,然後對證書中的明文資料以同樣的算法做哈希,隻需要對比兩個哈希值就可以判斷證書有沒有被篡改了,如果證書沒被篡改,則可以放心使用證書中的公鑰與 B 通信了。
HTTPS 通信流程
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-8HD5SxsD-1613829654732)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20210122163508612.png)]
看上面的截圖,前三行 [SYN], [SYN, ACK], [ACK] 是典型的 TCP 三次握手,那麼在三次握手後,用戶端向服務端以 TLSV1.2 協定向服務端發送了一個
client Hello
包,通過
Client Hello
, 用戶端會生成一個随機數,并告訴服務端自己支援的加密,哈希等算法,我們可以在這個封包裡看到這些内容:
其中,Random 就是用戶端選取的随機數,
Cipher Suites
中就是用戶端支援的算法。
接下來就是
Server Hello
, 在這一步,服務端同樣會生成一個随機數,并且會從用戶端支援的算法中選取一種,通過
Server Hello
的方式告訴用戶端:
可以看到
Server Hello
和
Client Hello
的封包内容差別不大,隻是
Client Hello
中的
Cipher Suite
有許多項,而
Server Hello
的隻有一項,因為服務端會選擇安全性最高的加密方式,需要注意的是這裡選擇的是一組算法,以這裡選擇的
0xc02f
為例,它的名字叫 TLS_ECDHE_RSA_WITH_AES_128_GCM_ SHA256 (0xc02f) 其中包括:
- 密鑰交換算法:ECDHE
- 身份驗證算法:RSA
- 對稱加密算法:AES_128_GCM
- 摘要算法:SHA256
在這之後,服務端在一個 TLS 包裡進行了三個負載:
- Certificate:發送證書
- Server Key Exchange:包含密鑰交換算法 DHE/ECDHE 所需要的額外參數。
- Server Hello Done:表明服務端相關資訊發送結束,這之後服務端會等待用戶端響應。
到這一步,用戶端已經拿到了伺服器的證書,會檢查證書是否有效,如果證書失效,用戶端浏覽器會阻止後續操作,反之,用戶端會繼續與服務端協商對稱加密密鑰:
用戶端向服務端發送一個響應(id = 67)包含三個負載:
- Client Key Exchange:類似 Server Key Exchange,用戶端生成一個新的随機數(Premaster secret),并使用數字證書中的公鑰加密後發給服務端。
- Change cipher Spec: 已被廢棄,不攜帶資料
- Encrypted handshake message:這個步驟用戶端和伺服器在握手完後都會進行,以告訴對方自己在整個握手過程中收到了什麼資料,發送了什麼資料,保證中間沒人篡改封包。
到現在為止,我們總結一下用戶端和伺服器做了什麼:
- 用戶端生成了一個随機數 Client Random 發給了服務端
- 服務端也生成了一個随機數 Server Random 發給了用戶端,同時,雙方還協商了以後要用到的哈希,加密算法。
- 服務端把自己的證書發給了用戶端。
- 用戶端又生成了一個新随機數,并用服務端證書中的公鑰進行加密後發給了服務端。
- 用戶端和服務端通過互發 Encrypted handshake message 確定了資料沒被 中間人篡改。
現在,根據三個随機數,用戶端和伺服器就會根據約定好的對稱加密算法生成最終的對稱加密密鑰,後續的資料傳輸就會使用該密鑰加密。
總結
HTTPS 建立連接配接的過程總結如下:
- TCP 三次握手
- 用戶端向伺服器發送
包,包含 TLS 版本,用戶端生成的随機數 Client Random, 用戶端支援的算法等資訊Client Hello
- 伺服器向用戶端發送
包,包含服務端生成的随機數 Server Random,服務端選擇的算法等資訊。Server Hello
- 服務端向用戶端發送證書。
- 用戶端檢查證書有效後,生成一個新的随機數 Premaster secret 并用證書中的公鑰加密發給服務端。
- 服務端使用自己的私鑰解密 Premaster secret。
- 用戶端和服務端分别使用三個随機數,依照約定的算法生成對話密鑰 session key。
- 用戶端和服務端使用 session key 加密後續的會話。
使用 HTTPS
nginx 配置示例:
server {
listen 443 ssl http2;
server_name *.junebao.top;
# 證書
ssl_certificate 1_www.junebao.top_bundle.crt;
# 私鑰
ssl_certificate_key 2_www.junebao.top.key;
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
ssl_prefer_server_ciphers on;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
HTTP/2 細節
實作
2015 年,HTTP/2 釋出。HTTP/2 是現行 HTTP 協定(HTTP/1.x)的替代,但它不是重寫,HTTP 方法/狀态碼/語義都與 HTTP/1.x 一樣。HTTP/2 基于 SPDY3,專注于性能,最大的一個目标是在使用者和網站間隻用一個連接配接(connection)。
那麼SPDY3是什麼呢?
SPDY是谷歌自行研發的 SPDY 協定,主要解決 HTTP/1.1 效率不高的問題。谷歌推出 SPDY,才算是正式改造 HTTP 協定本身。降低延遲,壓縮 header 等等,SPDY 的實踐證明了這些優化的效果,也最終帶來 HTTP/2 的誕生。
HTTP/2 由兩個規範(Specification)組成:
- Hypertext Transfer Protocol version 2 - RFC7540
- HPACK - Header Compression for HTTP/2 - RFC7541
那麼HTTP2在HTTP1.1的基礎上做了哪些改進
- 二進制傳輸
- 請求和響應複用
- Header壓縮
- Server Push(服務端推送)
二進制傳輸
HTTP/2 采用二進制格式傳輸資料,而非 HTTP 1.x 的文本格式,二進制協定解析起來更高效。 HTTP / 1 的請求和響應封包,都是由起始行,首部和實體正文(可選)組成,各部分之間以文本換行符分隔。HTTP/2 将請求和響應資料分割為更小的幀,并且它們采用二進制編碼。
新的二進制分幀機制改變了用戶端與伺服器之間交換資料的方式。 為了說明這個過程,我們需要了解 HTTP/2 的三個概念:
- 資料流:已建立的連接配接内的雙向位元組流,可以承載一條或多條消息。
- 消息:與邏輯請求或響應消息對應的完整的一系列幀。
- 幀:HTTP/2 通信的最小機關,每個幀都包含幀頭,至少也會辨別出目前幀所屬的資料流。
這些概念的關系總結如下:
- 所有通信都在一個 TCP 連接配接上完成,此連接配接可以承載任意數量的雙向資料流。
- 每個資料流都有一個唯一的辨別符和可選的優先級資訊,用于承載雙向消息。
- 每條消息都是一條邏輯 HTTP 消息(例如請求或響應),包含一個或多個幀。
- 幀是最小的通信機關,承載着特定類型的資料,例如 HTTP 标頭、消息負載等等。 來自不同資料流的幀可以交錯發送,然後再根據每個幀頭的資料流辨別符重新組裝。
請求和響應複用
在 HTTP/1.x 中,如果用戶端要想發起多個并行請求以提升性能,則必須使用多個 TCP 連接配接,這是 HTTP/1.x 傳遞模型的直接結果,該模型可以保證每個連接配接每次隻傳遞一個響應(響應排隊)。 更糟糕的是,這種模型也會導緻隊首阻塞,進而造成底層 TCP 連接配接的效率低下。
HTTP/2 中新的二進制分幀層突破了這些限制,實作了完整的請求和響應複用:用戶端和伺服器可以将 HTTP 消息分解為互不依賴的幀,然後交錯發送,最後再在另一端把它們重新組裝起來。
在 HTTP/2 中,有了二進制分幀之後,HTTP /2 不再依賴 TCP 連結去實作多流并行了,在 HTTP/2 中:
- 同域名下所有通信都在單個連接配接上完成。
- 單個連接配接可以承載任意數量的雙向資料流。
- 資料流以消息的形式發送,而消息又由一個或多個幀組成,多個幀之間可以亂序發送,因為根據幀首部的流辨別可以重新組裝。
這一特性,使性能有了極大提升:
- 同個域名隻需要占用一個 TCP 連接配接,使用一個連接配接并行發送多個請求和響應,消除了因多個 TCP 連接配接而帶來的延時和記憶體消耗。
- 并行交錯地發送多個請求,請求之間互不影響。
- 并行交錯地發送多個響應,響應之間互不幹擾。
- 在 HTTP/2 中,每個請求都可以帶一個 31bit 的優先值,0 表示最高優先級, 數值越大優先級越低。有了這個優先值,用戶端和伺服器就可以在處理不同的流時采取不同的政策,以最優的方式發送流、消息和幀。
Header壓縮
在 HTTP/1 中,我們使用文本的形式傳輸 header,在 header 攜帶 cookie 的情況下,可能每次都需要重複傳輸幾百到幾千的位元組。為了減少這塊的資源消耗并提升性能,HTTP/2 使用 HPACK 壓縮格式壓縮請求和響應标頭中繼資料,這種格式采用兩種強大的技術:
- 這種格式支援通過靜态霍夫曼代碼對傳輸的标頭字段進行編碼,進而減小了各個傳輸的大小。
- 這種格式要求用戶端和伺服器同時維護和更新一個包含之前見過的标頭字段的索引清單(換句話說,它可以建立一個共享的壓縮上下文),此清單随後會用作參考,對之前傳輸的值進行有效編碼。
利用霍夫曼編碼,可以在傳輸時對各個值進行壓縮,而利用之前傳輸值的索引清單,我們可以通過傳輸索引值的方式對重複值進行編碼,索引值可用于有效查詢和重構完整的标頭鍵值對。
作為一種進一步優化方式,HPACK 壓縮上下文包含一個靜态表和一個動态表:靜态表在規範中定義,并提供了一個包含所有連接配接都可能使用的常用 HTTP 标頭字段(例如,有效标頭名稱)的清單;動态表最初為空,将根據在特定連接配接内交換的值進行更新。 是以,為之前未見過的值采用靜态 Huffman 編碼,并替換每一側靜态表或動态表中已存在值的索引,可以減小每個請求的大小。
注:在 HTTP/2 中,請求和響應标頭字段的定義保持不變,僅有一些微小的差異:所有标頭字段名稱均為小寫,請求行現在拆分成各個
:method
、
:scheme
、
:authority
和
:path
僞标頭字段。
如需了解有關 HPACK 壓縮算法的完整詳情,請參閱 IETF HPACK - HTTP/2 的标頭壓縮。
Server Push
HTTP/2 新增的另一個強大的新功能是,伺服器可以對一個用戶端請求發送多個響應。 換句話說,除了對最初請求的響應外,伺服器還可以向用戶端推送額外資源如下圖所示,而無需用戶端明确地請求。
為什麼在浏覽器中需要一種此類機制呢?一個典型的網絡應用包含多種資源,用戶端需要檢查伺服器提供的文檔才能逐個找到它們。 那為什麼不讓伺服器提前推送這些資源,進而減少額外的延遲時間呢? 伺服器已經知道用戶端下一步要請求什麼資源,這時候伺服器推送即可派上用場。
事實上,如果您在網頁中内聯過 CSS、JavaScript,或者通過資料 URI 内聯過其他資産(請參閱資源内聯),那麼您就已經親身體驗過伺服器推送了。 對于将資源手動内聯到文檔中的過程,我們實際上是在将資源推送給用戶端,而不是等待用戶端請求。 使用 HTTP/2,我們不僅可以實作相同結果,還會獲得其他性能優勢。 推送資源可以進行以下處理:
- 由用戶端緩存
- 在不同頁面之間重用
- 與其他資源一起複用
- 由伺服器設定優先級
- 被用戶端拒絕
服務端推送如何實作
所有伺服器推送資料流都由
PUSH_PROMISE
幀發起,表明了伺服器向用戶端推送所述資源的意圖,并且需要先于請求推送資源的響應資料傳輸。 這種傳輸順序非常重要:用戶端需要了解伺服器打算推送哪些資源,以免為這些資源建立重複請求。 滿足此要求的最簡單政策是先于父響應(即,
DATA
幀)發送所有
PUSH_PROMISE
幀,其中包含所承諾資源的 HTTP 标頭。
在用戶端接收到
PUSH_PROMISE
幀後,它可以根據自身情況選擇拒絕資料流(通過
RST_STREAM
幀)。 (例如,如果資源已經位于緩存中,便可能會發生這種情況。) 這是一個相對于 HTTP/1.x 的重要提升。 相比之下,使用資源内聯(一種受歡迎的 HTTP/1.x“優化”)等同于“強制推送”:用戶端無法選擇拒絕、取消或單獨處理内聯的資源。
使用 HTTP/2,用戶端仍然完全掌控伺服器推送的使用方式。 用戶端可以限制并行推送的資料流數量;調整初始的流控制視窗以控制在資料流首次打開時推送的資料量;或完全停用伺服器推送。 這些優先級在 HTTP/2 連接配接開始時通過
SETTINGS
幀傳輸,可能随時更新。
過渡到 HTTP/2
上面說了這麼多,我們要如何啟用HTTP2呢?
如果你使用的是 nginx,那麼你隻需要加一個
http2
即可:
server {
listen 8888 ssl http2;
server_name *.junebao.top;
# ...
}
如果你使用 Golang 的 Gin 架構,他預設支援 HTTP/2,你可以使用
RunTLS()
使用 HTTP/2,如下:
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
engine := gin.Default()
engine.GET("./", func(context *gin.Context) {
context.JSON(200, map[string]string{"msg": "ok"})
})
// 服務端推送
engine.Static("/static", "./static")
engine.GET("/push", func(context *gin.Context) {
pusher := context.Writer.Pusher()
if pusher != nil {
err := pusher.Push("/static/test.js", nil)
if err != nil {
log.Println("push fail", err)
}
}
context.JSON(200, map[string]string{"msg": "ok"})
})
engine.RunTLS(":8888", "./root_cer.cer", "./root_private_key.pem")
}
如果你使用的是 spring boot 内置的 Tomcat 伺服器,那麼隻需要在配置檔案中添加配置:
server:
http2:
enabled: on
隻有 Tomcat 9 版本之後版本才支援 HTTP/2 協定。在 conf/server.xml 中增加内容:
<Connector port="8443" protocol="org.apache.coyote.http11.Http11AprProtocol" maxThreads="150" SSLEnabled="true">
<UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol"/>
<SSLHostConfig honorCipherOrder="false">
<Certificate certificateKeyFile="conf/ca.key" certificateFile="conf/ca.crt"/>
</SSLHostConfig>
</Connector>
HTTP/3 細節
為什麼要出現HTTP3
雖然 HTTP/2 解決了很多之前舊版本的問題,但是它還是存在一個巨大的問題,主要是底層支撐的
TCP 協定
造成的。
上文提到 HTTP/2 使用了多路複用,一般來說同一域名下隻需要使用一個 TCP 連接配接。但當這個連接配接中出現了丢包的情況,那就會導緻 HTTP/2 的表現情況反倒不如 HTTP/1 了。
因為在出現丢包的情況下,整個 TCP 都要開始等待重傳,也就導緻了後面的所有資料都被阻塞了。但是對于 HTTP/1.1 來說,可以開啟多個 TCP 連接配接,出現這種情況反到隻會影響其中一個連接配接,剩餘的 TCP 連接配接還可以正常傳輸資料。
那麼可能就會有人考慮到去修改 TCP 協定,其實這已經是一件不可能完成的任務了。因為 TCP 存在的時間實在太長,已經充斥在各種裝置中,并且這個協定是由作業系統實作的,更新起來不大現實。
基于這個原因,Google 就更起爐竈搞了一個基于 UDP 協定的 QUIC 協定,并且使用在了 HTTP/3 上,HTTP/3 之前名為 HTTP-over-QUIC,從這個名字中我們也可以發現,HTTP/3 最大的改造就是使用了 QUIC(快速 UDP Internet 連接配接)。
QUIC
QUIC(Quick UDP Internet Connection)是谷歌制定的一種基于UDP的低延遲時間的網際網路傳輸層協定。在2016年11月國際網際網路工程任務組(IETF)召開了第一次QUIC工作組會議,受到了業界的廣泛關注。這也意味着QUIC開始了它的标準化過程,成為新一代傳輸層協定 。
優勢:
- 高效地建立連接配接: 将 TLS 協商密鑰和協定作為打開連接配接握手地一部分,加上對 client Hello 地緩存,在大部分情況下,QUIC 建立連接配接隻需要 0RTT,而普通的 HTTPS 則至少需要 2RTT
- 改進地擁塞控制方案:TCP 擁塞控制包括:慢啟動,擁塞避免,快速重傳,快速恢複,QUIC 在 UDP 基礎上實作了相關算法并做了改進,使之具有可插拔,高效地特點。
- 基于 stream 和 connecton 級别的流量控制。
- 沒有 TCP 隊頭阻塞地多路複用:HTTP/2 通過二進制分幀層避免了 HTTP 隊頭阻塞,但由于依然使用 TCP 協定,就避免不了 TCP 隊頭阻塞,QUIC 基于 UDP,多個 stream 之間沒有依賴。這樣假如 stream2 丢了一個 udp packet,也隻會影響 stream2 的處理。不會影響 stream2 之前及之後的 stream 的處理,這也就在很大程度上緩解甚至消除了隊頭阻塞的影響。
- 加密認證地封包:TCP 協定頭部沒有經過任何加密和認證,是以在傳輸過程中很容易被中間網絡裝置篡改,注入和竊聽。比如修改序列号、滑動視窗。這些行為有可能是出于性能優化,也有可能是主動攻擊。但是 QUIC 的 packet 可以說是武裝到了牙齒。除了個别封包比如 PUBLIC_RESET 和 CHLO,所有封包頭部都是經過認證的,封包 Body 都是經過加密的.
- 連接配接遷移:一條 TCP 連接配接是由四元組辨別的(源 IP,源端口,目的 IP,目的端口)。什麼叫連接配接遷移呢?就是當其中任何一個元素發生變化時,這條連接配接依然維持着,能夠保持業務邏輯不中斷。當然這裡面主要關注的是用戶端的變化,因為用戶端不可控并且網絡環境經常發生變化,而服務端的 IP 和端口一般都是固定的,而 QUIC 連接配接不再以 IP 及端口四元組辨別,而是以一個 64 位的随機數作為 ID 來辨別,這樣就算 IP 或者端口發生變化時,隻要 ID 不變,這條連接配接依然維持着,上層業務邏輯感覺不到變化,不會中斷,也就不需要重連。由于這個 ID 是用戶端随機産生的,并且長度有 64 位,是以沖突機率非常低。
- 向前糾錯: 每個資料包除了它本身的内容之外,還包括了部分其他資料包的資料,是以少量的丢包可以通過其他包的備援資料直接組裝而無需重傳。向前糾錯犧牲了每個資料包可以發送資料的上限,但是減少了因為丢包導緻的資料重傳,因為資料重傳将會消耗更多的時間(包括确認資料包丢失、請求重傳、等待新資料包等步驟的時間消耗);假如說這次我要發送三個包,那麼協定會算出這三個包的異或值并單獨發出一個校驗包,也就是總共發出了四個包。當出現其中的非校驗包丢包的情況時,可以通過另外三個包計算出丢失的資料包的内容。當然這種技術隻能使用在丢失一個包的情況下,如果出現丢失多個包就不能使用糾錯機制了,隻能使用重傳的方式了。
- 證書壓縮等.
可見HTTP3在效率上和安全性上都有了很大程度上的修改,但是由于目前這個标準還在論證中,Nginx等也隻是在測試版中加入了對HTTP3的支援,等到技術真正的論證實作完成,我們就可以使用上快速且安全的HTTP3協定了,期待着這一天的到來。
參考
mozilla 開發文檔
谷歌開發文檔 HTTP2 簡介
Nginx緩存最佳實踐
讓網際網路更快:新一代QUIC協定在騰訊的技術實踐分享