合肥阿裏巧巧信息科技有限公司

18955110833

HTTP 知(zhī)識體(tǐ)系

2023-04-20 17:51 欄目: 技術學堂 查看()

HTTP 知(zhī)識體(tǐ)系

這是本文的思維導圖:

image.png

 

 

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

對于 TCP 而言,在傳輸的時候分(fēn)爲兩個部分(fēn):TCP數據部分(fēn)

HTTP 類似,也是header + body的結構,具體(tǐ)而言:

起始行 + 頭部 + 空行 + 實體(tǐ)

由于 http 請求報文響應報文是有一(yī)定區别,因此我(wǒ)(wǒ)們分(fēn)開(kāi)介紹。

起始行

對于請求報文來說,起始行類似下(xià)面這樣:

GET /home HTTP/1.1

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

對于響應報文來說,起始行一(yī)般張這個樣:

HTTP/1.1 200 OK

響應報文的起始行也叫做狀态行。由http版本、狀态碼和原因三部分(fēn)組成。

值得注意的是,在起始行中(zhōng),每兩個部分(fēn)之間用空格隔開(kāi),最後一(yī)個部分(fēn)後面應該接一(yī)個換行,嚴格遵循ABNF語法規範

頭部

展示一(yī)下(xià)請求頭和響應頭在報文中(zhōng)的位置:

image.png

 

不管是請求頭還是響應頭,其中(zhōng)的字段是相當多的,而且牽扯到http非常多的特性,這裏就不一(yī)一(yī)列舉的,重點看看這些頭部字段的格式:

1.          字段名不區分(fēn)大(dà)小(xiǎo)寫

1.          字段名不允許出現空格,不可以出現下(xià)劃線_

1.          字段名後面必須緊接着:

空行

很重要,用來區分(fēn)開(kāi)頭部實體(tǐ)

: 如果說在頭部中(zhōng)間故意加一(yī)個空行會怎麽樣?

那麽空行後的内容全部被視爲實體(tǐ)。

實體(tǐ)

就是具體(tǐ)的數據了,也就是body部分(fēn)。請求報文對應請求體(tǐ), 響應報文對應響應體(tǐ)

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

有哪些請求方法?

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

             GET: 通常用來獲取資(zī)源

             HEAD: 獲取資(zī)源的元信息

             POST: 提交數據,即上傳數據

             PUT: 修改數據

             DELETE: 删除資(zī)源(幾乎用不到)

             CONNECT: 建立連接隧道,用于代理服務器

             OPTIONS: 列出可對資(zī)源實行的請求方法,用來跨域請求

             TRACE: 追蹤請求-響應的傳輸路徑

GET POST 有什麽區别?

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

而後又(yòu)有這樣一(yī)些具體(tǐ)的差别:

             緩存的角度,GET 請求會被浏覽器主動緩存下(xià)來,留下(xià)曆史記錄,而 POST 默認不會。

             編碼的角度,GET 隻能進行 URL 編碼,隻能接收 ASCII 字符,而 POST 沒有限制。

             參數的角度,GET 一(yī)般放(fàng)在 URL 中(zhōng),因此不安全,POST 放(fàng)在請求體(tǐ)中(zhōng),更适合傳輸敏感信息。

             幂等性的角度,GET幂等的,而POST不是。(幂等表示執行相同的操作,結果也是相同的)

             TCP的角度,GET 請求會把請求報文一(yī)次性發出去(qù),而 POST 會分(fēn)爲兩個 TCP 數據包,首先發 header 部分(fēn),如果服務器響應 100(continue) 然後發 body 部分(fēn)。(火(huǒ)狐浏覽器除外(wài),它的 POST 請求隻發一(yī)個 TCP )

003: 如何理解 URI

URI, 全稱爲(Uniform Resource Identifier), 也就是統一(yī)資(zī)源标識符,它的作用很簡單,就是區分(fēn)互聯網上不同的資(zī)源。

但是,它并不是我(wǒ)(wǒ)們常說的網址, 網址指的是URL, 實際上URI包含了URNURL兩個部分(fēn),由于 URL 過于普及,就默認将 URI 視爲 URL 了。

URI 的結構

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

image.png

 

scheme 表示協議名,比如http, https, file等等。後面必須和://連在一(yī)起。

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

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

path表示請求路徑,标記資(zī)源所在位置。

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

fragment表示 URI 所定位的資(zī)源内的一(yī)個錨點,浏覽器可以根據這個錨點跳轉到對應的位置。

舉個例子:

https://www.baidu.com/s?wd=HTTP&rsv_spt=1

這個 URI 中(zhōng),httpsscheme部分(fēn),www.baidu.comhost:port部分(fēn)(注意,http https 的默認端口分(fēn)别爲80443),/spath部分(fēn),而wd=HTTP&rsv_spt=1就是query部分(fēn)。

URI 編碼

URI 隻能使用ASCII, ASCII 之外(wài)的字符是不支持顯示的,而且還有一(yī)部分(fēn)符号是界定符,如果不加以處理就會導緻解析出錯。

因此,URI 引入了編碼機制,将所有 ASCII 碼字符界定符轉爲十六進制字節值,然後在前面加個%

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

004: 如何理解 HTTP 狀态碼?

RFC 規定 HTTP 的狀态碼爲三位數,被分(fēn)爲五類:

             1xx: 表示目前是協議處理的中(zhōng)間狀态,還需要後續操作。

             2xx: 表示成功狀态。

             3xx: 重定向狀态,資(zī)源位置發生(shēng)變動,需要重新請求。

             4xx: 請求報文有誤。

             5xx: 服務器端發生(shēng)錯誤。

1xx

101 Switching Protocols。在HTTP升級爲WebSocket的時候,如果服務器同意變更,就會發送狀态碼 101

2xx

200 OK是見得最多的成功狀态碼。通常在響應體(tǐ)中(zhōng)放(fàng)有數據。

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

206 Partial Content顧名思義,表示部分(fēn)内容,它的使用場景爲 HTTP 分(fēn)塊下(xià)載和斷點續傳,當然也會帶上相應的響應頭字段Content-Range

3xx

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

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

而如果隻是暫時不可用,那麽直接返回302即可,和301不同的是,浏覽器并不會做緩存優化。

304 Not Modified: 當協商(shāng)緩存命中(zhōng)時會返回這個狀态碼。詳見浏覽器緩存

4xx

400 Bad Request: 開(kāi)發者經常看到一(yī)頭霧水,隻是籠統地提示了一(yī)下(xià)錯誤,并不知(zhī)道哪裏出錯了。

403 Forbidden: 這實際上并不是請求報文出錯,而是服務器禁止訪問,原因有很多,比如法律禁止、信息敏感。

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

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

406 Not Acceptable: 資(zī)源無法滿足客戶端的條件。

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

409 Conflict: 多個請求發生(shēng)了沖突。

413 Request Entity Too Large: 請求體(tǐ)的數據過大(dà)。

414 Request-URI Too Long: 請求行裏的 URI 太大(dà)。

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

431 Request Header Fields Too Large請求頭的字段内容太大(dà)。

5xx

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

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

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

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

005: 簡要概括一(yī)下(xià) HTTP 的特點?HTTP 有哪些缺點?

HTTP 特點

