(建議精讀)HTTP靈魂之問,鞏固你的 HTTP 知識體系

上回就已經承諾過你們,必定會出 HTTP 的系列文章,今天終於整理完成了。做爲一個 web 開發,HTTP 幾乎是每天要打交道的東西,但我發現大部分人對 HTTP 只是淺嘗輒止,對更多的細節及原理就瞭解不深了,在面試的時候感受很是吃力。這篇文章就是爲了幫助你們樹立完整的 HTTP 知識體系,並達到必定的深度,從容地應對各類靈魂之問,也同時提高本身做爲一個 web 開發的專業素養吧。這是本文的思惟導圖:javascript

001. HTTP 報文結構是怎樣的?

對於 TCP 而言,在傳輸的時候分爲兩個部分:TCP頭數據部分css

而 HTTP 相似,也是header + body的結構,具體而言:html

起始行 + 頭部 + 空行 + 實體
複製代碼

因爲 http 請求報文響應報文是有必定區別,所以咱們分開介紹。前端

起始行

對於請求報文來講,起始行相似下面這樣:java

GET /home HTTP/1.1
複製代碼

也就是方法 + 路徑 + http版本node

對於響應報文來講,起始行通常張這個樣:git

HTTP/1.1 200 OK
複製代碼

響應報文的起始行也叫作狀態行。由http版本、狀態碼和緣由三部分組成。github

值得注意的是,在起始行中,每兩個部分之間用空格隔開,最後一個部分後面應該接一個換行,嚴格遵循ABNF語法規範。web

頭部

展現一下請求頭和響應頭在報文中的位置:面試

無論是請求頭仍是響應頭,其中的字段是至關多的,並且牽扯到http很是多的特性,這裏就不一一列舉的,重點看看這些頭部字段的格式:

    1. 字段名不區分大小寫
    1. 字段名不容許出現空格,不能夠出現下劃線_
    1. 字段名後面必須緊接着:

空行

很重要,用來區分開頭部實體

問: 若是說在頭部中間故意加一個空行會怎麼樣?

那麼空行後的內容所有被視爲實體。

實體

就是具體的數據了,也就是body部分。請求報文對應請求體, 響應報文對應響應體

002. 如何理解 HTTP 的請求方法?

有哪些請求方法?

http/1.1規定了如下請求方法(注意,都是大寫):

  • GET: 一般用來獲取資源
  • HEAD: 獲取資源的元信息
  • POST: 提交數據,即上傳數據
  • PUT: 修改數據
  • DELETE: 刪除資源(幾乎用不到)
  • CONNECT: 創建鏈接隧道,用於代理服務器
  • OPTIONS: 列出可對資源實行的請求方法,用來跨域請求
  • TRACE: 追蹤請求-響應的傳輸路徑

GET 和 POST 有什麼區別?

首先最直觀的是語義上的區別。

然後又有這樣一些具體的差異:

  • 緩存的角度,GET 請求會被瀏覽器主動緩存下來,留下歷史記錄,而 POST 默認不會。
  • 編碼的角度,GET 只能進行 URL 編碼,只能接收 ASCII 字符,而 POST 沒有限制。
  • 參數的角度,GET 通常放在 URL 中,所以不安全,POST 放在請求體中,更適合傳輸敏感信息。
  • 冪等性的角度,GET冪等的,而POST不是。(冪等表示執行相同的操做,結果也是相同的)
  • TCP的角度,GET 請求會把請求報文一次性發出去,而 POST 會分爲兩個 TCP 數據包,首先發 header 部分,若是服務器響應 100(continue), 而後發 body 部分。(火狐瀏覽器除外,它的 POST 請求只發一個 TCP 包)

003: 如何理解 URI?

URI, 全稱爲(Uniform Resource Identifier), 也就是統一資源標識符,它的做用很簡單,就是區分互聯網上不一樣的資源。

可是,它並非咱們常說的網址, 網址指的是URL, 實際上URI包含了URNURL兩個部分,因爲 URL 過於普及,就默認將 URI 視爲 URL 了。

URI 的結構

URI 真正最完整的結構是這樣的。

可能你會有疑問,好像跟平時見到的不太同樣啊!先別急,咱們來一一拆解。

scheme 表示協議名,好比http, https, file等等。後面必須和://連在一塊兒。

user:passwd@ 表示登陸主機時的用戶信息,不過很不安全,不推薦使用,也不經常使用。

host:port表示主機名和端口。

path表示請求路徑,標記資源所在位置。

query表示查詢參數,爲key=val這種形式,多個鍵值對之間用&隔開。

fragment表示 URI 所定位的資源內的一個錨點,瀏覽器能夠根據這個錨點跳轉到對應的位置。

舉個例子:

https://www.baidu.com/s?wd=HTTP&rsv_spt=1
複製代碼

這個 URI 中,httpsscheme部分,www.baidu.comhost:port部分(注意,http 和 https 的默認端口分別爲80、443),/spath部分,而wd=HTTP&rsv_spt=1就是query部分。

URI 編碼

URI 只能使用ASCII, ASCII 以外的字符是不支持顯示的,並且還有一部分符號是界定符,若是不加以處理就會致使解析出錯。

所以,URI 引入了編碼機制,將全部非 ASCII 碼字符界定符轉爲十六進制字節值,而後在前面加個%

如,空格被轉義成了%20三元被轉義成了%E4%B8%89%E5%85%83

004: 如何理解 HTTP 狀態碼?

RFC 規定 HTTP 的狀態碼爲三位數,被分爲五類:

  • 1xx: 表示目前是協議處理的中間狀態,還須要後續操做。
  • 2xx: 表示成功狀態。
  • 3xx: 重定向狀態,資源位置發生變更,須要從新請求。
  • 4xx: 請求報文有誤。
  • 5xx: 服務器端發生錯誤。

接下來就一一分析這裏面具體的狀態碼。

1xx

101 Switching Protocols。在HTTP升級爲WebSocket的時候,若是服務器贊成變動,就會發送狀態碼 101。

2xx

200 OK是見得最多的成功狀態碼。一般在響應體中放有數據。

204 No Content含義與 200 相同,但響應頭後沒有 body 數據。

206 Partial Content顧名思義,表示部份內容,它的使用場景爲 HTTP 分塊下載和斷電續傳,固然也會帶上相應的響應頭字段Content-Range

3xx

301 Moved Permanently即永久重定向,對應着302 Found,即臨時重定向。

好比你的網站從 HTTP 升級到了 HTTPS 了,之前的站點不再用了,應當返回301,這個時候瀏覽器默認會作緩存優化,在第二次訪問的時候自動訪問重定向的那個地址。

而若是隻是暫時不可用,那麼直接返回302便可,和301不一樣的是,瀏覽器並不會作緩存優化。

304 Not Modified: 當協商緩存命中時會返回這個狀態碼。詳見瀏覽器緩存

4xx

400 Bad Request: 開發者常常看到一頭霧水,只是籠統地提示了一下錯誤,並不知道哪裏出錯了。

403 Forbidden: 這實際上並非請求報文出錯,而是服務器禁止訪問,緣由有不少,好比法律禁止、信息敏感。

404 Not Found: 資源未找到,表示沒在服務器上找到相應的資源。

405 Method Not Allowed: 請求方法不被服務器端容許。

406 Not Acceptable: 資源沒法知足客戶端的條件。

408 Request Timeout: 服務器等待了太長時間。

409 Conflict: 多個請求發生了衝突。

413 Request Entity Too Large: 請求體的數據過大。

414 Request-URI Too Long: 請求行裏的 URI 太大。

429 Too Many Request: 客戶端發送的請求過多。

431 Request Header Fields Too Large請求頭的字段內容太大。

5xx

500 Internal Server Error: 僅僅告訴你服務器出錯了,出了啥錯咱也不知道。

