关于 HTTP 的一切(HTTP/1.1,HTTP/2,HTTP/3,HTTPS, CORS, 缓存 ,无状态)
HTTP
為什么會(huì)出現(xiàn) HTTP 協(xié)議,從 HTTP1.0 到 HTTP3 經(jīng)歷了什么?HTTPS 又是怎么回事?
HTTP 是一種用于獲取類似于 HTML 這樣的資源的 應(yīng)用層通信協(xié)議, 他是萬維網(wǎng)的基礎(chǔ),是一種 CS 架構(gòu)的協(xié)議,通常來說,HTTP 協(xié)議一般由瀏覽器等 “客戶端” 發(fā)起,發(fā)起的這個(gè)請(qǐng)求被稱為 Request, 服務(wù)端接受到客戶端的請(qǐng)求后,會(huì)返回給客戶端所請(qǐng)求的資源,這一過程被稱為 Response,在大部分情況下,客戶端和服務(wù)器之間還可能存在許多 proxies,他們的作用可能各不相同,有些可能作為網(wǎng)關(guān)存在,有些可能作為緩存存在。
HTTP 協(xié)議有三個(gè)基本的特性:
HTTP 的歷史
HTTP 的歷史可以追溯到萬維網(wǎng)剛被發(fā)明的時(shí)候,1989年, Tim Berners-Lee 博士寫了一份關(guān)于建立一個(gè)通過網(wǎng)絡(luò)傳輸超文本系統(tǒng)的報(bào)告。該系統(tǒng)起初被命名為 Mesh,在隨后的1990年項(xiàng)目實(shí)施期間被更名為萬維網(wǎng)(*World Wide Web)。他以現(xiàn)有的 TCP IP 協(xié)議為基礎(chǔ)建造, 由四個(gè)部分組成:
- 用來表示超文本文檔的文本格式,即超文本標(biāo)記語言(HTML)
- 一個(gè)用來傳輸超文本的簡(jiǎn)單應(yīng)用層協(xié)議,即超文本傳輸協(xié)議(HTTP)
- 一個(gè)用來顯示或編輯超文本文檔的客戶端,即網(wǎng)絡(luò)瀏覽器,而第一個(gè)瀏覽器則被稱為 WorldWideWeb
- 一個(gè)用于提供可訪問文檔的服務(wù),httpd 的前身.
這四部分在 1990 年底完成,這時(shí)候的 HTTP 協(xié)議還很簡(jiǎn)單,后來為了于其他版本的協(xié)議區(qū)分,最初的 HTTP 協(xié)議被記為 HTTP/0.9,
后來,隨著計(jì)算機(jī)技術(shù)的發(fā)展,HTTP 協(xié)議也隨著 HTTP/1.0, HTTP/1.1, HTTP/2 等關(guān)鍵版本更迭變得更加高效實(shí)用。
HTTP/0.9 on-line
最初的 0.9 版本也被稱為單行協(xié)議(on-line), 基于 TCP 協(xié)議,該版本下只有一個(gè)可用的請(qǐng)求方法:GET, 請(qǐng)求格式也相當(dāng)簡(jiǎn)單:
GET /index.html它表示客戶端請(qǐng)求 index.html 的內(nèi)容,0.9 版本的 HTTP 響應(yīng)也同樣簡(jiǎn)單,他只允許響應(yīng) HTML 格式的字符串,如:
<html><h1> ..... </h1> </html>這一階段的響應(yīng)甚至沒有響應(yīng)頭,也沒有響應(yīng)碼或錯(cuò)誤代碼,一旦出現(xiàn)問題,服務(wù)端會(huì)響應(yīng)一段特殊的 HTML 字符串以便客戶端查看。 服務(wù)端在發(fā)送完數(shù)據(jù)后,就會(huì)立刻關(guān)閉 TCP 連接。
HTTP/1.0
0.9 版本的 HTTP 協(xié)議太過于簡(jiǎn)單甚至是簡(jiǎn)陋,而隨著瀏覽器和服務(wù)器的應(yīng)用被擴(kuò)展到越來越多的領(lǐng)域,0.9 版本的協(xié)議已經(jīng)不能適應(yīng),直到 1996年11月,RFC 1945 定義了 HTTP/1.0, 但他并不是官方標(biāo)準(zhǔn),該版本的 HTTP 協(xié)議較 0.9 版本有了一下改變:
版本號(hào)被添加到了請(qǐng)求頭上,像下面這樣:
GET /mypage.html HTTP/1.0引入了 HTTP頭的概念,無論是請(qǐng)求還是響應(yīng),允許傳輸元數(shù)據(jù),這使得協(xié)議更加靈活和具有拓展性。
請(qǐng)求方法拓展到了 GET,HEAD,POST
在新 HTTP 頭(Content-Type)的幫助下,可以傳輸不止 HTML 的任意格式的數(shù)據(jù)。
響應(yīng)時(shí)帶上了狀態(tài)碼,使得瀏覽器能夠知道響應(yīng)的狀態(tài)并作出響應(yīng)的處理。
…
HTTP/1.1
同 0.9 版本一樣,1.0 版本下,TCP 連接是不能復(fù)用的,數(shù)據(jù)發(fā)送完后服務(wù)端會(huì)立刻關(guān)閉連接,但由于建立 TCP 連接的代價(jià)較大,所以 1.0 版本的 HTTP 協(xié)議并不是足夠高效,加上 HTTP/1.0 多種不同的實(shí)現(xiàn)方式在實(shí)際運(yùn)用中顯得有些混亂,自1995年就開始了 HTTP 的第一個(gè)標(biāo)準(zhǔn)化版本的修訂工作,到1997年初,HTTP1.1 標(biāo)準(zhǔn)發(fā)布。
1.1 版本的改進(jìn)包括:
支持長(zhǎng)連接:在 HTTP1.1 中默認(rèn)開啟 Connection: keep-alive,允許在一個(gè) TCP 連接上傳輸多個(gè) HTTP 請(qǐng)求和響應(yīng),減少了建立和關(guān)閉連接造成的性能消耗。
支持 pipline: HTTP/1.1 還支持流水線(pipline)工作,流水線是指在同一條長(zhǎng)連接上發(fā)出連續(xù)的請(qǐng)求,而不用等待應(yīng)答返回。這樣可以避免連接延遲。
支持響應(yīng)分塊:對(duì)于比較大的響應(yīng),HTTP/1.2 通過 Transfer-Encoding 首部支持將其分割成多個(gè)任意大小的分塊,每個(gè)數(shù)據(jù)塊在發(fā)送時(shí)都會(huì)附上塊的長(zhǎng) 度,最后用一個(gè)零長(zhǎng)度的塊作為消息結(jié)束的標(biāo)志。
新的緩存控制機(jī)制:HTTP/1.1定義的 Cache-Control 頭用來區(qū)分對(duì)緩存機(jī)制的支持情況,同時(shí),還提供 If-None-Match, ETag , Last-Modified, If-Modified-Since 等實(shí)現(xiàn)緩存的驗(yàn)證等工作。
允許不同域名配置到同一IP的服務(wù)器上:在 HTTP/1.0 時(shí),認(rèn)為每臺(tái)服務(wù)器綁定一個(gè)唯一的 IP,但隨著技術(shù)的進(jìn)步,一臺(tái)服務(wù)器的多個(gè)虛擬主機(jī)會(huì)共享一個(gè)IP,為了區(qū)分同一服務(wù)器上的不同服務(wù),HTTP/1.1 在請(qǐng)求頭中加入了 HOST 字段,它指明了請(qǐng)求將要發(fā)送到的服務(wù)器主機(jī)名和端口號(hào),這是一個(gè)必須字段,請(qǐng)求缺少該字段服務(wù)端將會(huì)返回 400.
引入內(nèi)容協(xié)商機(jī)制,包括語言,編碼,類型等,并允許客戶端和服務(wù)器之間約定以最合適的內(nèi)容進(jìn)行交換。
使用了 100 狀態(tài)碼:HTTP/1.0 中,定義:
o 1xx: Informational - Not used, but reserved for future use在 2.0 版本時(shí),使用了這個(gè)保留的狀態(tài)碼,用來表示臨時(shí)響應(yīng)。
HTTPS
HTTP/1.1 之后,對(duì) HTTP 協(xié)議的拓展變得更加簡(jiǎn)單,但 HTTP 依然存在一個(gè)天然的缺陷就是明文傳輸數(shù)據(jù),直到 1994 年底,網(wǎng)景公司在 TCP/IP 協(xié)議棧的基礎(chǔ)上添加了 SSL 層用來加密傳輸,后來,在標(biāo)準(zhǔn)化的過程中, SSL 成了 TLS (Transport Layer Security 傳輸層安全協(xié)議),基于 HTTPS 通信的客戶端和服務(wù)器在建立完 TCP 連接之后會(huì)協(xié)商通信密鑰,在之后的通信過程中, 客戶端和服務(wù)器會(huì)使用該密鑰對(duì)數(shù)據(jù)進(jìn)行對(duì)稱加密,以防數(shù)據(jù)被竊取或篡改。(密鑰協(xié)商階段會(huì)使用非對(duì)稱加密)。
HTTP/2
HTTP/1.1 雖然允許連接復(fù)用和以流水線方式運(yùn)作,但在一個(gè) TCP 連接里面,所有數(shù)據(jù)依然還是按序發(fā)送的,服務(wù)器只能處理完一個(gè)請(qǐng)求再去處理另一個(gè)請(qǐng)求,如果第一個(gè)請(qǐng)求非常慢,就會(huì)造成后面的請(qǐng)求長(zhǎng)時(shí)間阻塞,這被稱為 隊(duì)頭阻塞(Head-of-line blocking),2009 年,谷歌公開了自行研發(fā)的 SPDY 協(xié)議,它基于 HTTPS,并采用多路復(fù)用解決了隊(duì)頭阻塞的問題,同時(shí),它還使用了 Header 壓縮等技術(shù)大大降低了延時(shí)并提高了帶寬利用率,在之后的 2015 到 2019 年間,谷歌在自家瀏覽器上實(shí)踐和證明了這個(gè)協(xié)議,SPDY 也成了 HTTP/2 的基石。
2015 年 5 月, HTTP/2 正式標(biāo)準(zhǔn)化,他與 1.x 版本 不同在于:
雖然 HTTP/2 2015 年就被標(biāo)準(zhǔn)化,在到目前為止,HTTP/1.1 任然被廣泛使用,據(jù) MySSL 的最新統(tǒng)計(jì),截至 2020 年 12 月,已有 65.84% 的站點(diǎn)支持了 HTTP/2.
HTTP/3
HTTP/3 是即將到來的第三個(gè)主要版本的 HTTP 協(xié)議,在 HTTP/3 中,將棄用 TCP 協(xié)議,改為使用基于 UDP 的 QUIC 協(xié)議實(shí)現(xiàn)。QUIC(快速UDP網(wǎng)絡(luò)連接)是一種實(shí)驗(yàn)性的網(wǎng)絡(luò)傳輸協(xié)議,由Google開發(fā),該協(xié)議旨在使網(wǎng)頁傳輸更快。
在2018年10月28日的郵件列表討論中,IETF(互聯(lián)網(wǎng)工程任務(wù)組) HTTP和QUIC工作組主席 Mark Nottingham 提出了將 HTTP-over-QUIC 更名為 HTTP/3 的正式請(qǐng)求,以“明確地將其標(biāo)識(shí)為HTTP語義的另一個(gè)綁定……使人們理解它與 QUIC 的不同”,并在最終確定并發(fā)布草案后,將 QUIC 工作組繼承到 HTTP 工作組, 在隨后的幾天討論中,Mark Nottingham 的提議得到了 IETF 成員的接受,他們?cè)?018年11月給出了官方批準(zhǔn),認(rèn)可 HTTP-over-QUIC 成為 HTTP/3。
2019年9月,HTTP/3支持已添加到 CloudFlare 和 Chrome 上。Firefox Nightly 也將在2019年秋季之后添加支持。
HTTP/1.1 細(xì)節(jié)
HTTP報(bào)文
HTTP 的報(bào)文都由消息頭和消息體兩部分組成,兩者之間以 CRLF(回車換行) 分割。
請(qǐng)求頭格式
請(qǐng)求頭第一行為請(qǐng)求行,其余為請(qǐng)求頭字段:如下:
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請(qǐng)求行由三部分組成:
他們以空格分隔,RFC2068 定義了其中不同的請(qǐng)求方法,他們分別為 OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE,除此之外,后來還添加了一個(gè) PATCH 方法。
| OPTIONS | 獲取目的資源所支持的通信選項(xiàng), 如檢測(cè)服務(wù)器所支持的請(qǐng)求方法或CORS預(yù)檢請(qǐng)求 | 不能攜帶請(qǐng)求體或數(shù)據(jù) | 可以攜帶響應(yīng)體,但一般有效數(shù)據(jù)被放在頭部如 Allow 等字段 | 冪等 | 不可緩存 | 安全 |
| GET | 用于獲取某個(gè)資源 | 參數(shù)一般攜帶在 URL 后面,沒有請(qǐng)求體 | 有響應(yīng)體 | 冪等 | 可緩存 | 安全 |
| HEAD | 用于請(qǐng)求資源的頭部信息,如下載前獲取大文件的大小 | 沒有請(qǐng)求體 | 沒有響應(yīng)體,響應(yīng)頭應(yīng)該與使用 GET 請(qǐng)求時(shí)的一樣 | 冪等 | 可緩存 | 安全 |
| POST | 將數(shù)據(jù)發(fā)送給服務(wù)器 | 數(shù)據(jù)放在請(qǐng)求體中 | 有響應(yīng)體 | 不冪等 | 可緩存(包含新鮮信息時(shí)) | 不安全 |
| PUT | 使用請(qǐng)求中的負(fù)載創(chuàng)建或替換目標(biāo)資源 | 數(shù)據(jù)放在請(qǐng)求體中 | 有響應(yīng)體 | 冪等 | 不可緩存 | 不安全 |
| DELETE | 刪除指定資源 | 可以由請(qǐng)求體 | 可以由響應(yīng)體 | 冪等 | 不可緩存 | 不安全 |
| TRACE | 回顯服務(wù)器收到的請(qǐng)求,主要用于測(cè)試或診斷。 | 無請(qǐng)求體 | 無響應(yīng)體 | 冪等 | 不可緩存 | 不安全 |
| PATCH | 作為 PUT 的補(bǔ)充,用于修改已知資源的部分 | 有請(qǐng)求體 | 無響應(yīng)體 | 非冪等 | 不可緩存 | 不安全 |
請(qǐng)求頭字段
RFC 2068 提供了 17 種請(qǐng)求頭字段,但 HTTP 協(xié)議是易于拓展的,我們可以根據(jù)自己的需要添加自己的請(qǐng)求頭,常見的請(qǐng)求頭字段包括:
| HOST | 指明了要發(fā)送到的服務(wù)器的主機(jī)號(hào)和端口號(hào),這是一個(gè)必須字段,缺失服務(wù)器一般會(huì)返回 400, 端口號(hào)默認(rèn) 80 和 443 | Host: www.baidu.com |
| ACCEPT | 告知服務(wù)器客戶端可以處理的內(nèi)容類型,用MIME類型來表示。 | Accept: text/html |
| User-Agent | 用戶代理標(biāo)識(shí) | |
| Cookies | 用于維持會(huì)話 | |
| … | … | … |
響應(yīng)頭格式
Response = Status-Line*( general-header| response-header| entity-header )CRLF[ message-body ]類似于請(qǐng)求頭,響應(yīng)頭包括狀態(tài)行和響應(yīng)頭字段兩部分組成。
狀態(tài)行包括協(xié)議版本,狀態(tài)碼,狀態(tài)描述三部分組成,類似:
http/2 200 ok目前 http 使用的狀態(tài)碼分為 5 類:
- 1xx: 信息響應(yīng)類
- 2xx: 正常響應(yīng)類
- 3xx: 重定向類
- 4xx: 客戶端錯(cuò)誤類
- 5xx: 服務(wù)端錯(cuò)誤類
常見狀態(tài)碼
| 100 | Continue | 迄今為止的所有內(nèi)容都是可行的,客戶端應(yīng)該繼續(xù)請(qǐng)求 |
| 200 | Ok | 請(qǐng)求成功 |
| 201 | Created | 該請(qǐng)求已成功,并因此創(chuàng)建了一個(gè)新的資源。這通常是在POST請(qǐng)求,或是某些PUT請(qǐng)求之后返回的響應(yīng)。 |
| 301 | Moved Permanently | 永久重定向 |
| 302 | Found | 臨時(shí)重定向 |
| 400 | Bad Request | 請(qǐng)求參數(shù)錯(cuò)誤或語義錯(cuò)誤 |
| 401 | Unauthorized | 請(qǐng)求未認(rèn)證 |
| 403 | Forbidden | 拒絕服務(wù) |
| 404 | Not Found | 資源不存在 |
| 429 | Too Many Requests | 超過請(qǐng)求速率限制(節(jié)流) |
| 500 | Internal Server Error | 服務(wù)端未知異常 |
| 501 | Not Implemented | 此請(qǐng)求方法不被服務(wù)端支持 |
| 502 | Bad Gateway | 網(wǎng)關(guān)錯(cuò)誤 |
| 503 | Service Unavailable | 服務(wù)不可用 |
| 504 | Gateway Timeout | 網(wǎng)關(guān)超時(shí) |
| 505 | HTTP Version Not Supported | HTTP 版本不被支持 |
無狀態(tài)的 HTTP
HTTP 是一個(gè)無狀態(tài)的協(xié)議,為了維持會(huì)話,每客戶端請(qǐng)求時(shí),都應(yīng)該攜帶一個(gè) “憑證”,證明 who am i, 目前維持會(huì)話常用的技術(shù)有:cookie, session, token, 等
cookie
RFC 6265 定義了 Cookie 的工作方式, Cookie 是服務(wù)器發(fā)送給客戶端并存儲(chǔ)在本地的一小段數(shù)據(jù),在用戶第一次登錄時(shí),服務(wù)器生成 Cookie 并在響應(yīng)頭里添加 Set-Cookie 字段,客戶端收到響應(yīng)后,將 Set-Cookie 字段的值(Cookie)存儲(chǔ)在本地,以后每次請(qǐng)求時(shí),客戶端會(huì)自動(dòng)通過 Cookie 字段攜帶 Cookie。
Cookie 以鍵值的形式儲(chǔ)存,除了必須的 Name 和 Value,還可以為 Cookie 設(shè)置以下屬性:
- Domain:指定了哪寫主機(jī)可以接收該 Cookie,默認(rèn)為 Origin, 不包含子域名。
- Path:規(guī)定了請(qǐng)求主機(jī)下的哪些路徑時(shí)要攜帶該 Cookie。
- Expires/Max-Age: 規(guī)定該 Cookie 過期時(shí)間或最大生存時(shí)間,該時(shí)間只與客戶端有關(guān)。
- HttpOnly: JavaScript Document.cookie API 無法訪問帶有 HttpOnly 屬性的cookie,用于預(yù)防 XSS 攻擊;用于持久化會(huì)話的 Cookie 一般應(yīng)該設(shè)置 HttpOnly 。
- Secure:標(biāo)記為 Secure 的 Cookie 只能使用 HTTPS 加密傳輸給服務(wù)器,因此可以防止中間人攻擊,但 Cookie 天生具有不安全性,任何敏感數(shù)據(jù)都不應(yīng)該使用 Cookie 傳輸,哪怕標(biāo)記了 secure.
- Priority:
- SameSite:要求該 Cookie 在跨站請(qǐng)求時(shí)不會(huì)被發(fā)送,用來阻止 CSRF 攻擊,它有三種可選的值:
- None:在同站請(qǐng)求和跨站請(qǐng)求時(shí)都會(huì)攜帶上 Cookie
- Strict:只會(huì)在訪問同站請(qǐng)求時(shí)帶上 Cookie
- Lex:與 Strict 類似,但用戶從外部站點(diǎn)導(dǎo)航至URL時(shí)(例如通過鏈接)除外,新版瀏覽器一般以 Lex 為默認(rèn)選項(xiàng)。
Cookie 被完全保存在客戶端,對(duì)客戶端用戶來說是透明的,用戶可以自己創(chuàng)建和修改 Cookie,所以將敏感信息(如用于持久化會(huì)話的用戶身份信息等)存放在 Cookie 中是十分危險(xiǎn)的,如果不得已需要使用 Cookie 來存儲(chǔ)和傳遞這類信息,應(yīng)該考慮使用 JWT 等類似機(jī)制。
由于 Cookie 的不安全性,絕大部分 Web 站點(diǎn)已經(jīng)開始停止使用 Cookie 持久化會(huì)話,但 Cookie 在一些對(duì)安全性要求不高的場(chǎng)景下依然被廣泛使用,如:
- 個(gè)性化設(shè)置
- 瀏覽器用戶行為跟蹤。
了解更多:
超級(jí) Cookie 和僵尸 Cookie
決戰(zhàn)僵尸 Cookie
SESSION
Cookie 不安全的根源在于它將會(huì)話信息保存在了客戶端,為此,就有了使用 Session 持久化會(huì)話的方案,用戶在第一次登錄時(shí),服務(wù)器會(huì)將用戶會(huì)話狀態(tài)信息保存在服務(wù)器內(nèi)存中,同時(shí)會(huì)為這段信息生成一串唯一索引,將這個(gè)索引作為 Cookie (Name 一般為 SESSION_IDSESSION_ID)返回給客戶端,客戶端下一次請(qǐng)求時(shí),會(huì)自動(dòng)攜帶這個(gè) SESSION_ID, 服務(wù)器只需要根據(jù) SESSION_ID 的值找到對(duì)應(yīng)的狀態(tài)信息就可以知道這次請(qǐng)求是誰發(fā)起的。
SESSION 很大程度上還是依賴于 Cookie,但這時(shí) Cookie 中保存的已經(jīng)是一段對(duì)客戶端來說無意義的字符串了,因此使用 Session 能安全的實(shí)現(xiàn)會(huì)話持久化,但 Session 信息被保存在服務(wù)器內(nèi)存中,可能造成服務(wù)器壓力過大,并且在分布式和前后端分離的環(huán)境下,Session 并不容易拓展。
TOKEN
Cookie 和 Session 都是開箱即用的 API,因此,他們不可避免地缺少靈活性,在一般開發(fā)中,往往采用更靈活地 Token,Token 與 Session 原理一致,都是將會(huì)話信息保存到服務(wù)器,然后向客戶端返回一個(gè)該信息的索引(token),但 Token 完全由開發(fā)者實(shí)現(xiàn),可以根據(jù)需要將會(huì)話信息存儲(chǔ)在內(nèi)存,數(shù)據(jù)庫,文件等地方,而對(duì)于該信息的索引,也可以根據(jù)具體需要選擇使用請(qǐng)求頭,請(qǐng)求體或者 Cookie 傳遞,也不必拘束于只 Cookie 傳遞。
JWT
全稱 json web token, 是一種客戶端存儲(chǔ)會(huì)話狀態(tài)的技術(shù),它使用數(shù)字簽名技術(shù)防止了負(fù)荷信息被篡改,jwt 包含三部分信息:
- Header:包含 token 類型和算法名稱
- Payload:存儲(chǔ)的負(fù)載信息(敏感信息不應(yīng)該明文存放在此)
- Signature:服務(wù)端使用私鑰對(duì) Header 和 Payload 的簽名,防止信息被篡改。
這三部分原本都是 json 字符串,最終他們會(huì)經(jīng)過 Base64 編碼后拼接到一起,使用 . 分割。
分布式解決方案
在分布式場(chǎng)景下,同一用戶的不同次請(qǐng)求可能會(huì)被打到不同的服務(wù)器上,這時(shí)如果還像單機(jī)時(shí)那樣存儲(chǔ),就會(huì)出問題,一般的解決方案包括:
- 粘性 session:將用戶綁定到一臺(tái)服務(wù)器上,如 Nginx 負(fù)載均衡策略使用 ip_hash, 但這樣如果當(dāng)前服務(wù)器發(fā)生故障,可能導(dǎo)致分配到這臺(tái)服務(wù)器上的用戶登錄信息失效,容錯(cuò)度低。
- session 復(fù)制:一臺(tái)服務(wù)器的 session 改變,就廣播給所有服務(wù)器,但會(huì)影響服務(wù)器性能
- session 共享:把所有服務(wù)器的 session 放在一起,如使用 redis 等分布式緩存做 session 集群。
- 客戶端記錄狀態(tài):使用諸如 JWT 之類的方法。
連接管理
連接管理是一個(gè) HTTP 的關(guān)鍵話題:打開和保持連接在很大程度上影響著網(wǎng)站和 Web 應(yīng)用程序的性能。在 HTTP/1.x 里有多種模型:短連接 ,長(zhǎng)連接和HTTP 流水線
短連接
HTTP 最早期的模型,也是 HTTP/1.x 的默認(rèn)模型,是短連接。每發(fā)起一個(gè)HTTP請(qǐng)求都會(huì)通過三次握手建立一個(gè)TCP連接,在接受到數(shù)據(jù)之后再通過四次揮手釋放連接,因?yàn)門CP連接的建立和釋放都是一個(gè)耗時(shí)操作,加之現(xiàn)代網(wǎng)頁可能需要多次連續(xù)請(qǐng)求才能渲染完成,這就顯得這種簡(jiǎn)單的模型效率低下。
TCP 協(xié)議握手本身就是耗費(fèi)時(shí)間的,所以 TCP 可以保持更多的熱連接來適應(yīng)負(fù)載。短連接破壞了 TCP 具備的能力,新的冷連接降低了其性能。
為此,HTTP/1.1 時(shí)新增加了兩種連接管理模式,分別是長(zhǎng)連接和流水線,在HTTP/2 中,又基于數(shù)據(jù)流采用了新的連接管理模式。
長(zhǎng)連接
長(zhǎng)連接是指在客戶端接受完數(shù)據(jù)后,不立刻關(guān)閉這個(gè) TCP 連接,這個(gè)連接還可以用來發(fā)送和接收其他 HTTP 數(shù)據(jù),這樣一來可以減少部分連接建立和釋放的耗時(shí),但這個(gè)連接也并不會(huì)一直保持,服務(wù)端可以設(shè)置 Keep-alive 標(biāo)頭來指定一個(gè)最小的連接保持時(shí)間(單位秒)和最大請(qǐng)求數(shù):
HTTP/1.1 200 OK Connection: Keep-Alive Keep-Alive: timeout=5, max=1000HTTP/1.0 里默認(rèn)并不使用長(zhǎng)連接。把 Connection 設(shè)置成 close 以外的其它參數(shù)都可以讓其保持長(zhǎng)連接,通常會(huì)設(shè)置為 retry-after。
在 HTTP/1.1 里,默認(rèn)就是長(zhǎng)連接的,協(xié)議頭都不用再去聲明它(但我們還是會(huì)把它加上,萬一某個(gè)時(shí)候因?yàn)槟撤N原因要退回到 HTTP/1.0 呢)。
長(zhǎng)連接并不總是好的,比如,他在空閑狀態(tài)下仍會(huì)消耗服務(wù)器資源,而在網(wǎng)絡(luò)重負(fù)載時(shí),還有可能遭受 DoS 攻擊。這種場(chǎng)景下,可以使用非長(zhǎng)連接,即盡快關(guān)閉那些空閑的連接,也能對(duì)性能有所提升。
流水線
默認(rèn)情況下,HTTP 請(qǐng)求是按順序發(fā)出的。下一個(gè)請(qǐng)求只有在當(dāng)前請(qǐng)求收到應(yīng)答過后才會(huì)被發(fā)出。由于會(huì)受到網(wǎng)絡(luò)延遲和帶寬的限制,在下一個(gè)請(qǐng)求被發(fā)送到服務(wù)器之前,可能需要等待很長(zhǎng)時(shí)間。
流水線是在同一條長(zhǎng)連接上發(fā)出連續(xù)的請(qǐng)求,而不用等待應(yīng)答返回。這樣可以避免連接延遲。理論上講,性能還會(huì)因?yàn)閮蓚€(gè) HTTP 請(qǐng)求有可能被打包到一個(gè) TCP 消息包中而得到提升,就算 HTTP 請(qǐng)求不斷的繼續(xù)導(dǎo)致 TCP 包的尺寸增加,通過設(shè)置 TCP 的 MSS(Maximum Segment Size) 選項(xiàng),流水線方式仍然足夠包含一系列簡(jiǎn)單的請(qǐng)求。
使用流水線的另一個(gè)需要注意的問題是錯(cuò)誤重傳,因此,只有冪等的方法,如 GET,HEAD,PUT, DELETE 等方法能夠安全地使用流水線。
流水線只是針對(duì)客戶端來說的,服務(wù)器依然和非流水線方式那樣工作,這就導(dǎo)致如果第一個(gè)請(qǐng)求非常耗時(shí),那流水線上后面的請(qǐng)求就會(huì)被阻塞住,這種現(xiàn)象被稱為Head-of-line blocking(隊(duì)頭阻塞),除此之外,復(fù)雜的網(wǎng)絡(luò)環(huán)境和代理服務(wù)器也可能會(huì)導(dǎo)致流水線不能像預(yù)期的那樣高效工作,因此,現(xiàn)代瀏覽器都沒有默認(rèn)啟用流水線,在 HTTP/2 里,有更高效的算法代替了流水線。
三者比較
CORS
在前后端分離開發(fā)時(shí),你也許遇到過類似這樣的報(bào)錯(cuò):
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,跨域資源共享),它首先是一個(gè)系統(tǒng),由一系列 HTTP 頭組成,這些 HTTP 頭決定了瀏覽器是否阻止前端 JavaScript 代碼獲取跨域請(qǐng)求的響應(yīng)。
之所以需要 CORS,是由于瀏覽器的同源安全策略:
同源安全策略
同源安全策略用來限制一個(gè)源(origin)的文檔或者它加載的腳本如何能與另一個(gè)源的資源進(jìn)行交互。它能幫助阻隔惡意文檔,減少可能被攻擊的媒介。
只有兩個(gè) URL 的協(xié)議,主機(jī),端口都相同時(shí),他們才被認(rèn)為是“同源的”,反之,如:http://www.a.com 和 https://www.a.com 則會(huì)被認(rèn)為是不同源的(協(xié)議不同),在默認(rèn)情況下,同源策略會(huì)阻止通過不同源的URL獲取資源,而 CORS 就是提供了一種機(jī)制,以允許不同源的資源進(jìn)行共享。
原理
CORS 的原理很簡(jiǎn)單,它通過添加一組 HTTP 頭,允許服務(wù)器聲明哪些源站通過瀏覽器有權(quán)限訪問哪些資源。另外,規(guī)范要求,對(duì)那些可能對(duì)服務(wù)器數(shù)據(jù)產(chǎn)生副作用的 HTTP 請(qǐng)求方法(非簡(jiǎn)單請(qǐng)求),瀏覽器必須首先使用 OPTIONS 方法發(fā)起一個(gè)預(yù)檢請(qǐng)求,從而獲知服務(wù)端是否允許該跨源請(qǐng)求。服務(wù)器確認(rèn)允許之后,才發(fā)起實(shí)際的 HTTP 請(qǐng)求。在預(yù)檢請(qǐng)求的返回中,服務(wù)器端也可以通知客戶端,是否需要攜帶身份憑證(包括 Cookies 或 HTTP 認(rèn)證相關(guān)數(shù)據(jù))。
上面說到的 “可能對(duì)服務(wù)器數(shù)據(jù)產(chǎn)生副作用的 HTTP 請(qǐng)求” 就是非簡(jiǎn)單請(qǐng)求(not-so-simple request),與之對(duì)應(yīng)的是簡(jiǎn)單請(qǐng)求(simple request),同時(shí)滿足以下幾個(gè)條件的,屬于簡(jiǎn)單請(qǐng)求。
請(qǐng)求方法是以下三種方法之一:
- HEAD
- GET
- POST
首部字段只包含被用戶代理自動(dòng)設(shè)置的首部字段(例如 Connection ,User-Agent)和允許人為設(shè)置的字段為 Fetch 規(guī)范定義的 對(duì) CORS 安全的首部字段集合。該集合為:
-
Accept
-
Accept-Language
-
Content-Language
-
Content-Type, 只限于三個(gè)值application/x-www-form-urlencoded、multipart/form-data、text/plain
-
DPR
-
Downlink
-
Save-Data
-
Width
-
Viewport-Width
請(qǐng)求中的任意XMLHttpRequestUpload 對(duì)象均沒有注冊(cè)任何事件監(jiān)聽器;XMLHttpRequestUpload 對(duì)象可以使用 XMLHttpRequest.upload 屬性訪問。
請(qǐng)求中沒有使用 ReadableStream 對(duì)象。
只要有其一不滿足,就是費(fèi)簡(jiǎn)單請(qǐng)求,非簡(jiǎn)單請(qǐng)求在正式請(qǐng)求之前會(huì)先使用 OPTION 方法像服務(wù)器發(fā)起一個(gè) 預(yù)檢請(qǐng)求,如下面這個(gè)請(qǐng)求:
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);} }當(dāng)前域?yàn)?foo.example.com,請(qǐng)求 bar.other, 屬于跨域請(qǐng)求,并且請(qǐng)求時(shí)自己添加了一個(gè)請(qǐng)求頭 X-PINGOTHER ,并且 Content-Type 類型為 application/xml, 所以它屬于一個(gè)非簡(jiǎn)單請(qǐng)求,在實(shí)際請(qǐng)求之前需要使用 OPTION 方法發(fā)一個(gè)預(yù)檢請(qǐng)求:
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-TypeHTTP/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預(yù)檢請(qǐng)求頭頭中最重要的部分有下面幾個(gè):
- Host: 要請(qǐng)求的域
- Origin: 發(fā)起請(qǐng)求的域,Host 和 Origin 不一樣,說明是跨域請(qǐng)求
- Access-Control-Request-Method: 正式的請(qǐng)求將要使用的方法
- Access-Control-Request-Headers: 正式請(qǐng)求將攜帶的自定義字段
服務(wù)器在收到這樣的預(yù)檢請(qǐng)求后就可以根據(jù)請(qǐng)求頭決定是否允許即將發(fā)送的實(shí)際請(qǐng)求,在服務(wù)器的響應(yīng)中,最重要的字段有以下幾個(gè):
- Access-Control-Allow-Origin: 服務(wù)器允許的域,允許所有域該值設(shè)置為 *
- Access-Control-Allow-Methods: 服務(wù)器允許的請(qǐng)求方法,允許所有方法設(shè)置為 *
- Access-Control-Allow-Headers: 服務(wù)器允許的請(qǐng)求頭
- Access-Control-Max-Age: 該響應(yīng)的有效時(shí)間為 86400 秒,也就是 24 小時(shí)。在有效時(shí)間內(nèi),瀏覽器無須為同一請(qǐng)求再次發(fā)起預(yù)檢請(qǐng)求。
接受到響應(yīng)后,瀏覽器會(huì)自動(dòng)判斷實(shí)際請(qǐng)求是否被允許,如果不被允許,將會(huì)報(bào)上面的錯(cuò)誤。
對(duì)于簡(jiǎn)單請(qǐng)求,通過請(qǐng)求中的 Origin 和響應(yīng)中的 Access-Control-Allow-Origin 就可以實(shí)現(xiàn)簡(jiǎn)單的訪問控制,如果請(qǐng)求的 Origin 不在許可范圍內(nèi),服務(wù)器會(huì)返回一個(gè)正常的響應(yīng),瀏覽器發(fā)現(xiàn)這個(gè)響應(yīng)的頭信息沒有包含Access-Control-Allow-Origin字段,就知道出錯(cuò)了,從而拋出一個(gè)錯(cuò)誤,被XMLHttpRequest的onerror回調(diào)函數(shù)捕獲。注意,這種錯(cuò)誤無法通過狀態(tài)碼識(shí)別,因?yàn)镠TTP回應(yīng)的狀態(tài)碼有可能是200。
HTTP 緩存
緩存是一種保存資源副本并在下次請(qǐng)求時(shí)直接使用該副本的技術(shù)。當(dāng) web 緩存發(fā)現(xiàn)請(qǐng)求的資源已經(jīng)被存儲(chǔ),它會(huì)攔截請(qǐng)求,返回該資源的拷貝,而不會(huì)去源服務(wù)器重新下載。這樣帶來的好處有:緩解服務(wù)器端壓力,提升性能(獲取資源的耗時(shí)更短了)。對(duì)于網(wǎng)站來說,緩存是達(dá)到高性能的重要組成部分。緩存需要合理配置,因?yàn)椴⒉皇撬匈Y源都是永久不變的:重要的是對(duì)一個(gè)資源的緩存應(yīng)截止到其下一次發(fā)生改變(即不能緩存過期的資源)。
緩存有很多種,以服務(wù)對(duì)象分類,緩存可以分為私有緩存和共享緩存,以行為分類,又可以把它分為強(qiáng)制緩存和對(duì)比緩存。
- 私有緩存:又叫瀏覽器緩存,只能用于單獨(dú)用戶。瀏覽器緩存擁有用戶通過HTTP下載的所有文檔。這些緩存為瀏覽過的文檔提供向后/向前導(dǎo)航,保存網(wǎng)頁,查看源碼等功能,可以避免再次向服務(wù)器發(fā)起多余的請(qǐng)求。它同樣可以提供緩存內(nèi)容的離線瀏覽。
- 共享緩存:又叫代理緩存,共享緩存可以被多個(gè)用戶使用。例如,ISP 或你所在的公司可能會(huì)架設(shè)一個(gè) web 代理來作為本地網(wǎng)絡(luò)基礎(chǔ)的一部分提供給用戶。這樣熱門的資源就會(huì)被重復(fù)使用,減少網(wǎng)絡(luò)擁堵與延遲。
- 強(qiáng)制緩存:緩存數(shù)據(jù)未失效時(shí),都可以使用緩存。
- 對(duì)比緩存:使用數(shù)據(jù)前需要請(qǐng)求服務(wù)器驗(yàn)證緩存是否失效。
緩存控制
緩存的原理很簡(jiǎn)單:客戶端在從服務(wù)器獲取到數(shù)據(jù)后,可以選擇將這些數(shù)據(jù)存儲(chǔ)下來,下一次請(qǐng)求同樣的數(shù)據(jù)時(shí),就可以不請(qǐng)求服務(wù)器直接返回先前存儲(chǔ)的數(shù)據(jù)了,正確使用緩存可以提高響應(yīng)速度,降低服務(wù)器壓力;
這里的客戶端可以是瀏覽器(如私有緩存),也可以是請(qǐng)求鏈路上的中間代理(如共享緩存),但對(duì)服務(wù)器來說,他們都是一樣的,而服務(wù)器并沒有辦法主動(dòng)向客戶端推送數(shù)據(jù),這就導(dǎo)致必須有一種機(jī)制去保證緩存是“新鮮”的,HTTP 協(xié)議通過一些列的頭字段實(shí)現(xiàn)了緩存控制,其中最重要的字段是 Cache-Control,他有以下幾種值:
強(qiáng)制緩存
在某個(gè)資源的響應(yīng)中,如果 Cache-Control:max-age=31536000, 則表明這個(gè)資源在未來一年內(nèi)再次請(qǐng)求可以直接從緩存中拿,如第一次請(qǐng)求 avatar.png 時(shí),響應(yīng)里標(biāo)明最大有效時(shí)間為 600s (10 分鐘),第二次再次請(qǐng)求該資源時(shí),從 size 列就可以看倒該資源直接從緩存返回。
| [外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-GR4bBSEk-1613829654718)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20210115164159878.png)] |
這里的緩存就是強(qiáng)制緩存,只要在10分鐘內(nèi),都可以使用緩存的資源。
如果過了10分鐘,緩存中的這個(gè)資源就可能是過期了的,這時(shí)就需要詢問服務(wù)器這個(gè)資源是不是“新鮮”的,具體客戶端會(huì)向服務(wù)器發(fā)起一個(gè)攜帶 [If-None-Match](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) 頭的請(qǐng)求:
如果這個(gè)資源是“新鮮”的,服務(wù)器會(huì)返回 304 (Not Modified)(該響應(yīng)不會(huì)有帶有實(shí)體信息),如果服務(wù)器發(fā)現(xiàn)這個(gè)資源已經(jīng)過期了,則會(huì)返回新的資源。
對(duì)比緩存
與強(qiáng)制緩存不同,對(duì)比緩存每次使用緩存數(shù)據(jù)前都會(huì)向服務(wù)器查詢?cè)撡Y源是否有效,但由于查詢和響應(yīng)大部分情況下都只包含頭部,所以比起不使用緩存,對(duì)比緩存也可以大大提高響應(yīng)速度和降低服務(wù)器壓力。它依賴于下面幾個(gè)頭部字段:
- Last-Modified: 響應(yīng)頭字段,告訴客戶端這個(gè)資源最后更新的時(shí)間
- If-Modified-Since: 請(qǐng)求頭字段,如果請(qǐng)求頭中攜帶了這個(gè)字段,服務(wù)器會(huì)將該字段的值和資源最后修改的時(shí)間做對(duì)比,如果最后修改的時(shí)間大于字段值,說明數(shù)據(jù)已經(jīng)被修改,則響應(yīng) 200, 返回最新的資源,否則,響應(yīng) 304 告訴客戶端資源未修改,可以使用緩存。
上面兩個(gè)頭部字段是根據(jù)修改時(shí)間判斷資源是否是新鮮的,這樣做的準(zhǔn)確度不是很高,還有一組頭部字段 ETag 和 If-None-Match 使用資源的唯一標(biāo)識(shí)來判斷資源是否被修改:
- ETag: 響應(yīng)頭字段,用于服務(wù)器告訴客戶端資源的唯一標(biāo)識(shí)(標(biāo)識(shí)的生成規(guī)則由服務(wù)端確定)
- If-None-Match: 請(qǐng)求頭字段,如果請(qǐng)求頭中包含此字段,服務(wù)端會(huì)對(duì)比該字段的值與最新的資源的標(biāo)識(shí),如果不相同,說明資源被修改,響應(yīng) 200, 返回最新的資源,否則,響應(yīng) 304.
ETag 和 If-None-Match 的優(yōu)先級(jí)高于 Last-Modified 和 If-Modified-Since
除此之外,與緩存相關(guān)的還有一個(gè)請(qǐng)求頭:Vary, 用來決定客戶端使用新資源還是緩存資源,使用vary頭有利于內(nèi)容服務(wù)的動(dòng)態(tài)多樣性。例如,使用Vary: User-Agent頭,緩存服務(wù)器需要通過UA判斷是否使用緩存的頁面。如果需要區(qū)分移動(dòng)端和桌面端的展示內(nèi)容,利用這種方式就能避免在不同的終端展示錯(cuò)誤的布局。另外,它可以幫助 Google 或者其他搜索引擎更好地發(fā)現(xiàn)頁面的移動(dòng)版本,并且告訴搜索引擎沒有引入Cloaking。
使用緩存
說了這么多,我們應(yīng)該怎么通過使用緩存來提高站點(diǎn)的性能呢?
首先,對(duì)于私有緩存,開發(fā)者一般是不需要關(guān)注的,瀏覽器會(huì)自動(dòng)緩存請(qǐng)求成功的 GET 數(shù)據(jù),用來支持后退等功能。我們一般關(guān)注的是共有緩存,也就是在代理服務(wù)器上緩存數(shù)據(jù),客戶端請(qǐng)求到代理服務(wù)器上后,就可以直接返回了,下面以 Nginx 為例,簡(jiǎn)單說明如何使用緩存。
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 用來配置緩存數(shù)據(jù)保存的路徑,里面的主要字段含義如下:
- levels: 在單個(gè)目錄中包含大量文件會(huì)降低文件訪問速度,因此我們建議對(duì)大多數(shù)部署使用兩級(jí)目錄層次結(jié)構(gòu)。如果未包含 levels Nginx會(huì)將所有文件放在同一目錄中。
- keys_zone: 設(shè)置共享內(nèi)存區(qū)域,用于存儲(chǔ)緩存鍵和元數(shù)據(jù),后面的參數(shù)表示該區(qū)域的大小,一般來說,1 MB區(qū)域可以存儲(chǔ)大約8,000個(gè) key 數(shù)據(jù)。
- max-size: 緩存能占的最大內(nèi)存。
- inactive:指定項(xiàng)目在未被訪問的情況下可以保留在緩存中的時(shí)間長(zhǎng)度。在此示例中,緩存管理器進(jìn)程會(huì)自動(dòng)從緩存中刪除1分鐘未請(qǐng)求的文件,無論其是否已過期。默認(rèn)值為10分鐘(10m)。非活動(dòng)內(nèi)容與過期內(nèi)容不同。Nginx 不會(huì)自動(dòng)刪除緩存header定義為已過期內(nèi)容(例如 Cache-Control:max-age=120)。過期(陳舊)內(nèi)容僅在指定時(shí)間內(nèi)未被訪問時(shí)被刪除。訪問過期內(nèi)容時(shí),Nginx 會(huì)從原始服務(wù)器刷新它并重置inactive計(jì)時(shí)器。
其次,我們?cè)?Location 塊中配置了幾個(gè)值:
- proxy_cache:定義用于緩存的共享內(nèi)存區(qū)域。
- proxy_cache_valid: 指定哪些狀態(tài)的響應(yīng)可以被緩存。
- proxy_cache_methods: 哪些方法的請(qǐng)求可以被緩存。
除此之外,我們添加了一個(gè)響應(yīng)頭部字段 X-Proxy-Cache 用來查看緩存是否生效。
這里只是簡(jiǎn)單對(duì)數(shù)據(jù)進(jìn)行了緩存,服務(wù)端沒有提供緩存驗(yàn)證的功能,所以可能出現(xiàn)服務(wù)端數(shù)據(jù)已經(jīng)改變但緩存沒更新的情況。
HTTPS 細(xì)節(jié)
HTTPS 的原理
密碼學(xué)基礎(chǔ)
對(duì)稱加密和非對(duì)稱加密
-
對(duì)稱加密: 加密和解密時(shí)使用的密鑰是一樣的,比如 DES, 優(yōu)點(diǎn)是速度快,缺點(diǎn)是在協(xié)商密鑰時(shí),可能會(huì)泄露密鑰
-
非對(duì)稱加密:有兩個(gè)密鑰,公鑰和私鑰,使用公鑰加密,私鑰解密,公鑰是公開的,比如 RSA。
非對(duì)稱加密有一個(gè)形象的比喻:
A有一份機(jī)密文件,想要發(fā)給B,發(fā)之前先向B要一個(gè)打開的保險(xiǎn)柜,把文件裝進(jìn)保險(xiǎn)柜鎖住后再發(fā)給B,整個(gè)過程中保險(xiǎn)柜密碼只有B知道,這里的保險(xiǎn)柜相當(dāng)于公鑰,保險(xiǎn)柜密碼相當(dāng)于私鑰。
CA機(jī)構(gòu)和證書
公鑰是公開的,那怎么證明這個(gè)公鑰是屬于你的呢?接上面的比喻,如果有一個(gè)中間人 C,在 B 向 A 發(fā)保險(xiǎn)柜的時(shí)候?qū)?B 的保險(xiǎn)柜換成自己的發(fā)給 A,這樣 C 就可以竊取到文件,A 如果想要驗(yàn)證這個(gè)保險(xiǎn)柜是不是 B 的,就需要一個(gè) A、B 都信任的第三方機(jī)構(gòu),B 在發(fā)給 A 之前請(qǐng)求第三方機(jī)構(gòu)在保險(xiǎn)箱上蓋一個(gè)戳,A 收到后再請(qǐng)求第三方機(jī)構(gòu)檢查戳是不是真的就可以了,這里的第三方機(jī)構(gòu)就是 CA 機(jī)構(gòu),這個(gè)戳就是證書,具體來說:
每個(gè)CA機(jī)構(gòu)都會(huì)有自己的一組密鑰對(duì)(CA 的公鑰是通信雙方都信任的),現(xiàn)在 B 有一個(gè)公鑰,他要證明這個(gè)公鑰是他的,就需要向 CA 機(jī)構(gòu)請(qǐng)求一份該公鑰的證書,請(qǐng)求時(shí),B 需要向 CA 機(jī)構(gòu)提供自己的信息以及要認(rèn)證的公鑰(這些信息會(huì)組成CSR文件),CA 機(jī)構(gòu)收到請(qǐng)求后,會(huì)檢查 CSR 的真實(shí)性,檢查無誤后,CA 會(huì)將 CSR 的內(nèi)容哈希后用自己的私鑰簽名,然后將 CSR 中的信息和簽名組合成證書發(fā)給 B。
所以一份證書中包含的典型內(nèi)容包括:
B 有了 CA 機(jī)構(gòu)的證書,在向 A 發(fā)送公鑰時(shí)就只需要發(fā)送證書了,A 收到證書,用 CA 機(jī)構(gòu)的公鑰解密簽名,然后對(duì)證書中的明文數(shù)據(jù)以同樣的算法做哈希,只需要對(duì)比兩個(gè)哈希值就可以判斷證書有沒有被篡改了,如果證書沒被篡改,則可以放心使用證書中的公鑰與 B 通信了。
HTTPS 通信流程
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-8HD5SxsD-1613829654732)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20210122163508612.png)]
看上面的截圖,前三行 [SYN], [SYN, ACK], [ACK] 是典型的 TCP 三次握手,那么在三次握手后,客戶端向服務(wù)端以 TLSV1.2 協(xié)議向服務(wù)端發(fā)送了一個(gè) client Hello 包,通過 Client Hello, 客戶端會(huì)生成一個(gè)隨機(jī)數(shù),并告訴服務(wù)端自己支持的加密,哈希等算法,我們可以在這個(gè)報(bào)文里看到這些內(nèi)容:
其中,Random 就是客戶端選取的隨機(jī)數(shù),Cipher Suites 中就是客戶端支持的算法。
接下來就是 Server Hello, 在這一步,服務(wù)端同樣會(huì)生成一個(gè)隨機(jī)數(shù),并且會(huì)從客戶端支持的算法中選取一種,通過 Server Hello 的方式告訴客戶端:
可以看到 Server Hello 和 Client Hello 的報(bào)文內(nèi)容區(qū)別不大,只是 Client Hello 中的 Cipher Suite 有許多項(xiàng),而 Server Hello 的只有一項(xiàng),因?yàn)榉?wù)端會(huì)選擇安全性最高的加密方式,需要注意的是這里選擇的是一組算法,以這里選擇的 0xc02f 為例,它的名字叫 TLS_ECDHE_RSA_WITH_AES_128_GCM_ SHA256 (0xc02f) 其中包括:
- 密鑰交換算法:ECDHE
- 身份驗(yàn)證算法:RSA
- 對(duì)稱加密算法:AES_128_GCM
- 摘要算法:SHA256
在這之后,服務(wù)端在一個(gè) TLS 包里進(jìn)行了三個(gè)負(fù)載:
- Certificate:發(fā)送證書
- Server Key Exchange:包含密鑰交換算法 DHE/ECDHE 所需要的額外參數(shù)。
- Server Hello Done:表明服務(wù)端相關(guān)信息發(fā)送結(jié)束,這之后服務(wù)端會(huì)等待客戶端響應(yīng)。
到這一步,客戶端已經(jīng)拿到了服務(wù)器的證書,會(huì)檢查證書是否有效,如果證書失效,客戶端瀏覽器會(huì)阻止后續(xù)操作,反之,客戶端會(huì)繼續(xù)與服務(wù)端協(xié)商對(duì)稱加密密鑰:
客戶端向服務(wù)端發(fā)送一個(gè)響應(yīng)(id = 67)包含三個(gè)負(fù)載:
- Client Key Exchange:類似 Server Key Exchange,客戶端生成一個(gè)新的隨機(jī)數(shù)(Premaster secret),并使用數(shù)字證書中的公鑰加密后發(fā)給服務(wù)端。
- Change cipher Spec: 已被廢棄,不攜帶數(shù)據(jù)
- Encrypted handshake message:這個(gè)步驟客戶端和服務(wù)器在握手完后都會(huì)進(jìn)行,以告訴對(duì)方自己在整個(gè)握手過程中收到了什么數(shù)據(jù),發(fā)送了什么數(shù)據(jù),保證中間沒人篡改報(bào)文。
到現(xiàn)在為止,我們總結(jié)一下客戶端和服務(wù)器做了什么:
現(xiàn)在,根據(jù)三個(gè)隨機(jī)數(shù),客戶端和服務(wù)器就會(huì)根據(jù)約定好的對(duì)稱加密算法生成最終的對(duì)稱加密密鑰,后續(xù)的數(shù)據(jù)傳輸就會(huì)使用該密鑰加密。
總結(jié)
HTTPS 建立連接的過程總結(jié)如下:
使用 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 細(xì)節(jié)
實(shí)現(xiàn)
2015 年,HTTP/2 發(fā)布。HTTP/2 是現(xiàn)行 HTTP 協(xié)議(HTTP/1.x)的替代,但它不是重寫,HTTP 方法/狀態(tài)碼/語義都與 HTTP/1.x 一樣。HTTP/2 基于 SPDY3,專注于性能,最大的一個(gè)目標(biāo)是在用戶和網(wǎng)站間只用一個(gè)連接(connection)。
那么SPDY3是什么呢?
SPDY是谷歌自行研發(fā)的 SPDY 協(xié)議,主要解決 HTTP/1.1 效率不高的問題。谷歌推出 SPDY,才算是正式改造 HTTP 協(xié)議本身。降低延遲,壓縮 header 等等,SPDY 的實(shí)踐證明了這些優(yōu)化的效果,也最終帶來 HTTP/2 的誕生。
HTTP/2 由兩個(gè)規(guī)范(Specification)組成:
那么HTTP2在HTTP1.1的基礎(chǔ)上做了哪些改進(jìn)
- 二進(jìn)制傳輸
- 請(qǐng)求和響應(yīng)復(fù)用
- Header壓縮
- Server Push(服務(wù)端推送)
二進(jìn)制傳輸
HTTP/2 采用二進(jìn)制格式傳輸數(shù)據(jù),而非 HTTP 1.x 的文本格式,二進(jìn)制協(xié)議解析起來更高效。 HTTP / 1 的請(qǐng)求和響應(yīng)報(bào)文,都是由起始行,首部和實(shí)體正文(可選)組成,各部分之間以文本換行符分隔。HTTP/2 將請(qǐng)求和響應(yīng)數(shù)據(jù)分割為更小的幀,并且它們采用二進(jìn)制編碼。
新的二進(jìn)制分幀機(jī)制改變了客戶端與服務(wù)器之間交換數(shù)據(jù)的方式。 為了說明這個(gè)過程,我們需要了解 HTTP/2 的三個(gè)概念:
- 數(shù)據(jù)流:已建立的連接內(nèi)的雙向字節(jié)流,可以承載一條或多條消息。
- 消息:與邏輯請(qǐng)求或響應(yīng)消息對(duì)應(yīng)的完整的一系列幀。
- 幀:HTTP/2 通信的最小單位,每個(gè)幀都包含幀頭,至少也會(huì)標(biāo)識(shí)出當(dāng)前幀所屬的數(shù)據(jù)流。
這些概念的關(guān)系總結(jié)如下:
- 所有通信都在一個(gè) TCP 連接上完成,此連接可以承載任意數(shù)量的雙向數(shù)據(jù)流。
- 每個(gè)數(shù)據(jù)流都有一個(gè)唯一的標(biāo)識(shí)符和可選的優(yōu)先級(jí)信息,用于承載雙向消息。
- 每條消息都是一條邏輯 HTTP 消息(例如請(qǐng)求或響應(yīng)),包含一個(gè)或多個(gè)幀。
- 幀是最小的通信單位,承載著特定類型的數(shù)據(jù),例如 HTTP 標(biāo)頭、消息負(fù)載等等。 來自不同數(shù)據(jù)流的幀可以交錯(cuò)發(fā)送,然后再根據(jù)每個(gè)幀頭的數(shù)據(jù)流標(biāo)識(shí)符重新組裝。
請(qǐng)求和響應(yīng)復(fù)用
在 HTTP/1.x 中,如果客戶端要想發(fā)起多個(gè)并行請(qǐng)求以提升性能,則必須使用多個(gè) TCP 連接,這是 HTTP/1.x 交付模型的直接結(jié)果,該模型可以保證每個(gè)連接每次只交付一個(gè)響應(yīng)(響應(yīng)排隊(duì))。 更糟糕的是,這種模型也會(huì)導(dǎo)致隊(duì)首阻塞,從而造成底層 TCP 連接的效率低下。
HTTP/2 中新的二進(jìn)制分幀層突破了這些限制,實(shí)現(xiàn)了完整的請(qǐng)求和響應(yīng)復(fù)用:客戶端和服務(wù)器可以將 HTTP 消息分解為互不依賴的幀,然后交錯(cuò)發(fā)送,最后再在另一端把它們重新組裝起來。
在 HTTP/2 中,有了二進(jìn)制分幀之后,HTTP /2 不再依賴 TCP 鏈接去實(shí)現(xiàn)多流并行了,在 HTTP/2 中:
- 同域名下所有通信都在單個(gè)連接上完成。
- 單個(gè)連接可以承載任意數(shù)量的雙向數(shù)據(jù)流。
- 數(shù)據(jù)流以消息的形式發(fā)送,而消息又由一個(gè)或多個(gè)幀組成,多個(gè)幀之間可以亂序發(fā)送,因?yàn)楦鶕?jù)幀首部的流標(biāo)識(shí)可以重新組裝。
這一特性,使性能有了極大提升:
- 同個(gè)域名只需要占用一個(gè) TCP 連接,使用一個(gè)連接并行發(fā)送多個(gè)請(qǐng)求和響應(yīng),消除了因多個(gè) TCP 連接而帶來的延時(shí)和內(nèi)存消耗。
- 并行交錯(cuò)地發(fā)送多個(gè)請(qǐng)求,請(qǐng)求之間互不影響。
- 并行交錯(cuò)地發(fā)送多個(gè)響應(yīng),響應(yīng)之間互不干擾。
- 在 HTTP/2 中,每個(gè)請(qǐng)求都可以帶一個(gè) 31bit 的優(yōu)先值,0 表示最高優(yōu)先級(jí), 數(shù)值越大優(yōu)先級(jí)越低。有了這個(gè)優(yōu)先值,客戶端和服務(wù)器就可以在處理不同的流時(shí)采取不同的策略,以最優(yōu)的方式發(fā)送流、消息和幀。
Header壓縮
在 HTTP/1 中,我們使用文本的形式傳輸 header,在 header 攜帶 cookie 的情況下,可能每次都需要重復(fù)傳輸幾百到幾千的字節(jié)。為了減少這塊的資源消耗并提升性能,HTTP/2 使用 HPACK 壓縮格式壓縮請(qǐng)求和響應(yīng)標(biāo)頭元數(shù)據(jù),這種格式采用兩種強(qiáng)大的技術(shù):
利用霍夫曼編碼,可以在傳輸時(shí)對(duì)各個(gè)值進(jìn)行壓縮,而利用之前傳輸值的索引列表,我們可以通過傳輸索引值的方式對(duì)重復(fù)值進(jìn)行編碼,索引值可用于有效查詢和重構(gòu)完整的標(biāo)頭鍵值對(duì)。
作為一種進(jìn)一步優(yōu)化方式,HPACK 壓縮上下文包含一個(gè)靜態(tài)表和一個(gè)動(dòng)態(tài)表:靜態(tài)表在規(guī)范中定義,并提供了一個(gè)包含所有連接都可能使用的常用 HTTP 標(biāo)頭字段(例如,有效標(biāo)頭名稱)的列表;動(dòng)態(tài)表最初為空,將根據(jù)在特定連接內(nèi)交換的值進(jìn)行更新。 因此,為之前未見過的值采用靜態(tài) Huffman 編碼,并替換每一側(cè)靜態(tài)表或動(dòng)態(tài)表中已存在值的索引,可以減小每個(gè)請(qǐng)求的大小。
注:在 HTTP/2 中,請(qǐng)求和響應(yīng)標(biāo)頭字段的定義保持不變,僅有一些微小的差異:所有標(biāo)頭字段名稱均為小寫,請(qǐng)求行現(xiàn)在拆分成各個(gè) :method、:scheme、:authority 和 :path 偽標(biāo)頭字段。
如需了解有關(guān) HPACK 壓縮算法的完整詳情,請(qǐng)參閱 IETF HPACK - HTTP/2 的標(biāo)頭壓縮。
Server Push
HTTP/2 新增的另一個(gè)強(qiáng)大的新功能是,服務(wù)器可以對(duì)一個(gè)客戶端請(qǐng)求發(fā)送多個(gè)響應(yīng)。 換句話說,除了對(duì)最初請(qǐng)求的響應(yīng)外,服務(wù)器還可以向客戶端推送額外資源如下圖所示,而無需客戶端明確地請(qǐng)求。
為什么在瀏覽器中需要一種此類機(jī)制呢?一個(gè)典型的網(wǎng)絡(luò)應(yīng)用包含多種資源,客戶端需要檢查服務(wù)器提供的文檔才能逐個(gè)找到它們。 那為什么不讓服務(wù)器提前推送這些資源,從而減少額外的延遲時(shí)間呢? 服務(wù)器已經(jīng)知道客戶端下一步要請(qǐng)求什么資源,這時(shí)候服務(wù)器推送即可派上用場(chǎng)。
事實(shí)上,如果您在網(wǎng)頁中內(nèi)聯(lián)過 CSS、JavaScript,或者通過數(shù)據(jù) URI 內(nèi)聯(lián)過其他資產(chǎn)(請(qǐng)參閱資源內(nèi)聯(lián)),那么您就已經(jīng)親身體驗(yàn)過服務(wù)器推送了。 對(duì)于將資源手動(dòng)內(nèi)聯(lián)到文檔中的過程,我們實(shí)際上是在將資源推送給客戶端,而不是等待客戶端請(qǐng)求。 使用 HTTP/2,我們不僅可以實(shí)現(xiàn)相同結(jié)果,還會(huì)獲得其他性能優(yōu)勢(shì)。 推送資源可以進(jìn)行以下處理:
- 由客戶端緩存
- 在不同頁面之間重用
- 與其他資源一起復(fù)用
- 由服務(wù)器設(shè)定優(yōu)先級(jí)
- 被客戶端拒絕
服務(wù)端推送如何實(shí)現(xiàn)
所有服務(wù)器推送數(shù)據(jù)流都由 PUSH_PROMISE 幀發(fā)起,表明了服務(wù)器向客戶端推送所述資源的意圖,并且需要先于請(qǐng)求推送資源的響應(yīng)數(shù)據(jù)傳輸。 這種傳輸順序非常重要:客戶端需要了解服務(wù)器打算推送哪些資源,以免為這些資源創(chuàng)建重復(fù)請(qǐng)求。 滿足此要求的最簡(jiǎn)單策略是先于父響應(yīng)(即,DATA 幀)發(fā)送所有 PUSH_PROMISE 幀,其中包含所承諾資源的 HTTP 標(biāo)頭。
在客戶端接收到 PUSH_PROMISE 幀后,它可以根據(jù)自身情況選擇拒絕數(shù)據(jù)流(通過 RST_STREAM 幀)。 (例如,如果資源已經(jīng)位于緩存中,便可能會(huì)發(fā)生這種情況。) 這是一個(gè)相對(duì)于 HTTP/1.x 的重要提升。 相比之下,使用資源內(nèi)聯(lián)(一種受歡迎的 HTTP/1.x“優(yōu)化”)等同于“強(qiáng)制推送”:客戶端無法選擇拒絕、取消或單獨(dú)處理內(nèi)聯(lián)的資源。
使用 HTTP/2,客戶端仍然完全掌控服務(wù)器推送的使用方式。 客戶端可以限制并行推送的數(shù)據(jù)流數(shù)量;調(diào)整初始的流控制窗口以控制在數(shù)據(jù)流首次打開時(shí)推送的數(shù)據(jù)量;或完全停用服務(wù)器推送。 這些優(yōu)先級(jí)在 HTTP/2 連接開始時(shí)通過 SETTINGS 幀傳輸,可能隨時(shí)更新。
過渡到 HTTP/2
上面說了這么多,我們要如何啟用HTTP2呢?
如果你使用的是 nginx,那么你只需要加一個(gè) http2 即可:
server {listen 8888 ssl http2;server_name *.junebao.top;# ... }如果你使用 Golang 的 Gin 框架,他默認(rèn)支持 HTTP/2,你可以使用 RunTLS() 使用 HTTP/2,如下:
package mainimport ("github.com/gin-gonic/gin" )func main() {engine := gin.Default()engine.GET("./", func(context *gin.Context) {context.JSON(200, map[string]string{"msg": "ok"})})// 服務(wù)端推送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 內(nèi)置的 Tomcat 服務(wù)器,那么只需要在配置文件中添加配置:
server:http2:enabled: on只有 Tomcat 9 版本之后版本才支持 HTTP/2 協(xié)議。在 conf/server.xml 中增加內(nèi)容:
<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 細(xì)節(jié)
為什么要出現(xiàn)HTTP3
雖然 HTTP/2 解決了很多之前舊版本的問題,但是它還是存在一個(gè)巨大的問題,主要是底層支撐的 TCP 協(xié)議造成的。
上文提到 HTTP/2 使用了多路復(fù)用,一般來說同一域名下只需要使用一個(gè) TCP 連接。但當(dāng)這個(gè)連接中出現(xiàn)了丟包的情況,那就會(huì)導(dǎo)致 HTTP/2 的表現(xiàn)情況反倒不如 HTTP/1 了。
因?yàn)樵诔霈F(xiàn)丟包的情況下,整個(gè) TCP 都要開始等待重傳,也就導(dǎo)致了后面的所有數(shù)據(jù)都被阻塞了。但是對(duì)于 HTTP/1.1 來說,可以開啟多個(gè) TCP 連接,出現(xiàn)這種情況反到只會(huì)影響其中一個(gè)連接,剩余的 TCP 連接還可以正常傳輸數(shù)據(jù)。
那么可能就會(huì)有人考慮到去修改 TCP 協(xié)議,其實(shí)這已經(jīng)是一件不可能完成的任務(wù)了。因?yàn)?TCP 存在的時(shí)間實(shí)在太長(zhǎng),已經(jīng)充斥在各種設(shè)備中,并且這個(gè)協(xié)議是由操作系統(tǒng)實(shí)現(xiàn)的,更新起來不大現(xiàn)實(shí)。
基于這個(gè)原因,Google 就更起爐灶搞了一個(gè)基于 UDP 協(xié)議的 QUIC 協(xié)議,并且使用在了 HTTP/3 上,HTTP/3 之前名為 HTTP-over-QUIC,從這個(gè)名字中我們也可以發(fā)現(xiàn),HTTP/3 最大的改造就是使用了 QUIC(快速 UDP Internet 連接)。
QUIC
QUIC(Quick UDP Internet Connection)是谷歌制定的一種基于UDP的低時(shí)延的互聯(lián)網(wǎng)傳輸層協(xié)議。在2016年11月國際互聯(lián)網(wǎng)工程任務(wù)組(IETF)召開了第一次QUIC工作組會(huì)議,受到了業(yè)界的廣泛關(guān)注。這也意味著QUIC開始了它的標(biāo)準(zhǔn)化過程,成為新一代傳輸層協(xié)議 。
優(yōu)勢(shì):
可見HTTP3在效率上和安全性上都有了很大程度上的修改,但是由于目前這個(gè)標(biāo)準(zhǔn)還在論證中,Nginx等也只是在測(cè)試版中加入了對(duì)HTTP3的支持,等到技術(shù)真正的論證實(shí)現(xiàn)完成,我們就可以使用上快速且安全的HTTP3協(xié)議了,期待著這一天的到來。
參考
mozilla 開發(fā)文檔
谷歌開發(fā)文檔 HTTP2 簡(jiǎn)介
Nginx緩存最佳實(shí)踐
讓互聯(lián)網(wǎng)更快:新一代QUIC協(xié)議在騰訊的技術(shù)實(shí)踐分享
總結(jié)
以上是生活随笔為你收集整理的关于 HTTP 的一切(HTTP/1.1,HTTP/2,HTTP/3,HTTPS, CORS, 缓存 ,无状态)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 算法课 - 最大流问题
- 下一篇: QQ机器人闪照转发/撤回消息转发【最新b