HTTP 的特點概括如下(xià):

1.          靈活可擴展,主要體(tǐ)現在兩個方面。一(yī)個是語義上的自由,隻規定了基本格式,比如空格分(fēn)隔單詞,換行分(fēn)隔字段,其他的各個部分(fēn)都沒有嚴格的語法限制。另一(yī)個是傳輸形式的多樣性,不僅僅可以傳輸文本,還能傳輸圖片、視頻(pín)等任意數據,非常方便。

2.          可靠傳輸。HTTP 基于 TCP/IP,因此把這一(yī)特性繼承了下(xià)來。這屬于 TCP 的特性,不具體(tǐ)介紹了。

3.          請求-應答。也就是一(yī)發一(yī)收有來有回 當然這個請求方和應答方不單單指客戶端和服務器之間,如果某台服務器作爲代理來連接後端的服務端,那麽這台服務器也會扮演請求方的角色。

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

HTTP 缺點

無狀态

所謂的優點和缺點還是要分(fēn)場景來看的,對于 HTTP 而言,最具争議的地方在于它的無狀态

在需要長連接的場景中(zhōng),需要保存大(dà)量的上下(xià)文信息,以免傳輸大(dà)量重複的信息,那麽這時候無狀态就是 http 的缺點了。

但與此同時,另外(wài)一(yī)些應用僅僅隻是爲了獲取一(yī)些數據,不需要保存連接上下(xià)文信息,無狀态反而減少了網絡開(kāi)銷,成爲了 http 的優點。

明文傳輸

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

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

隊頭阻塞問題

http 開(kāi)啓長連接時,共用一(yī)個 TCP 連接,同一(yī)時刻隻能處理一(yī)個請求,那麽當前請求耗時過長的情況下(xià),其它的請求隻能處于阻塞狀态,也就是著名的隊頭阻塞問題。接下(xià)來會有一(yī)小(xiǎo)節讨論這個問題。

006: Accept 系列字段了解多少?

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

數據格式

上一(yī)節談到 HTTP 靈活的特性,它支持非常多的數據格式,那麽這麽多格式的數據一(yī)起到達客戶端,客戶端怎麽知(zhī)道它的格式呢?

當然,最低效的方式是直接猜,有沒有更好的方式呢?直接指定可以嗎(ma)?

答案是肯定的。不過首先需要介紹一(yī)個标準——MIME(Multipurpose Internet Mail Extensions, 多用途互聯網郵件擴展)。它首先用在電(diàn)子郵件系統中(zhōng),讓郵件可以發任意類型的數據,這對于 HTTP 來說也是通用的。

因此,HTTP MIME type取了一(yī)部分(fēn)來标記報文 body 部分(fēn)的數據類型,這些類型體(tǐ)現在Content-Type這個字段,當然這是針對于發送端而言,接收端想要收到特定類型的數據,也可以用Accept字段。

具體(tǐ)而言,這兩個字段的取值可以分(fēn)爲下(xià)面幾類:

             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

壓縮方式

當然一(yī)般這些數據都是會進行編碼壓縮的,采取什麽樣的壓縮方式就體(tǐ)現在了發送方的Content-Encoding字段上, 同樣的,接收什麽樣的壓縮方式體(tǐ)現在了接受方的Accept-Encoding字段上。這個字段的取值有下(xià)面幾種:

             gzip: 當今最流行的壓縮格式

             deflate: 另外(wài)一(yī)種著名的壓縮格式

             br: 一(yī)種專門爲 HTTP 發明的壓縮算法

// 發送端
Content-Encoding: gzip
// 接收端
Accept-Encoding: gzip

支持語言

對于發送方而言,還有一(yī)個Content-Language字段,在需要實現國際化的方案當中(zhōng),可以用來指定支持的語言,在接受方對應的字段爲Accept-Language。如:

// 發送端
Content-Language: zh-CN, zh, en
// 接收端
Accept-Language: zh-CN, zh, en

字符集

最後是一(yī)個比較特殊的字段, 在接收端對應爲Accept-Charset,指定可以接受的字符集,而在發送端并沒有對應的Content-Charset, 而是直接放(fàng)在了Content-Type中(zhōng),以charset屬性指定。如:

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

以一(yī)張圖來總結一(yī)下(xià)吧:

image.png

 

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

定長包體(tǐ)

對于定長包體(tǐ)而言,發送端在傳輸的時候一(yī)般會帶上 Content-Length, 來指明包體(tǐ)的長度。

我(wǒ)(wǒ)們用一(yī)個nodejs服務器來模拟一(yī)下(xià):

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

浏覽器中(zhōng)顯示如下(xià):

helloworld

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

我(wǒ)(wǒ)們試着把這個長度設置的小(xiǎo)一(yī)些:

res.setHeader('Content-Length', 8);

重啓服務,再次訪問,現在浏覽器中(zhōng)内容如下(xià):

helloworld

那後面的ld哪裏去(qù)了呢?實際上在 http 的響應體(tǐ)中(zhōng)直接被截去(qù)了。

然後我(wǒ)(wǒ)們試着将這個長度設置得大(dà)一(yī)些:

res.setHeader('Content-Length', 12);

此時浏覽器直接無法顯示了。可以看到Content-Length對于 http 傳輸過程起到了十分(fēn)關鍵的作用,如果設置不當可以直接導緻傳輸失敗。

不定長包體(tǐ)

上述是針對于定長包體(tǐ),那麽對于不定長包體(tǐ)而言是如何傳輸的呢?

這裏就必須介紹另外(wài)一(yī)個 http 頭部字段了:

Transfer-Encoding: chunked

表示分(fēn)塊傳輸數據,設置這個字段後會自動産生(shēng)兩個效果:

             Content-Length 字段會被忽略

             基于長連接持續推送動态内容

我(wǒ)(wǒ)們依然以一(yī)個實際的例子來模拟分(fēn)塊傳輸,nodejs 程序如下(xià):

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("第一(yī)次傳輸<br/>");
    }, 1000);
    setTimeout(() => {
      res.write("第二次傳輸");
      res.end()
    }, 2000);
  }
})

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

telnet 抓到的響應如下(xià):

image.png

 

注意,Connection: keep-alive及之前的爲響應行和響應頭,後面的内容爲響應體(tǐ),這兩部分(fēn)用換行符隔開(kāi)。

響應體(tǐ)的結構比較有意思,如下(xià)所示:

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

最後是留有有一(yī)個空行的,這一(yī)點請注意。

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

008: HTTP 如何處理大(dà)文件的傳輸?

對于幾百 M 甚至上 G 的大(dà)文件來說,如果要一(yī)口氣全部傳輸過來顯然是不現實的,會有大(dà)量的等待時間,嚴重影響用戶體(tǐ)驗。因此,HTTP 針對這一(yī)場景,采取了範圍請求的解決方案,允許客戶端僅僅請求一(yī)個資(zī)源的一(yī)部分(fēn)。

如何支持

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

Accept-Ranges: none

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

Range 字段拆解

而對于客戶端而言,它需要指定請求哪一(yī)部分(fēn),通過Range這個請求頭字段确定,格式爲bytes=x-y。接下(xià)來就來讨論一(yī)下(xià)這個 Range 的書(shū)寫格式:

             0-499表示從開(kāi)始到第 499 個字節。

             500- 表示從第 500 字節到文件終點。

             -100表示文件的最後100個字節。