501 Not Implemented: 表示客戶端請求的功能還不支持。

502 Bad Gateway: 服務器自身是正常的,但訪問的時候出錯了,啥錯誤咱也不知道。

503 Service Unavailable: 表示服務器當前很忙,暫時沒法響應服務。

005: 簡要歸納一下 HTTP 的特色?HTTP 有哪些缺點?

HTTP 特色

HTTP 的特色歸納以下:

  1. 靈活可擴展,主要體如今兩個方面。一個是語義上的自由,只規定了基本格式,好比空格分隔單詞,換行分隔字段,其餘的各個部分都沒有嚴格的語法限制。另外一個是傳輸形式的多樣性,不只僅能夠傳輸文本,還能傳輸圖片、視頻等任意數據,很是方便。

  2. 可靠傳輸。HTTP 基於 TCP/IP,所以把這一特性繼承了下來。這屬於 TCP 的特性,不具體介紹了。

  3. 請求-應答。也就是一發一收有來有回, 固然這個請求方和應答方不僅僅指客戶端和服務器之間,若是某臺服務器做爲代理來鏈接後端的服務端,那麼這臺服務器也會扮演請求方的角色。

  4. 無狀態。這裏的狀態是指通訊過程的上下文信息,而每次 http 請求都是獨立、無關的,默認不須要保留狀態信息。

HTTP 缺點

無狀態

所謂的優勢和缺點仍是要分場景來看的,對於 HTTP 而言,最具爭議的地方在於它的無狀態

在須要長鏈接的場景中,須要保存大量的上下文信息,以避免傳輸大量重複的信息,那麼這時候無狀態就是 http 的缺點了。

但與此同時,另一些應用僅僅只是爲了獲取一些數據,不須要保存鏈接上下文信息,無狀態反而減小了網絡開銷,成爲了 http 的優勢。

明文傳輸

即協議裏的報文(主要指的是頭部)不使用二進制數據,而是文本形式。

這固然對於調試提供了便利,但同時也讓 HTTP 的報文信息暴露給了外界,給攻擊者也提供了便利。WIFI陷阱就是利用 HTTP 明文傳輸的缺點,誘導你連上熱點,而後瘋狂抓你全部的流量,從而拿到你的敏感信息。

隊頭阻塞問題

當 http 開啓長鏈接時,共用一個 TCP 鏈接,同一時刻只能處理一個請求,那麼當前請求耗時過長的狀況下,其它的請求只能處於阻塞狀態,也就是著名的隊頭阻塞問題。接下來會有一小節討論這個問題。

006: 對 Accept 系列字段瞭解多少?

對於Accept系列字段的介紹分爲四個部分: 數據格式壓縮方式支持語言字符集

數據格式

上一節談到 HTTP 靈活的特性,它支持很是多的數據格式,那麼這麼多格式的數據一塊兒到達客戶端,客戶端怎麼知道它的格式呢?

固然,最低效的方式是直接猜,有沒有更好的方式呢?直接指定能夠嗎?

答案是確定的。不過首先須要介紹一個標準——MIME(Multipurpose Internet Mail Extensions, 多用途互聯網郵件擴展)。它首先用在電子郵件系統中,讓郵件能夠發任意類型的數據,這對於 HTTP 來講也是通用的。

所以,HTTP 從MIME type取了一部分來標記報文 body 部分的數據類型,這些類型體如今Content-Type這個字段,固然這是針對於發送端而言,接收端想要收到特定類型的數據,也能夠用Accept字段。

具體而言,這兩個字段的取值能夠分爲下面幾類:

  • text: text/html, text/plain, text/css 等
  • image: image/gif, image/jpeg, image/png 等
  • audio/video: audio/mpeg, video/mp4 等
  • application: application/json, application/javascript, application/pdf, application/octet-stream

壓縮方式

固然通常這些數據都是會進行編碼壓縮的,採起什麼樣的壓縮方式就體如今了發送方的Content-Encoding字段上, 一樣的,接收什麼樣的壓縮方式體如今了接受方的Accept-Encoding字段上。這個字段的取值有下面幾種:

  • gzip: 當今最流行的壓縮格式
  • deflate: 另一種著名的壓縮格式
  • br: 一種專門爲 HTTP 發明的壓縮算法
// 發送端
Content-Encoding: gzip
// 接收端
Accept-Encoding: gizp
複製代碼

支持語言

對於發送方而言,還有一個Content-Language字段,在須要實現國際化的方案當中,能夠用來指定支持的語言,在接受方對應的字段爲Accept-Language。如:

// 發送端
Content-Language: zh-CN, zh, en
// 接收端
Accept-Language: zh-CN, zh, en
複製代碼

字符集

最後是一個比較特殊的字段, 在接收端對應爲Accept-Charset,指定能夠接受的字符集,而在發送端並無對應的Content-Charset, 而是直接放在了Content-Type中,以charset屬性指定。如:

// 發送端
Content-Type: text/html; charset=utf-8
// 接收端
Accept-Charset: charset=utf-8
複製代碼

最後以一張圖來總結一下吧:

007: 對於定長和不定長的數據,HTTP 是怎麼傳輸的?

定長包體

對於定長包體而言,發送端在傳輸的時候通常會帶上 Content-Length, 來指明包體的長度。

咱們用一個nodejs服務器來模擬一下:

const http = require('http');

const server = http.createServer();

server.on('request', (req, res) => {
  if(req.url === '/') {
    res.setHeader('Content-Type', 'text/plain');
    res.setHeader('Content-Length', 10);
    res.write("helloworld");
  }
})

server.listen(8081, () => {
  console.log("成功啓動");
})
複製代碼

啓動後訪問: localhost:8081

瀏覽器中顯示以下:

helloworld
複製代碼

這是長度正確的狀況,那不正確的狀況是如何處理的呢?

咱們試着把這個長度設置的小一些:

res.setHeader('Content-Length', 8);
複製代碼

重啓服務,再次訪問,如今瀏覽器中內容以下:

hellowor
複製代碼

那後面的ld哪裏去了呢?實際上在 http 的響應體中直接被截去了。

而後咱們試着將這個長度設置得大一些:

res.setHeader('Content-Length', 12);
複製代碼

此時瀏覽器顯示以下:

直接沒法顯示了。能夠看到Content-Length對於 http 傳輸過程起到了十分關鍵的做用,若是設置不當能夠直接致使傳輸失敗。

不定長包體

上述是針對於定長包體,那麼對於不定長包體而言是如何傳輸的呢?

這裏就必須介紹另一個 http 頭部字段了:

Transfer-Encoding: chunked
複製代碼

表示分塊傳輸數據,設置這個字段後會自動產生兩個效果:

  • Content-Length 字段會被忽略
  • 基於長鏈接持續推送動態內容

咱們依然以一個實際的例子來模擬分塊傳輸,nodejs 程序以下:

const http = require('http');

const server = http.createServer();

server.on('request', (req, res) => {
  if(req.url === '/') {
    res.setHeader('Content-Type', 'text/html; charset=utf8');
    res.setHeader('Content-Length', 10);
    res.setHeader('Transfer-Encoding', 'chunked');
    res.write("<p>來啦</p>");
    setTimeout(() => {
      res.write("第一次傳輸<br/>");
    }, 1000);
    setTimeout(() => {
      res.write("第二次傳輸");
      res.end()
    }, 2000);
  }
})

server.listen(8009, () => {
  console.log("成功啓動");
})
複製代碼

訪問效果入下:

用 telnet 抓到的響應以下:

注意,Connection: keep-alive及以前的爲響應行和響應頭,後面的內容爲響應體,這兩部分用換行符隔開。

響應體的結構比較有意思,以下所示:

chunk長度(16進制的數)
第一個chunk的內容
chunk長度(16進制的數)
第二個chunk的內容
......
0

複製代碼

最後是留有有一個空行的,這一點請你們注意。