服務器收到請求之後,首先驗證範圍是否合法,如果越界了那麽返回416錯誤碼,否則讀取相應片段,返回206狀态碼。

同時,服務器需要添加Content-Range字段,這個字段的格式根據請求頭中(zhōng)Range字段的不同而有所差異。

具體(tǐ)來說,請求單段數據和請求多段數據,響應頭是不一(yī)樣的。

舉個例子:

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

接下(xià)來我(wǒ)(wǒ)們就分(fēn)别來讨論着兩種情況。

單段數據

對于單段數據的請求,返回的響應如下(xià):

接下(xià)來我(wǒ)(wǒ)們就分(fēn)别來讨論着兩種情況。

單段數據
對于單段數據的請求,返回的響應如下(xià):

值得注意的是Content-Range字段,0-9表示請求的返回,100表示資(zī)源的總大(dà)小(xiǎo),很好理解。

多段數據

接下(xià)來我(wǒ)(wǒ)們看看多段請求的情況。得到的響應會是下(xià)面這個形式:

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--

這個時候出現了一(yī)個非常關鍵的字段Content-Type: multipart/byteranges;boundary=00000010101,它代表了信息量是這樣的:

             請求一(yī)定是多段數據請求

             響應體(tǐ)中(zhōng)的分(fēn)隔符是 00000010101

因此,在響應體(tǐ)中(zhōng)各段數據之間會由這裏指定的分(fēn)隔符分(fēn)開(kāi),而且在最後的分(fēn)隔末尾添上--表示結束。

以上就是 http 針對大(dà)文件傳輸所采用的手段.

009: HTTP 中(zhōng)如何處理表單數據的提交?

http 中(zhōng),有兩種主要的表單提交的方式,體(tǐ)現在兩種不同的Content-Type取值:

             application/x-www-form-urlencoded

             multipart/form-data

由于表單提交一(yī)般是POST請求,很少考慮GET,因此這裏我(wǒ)(wǒ)們将默認提交的數據放(fàng)在請求體(tǐ)中(zhōng)。

application/x-www-form-urlencoded

對于application/x-www-form-urlencoded格式的表單内容,有以下(xià)特點:

             其中(zhōng)的數據會被編碼成以&分(fēn)隔的鍵值對

             字符以URL編碼方式編碼。

如:

// 轉換過程: {a: 1, b: 2} -> a=1&b=2 -> 如下(xià)(最終形式)
"a%3D1%26b%3D2"

multipart/form-data

對于multipart/form-data而言:

             請求頭中(zhōng)的Content-Type字段會包含boundary,且boundary的值有浏覽器默認指定。例: Content-Type: multipart/form-data;boundary=----WebkitFormBoundaryRRJKeWfHPGrS4LKe

             數據會分(fēn)爲多個部分(fēn),每兩個部分(fēn)之間通過分(fēn)隔符來分(fēn)隔,每部分(fēn)表述均有 HTTP 頭部描述子包體(tǐ),如Content-Type,在最後的分(fēn)隔符會加上--表示結束。

相應的請求體(tǐ)是下(xià)面這樣:

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

小(xiǎo)結

值得一(yī)提的是,multipart/form-data 格式最大(dà)的特點在于:每一(yī)個表單元素都是獨立的資(zī)源表述。另外(wài),你可能在寫業務的過程中(zhōng),并沒有注意到其中(zhōng)還有boundary的存在,如果你打開(kāi)抓包工(gōng)具,确實可以看到不同的表單元素被拆分(fēn)開(kāi)了,之所以在平時感覺不到,是以爲浏覽器和 HTTP 給你封裝了這一(yī)系列操作。

而且,在實際的場景中(zhōng),對于圖片等文件的上傳,基本采用multipart/form-data而不用application/x-www-form-urlencoded,因爲沒有必要做 URL 編碼,帶來巨大(dà)耗時的同時也占用了更多的空間。

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

什麽是 HTTP 隊頭阻塞?

從前面的小(xiǎo)節可以知(zhī)道,HTTP 傳輸是基于請求-應答的模式進行的,報文必須是一(yī)發一(yī)收,但值得注意的是,裏面的任務被放(fàng)在一(yī)個任務隊列中(zhōng)串行執行,一(yī)旦隊首的請求處理太慢(màn),就會阻塞後面請求的處理。這就是著名的HTTP隊頭阻塞問題。

并發連接

對于一(yī)個域名允許分(fēn)配多個長連接,那麽相當于增加了任務隊列,不至于一(yī)個隊伍的任務阻塞其它所有任務。在RFC2616規定過客戶端最多并發 2 個連接,不過事實上在現在的浏覽器标準中(zhōng),這個上限要多很多,Chrome 中(zhōng)是 6 個。

但其實,即使是提高了并發連接,還是不能滿足人們對性能的需求。

域名分(fēn)片

一(yī)個域名不是可以并發 6 個長連接嗎(ma)?那我(wǒ)(wǒ)就多分(fēn)幾個域名。

比如 content1.sanyuan.com content2.sanyuan.com

這樣一(yī)個sanyuan.com域名下(xià)可以分(fēn)出非常多的二級域名,而它們都指向同樣的一(yī)台服務器,能夠并發的長連接數更多了,事實上也更好地解決了隊頭阻塞的問題。

011: Cookie 了解多少?

Cookie 簡介

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

HTTP 爲此引入了 CookieCookie 本質上就是浏覽器裏面存儲的一(yī)個很小(xiǎo)的文本文件,内部以鍵值對的方式來存儲(chrome開(kāi)發者面闆的Application這一(yī)欄可以看到)。向同一(yī)個域名下(xià)發送請求,都會攜帶相同的 Cookie,服務器拿到 Cookie 進行解析,便能拿到客戶端的狀态。而服務端可以通過響應頭中(zhōng)的Set-Cookie字段來對客戶端寫入Cookie。舉例如下(xià):

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

Cookie 屬性

生(shēng)存周期

Cookie 的有效期可以通過ExpiresMax-Age兩個屬性來設置。

             Expires過期時間

             Max-Age用的是一(yī)段時間間隔,單位是秒,從浏覽器收到報文開(kāi)始計算。

Cookie 過期,則這個 Cookie 會被删除,并不會發送給服務端。

作用域

關于作用域也有兩個屬性: Domainpath, Cookie 綁定了域名和路徑,在發送請求之前,發現域名或者路徑和這兩個屬性不匹配,那麽就不會帶上 Cookie。值得注意的是,對于路徑來說,/表示域名下(xià)的任意路徑都允許使用 Cookie

安全相關

如果帶上Secure,說明隻能通過 HTTPS 傳輸 cookie

如果 cookie 字段帶上HttpOnly,那麽說明隻能通過 HTTP 協議傳輸,不能通過 JS 訪問,這也是預防 XSS 攻擊的重要手段。

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

SameSite可以設置爲三個值,StrictLaxNone

a. Strict模式下(xià),浏覽器完全禁止第三方請求攜帶Cookie。比如請求sanyuan.com網站隻能在sanyuan.com域名當中(zhōng)請求才能攜帶 Cookie,在其他網站請求都不能。