以上即是 http 對於定長數據不定長數據的傳輸方式。

008: HTTP 如何處理大文件的傳輸?

對於幾百 M 甚至上 G 的大文件來講,若是要一口氣所有傳輸過來顯然是不現實的,會有大量的等待時間,嚴重影響用戶體驗。所以,HTTP 針對這一場景,採起了範圍請求的解決方案,容許客戶端僅僅請求一個資源的一部分。

如何支持

固然,前提是服務器要支持範圍請求,要支持這個功能,就必須加上這樣一個響應頭:

Accept-Ranges: none
複製代碼

用來告知客戶端這邊是支持範圍請求的。

Range 字段拆解

而對於客戶端而言,它須要指定請求哪一部分,經過Range這個請求頭字段肯定,格式爲bytes=x-y。接下來就來討論一下這個 Range 的書寫格式:

  • 0-499表示從開始到第 499 個字節。
  • 500- 表示從第 500 字節到文件終點。
  • -100表示文件的最後100個字節。

服務器收到請求以後,首先驗證範圍是否合法,若是越界了那麼返回416錯誤碼,不然讀取相應片斷,返回206狀態碼。

同時,服務器須要添加Content-Range字段,這個字段的格式根據請求頭中Range字段的不一樣而有所差別。

具體來講,請求單段數據和請求多段數據,響應頭是不同的。

舉個例子:

// 單段數據
Range: bytes=0-9
// 多段數據
Range: bytes=0-9, 30-39

複製代碼

接下來咱們就分別來討論着兩種狀況。

單段數據

對於單段數據的請求,返回的響應以下:

HTTP/1.1 206 Partial Content
Content-Length: 10
Accept-Ranges: bytes
Content-Range: bytes 0-9/100

i am xxxxx
複製代碼

值得注意的是Content-Range字段,0-9表示請求的返回,100表示資源的總大小,很好理解。

多段數據

接下來咱們看看多段請求的狀況。獲得的響應會是下面這個形式:

HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=00000010101
Content-Length: 189
Connection: keep-alive
Accept-Ranges: bytes


--00000010101
Content-Type: text/plain
Content-Range: bytes 0-9/96

i am xxxxx
--00000010101
Content-Type: text/plain
Content-Range: bytes 20-29/96

eex jspy e
--00000010101--
複製代碼

這個時候出現了一個很是關鍵的字段Content-Type: multipart/byteranges;boundary=00000010101,它表明了信息量是這樣的:

  • 請求必定是多段數據請求
  • 響應體中的分隔符是 00000010101

所以,在響應體中各段數據之間會由這裏指定的分隔符分開,並且在最後的分隔末尾添上--表示結束。

以上就是 http 針對大文件傳輸所採用的手段。

009: HTTP 中如何處理表單數據的提交?

在 http 中,有兩種主要的表單提交的方式,體如今兩種不一樣的Content-Type取值:

  • application/x-www-form-urlencoded
  • multipart/form-data

因爲表單提交通常是POST請求,不多考慮GET,所以這裏咱們將默認提交的數據放在請求體中。

application/x-www-form-urlencoded

對於application/x-www-form-urlencoded格式的表單內容,有如下特色:

  • 其中的數據會被編碼成以&分隔的鍵值對
  • 字符以URL編碼方式編碼。

如:

// 轉換過程: {a: 1, b: 2} -> a=1&b=2 -> 以下(最終形式)
"a%3D1%26b%3D2"
複製代碼

multipart/form-data

對於multipart/form-data而言:

  • 請求頭中的Content-Type字段會包含boundary,且boundary的值有瀏覽器默認指定。例: Content-Type: multipart/form-data;boundary=----WebkitFormBoundaryRRJKeWfHPGrS4LKe
  • 數據會分爲多個部分,每兩個部分之間經過分隔符來分隔,每部分表述均有 HTTP 頭部描述子包體,如Content-Type,在最後的分隔符會加上--表示結束。

相應的請求體是下面這樣:

Content-Disposition: form-data;name="data1";
Content-Type: text/plain
data1
----WebkitFormBoundaryRRJKeWfHPGrS4LKe
Content-Disposition: form-data;name="data2";
Content-Type: text/plain
data2
----WebkitFormBoundaryRRJKeWfHPGrS4LKe--
複製代碼

小結

值得一提的是,multipart/form-data 格式最大的特色在於:每個表單元素都是獨立的資源表述。另外,你可能在寫業務的過程當中,並無注意到其中還有boundary的存在,若是你打開抓包工具,確實能夠看到不一樣的表單元素被拆分開了,之因此在平時感受不到,是覺得瀏覽器和 HTTP 給你封裝了這一系列操做。

並且,在實際的場景中,對於圖片等文件的上傳,基本採用multipart/form-data而不用application/x-www-form-urlencoded,由於沒有必要作 URL 編碼,帶來巨大耗時的同時也佔用了更多的空間。

010: HTTP1.1 如何解決 HTTP 的隊頭阻塞問題?

什麼是 HTTP 隊頭阻塞?

從前面的小節能夠知道,HTTP 傳輸是基於請求-應答的模式進行的,報文必須是一發一收,但值得注意的是,裏面的任務被放在一個任務隊列中串行執行,一旦隊首的請求處理太慢,就會阻塞後面請求的處理。這就是著名的HTTP隊頭阻塞問題。

併發鏈接

對於一個域名容許分配多個長鏈接,那麼至關於增長了任務隊列,不至於一個隊伍的任務阻塞其它全部任務。在RFC2616規定過客戶端最多併發 2 個鏈接,不過事實上在如今的瀏覽器標準中,這個上限要多不少,Chrome 中是 6 個。

但其實,即便是提升了併發鏈接,仍是不能知足人們對性能的需求。

域名分片

一個域名不是能夠併發 6 個長鏈接嗎?那我就多分幾個域名。

好比 content1.sanyuan.com 、content2.sanyuan.com。

這樣一個sanyuan.com域名下能夠分出很是多的二級域名,而它們都指向一樣的一臺服務器,可以併發的長鏈接數更多了,事實上也更好地解決了隊頭阻塞的問題。

011: 對 Cookie 瞭解多少?

Cookie 簡介

前面說到了 HTTP 是一個無狀態的協議,每次 http 請求都是獨立、無關的,默認不須要保留狀態信息。但有時候須要保存一些狀態,怎麼辦呢?

HTTP 爲此引入了 Cookie。Cookie 本質上就是瀏覽器裏面存儲的一個很小的文本文件,內部以鍵值對的方式來存儲(在chrome開發者面板的Application這一欄能夠看到)。向同一個域名下發送請求,都會攜帶相同的 Cookie,服務器拿到 Cookie 進行解析,便能拿到客戶端的狀態。而服務端能夠經過響應頭中的Set-Cookie字段來對客戶端寫入Cookie。舉例以下:

// 請求頭
Cookie: a=xxx;b=xxx
// 響應頭
Set-Cookie: a=xxx
set-Cookie: b=xxx
複製代碼

Cookie 屬性

生存週期

Cookie 的有效期能夠經過ExpiresMax-Age兩個屬性來設置。

  • Expires過時時間
  • Max-Age用的是一段時間間隔,單位是秒,從瀏覽器收到報文開始計算。

若 Cookie 過時,則這個 Cookie 會被刪除,並不會發送給服務端。

做用域

關於做用域也有兩個屬性: Domainpath, 給 Cookie 綁定了域名和路徑,在發送請求以前,發現域名或者路徑和這兩個屬性不匹配,那麼就不會帶上 Cookie。值得注意的是,對於路徑來講,/表示域名下的任意路徑都容許使用 Cookie。

安全相關

若是帶上Secure,說明只能經過 HTTPS 傳輸 cookie。

若是 cookie 字段帶上HttpOnly,那麼說明只能經過 HTTP 協議傳輸,不能經過 JS 訪問,這也是預防 XSS 攻擊的重要手段。

相應的,對於 CSRF 攻擊的預防,也有SameSite屬性。

SameSite能夠設置爲三個值,StrictLaxNone

a.Strict模式下,瀏覽器徹底禁止第三方請求攜帶Cookie。好比請求sanyuan.com網站只能在sanyuan.com域名當中請求才能攜帶 Cookie,在其餘網站請求都不能。

b.Lax模式,就寬鬆一點了,可是隻能在 get 方法提交表單況或者a 標籤發送 get 請求的狀況下能夠攜帶 Cookie,其餘狀況均不能。

c.None模式下,也就是默認模式,請求會自動攜帶上 Cookie。

Cookie 的缺點

  1. 容量缺陷。Cookie 的體積上限只有4KB,只能用來存儲少許的信息。

  2. 性能缺陷。Cookie 緊跟域名,無論域名下面的某一個地址需不須要這個 Cookie ,請求都會攜帶上完整的 Cookie,這樣隨着請求數的增多,其實會形成巨大的性能浪費的,由於請求攜帶了不少沒必要要的內容。但能夠經過DomainPath指定做用域來解決。

  3. 安全缺陷。因爲 Cookie 以純文本的形式在瀏覽器和服務器中傳遞,很容易被非法用戶截獲,而後進行一系列的篡改,在 Cookie 的有效期內從新發送給服務器,這是至關危險的。另外,在HttpOnly爲 false 的狀況下,Cookie 信息能直接經過 JS 腳原本讀取。

012: 如何理解 HTTP 代理?

咱們知道在 HTTP 是基於請求-響應模型的協議,通常由客戶端發請求,服務器來進行響應。

固然,也有特殊狀況,就是代理服務器的狀況。引入代理以後,做爲代理的服務器至關於一箇中間人的角色,對於客戶端而言,表現爲服務器進行響應;而對於源服務器,表現爲客戶端發起請求,具備雙重身份

那代理服務器究竟是用來作什麼的呢?

功能

  1. 負載均衡。客戶端的請求只會先到達代理服務器,後面到底有多少源服務器,IP 都是多少,客戶端是不知道的。所以,這個代理服務器能夠拿到這個請求以後,能夠經過特定的算法分發給不一樣的源服務器,讓各臺源服務器的負載儘可能平均。固然,這樣的算法有不少,包括隨機算法輪詢一致性hashLRU(最近最少使用)等等,不過這些算法並非本文的重點,你們有興趣本身能夠研究一下。

  2. 保障安全。利用心跳機制監控後臺的服務器,一旦發現故障機就將其踢出集羣。而且對於上下行的數據進行過濾,對非法 IP 限流,這些都是代理服務器的工做。

  3. 緩存代理。將內容緩存到代理服務器,使得客戶端能夠直接從代理服務器得到而不用到源服務器那裏。下一節詳細拆解。

相關頭部字段

Via

代理服務器須要標明本身的身份,在 HTTP 傳輸中留下本身的痕跡,怎麼辦呢?

經過Via字段來記錄。舉個例子,如今中間有兩臺代理服務器,在客戶端發送請求後會經歷這樣一個過程:

客戶端 -> 代理1 -> 代理2 -> 源服務器
複製代碼

在源服務器收到請求後,會在請求頭拿到這個字段:

Via: proxy_server1, proxy_server2
複製代碼

而源服務器響應時,最終在客戶端會拿到這樣的響應頭:

Via: proxy_server2, proxy_server1
複製代碼

能夠看到,Via中代理的順序即爲在 HTTP 傳輸中報文傳達的順序。

X-Forwarded-For

字面意思就是爲誰轉發, 它記錄的是請求方IP地址(注意,和Via區分開,X-Forwarded-For記錄的是請求方這一個IP)。

X-Real-IP

是一種獲取用戶真實 IP 的字段,無論中間通過多少代理,這個字段始終記錄最初的客戶端的IP。

相應的,還有X-Forwarded-HostX-Forwarded-Proto,分別記錄客戶端(注意哦,不包括代理)的域名協議名

X-Forwarded-For產生的問題

前面能夠看到,X-Forwarded-For這個字段記錄的是請求方的 IP,這意味着每通過一個不一樣的代理,這個字段的名字都要變,從客戶端代理1,這個字段是客戶端的 IP,從代理1代理2,這個字段就變爲了代理1的 IP。

可是這會產生兩個問題:

  1. 意味着代理必須解析 HTTP 請求頭,而後修改,比直接轉發數據性能降低。

  2. 在 HTTPS 通訊加密的過程當中,原始報文是不容許修改的。

由此產生了代理協議,通常使用明文版本,只須要在 HTTP 請求行上面加上這樣格式的文本便可:

// PROXY + TCP4/TCP6 + 請求方地址 + 接收方地址 + 請求端口 + 接收端口
PROXY TCP4 0.0.0.1 0.0.0.2 1111 2222
GET / HTTP/1.1
...
複製代碼

這樣就能夠解決X-Forwarded-For帶來的問題了。

013: 如何理解 HTTP 緩存及緩存代理?

關於強緩存協商緩存的內容,我已經在能不能說一說瀏覽器緩存作了詳細分析,小結以下:

首先經過 Cache-Control 驗證強緩存是否可用

  • 若是強緩存可用,直接使用
  • 不然進入協商緩存,即發送 HTTP 請求,服務器經過請求頭中的If-Modified-Since或者If-None-Match這些條件請求字段檢查資源是否更新
    • 若資源更新,返回資源和200狀態碼
    • 不然,返回304,告訴瀏覽器直接從緩存獲取資源

這一節咱們主要來講說另一種緩存方式: 代理緩存

爲何產生代理緩存?

對於源服務器來講,它也是有緩存的,好比Redis, Memcache,但對於 HTTP 緩存來講,若是每次客戶端緩存失效都要到源服務器獲取,那給源服務器的壓力是很大的。

由此引入了緩存代理的機制。讓代理服務器接管一部分的服務端HTTP緩存,客戶端緩存過時後就近到代理緩存中獲取,代理緩存過時了才請求源服務器,這樣流量巨大的時候能明顯下降源服務器的壓力。

那緩存代理到底是如何作到的呢?

總的來講,緩存代理的控制分爲兩部分,一部分是源服務器端的控制,一部分是客戶端的控制。

源服務器的緩存控制

private 和 public

在源服務器的響應頭中,會加上Cache-Control這個字段進行緩存控制字段,那麼它的值當中能夠加入private或者public表示是否容許代理服務器緩存,前者禁止,後者爲容許。

好比對於一些很是私密的數據,若是緩存到代理服務器,別人直接訪問代理就能夠拿到這些數據,是很是危險的,所以對於這些數據通常是不會容許代理服務器進行緩存的,將響應頭部的Cache-Control設爲private,而不是public

proxy-revalidate

must-revalidate的意思是客戶端緩存過時就去源服務器獲取,而proxy-revalidate則表示代理服務器的緩存過時後到源服務器獲取。

s-maxage

sshare的意思,限定了緩存在代理服務器中能夠存放多久,和限制客戶端緩存時間的max-age並不衝突。

講了這幾個字段,咱們不妨來舉個小例子,源服務器在響應頭中加入這樣一個字段:

Cache-Control: public, max-age=1000, s-maxage=2000
複製代碼

至關於源服務器說: 我這個響應是容許代理服務器緩存的,客戶端緩存過時了到代理中拿,而且在客戶端的緩存時間爲 1000 秒,在代理服務器中的緩存時間爲 2000 s。

客戶端的緩存控制

max-stale 和 min-fresh

在客戶端的請求頭中,能夠加入這兩個字段,來對代理服務器上的緩存進行寬容限制操做。好比:

max-stale: 5
複製代碼