b. Lax模式,就寬松一(yī)點了,但是隻能在 get 方法提交表單況或者a 标簽發送 get 請求的情況下(xià)可以攜帶 Cookie,其他情況均不能。

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

Cookie 的缺點

1.          容量缺陷。Cookie 的體(tǐ)積上限隻有4KB,隻能用來存儲少量的信息。

2.          性能缺陷。Cookie 緊跟域名,不管域名下(xià)面的某一(yī)個地址需不需要這個 Cookie ,請求都會攜帶上完整的 Cookie,這樣随着請求數的增多,其實會造成巨大(dà)的性能浪費(fèi)的,因爲請求攜帶了很多不必要的内容。但可以通過DomainPath指定作用域來解決。

3.          安全缺陷。由于 Cookie 以純文本的形式在浏覽器和服務器中(zhōng)傳遞,很容易被非法用戶截獲,然後進行一(yī)系列的篡改,在 Cookie 的有效期内重新發送給服務器,這是相當危險的。另外(wài),在HttpOnly false 的情況下(xià),Cookie 信息能直接通過 JS 腳本來讀取。

012: 如何理解 HTTP 代理?

我(wǒ)(wǒ)們知(zhī)道在 HTTP 是基于請求-響應模型的協議,一(yī)般由客戶端發請求,服務器來進行響應。

當然,也有特殊情況,就是代理服務器的情況。引入代理之後,作爲代理的服務器相當于一(yī)個中(zhōng)間人的角色,對于客戶端而言,表現爲服務器進行響應;而對于源服務器,表現爲客戶端發起請求,具有雙重身份

那代理服務器到底是用來做什麽的呢?

功能

1.          負載均衡。客戶端的請求隻會先到達代理服務器,後面到底有多少源服務器,IP 都是多少,客戶端是不知(zhī)道的。因此,這個代理服務器可以拿到這個請求之後,可以通過特定的算法分(fēn)發給不同的源服務器,讓各台源服務器的負載盡量平均。當然,這樣的算法有很多,包括随機算法輪詢一(yī)緻性hashLRU(最近最少使用)等等,不過這些算法并不是本文的重點,大(dà)家有興趣自己可以研究一(yī)下(xià)。

2.          保障安全。利用心跳機制監控後台的服務器,一(yī)旦發現故障機就将其踢出集群。并且對于上下(xià)行的數據進行過濾,對非法 IP 限流,這些都是代理服務器的工(gōng)作。

3.          緩存代理。将内容緩存到代理服務器,使得客戶端可以直接從代理服務器獲得而不用到源服務器那裏。下(xià)一(yī)節詳細拆解。

相關頭部字段

Via

代理服務器需要标明自己的身份,在 HTTP 傳輸中(zhōng)留下(xià)自己的痕迹,怎麽辦呢?

通過Via字段來記錄。舉個例子,現在中(zhōng)間有兩台代理服務器,在客戶端發送請求後會經曆這樣一(yī)個過程:

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

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

Via: proxy_server1, proxy_server2

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

Via: proxy_server2, proxy_server1

可以看到,Via中(zhōng)代理的順序即爲在 HTTP 傳輸中(zhōng)報文傳達的順序。

X-Forwarded-For

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

X-Real-IP

是一(yī)種獲取用戶真實 IP 的字段,不管中(zhōng)間經過多少代理,這個字段始終記錄最初的客戶端的IP

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

X-Forwarded-For産生(shēng)的問題

前面可以看到,X-Forwarded-For這個字段記錄的是請求方的 IP,這意味着每經過一(yī)個不同的代理,這個字段的名字都要變,從客戶端代理1,這個字段是客戶端的 IP,從代理1代理2,這個字段就變爲了代理1 IP

但是這會産生(shēng)兩個問題:

1.          意味着代理必須解析 HTTP 請求頭,然後修改,比直接轉發數據性能下(xià)降。

2.          HTTPS 通信加密的過程中(zhōng),原始報文是不允許修改的。

由此産生(shēng)了代理協議,一(yī)般使用明文版本,隻需要在 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 緩存及緩存代理?

關于強緩存協商(shāng)緩存的内容,我(wǒ)(wǒ)已經在能不能說一(yī)說浏覽器緩存做了詳細分(fēn)析,小(xiǎo)結如下(xià):

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

             如果強緩存可用,直接使用

             否則進入協商(shāng)緩存,即發送 HTTP 請求,服務器通過請求頭中(zhōng)的

      If-Modified-Since

                或者

      If-None-Match

                這些

                條件請求

                字段檢查資(zī)源是否更新

            若資(zī)源更新,返回資(zī)源和200狀态碼

            否則,返回304,告訴浏覽器直接從緩存獲取資(zī)源

這一(yī)節我(wǒ)(wǒ)們主要來說說另外(wài)一(yī)種緩存方式: 代理緩存

爲什麽産生(shēng)代理緩存?

對于源服務器來說,它也是有緩存的,比如Redis, Memcache,但對于 HTTP 緩存來說,如果每次客戶端緩存失效都要到源服務器獲取,那給源服務器的壓力是很大(dà)的。

由此引入了緩存代理的機制。讓代理服務器接管一(yī)部分(fēn)的服務端HTTP緩存,客戶端緩存過期後就近到代理緩存中(zhōng)獲取,代理緩存過期了才請求源服務器,這樣流量巨大(dà)的時候能明顯降低源服務器的壓力。

那緩存代理究竟是如何做到的呢?

總的來說,緩存代理的控制分(fēn)爲兩部分(fēn),一(yī)部分(fēn)是源服務器端的控制,一(yī)部分(fēn)是客戶端的控制。

源服務器的緩存控制

private public

在源服務器的響應頭中(zhōng),會加上Cache-Control這個字段進行緩存控制字段,那麽它的值當中(zhōng)可以加入private或者public表示是否允許代理服務器緩存,前者禁止,後者爲允許。

比如對于一(yī)些非常私密的數據,如果緩存到代理服務器,别人直接訪問代理就可以拿到這些數據,是非常危險的,因此對于這些數據一(yī)般是不會允許代理服務器進行緩存的,将響應頭部的Cache-Control設爲private,而不是public

proxy-revalidate

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

s-maxage

sshare的意思,限定了緩存在代理服務器中(zhōng)可以存放(fàng)多久,和限制客戶端緩存時間的max-age并不沖突。

講了這幾個字段,我(wǒ)(wǒ)們不妨來舉個小(xiǎo)例子,源服務器在響應頭中(zhōng)加入這樣一(yī)個字段:

Cache-Control: public, max-age=1000, s-maxage=2000

相當于源服務器說: 我(wǒ)(wǒ)這個響應是允許代理服務器緩存的,客戶端緩存過期了到代理中(zhōng)拿,并且在客戶端的緩存時間爲 1000 秒,在代理服務器中(zhōng)的緩存時間爲 2000 s

客戶端的緩存控制

max-stale min-fresh

在客戶端的請求頭中(zhōng),可以加入這兩個字段,來對代理服務器上的緩存進行寬容限制操作。比如:

max-stale: 5

表示客戶端到代理服務器上拿緩存的時候,即使代理緩存過期了也不要緊,隻要過期時間在5秒之内,還是可以從代理中(zhōng)獲取的。