表示客戶端到代理服務器上拿緩存的時候,即便代理緩存過時了也沒關係,只要過時時間在5秒以內,仍是能夠從代理中獲取的。

又好比:

min-fresh: 5
複製代碼

表示代理緩存須要必定的新鮮度,不要等到緩存恰好到期再拿,必定要在到期前 5 秒以前的時間拿,不然拿不到。

only-if-cached

這個字段加上後表示客戶端只會接受代理緩存,而不會接受源服務器的響應。若是代理緩存無效,則直接返回504(Gateway Timeout)

以上即是緩存代理的內容,涉及的字段比較多,但願能好好回顧一下,加深理解。

014: 什麼是跨域?瀏覽器如何攔截響應?如何解決?

在先後端分離的開發模式中,常常會遇到跨域問題,即 Ajax 請求發出去了,服務器也成功響應了,前端就是拿不到這個響應。接下來咱們就來好好討論一下這個問題。

什麼是跨域

回顧一下 URI 的組成:

瀏覽器遵循同源政策(scheme(協議)host(主機)port(端口)都相同則爲同源)。非同源站點有這樣一些限制:

  • 不能讀取和修改對方的 DOM
  • 不讀訪問對方的 Cookie、IndexDB 和 LocalStorage
  • 限制 XMLHttpRequest 請求。(後面的話題着重圍繞這個)

當瀏覽器向目標 URI 發 Ajax 請求時,只要當前 URL 和目標 URL 不一樣源,則產生跨域,被稱爲跨域請求

跨域請求的響應通常會被瀏覽器所攔截,注意,是被瀏覽器攔截,響應實際上是成功到達客戶端了。那這個攔截是如何發生呢?

首先要知道的是,瀏覽器是多進程的,以 Chrome 爲例,進程組成以下:

WebKit 渲染引擎V8 引擎都在渲染進程當中。

xhr.send被調用,即 Ajax 請求準備發送的時候,其實還只是在渲染進程的處理。爲了防止黑客經過腳本觸碰到系統資源,瀏覽器將每個渲染進程裝進了沙箱,而且爲了防止 CPU 芯片一直存在的SpectreMeltdown漏洞,採起了站點隔離的手段,給每個不一樣的站點(一級域名不一樣)分配了沙箱,互不干擾。具體見YouTube上Chromium安全團隊的演講視頻

在沙箱當中的渲染進程是沒有辦法發送網絡請求的,那怎麼辦?只能經過網絡進程來發送。那這樣就涉及到進程間通訊(IPC,Inter Process Communication)了。接下來咱們看看 chromium 當中進程間通訊是如何完成的,在 chromium 源碼中調用順序以下:

可能看了你會比較懵,若是想深刻了解能夠去看看 chromium 最新的源代碼,IPC源碼地址Chromium IPC源碼解析文章

總的來講就是利用Unix Domain Socket套接字,配合事件驅動的高性能網絡併發庫libevent完成進程的 IPC 過程。

好,如今數據傳遞給了瀏覽器主進程,主進程接收到後,才真正地發出相應的網絡請求。

在服務端處理完數據後,將響應返回,主進程檢查到跨域,且沒有cors(後面會詳細說)響應頭,將響應體所有丟掉,並不會發送給渲染進程。這就達到了攔截數據的目的。

接下來咱們來講一說解決跨域問題的幾種方案。

CORS

CORS 實際上是 W3C 的一個標準,全稱是跨域資源共享。它須要瀏覽器和服務器的共同支持,具體來講,非 IE 和 IE10 以上支持CORS,服務器須要附加特定的響應頭,後面具體拆解。不過在弄清楚 CORS 的原理以前,咱們須要清楚兩個概念: 簡單請求非簡單請求

瀏覽器根據請求方法和請求頭的特定字段,將請求作了一下分類,具體來講規則是這樣,凡是知足下面條件的屬於簡單請求:

  • 請求方法爲 GET、POST 或者 HEAD
  • 請求頭的取值範圍: Accept、Accept-Language、Content-Language、Content-Type(只限於三個值application/x-www-form-urlencodedmultipart/form-datatext/plain)

瀏覽器畫了這樣一個圈,在這個圈裏面的就是簡單請求, 圈外面的就是非簡單請求,而後針對這兩種不一樣的請求進行不一樣的處理。

簡單請求

請求發出去以前,瀏覽器作了什麼?

它會自動在請求頭當中,添加一個Origin字段,用來講明請求來自哪一個。服務器拿到請求以後,在迴應時對應地添加Access-Control-Allow-Origin字段,若是Origin不在這個字段的範圍中,那麼瀏覽器就會將響應攔截。

所以,Access-Control-Allow-Origin字段是服務器用來決定瀏覽器是否攔截這個響應,這是必需的字段。與此同時,其它一些可選的功能性的字段,用來描述若是不會攔截,這些字段將會發揮各自的做用。

Access-Control-Allow-Credentials。這個字段是一個布爾值,表示是否容許發送 Cookie,對於跨域請求,瀏覽器對這個字段默認值設爲 false,而若是須要拿到瀏覽器的 Cookie,須要添加這個響應頭並設爲true, 而且在前端也須要設置withCredentials屬性:

let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
複製代碼

Access-Control-Expose-Headers。這個字段是給 XMLHttpRequest 對象賦能,讓它不只能夠拿到基本的 6 個響應頭字段(包括Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma), 還能拿到這個字段聲明的響應頭字段。好比這樣設置:

Access-Control-Expose-Headers: aaa
複製代碼

那麼在前端能夠經過 XMLHttpRequest.getResponseHeader('aaa') 拿到 aaa 這個字段的值。

非簡單請求

非簡單請求相對而言會有些不一樣,體如今兩個方面: 預檢請求響應字段

咱們以 PUT 方法爲例。

var url = 'http://xxx.com';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'xxx');
xhr.send();
複製代碼

當這段代碼執行後,首先會發送預檢請求。這個預檢請求的請求行和請求體是下面這個格式:

OPTIONS / HTTP/1.1
Origin: 當前地址
Host: xxx.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
複製代碼

預檢請求的方法是OPTIONS,同時會加上Origin源地址和Host目標地址,這很簡單。同時也會加上兩個關鍵的字段:

  • Access-Control-Request-Method, 列出 CORS 請求用到哪一個HTTP方法
  • Access-Control-Request-Headers,指定 CORS 請求將要加上什麼請求頭

這是預檢請求。接下來是響應字段,響應字段也分爲兩部分,一部分是對於預檢請求的響應,一部分是對於 CORS 請求的響應。

預檢請求的響應。以下面的格式:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
複製代碼

其中有這樣幾個關鍵的響應頭字段:

  • Access-Control-Allow-Origin: 表示能夠容許請求的源,能夠填具體的源名,也能夠填*表示容許任意源請求。
  • Access-Control-Allow-Methods: 表示容許的請求方法列表。
  • Access-Control-Allow-Credentials: 簡單請求中已經介紹。
  • Access-Control-Allow-Headers: 表示容許發送的請求頭字段
  • Access-Control-Max-Age: 預檢請求的有效期,在此期間,不用發出另一條預檢請求。

在預檢請求的響應返回後,若是請求不知足響應頭的條件,則觸發XMLHttpRequestonerror方法,固然後面真正的CORS請求也不會發出去了。

CORS 請求的響應。繞了這麼一大轉,到了真正的 CORS 請求就容易多了,如今它和簡單請求的狀況是同樣的。瀏覽器自動加上Origin字段,服務端響應頭返回Access-Control-Allow-Origin。能夠參考以上簡單請求部分的內容。

JSONP

雖然XMLHttpRequest對象遵循同源政策,可是script標籤不同,它能夠經過 src 填上目標地址從而發出 GET 請求,實現跨域請求並拿到響應。這也就是 JSONP 的原理,接下來咱們就來封裝一個 JSONP:

const jsonp = ({ url, params, callbackName }) => {
  const generateURL = () => {
    let dataStr = '';
    for(let key in params) {
      dataStr += `${key}=${params[key]}&`;
    }
    dataStr += `callback=${callbackName}`;
    return `${url}?${dataStr}`;
  };
  return new Promise((resolve, reject) => {
    // 初始化回調函數名稱
    callbackName = callbackName || Math.random().toString.replace(',', ''); 
    // 建立 script 元素並加入到當前文檔中
    let scriptEle = document.createElement('script');
    scriptEle.src = generateURL();
    document.body.appendChild(scriptEle);
    // 綁定到 window 上,爲了後面調用
    window[callbackName] = (data) => {
      resolve(data);
      // script 執行完了,成爲無用元素,須要清除
      document.body.removeChild(scriptEle);
    }
  });
}
複製代碼

固然在服務端也會有響應的操做, 以 express 爲例:

let express = require('express')
let app = express()
app.get('/', function(req, res) {
  let { a, b, callback } = req.query
  console.log(a); // 1
  console.log(b); // 2
  // 注意哦,返回給script標籤,瀏覽器直接把這部分字符串執行
  res.end(`${callback}('數據包')`);
})
app.listen(3000)
複製代碼

前端這樣簡單地調用一下就行了:

jsonp({
  url: 'http://localhost:3000',
  params: { 
    a: 1,
    b: 2
  }
}).then(data => {
  // 拿到數據進行處理
  console.log(data); // 數據包
})
複製代碼

CORS相比,JSONP 最大的優點在於兼容性好,IE 低版本不能使用 CORS 但能夠使用 JSONP,缺點也很明顯,請求方法單一,只支持 GET 請求。

Nginx

Nginx 是一種高性能的反向代理服務器,能夠用來輕鬆解決跨域問題。

what?反向代理?我給你看一張圖你就懂了。

正向代理幫助客戶端訪問客戶端本身訪問不到的服務器,而後將結果返回給客戶端。

反向代理拿到客戶端的請求,將請求轉發給其餘的服務器,主要的場景是維持服務器集羣的負載均衡,換句話說,反向代理幫其它的服務器拿到請求,而後選擇一個合適的服務器,將請求轉交給它。

所以,二者的區別就很明顯了,正向代理服務器是幫客戶端作事情,而反向代理服務器是幫其它的服務器作事情。

好了,那 Nginx 是如何來解決跨域的呢?

好比說如今客戶端的域名爲client.com,服務器的域名爲server.com,客戶端向服務器發送 Ajax 請求,固然會跨域了,那這個時候讓 Nginx 登場了,經過下面這個配置:

server {
  listen  80;
  server_name  client.com;
  location /api {
    proxy_pass server.com;
  }
}
複製代碼

Nginx 至關於起了一個跳板機,這個跳板機的域名也是client.com,讓客戶端首先訪問 client.com/api,這固然沒有跨域,而後 Nginx 服務器做爲反向代理,將請求轉發給server.com,當響應返回時又將響應給到客戶端,這就完成整個跨域請求的過程。

其實還有一些不太經常使用的方式,你們瞭解便可,好比postMessage,固然WebSocket也是一種方式,可是已經不屬於 HTTP 的範疇,另一些奇技淫巧就不建議你們去死記硬背了,一方面歷來不用,名字都可貴記住,另外一方面臨時背下來,面試官也不會對你印象加分,由於看得出來是背的。固然沒有背並不表明減分,把跨域原理和前面三種主要的跨域方式理解清楚,經得起更深一步的推敲,反而會讓別人以爲你是一個靠譜的人。

015: TLS1.2 握手的過程是怎樣的?

以前談到了 HTTP 是明文傳輸的協議,傳輸保文對外徹底透明,很是不安全,那如何進一步保證安全性呢?

由此產生了 HTTPS,其實它並非一個新的協議,而是在 HTTP 下面增長了一層 SSL/TLS 協議,簡單的講,HTTPS = HTTP + SSL/TLS

那什麼是 SSL/TLS 呢?

SSL 即安全套接層(Secure Sockets Layer),在 OSI 七層模型中處於會話層(第 5 層)。以前 SSL 出過三個大版本,當它發展到第三個大版本的時候才被標準化,成爲 TLS(傳輸層安全,Transport Layer Security),並被當作 TLS1.0 的版本,準確地說,TLS1.0 = SSL3.1

如今主流的版本是 TLS/1.2, 以前的 TLS1.0、TLS1.1 都被認爲是不安全的,在不久的未來會被徹底淘汰。所以咱們接下來主要討論的是 TLS1.2, 固然在 2018 年推出了更加優秀的 TLS1.3,大大優化了 TLS 握手過程,這個咱們放在下一節再去說。

TLS 握手的過程比較複雜,寫文章以前我查閱了大量的資料,發現對 TLS 初學者很是不友好,也有不少知識點說的含糊不清,能夠說這個整理的過程是至關痛苦了。但願我下面的拆解可以幫你理解得更順暢些吧 : )

傳統 RSA 握手

先來講說傳統的 TLS 握手,也是你們在網上常常看到的。我以前也寫過這樣的文章,(傳統RSA版本)HTTPS爲何讓數據傳輸更安全,其中也介紹到了對稱加密非對稱加密的概念,建議你們去讀一讀,再也不贅述。之因此稱它爲 RSA 版本,是由於它在加解密pre_random的時候採用的是 RSA 算法。

TLS 1.2 握手過程

如今咱們來說講主流的 TLS 1.2 版本所採用的方式。

剛開始你可能會比較懵,先彆着急,過一遍下面的流程再來看會豁然開朗。

step 1: Client Hello

首先,瀏覽器發送 client_random、TLS版本、加密套件列表。

client_random 是什麼?用來最終 secret 的一個參數。

加密套件列表是什麼?我舉個例子,加密套件列表通常張這樣:

TLS_ECDHE_WITH_AES_128_GCM_SHA256
複製代碼

意思是TLS握手過程當中,使用ECDHE算法生成pre_random(這個數後面會介紹),128位的AES算法進行對稱加密,在對稱加密的過程當中使用主流的GCM分組模式,由於對稱加密中很重要的一個問題就是如何分組。最後一個是哈希摘要算法,採用SHA256算法。

其中值得解釋一下的是這個哈希摘要算法,試想一個這樣的場景,服務端如今給客戶端發消息來了,客戶端並不知道此時的消息究竟是服務端發的,仍是中間人僞造的消息呢?如今引入這個哈希摘要算法,將服務端的證書信息經過這個算法生成一個摘要(能夠理解爲比較短的字符串),用來標識這個服務端的身份,用私鑰加密後把加密後的標識本身的公鑰傳給客戶端。客戶端拿到這個公鑰來解密,生成另一份摘要。兩個摘要進行對比,若是相同則能確認服務端的身份。這也就是所謂數字簽名的原理。其中除了哈希算法,最重要的過程是私鑰加密,公鑰解密

step 2: Server Hello

能夠看到服務器一口氣給客戶端回覆了很是多的內容。

server_random也是最後生成secret的一個參數, 同時確認 TLS 版本、須要使用的加密套件和本身的證書,這都不難理解。那剩下的server_params是幹嗎的呢?

咱們先埋個伏筆,如今你只須要知道,server_random到達了客戶端。

step 3: Client 驗證證書,生成secret

客戶端驗證服務端傳來的證書簽名是否經過,若是驗證經過,則傳遞client_params這個參數給服務器。

接着客戶端經過ECDHE算法計算出pre_random,其中傳入兩個參數:server_paramsclient_params。如今你應該清楚這個兩個參數的做用了吧,因爲ECDHE基於橢圓曲線離散對數,這兩個參數也稱做橢圓曲線的公鑰