又(yòu)比如:

min-fresh: 5

表示代理緩存需要一(yī)定的新鮮度,不要等到緩存剛好到期再拿,一(yī)定要在到期前 5 之前的時間拿,否則拿不到。

only-if-cached

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

以上便是緩存代理的内容,涉及的字段比較多,希望能好好回顧一(yī)下(xià),加深理解。

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

在前後端分(fēn)離(lí)的開(kāi)發模式中(zhōng),經常會遇到跨域問題,即 Ajax 請求發出去(qù)了,服務器也成功響應了,前端就是拿不到這個響應。接下(xià)來我(wǒ)(wǒ)們就來好好讨論一(yī)下(xià)這個問題。

什麽是跨域

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

             不能讀取和修改對方的 DOM

             不讀訪問對方的 CookieIndexDB LocalStorage

             限制 XMLHttpRequest 請求。(後面的話(huà)題着重圍繞這個)

當浏覽器向目标 URI Ajax 請求時,隻要當前 URL 和目标 URL 不同源,則産生(shēng)跨域,被稱爲跨域請求

跨域請求的響應一(yī)般會被浏覽器所攔截,注意,是被浏覽器攔截,響應其實是成功到達客戶端了。那這個攔截是如何發生(shēng)呢?

首先要知(zhī)道的是,浏覽器是多進程的,以 Chrome 爲例,進程組成如下(xià):

image.png

 

WebKit 渲染引擎V8 引擎都在渲染進程當中(zhōng)。

xhr.send被調用,即 Ajax 請求準備發送的時候,其實還隻是在渲染進程的處理。爲了防止黑客通過腳本觸碰到系統資(zī)源,浏覽器将每一(yī)個渲染進程裝進了沙箱,并且爲了防止 CPU 芯片一(yī)直存在的Spectre Meltdown漏洞,采取了站點隔離(lí)的手段,給每一(yī)個不同的站點(一(yī)級域名不同)分(fēn)配了沙箱,互不幹擾。具體(tǐ)見YouTubeChromium安全團隊的演講視頻(pín)

在沙箱當中(zhōng)的渲染進程是沒有辦法發送網絡請求的,那怎麽辦?隻能通過網絡進程來發送。那這樣就涉及到進程間通信(IPCInter Process Communication)了。接下(xià)來我(wǒ)(wǒ)們看看 chromium 當中(zhōng)進程間通信是如何完成的,在 chromium 源碼中(zhōng)調用順序如下(xià):

99.jpg

 

可能看了你會比較懵,如果想深入了解可以去(qù)看看 chromium 最新的源代碼,IPC源碼地址Chromium IPC源碼解析文章

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

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

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

接下(xià)來我(wǒ)(wǒ)們來說一(yī)說解決跨域問題的幾種方案。

CORS

CORS 其實是 W3C 的一(yī)個标準,全稱是跨域資(zī)源共享。它需要浏覽器和服務器的共同支持,具體(tǐ)來說,非 IE IE10 以上支持CORS,服務器需要附加特定的響應頭,後面具體(tǐ)拆解。不過在弄清楚 CORS 的原理之前,我(wǒ)(wǒ)們需要清楚兩個概念: 簡單請求非簡單請求

浏覽器根據請求方法和請求頭的特定字段,将請求做了一(yī)下(xià)分(fēn)類,具體(tǐ)來說規則是這樣,凡是滿足下(xià)面條件的屬于簡單請求:

             請求方法爲 GETPOST 或者 HEAD

             請求頭的取值範圍: AcceptAccept-LanguageContent-LanguageContent-Type(隻限于三個值application/x-www-form-urlencodedmultipart/form-datatext/plain)

浏覽器畫了這樣一(yī)個圈,在這個圈裏面的就是簡單請求, 圈外(wài)面的就是非簡單請求,然後針對這兩種不同的請求進行不同的處理。

簡單請求

請求發出去(qù)之前,浏覽器做了什麽?

它會自動在請求頭當中(zhōng),添加一(yī)個Origin字段,用來說明請求來自哪個。服務器拿到請求之後,在回應時對應地添加Access-Control-Allow-Origin字段,如果Origin不在這個字段的範圍中(zhōng),那麽浏覽器就會将響應攔截。

因此,Access-Control-Allow-Origin字段是服務器用來決定浏覽器是否攔截這個響應,這是必需的字段。與此同時,其它一(yī)些可選的功能性的字段,用來描述如果不會攔截,這些字段将會發揮各自的作用。

Access-Control-Allow-Credentials。這個字段是一(yī)個布爾值,表示是否允許發送 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 這個字段的值。

非簡單請求

非簡單請求相對而言會有些不同,體(tǐ)現在兩個方面: 預檢請求響應字段

我(wǒ)(wǒ)們以 PUT 方法爲例。

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

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

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 請求将要加上什麽請求頭

這是預檢請求。接下(xià)來是響應字段,響應字段也分(fēn)爲兩部分(fēn),一(yī)部分(fēn)是對于預檢請求的響應,一(yī)部分(fēn)是對于 CORS 請求的響應。

預檢請求的響應。如下(xià)面的格式:

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

其中(zhōng)有這樣幾個關鍵的響應頭字段:

             Access-Control-Allow-Origin: 表示可以允許請求的源,可以填具體(tǐ)的源名,也可以填*表示允許任意源請求。

             Access-Control-Allow-Methods: 表示允許的請求方法列表。

             Access-Control-Allow-Credentials: 簡單請求中(zhōng)已經介紹。

             Access-Control-Allow-Headers: 表示允許發送的請求頭字段

             Access-Control-Max-Age: 預檢請求的有效期,在此期間,不用發出另外(wài)一(yī)條預檢請求。

在預檢請求的響應返回後,如果請求不滿足響應頭的條件,則觸發XMLHttpRequestonerror方法,當然後面真正的CORS請求也不會發出去(qù)了。

CORS 請求的響應。繞了這麽一(yī)大(dà)轉,到了真正的 CORS 請求就容易多了,現在它和簡單請求的情況是一(yī)樣的。浏覽器自動加上Origin字段,服務端響應頭返回Access-Control-Allow-Origin。可以參考以上簡單請求部分(fēn)的内容。

JSONP

雖然XMLHttpRequest對象遵循同源政策,但是script标簽不一(yī)樣,它可以通過 src 填上目标地址從而發出 GET 請求,實現跨域請求并拿到響應。這也就是 JSONP 的原理,接下(xià)來我(wǒ)(wǒ)們就來封裝一(yī)個 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 元素并加入到當前文檔中(zhōng)
    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标簽,浏覽器直接把這部分(fēn)字符串執行
  res.end(`${callback}('數據包')`);
})
app.listen(3000)

前端這樣簡單地調用一(yī)下(xià)就好了:

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

CORS相比,JSONP 最大(dà)的優勢在于兼容性好,IE 低版本不能使用 CORS 但可以使用 JSONP,缺點也很明顯,請求方法單一(yī),隻支持 GET 請求。

Nginx

Nginx 是一(yī)種高性能的反向代理服務器,可以用來輕松解決跨域問題。

image.png

 image.png

正向代理幫助客戶端訪問客戶端自己訪問不到的服務器,然後将結果返回給客戶端。