客戶端如今擁有了client_randomserver_randompre_random,接下來將這三個數經過一個僞隨機數函數來計算出最終的secret

step4: Server 生成 secret

剛剛客戶端不是傳了client_params過來了嗎?

如今服務端開始用ECDHE算法生成pre_random,接着用和客戶端一樣的僞隨機數函數生成最後的secret

注意事項

TLS的過程基本上講完了,但還有兩點須要注意。

第一、實際上 TLS 握手是一個雙向認證的過程,從 step1 中能夠看到,客戶端有能力驗證服務器的身份,那服務器能不能驗證客戶端的身份呢?

固然是能夠的。具體來講,在 step3中,客戶端傳送client_params,實際上給服務器傳一個驗證消息,讓服務器將相同的驗證流程(哈希摘要 + 私鑰加密 + 公鑰解密)走一遍,確認客戶端的身份。

第二、當客戶端生成secret後,會給服務端發送一個收尾的消息,告訴服務器以後的都用對稱加密,對稱加密的算法就用第一次約定的。服務器生成完secret也會向客戶端發送一個收尾的消息,告訴客戶端之後就直接用對稱加密來通訊。

這個收尾的消息包括兩部分,一部分是Change Cipher Spec,意味着後面加密傳輸了,另外一個是Finished消息,這個消息是對以前全部發送的數據作的摘要,對摘要進行加密,讓對方驗證一下。

當雙方都驗證經過以後,握手才正式結束。後面的 HTTP 正式開始傳輸加密報文。

RSA 和 ECDHE 握手過程的區別

  1. ECDHE 握手,也就是主流的 TLS1.2 握手中,使用ECDHE實現pre_random的加密解密,沒有用到 RSA。

  2. 使用 ECDHE 還有一個特色,就是客戶端發送完收尾消息後能夠提早搶跑,直接發送 HTTP 報文,節省了一個 RTT,沒必要等到收尾消息到達服務器,而後等服務器返回收尾消息給本身,直接開始發請求。這也叫TLS False Start

016: TLS 1.3 作了哪些改進?

TLS 1.2 雖然存在了 10 多年,經歷了無數的考驗,但歷史的車輪老是不斷向前的,爲了得到更強的安全、更優秀的性能,在2018年就推出了 TLS1.3,對於TLS1.2作了一系列的改進,主要分爲這幾個部分:強化安全提升性能

強化安全

在 TLS1.3 中廢除了很是多的加密算法,最後只保留五個加密套件:

  • TLS_AES_128_GCM_SHA256
  • TLS_AES_256_GCM_SHA384
  • TLS_CHACHA20_POLY1305_SHA256
  • TLS_AES_128_GCM_SHA256
  • TLS_AES_128_GCM_8_SHA256

能夠看到,最後剩下的對稱加密算法只有 AESCHACHA20,以前主流的也會這兩種。分組模式也只剩下 GCMPOLY1305, 哈希摘要算法只剩下了 SHA256SHA384 了。

那你可能會問了, 以前RSA這麼重要的非對稱加密算法怎麼不在了?

我以爲有兩方面的緣由:

第一、2015年發現了FREAK攻擊,即已經有人發現了 RSA 的漏洞,可以進行破解了。

第二、一旦私鑰泄露,那麼中間人能夠經過私鑰計算出以前全部報文的secret,破解以前全部的密文。

爲何?回到 RSA 握手的過程當中,客戶端拿到服務器的證書後,提取出服務器的公鑰,而後生成pre_random並用公鑰加密傳給服務器,服務器經過私鑰解密,從而拿到真實的pre_random。當中間人拿到了服務器私鑰,而且截獲以前全部報文的時候,那麼就能拿到pre_randomserver_randomclient_random並根據對應的隨機數函數生成secret,也就是拿到了 TLS 最終的會話密鑰,每個歷史報文都能經過這樣的方式進行破解。

ECDHE在每次握手時都會生成臨時的密鑰對,即便私鑰被破解,以前的歷史消息並不會收到影響。這種一次破解並不影響歷史信息的性質也叫前向安全性

RSA 算法不具有前向安全性,而 ECDHE 具有,所以在 TLS1.3 中完全取代了RSA

提高性能

握手改進

流程以下:

大致的方式和 TLS1.2 差很少,不過和 TLS 1.2 相比少了一個 RTT, 服務端沒必要等待對方驗證證書以後纔拿到client_params,而是直接在第一次握手的時候就可以拿到, 拿到以後當即計算secret,節省了以前沒必要要的等待時間。同時,這也意味着在第一次握手的時候客戶端須要傳送更多的信息,一口氣給傳完。

這種 TLS 1.3 握手方式也被叫作1-RTT握手。但其實這種1-RTT的握手方式仍是有一些優化的空間的,接下來咱們來一一介紹這些優化方式。

會話複用

會話複用有兩種方式: Session IDSession Ticket

先說說最先出現的Seesion ID,具體作法是客戶端和服務器首次鏈接後各自保存會話的 ID,並存儲會話密鑰,當再次鏈接時,客戶端發送ID過來,服務器查找這個 ID 是否存在,若是找到了就直接複用以前的會話狀態,會話密鑰不用從新生成,直接用原來的那份。

但這種方式也存在一個弊端,就是當客戶端數量龐大的時候,對服務端的存儲壓力很是大。

於是出現了第二種方式——Session Ticket。它的思路就是: 服務端的壓力大,那就把壓力分攤給客戶端唄。具體來講,雙方鏈接成功後,服務器加密會話信息,用Session Ticket消息發給客戶端,讓客戶端保存下來。下次重連的時候,就把這個 Ticket 進行解密,驗證它過沒過時,若是沒過時那就直接恢復以前的會話狀態。

這種方式雖然減少了服務端的存儲壓力,但與帶來了安全問題,即每次用一個固定的密鑰來解密 Ticket 數據,一旦黑客拿到這個密鑰,以前全部的歷史記錄也被破解了。所以爲了儘可能避免這樣的問題,密鑰須要按期進行更換。

總的來講,這些會話複用的技術在保證1-RTT的同時,也節省了生成會話密鑰這些算法所消耗的時間,是一筆可觀的性能提高。

PSK

剛剛說的都是1-RTT狀況下的優化,那能不能優化到0-RTT呢?

答案是能夠的。作法其實也很簡單,在發送Session Ticket的同時帶上應用數據,不用等到服務端確認,這種方式被稱爲Pre-Shared Key,即 PSK。

這種方式雖然方便,但也帶來了安全問題。中間人截獲PSK的數據,不斷向服務器重複發,相似於 TCP 第一次握手攜帶數據,增長了服務器被攻擊的風險。

總結

TLS1.3 在 TLS1.2 的基礎上廢除了大量的算法,提高了安全性。同時利用會話複用節省了從新生成密鑰的時間,利用 PSK 作到了0-RTT鏈接。

017: HTTP/2 有哪些改進?

因爲 HTTPS 在安全方面已經作的很是好了,HTTP 改進的關注點放在了性能方面。對於 HTTP/2 而言,它對於性能的提高主要在於兩點:

  • 頭部壓縮
  • 多路複用

固然還有一些顛覆性的功能實現:

  • 設置請求優先級
  • 服務器推送

這些重大的提高本質上也是爲了解決 HTTP 自己的問題而產生的。接下來咱們來看看 HTTP/2 解決了哪些問題,以及解決方式具體是如何的。

頭部壓縮

在 HTTP/1.1 及以前的時代,請求體通常會有響應的壓縮編碼過程,經過Content-Encoding頭部字段來指定,但你有沒有想過頭部字段自己的壓縮呢?當請求字段很是複雜的時候,尤爲對於 GET 請求,請求報文幾乎全是請求頭,這個時候仍是存在很是大的優化空間的。HTTP/2 針對頭部字段,也採用了對應的壓縮算法——HPACK,對請求頭進行壓縮。