反向代理拿到客戶端的請求,将請求轉發給其他的服務器,主要的場景是維持服務器集群的負載均衡,換句話(huà)說,反向代理幫其它的服務器拿到請求,然後選擇一(yī)個合适的服務器,将請求轉交給它。

因此,兩者的區别就很明顯了,正向代理服務器是幫客戶端做事情,而反向代理服務器是幫其它的服務器做事情。

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

比如說現在客戶端的域名爲client.com,服務器的域名爲server.com,客戶端向服務器發送 Ajax 請求,當然會跨域了,那這個時候讓 Nginx 登場了,通過下(xià)面這個配置:

server {
  listen  80;
  server_name  client.com;
  location /api {
    proxy_pass server.com;
  }
}

Nginx 相當于起了一(yī)個跳闆機,這個跳闆機的域名也是client.com,讓客戶端首先訪問 client.com/api,這當然沒有跨域,然後 Nginx 服務器作爲反向代理,将請求轉發給server.com,當響應返回時又(yòu)将響應給到客戶端,這就完成整個跨域請求的過程。

其實還有一(yī)些不太常用的方式,大(dà)家了解即可,比如postMessage,當然WebSocket也是一(yī)種方式,但是已經不屬于 HTTP 的範疇,另外(wài)一(yī)些奇技淫巧就不建議大(dà)家去(qù)死記硬背了,一(yī)方面從來不用,名字都難得記住,另一(yī)方面臨時背下(xià)來,面試官也不會對你印象加分(fēn),因爲看得出來是背的。當然沒有背并不代表減分(fēn),把跨域原理和前面三種主要的跨域方式理解清楚,經得起更深一(yī)步的推敲,反而會讓别人覺得你是一(yī)個靠譜的人。

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

之前談到了 HTTP 是明文傳輸的協議,傳輸保文對外(wài)完全透明,非常不安全,那如何進一(yī)步保證安全性呢?

由此産生(shēng)了 HTTPS,其實它并不是一(yī)個新的協議,而是在 HTTP 下(xià)面增加了一(yī)層 SSL/TLS 協議,簡單的講,HTTPS = HTTP + SSL/TLS

那什麽是 SSL/TLS 呢?

SSL 即安全套接層(Secure Sockets Layer),在 OSI 七層模型中(zhōng)處于會話(huà)層( 5 )。之前 SSL 出過三個大(dà)版本,當它發展到第三個大(dà)版本的時候才被标準化,成爲 TLS(傳輸層安全,Transport Layer Security),并被當做 TLS1.0 的版本,準确地說,TLS1.0 = SSL3.1

現在主流的版本是 TLS/1.2, 之前的 TLS1.0TLS1.1 都被認爲是不安全的,在不久的将來會被完全淘汰。因此我(wǒ)(wǒ)們接下(xià)來主要讨論的是 TLS1.2, 當然在 2018 年推出了更加優秀的 TLS1.3,大(dà)大(dà)優化了 TLS 握手過程,這個我(wǒ)(wǒ)們放(fàng)在下(xià)一(yī)節再去(qù)說。

傳統 RSA 握手

先來說說傳統的 TLS 握手,也是大(dà)家在網上經常看到的。我(wǒ)(wǒ)之前也寫過這樣的文章,(傳統RSA版本)HTTPS爲什麽讓數據傳輸更安全,其中(zhōng)也介紹到了對稱加密非對稱加密的概念,建議大(dà)家去(qù)讀一(yī)讀,不再贅述。之所以稱它爲 RSA 版本,是因爲它在加解密pre_random的時候采用的是 RSA 算法。

TLS 1.2 握手過程

現在我(wǒ)(wǒ)們來講講主流的 TLS 1.2 版本所采用的方式。

image.png

 

剛開(kāi)始你可能會比較懵,先别着急,過一(yī)遍下(xià)面的流程再來看會豁然開(kāi)朗。

step 1: Client Hello

首先,浏覽器發送 client_randomTLS版本、加密套件列表。

client_random 是什麽?用來最終 secret 的一(yī)個參數。

加密套件列表是什麽?我(wǒ)(wǒ)舉個例子,加密套件列表一(yī)般張這樣:

TLS_ECDHE_WITH_AES_128_GCM_SHA256

意思是TLS握手過程中(zhōng),使用ECDHE算法生(shēng)成pre_random(這個數後面會介紹)128位的AES算法進行對稱加密,在對稱加密的過程中(zhōng)使用主流的GCM分(fēn)組模式,因爲對稱加密中(zhōng)很重要的一(yī)個問題就是如何分(fēn)組。最後一(yī)個是哈希摘要算法,采用SHA256算法。

其中(zhōng)值得解釋一(yī)下(xià)的是這個哈希摘要算法,試想一(yī)個這樣的場景,服務端現在給客戶端發消息來了,客戶端并不知(zhī)道此時的消息到底是服務端發的,還是中(zhōng)間人僞造的消息呢?現在引入這個哈希摘要算法,将服務端的證書(shū)信息通過這個算法生(shēng)成一(yī)個摘要(可以理解爲比較短的字符串),用來标識這個服務端的身份,用私鑰加密後把加密後的标識自己的公鑰傳給客戶端。客戶端拿到這個公鑰來解密,生(shēng)成另外(wài)一(yī)份摘要。兩個摘要進行對比,如果相同則能确認服務端的身份。這也就是所謂數字簽名的原理。其中(zhōng)除了哈希算法,最重要的過程是私鑰加密,公鑰解密

step 2: Server Hello

可以看到服務器一(yī)口氣給客戶端回複了非常多的内容。

server_random也是最後生(shēng)成secret的一(yī)個參數, 同時确認 TLS 版本、需要使用的加密套件和自己的證書(shū),這都不難理解。那剩下(xià)的server_params是幹嘛的呢?

我(wǒ)(wǒ)們先埋個伏筆,現在你隻需要知(zhī)道,server_random到達了客戶端。

step 3: Client 驗證證書(shū),生(shēng)成secret

客戶端驗證服務端傳來的證書(shū)簽名是否通過,如果驗證通過,則傳遞client_params這個參數給服務器。

接着客戶端通過ECDHE算法計算出pre_random,其中(zhōng)傳入兩個參數:server_paramsclient_params。現在你應該清楚這個兩個參數的作用了吧,由于ECDHE基于橢圓曲線離(lí)散對數,這兩個參數也稱作橢圓曲線的公鑰

客戶端現在擁有了client_randomserver_randompre_random,接下(xià)來将這三個數通過一(yī)個僞随機數函數來計算出最終的secret

step4: Server 生(shēng)成 secret

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

現在服務端開(kāi)始用ECDHE算法生(shēng)成pre_random,接着用和客戶端同樣的僞随機數函數生(shēng)成最後的secret

注意事項

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

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

當然是可以的。具體(tǐ)來說,在 step3中(zhōng),客戶端傳送client_params,實際上給服務器傳一(yī)個驗證消息,讓服務器将相同的驗證流程(哈希摘要 + 私鑰加密 + 公鑰解密)走一(yī)遍,确認客戶端的身份。

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