HPACK 算法是專門爲 HTTP/2 服務的,它主要的亮點有兩個:

  • 首先是在服務器和客戶端之間創建哈希表,將用到的字段存放在這張表中,那麼在傳輸的時候對於以前出現過的值,只須要把索引(好比0,1,2,...)傳給對方便可,對方拿到索引查表就好了。這種傳索引的方式,能夠說讓請求頭字段獲得極大程度的精簡和複用。

HTTP/2 當中廢除了起始行的概念,將起始行中的請求方法、URI、狀態碼轉換成了頭字段,不過這些字段都有一個":"前綴,用來和其它請求頭區分開。

  • 其次是對於整數和字符串進行哈夫曼編碼,哈夫曼編碼的原理就是先將全部出現的字符創建一張索引表,而後讓出現次數多的字符對應的索引儘量短,傳輸的時候也是傳輸這樣的索引序列,能夠達到很是高的壓縮率。

多路複用

HTTP 隊頭阻塞

咱們以前討論了 HTTP 隊頭阻塞的問題,其根本緣由在於HTTP 基於請求-響應的模型,在同一個 TCP 長鏈接中,前面的請求沒有獲得響應,後面的請求就會被阻塞。

後面咱們又討論到用併發鏈接域名分片的方式來解決這個問題,但這並無真正從 HTTP 自己的層面解決問題,只是增長了 TCP 鏈接,分攤風險而已。並且這麼作也有弊端,多條 TCP 鏈接會競爭有限的帶寬,讓真正優先級高的請求不能優先處理。

而 HTTP/2 便從 HTTP 協議自己解決了隊頭阻塞問題。注意,這裏並非指的TCP隊頭阻塞,而是HTTP隊頭阻塞,二者並非一回事。TCP 的隊頭阻塞是在數據包層面,單位是數據包,前一個報文沒有收到便不會將後面收到的報文上傳給 HTTP,而HTTP 的隊頭阻塞是在 HTTP 請求-響應層面,前一個請求沒處理完,後面的請求就要阻塞住。二者所在的層次不同。

那麼 HTTP/2 如何來解決所謂的隊頭阻塞呢?

二進制分幀

首先,HTTP/2 認爲明文傳輸對機器而言太麻煩了,不方便計算機的解析,由於對於文本而言會有多義性的字符,好比回車換行究竟是內容仍是分隔符,在內部須要用到狀態機去識別,效率比較低。因而 HTTP/2 乾脆把報文所有換成二進制格式,所有傳輸01串,方便了機器的解析。

原來Headers + Body的報文格式現在被拆分紅了一個個二進制的幀,用Headers幀存放頭部字段,Data幀存放請求體數據。分幀以後,服務器看到的再也不是一個個完整的 HTTP 請求報文,而是一堆亂序的二進制幀。這些二進制幀不存在前後關係,所以也就不會排隊等待,也就沒有了 HTTP 的隊頭阻塞問題。

通訊雙方均可以給對方發送二進制幀,這種二進制幀的雙向傳輸的序列,也叫作(Stream)。HTTP/2 用來在一個 TCP 鏈接上來進行多個數據幀的通訊,這就是多路複用的概念。

可能你會有一個疑問,既然是亂序首發,那最後如何來處理這些亂序的數據幀呢?

首先要聲明的是,所謂的亂序,指的是不一樣 ID 的 Stream 是亂序的,但同一個 Stream ID 的幀必定是按順序傳輸的。二進制幀到達後對方會將 Stream ID 相同的二進制幀組裝成完整的請求報文響應報文。固然,在二進制幀當中還有其餘的一些字段,實現了優先級流量控制等功能,咱們放到下一節再來介紹。

服務器推送

另外值得一說的是 HTTP/2 的服務器推送(Server Push)。在 HTTP/2 當中,服務器已經再也不是徹底被動地接收請求,響應請求,它也能新建 stream 來給客戶端發送消息,當 TCP 鏈接創建以後,好比瀏覽器請求一個 HTML 文件,服務器就能夠在返回 HTML 的基礎上,將 HTML 中引用到的其餘資源文件一塊兒返回給客戶端,減小客戶端的等待。

總結

固然,HTTP/2 新增那麼多的特性,是否是 HTTP 的語法要從新學呢?不須要,HTTP/2 徹底兼容以前 HTTP 的語法和語義,如請求頭、URI、狀態碼、頭部字段都沒有改變,徹底不用擔憂。同時,在安全方面,HTTP 也支持 TLS,而且如今主流的瀏覽器都公開只支持加密的 HTTP/2, 所以你如今能看到的 HTTP/2 也基本上都是跑在 TLS 上面的了。最後放一張分層圖給你們參考:

018: HTTP/2 中的二進制幀是如何設計的?

幀結構

HTTP/2 中傳輸的幀結構以下圖所示:

每一個幀分爲幀頭幀體。先是三個字節的幀長度,這個長度表示的是幀體的長度。

而後是幀類型,大概能夠分爲數據幀控制幀兩種。數據幀用來存放 HTTP 報文,控制幀用來管理的傳輸。

接下來的一個字節是幀標誌,裏面一共有 8 個標誌位,經常使用的有 END_HEADERS表示頭數據結束,END_STREAM表示單方向數據發送結束。

後 4 個字節是Stream ID, 也就是流標識符,有了它,接收方就能從亂序的二進制幀中選擇出 ID 相同的幀,按順序組裝成請求/響應報文。

流的狀態變化

從前面能夠知道,在 HTTP/2 中,所謂的,其實就是二進制幀的雙向傳輸的序列。那麼在 HTTP/2 請求和響應的過程當中,流的狀態是如何變化的呢?

HTTP/2 其實也是借鑑了 TCP 狀態變化的思想,根據幀的標誌位來實現具體的狀態改變。這裏咱們以一個普通的請求-響應過程爲例來講明:

最開始二者都是空閒狀態,當客戶端發送Headers幀後,開始分配Stream ID, 此時客戶端的打開, 服務端接收以後服務端的也打開,兩端的都打開以後,就能夠互相傳遞數據幀和控制幀了。

當客戶端要關閉時,向服務端發送END_STREAM幀,進入半關閉狀態, 這個時候客戶端只能接收數據,而不能發送數據。

服務端收到這個END_STREAM幀後也進入半關閉狀態,不過此時服務端的狀況是隻能發送數據,而不能接收數據。隨後服務端也向客戶端發送END_STREAM幀,表示數據發送完畢,雙方進入關閉狀態

若是下次要開啓新的,流 ID 須要自增,直到上限爲止,到達上限後開一個新的 TCP 鏈接重頭開始計數。因爲流 ID 字段長度爲 4 個字節,最高位又被保留,所以範圍是 0 ~ 2的 31 次方,大約 21 億個。

流的特性

剛剛談到了流的狀態變化過程,這裏順便就來總結一下傳輸的特性:

  • 併發性。一個 HTTP/2 鏈接上能夠同時發多個幀,這一點和 HTTP/1 不一樣。這也是實現多路複用的基礎。
  • 自增性。流 ID 是不可重用的,而是會按順序遞增,達到上限以後又新開 TCP 鏈接從頭開始。
  • 雙向性。客戶端和服務端均可以建立流,互不干擾,雙方均可以做爲發送方或者接收方
  • 可設置優先級。能夠設置數據幀的優先級,讓服務端先處理重要資源,優化用戶體驗。

以上就是對 HTTP/2 中二進制幀的介紹,但願對你有所啓發。

最後

文章首發於個人博客,若是以爲對你有幫助的話,但願能幫忙點一個 star,很是感謝~

參考:

《web協議詳解與抓包實戰——陶輝》

《透視 HTTP 協議》——chrono

Chromium IPC 源碼

前端開發者必備的Nginx知識 ——conardli

相關文章
相關標籤/搜索