這個收尾的消息包括兩部分(fēn),一(yī)部分(fēn)是Change Cipher Spec,意味着後面加密傳輸了,另一(yī)個是Finished消息,這個消息是對之前所有發送的數據做的摘要,對摘要進行加密,讓對方驗證一(yī)下(xià)。

當雙方都驗證通過之後,握手才正式結束。後面的 HTTP 正式開(kāi)始傳輸加密報文。

RSA ECDHE 握手過程的區别

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

2.          使用 ECDHE 還有一(yī)個特點,就是客戶端發送完收尾消息後可以提前搶跑,直接發送 HTTP 報文,節省了一(yī)個 RTT,不必等到收尾消息到達服務器,然後等服務器返回收尾消息給自己,直接開(kāi)始發請求。這也叫TLS False Start

016: TLS 1.3 做了哪些改進?

TLS 1.2 雖然存在了 10 多年,經曆了無數的考驗,但曆史的車(chē)輪總是不斷向前的,爲了獲得更強的安全、更優秀的性能,在2018就推出了 TLS1.3,對于TLS1.2做了一(yī)系列的改進,主要分(fēn)爲這幾個部分(fēn):強化安全提高性能

強化安全

TLS1.3 中(zhōng)廢除了非常多的加密算法,最後隻保留五個加密套件:

             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

可以看到,最後剩下(xià)的對稱加密算法隻有 AES CHACHA20,之前主流的也會這兩種。分(fēn)組模式也隻剩下(xià) GCM POLY1305, 哈希摘要算法隻剩下(xià)了 SHA256 SHA384 了。

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

我(wǒ)(wǒ)覺得有兩方面的原因:

第一(yī)2015年發現了FREAK攻擊,即已經有人發現了 RSA 的漏洞,能夠進行破解了。

第二、一(yī)旦私鑰洩露,那麽中(zhōng)間人可以通過私鑰計算出之前所有報文的secret,破解之前所有的密文。

爲什麽?回到 RSA 握手的過程中(zhōng),客戶端拿到服務器的證書(shū)後,提取出服務器的公鑰,然後生(shēng)成pre_random并用公鑰加密傳給服務器,服務器通過私鑰解密,從而拿到真實的pre_random。當中(zhōng)間人拿到了服務器私鑰,并且截獲之前所有報文的時候,那麽就能拿到pre_randomserver_randomclient_random并根據對應的随機數函數生(shēng)成secret,也就是拿到了 TLS 最終的會話(huà)密鑰,每一(yī)個曆史報文都能通過這樣的方式進行破解。

ECDHE在每次握手時都會生(shēng)成臨時的密鑰對,即使私鑰被破解,之前的曆史消息并不會收到影響。這種一(yī)次破解并不影響曆史信息的性質也叫前向安全性

RSA 算法不具備前向安全性,而 ECDHE 具備,因此在 TLS1.3 中(zhōng)徹底取代了RSA

提升性能

握手改進

流程如下(xià):

image.png

 

大(dà)體(tǐ)的方式和 TLS1.2 差不多,不過和 TLS 1.2 相比少了一(yī)個 RTT 服務端不必等待對方驗證證書(shū)之後才拿到client_params,而是直接在第一(yī)次握手的時候就能夠拿到, 拿到之後立即計算secret,節省了之前不必要的等待時間。同時,這也意味着在第一(yī)次握手的時候客戶端需要傳送更多的信息,一(yī)口氣給傳完。

這種 TLS 1.3 握手方式也被叫做1-RTT握手。但其實這種1-RTT的握手方式還是有一(yī)些優化的空間的,接下(xià)來我(wǒ)(wǒ)們來一(yī)一(yī)介紹這些優化方式。

會話(huà)複用

會話(huà)複用有兩種方式: Session IDSession Ticket

先說說最早出現的Seesion ID,具體(tǐ)做法是客戶端和服務器首次連接後各自保存會話(huà)的 ID,并存儲會話(huà)密鑰,當再次連接時,客戶端發送ID過來,服務器查找這個 ID 是否存在,如果找到了就直接複用之前的會話(huà)狀态,會話(huà)密鑰不用重新生(shēng)成,直接用原來的那份。

但這種方式也存在一(yī)個弊端,就是當客戶端數量龐大(dà)的時候,對服務端的存儲壓力非常大(dà)。

因而出現了第二種方式——Session Ticket。它的思路就是: 服務端的壓力大(dà),那就把壓力分(fēn)攤給客戶端呗。具體(tǐ)來說,雙方連接成功後,服務器加密會話(huà)信息,用Session Ticket消息發給客戶端,讓客戶端保存下(xià)來。下(xià)次重連的時候,就把這個 Ticket 進行解密,驗證它過沒過期,如果沒過期那就直接恢複之前的會話(huà)狀态。

這種方式雖然減小(xiǎo)了服務端的存儲壓力,但與帶來了安全問題,即每次用一(yī)個固定的密鑰來解密 Ticket 數據,一(yī)旦黑客拿到這個密鑰,之前所有的曆史記錄也被破解了。因此爲了盡量避免這樣的問題,密鑰需要定期進行更換。

總的來說,這些會話(huà)複用的技術在保證1-RTT的同時,也節省了生(shēng)成會話(huà)密鑰這些算法所消耗的時間,是一(yī)筆可觀的性能提升。

PSK

剛剛說的都是1-RTT情況下(xià)的優化,那能不能優化到0-RTT呢?

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

這種方式雖然方便,但也帶來了安全問題。中(zhōng)間人截獲PSK的數據,不斷向服務器重複發,類似于 TCP 第一(yī)次握手攜帶數據,增加了服務器被攻擊的風險。

總結

TLS1.3 TLS1.2 的基礎上廢除了大(dà)量的算法,提升了安全性。同時利用會話(huà)複用節省了重新生(shēng)成密鑰的時間,利用 PSK 做到了0-RTT連接。

017: HTTP/2 有哪些改進?

由于 HTTPS 在安全方面已經做的非常好了,HTTP 改進的關注點放(fàng)在了性能方面。對于 HTTP/2 而言,它對于性能的提升主要在于兩點:

             頭部壓縮

             多路複用

當然還有一(yī)些颠覆性的功能實現:

             設置請求優先級

             服務器推送

這些重大(dà)的提升本質上也是爲了解決 HTTP 本身的問題而産生(shēng)的。接下(xià)來我(wǒ)(wǒ)們來看看 HTTP/2 解決了哪些問題,以及解決方式具體(tǐ)是如何的。

頭部壓縮

HTTP/1.1 及之前的時代,請求體(tǐ)一(yī)般會有響應的壓縮編碼過程,通過Content-Encoding頭部字段來指定,但你有沒有想過頭部字段本身的壓縮呢?當請求字段非常複雜(zá)的時候,尤其對于 GET 請求,請求報文幾乎全是請求頭,這個時候還是存在非常大(dà)的優化空間的。HTTP/2 針對頭部字段,也采用了對應的壓縮算法——HPACK,對請求頭進行壓縮。

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

             首先是在服務器和客戶端之間建立哈希表,将用到的字段存放(fàng)在這張表中(zhōng),那麽在傳輸的時候對于之前出現過的值,隻需要把索引(比如012...)傳給對方即可,對方拿到索引查表就行了。這種傳索引的方式,可以說讓請求頭字段得到極大(dà)程度的精簡和複用。

 

HTTP/2 當中(zhōng)廢除了起始行的概念,将起始行中(zhōng)的請求方法、URI、狀态碼轉換成了頭字段,不過這些字段都有一(yī)個":"前綴,用來和其它請求頭區分(fēn)開(kāi)。

             其次是對于整數和字符串進行哈夫曼編碼,哈夫曼編碼的原理就是先将所有出現的字符建立一(yī)張索引表,然後讓出現次數多的字符對應的索引盡可能短,傳輸的時候也是傳輸這樣的索引序列,可以達到非常高的壓縮率。

多路複用

HTTP 隊頭阻塞

我(wǒ)(wǒ)們之前讨論了 HTTP 隊頭阻塞的問題,其根本原因在于HTTP 基于請求-響應的模型,在同一(yī)個 TCP 長連接中(zhōng),前面的請求沒有得到響應,後面的請求就會被阻塞。

後面我(wǒ)(wǒ)們又(yòu)讨論到用并發連接域名分(fēn)片的方式來解決這個問題,但這并沒有真正從 HTTP 本身的層面解決問題,隻是增加了 TCP 連接,分(fēn)攤風險而已。而且這麽做也有弊端,多條 TCP 連接會競争有限的帶寬,讓真正優先級高的請求不能優先處理。

HTTP/2 便從 HTTP 協議本身解決了隊頭阻塞問題。注意,這裏并不是指的TCP隊頭阻塞,而是HTTP隊頭阻塞,兩者并不是一(yī)回事。TCP 的隊頭阻塞是在數據包層面,單位是數據包,前一(yī)個報文沒有收到便不會将後面收到的報文上傳給 HTTP,而HTTP 的隊頭阻塞是在 HTTP 請求-響應層面,前一(yī)個請求沒處理完,後面的請求就要阻塞住。兩者所在的層次不一(yī)樣。

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

二進制分(fēn)幀

首先,HTTP/2 認爲明文傳輸對機器而言太麻煩了,不方便計算機的解析,因爲對于文本而言會有多義性的字符,比如回車(chē)換行到底是内容還是分(fēn)隔符,在内部需要用到狀态機去(qù)識别,效率比較低。于是 HTTP/2 幹脆把報文全部換成二進制格式,全部傳輸01串,方便了機器的解析。

原來Headers + Body的報文格式如今被拆分(fēn)成了一(yī)個個二進制的幀,用Headers存放(fàng)頭部字段,Data存放(fàng)請求體(tǐ)數據。分(fēn)幀之後,服務器看到的不再是一(yī)個個完整的 HTTP 請求報文,而是一(yī)堆亂序的二進制幀。這些二進制幀不存在先後關系,因此也就不會排隊等待,也就沒有了 HTTP 的隊頭阻塞問題。

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

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

首先要聲明的是,所謂的亂序,指的是不同 ID Stream 是亂序的,但同一(yī)個 Stream ID 的幀一(yī)定是按順序傳輸的。二進制幀到達後對方會将 Stream ID 相同的二進制幀組裝成完整的請求報文響應報文。當然,在二進制幀當中(zhōng)還有其他的一(yī)些字段,實現了優先級流量控制等功能,我(wǒ)(wǒ)們放(fàng)到下(xià)一(yī)節再來介紹。

服務器推送

另外(wài)值得一(yī)說的是 HTTP/2 的服務器推送(Server Push)。在 HTTP/2 當中(zhōng),服務器已經不再是完全被動地接收請求,響應請求,它也能新建 stream 來給客戶端發送消息,當 TCP 連接建立之後,比如浏覽器請求一(yī)個 HTML 文件,服務器就可以在返回 HTML 的基礎上,将 HTML 中(zhōng)引用到的其他資(zī)源文件一(yī)起返回給客戶端,減少客戶端的等待。

總結

當然,HTTP/2 新增那麽多的特性,是不是 HTTP 的語法要重新學呢?不需要,HTTP/2 完全兼容之前 HTTP 的語法和語義,如請求頭、URI、狀态碼、頭部字段都沒有改變,完全不用擔心。同時,在安全方面,HTTP 也支持 TLS,并且現在主流的浏覽器都公開(kāi)隻支持加密的 HTTP/2, 因此你現在能看到的 HTTP/2 也基本上都是跑在 TLS 上面的了。


018: HTTP/2 中(zhōng)的二進制幀是如何設計的?

幀結構

HTTP/2 中(zhōng)傳輸的幀結構如下(xià)圖所示:

image.png

 

每個幀分(fēn)爲幀頭幀體(tǐ)。先是三個字節的幀長度,這個長度表示的是幀體(tǐ)的長度。

然後是幀類型,大(dà)概可以分(fēn)爲數據幀控制幀兩種。數據幀用來存放(fàng) HTTP 報文,控制幀用來管理的傳輸。

接下(xià)來的一(yī)個字節是幀标志(zhì),裏面一(yī)共有 8 個标志(zhì)位,常用的有 END_HEADERS表示頭數據結束,END_STREAM表示單方向數據發送結束。

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

流的狀态變化

從前面可以知(zhī)道,在 HTTP/2 中(zhōng),所謂的,其實就是二進制幀的雙向傳輸的序列。那麽在 HTTP/2 請求和響應的過程中(zhōng),流的狀态是如何變化的呢?

HTTP/2 其實也是借鑒了 TCP 狀态變化的思想,根據幀的标志(zhì)位來實現具體(tǐ)的狀态改變。這裏我(wǒ)(wǒ)們以一(yī)個普通的請求-響應過程爲例來說明:

image.png

 

 

最開(kāi)始兩者都是空閑狀态,當客戶端發送Headers後,開(kāi)始分(fēn)配Stream ID, 此時客戶端的打開(kāi), 服務端接收之後服務端的也打開(kāi),兩端的都打開(kāi)之後,就可以互相傳遞數據幀和控制幀了。

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

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

如果下(xià)次要開(kāi)啓新的,流 ID 需要自增,直到上限爲止,到達上限後開(kāi)一(yī)個新的 TCP 連接重頭開(kāi)始計數。由于流 ID 字段長度爲 4 個字節,最高位又(yòu)被保留,因此範圍是 0 ~ 2 31 次方,大(dà)約 21 億個。

流的特性

剛剛談到了流的狀态變化過程,這裏順便就來總結一(yī)下(xià)傳輸的特性:

             并發性。一(yī)個 HTTP/2 連接上可以同時發多個幀,這一(yī)點和 HTTP/1 不同。這也是實現多路複用的基礎。

             自增性。流 ID 是不可重用的,而是會按順序遞增,達到上限之後又(yòu)新開(kāi) TCP 連接從頭開(kāi)始。

             雙向性。客戶端和服務端都可以創建流,互不幹擾,雙方都可以作爲發送方或者接收方

             可設置優先級。可以設置數據幀的優先級,讓服務端先處理重要資(zī)源,優化用戶體(tǐ)驗。

掃二維碼與項目經理溝通

我(wǒ)(wǒ)們在微信上24小(xiǎo)時期待你的聲音

解答本文疑問/技術咨詢/運營咨詢/技術建議/互聯網交流

鄭重申明:善達信息以外(wài)的任何單位或個人,不得使用該案例作爲工(gōng)作成功展示!