<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 26919字,需瀏覽 54分鐘

           ·

          2020-03-26 23:29

          上回就已經(jīng)承諾過(guò)大家,一定會(huì)出 HTTP 的系列文章,今天終于整理完成了。作為一個(gè) web 開(kāi)發(fā),HTTP 幾乎是天天要打交道的東西,但我發(fā)現(xiàn)大部分人對(duì) HTTP 只是淺嘗輒止,對(duì)更多的細(xì)節(jié)及原理就了解不深了,在面試的時(shí)候感覺(jué)非常吃力。這篇文章就是為了幫助大家樹(shù)立完整的 HTTP 知識(shí)體系,并達(dá)到一定的深度,從容地應(yīng)對(duì)各種靈魂之問(wèn),也同時(shí)提升自己作為一個(gè) web 開(kāi)發(fā)的專(zhuān)業(yè)素養(yǎng)吧。這是本文的思維導(dǎo)圖:

          32ab750b90f8c3f38318609bf1339628.webp

          001. HTTP 報(bào)文結(jié)構(gòu)是怎樣的?

          對(duì)于 TCP 而言,在傳輸?shù)臅r(shí)候分為兩個(gè)部分:TCP頭數(shù)據(jù)部分。

          而 HTTP 類(lèi)似,也是header + body的結(jié)構(gòu),具體而言:

          起始行 + 頭部 + 空行 + 實(shí)體

          由于 http 請(qǐng)求報(bào)文響應(yīng)報(bào)文是有一定區(qū)別,因此我們分開(kāi)介紹。

          起始行

          對(duì)于請(qǐng)求報(bào)文來(lái)說(shuō),起始行類(lèi)似下面這樣:

          GET /home HTTP/1.1

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

          對(duì)于響應(yīng)報(bào)文來(lái)說(shuō),起始行一般張這個(gè)樣:

          HTTP/1.1 200 OK

          響應(yīng)報(bào)文的起始行也叫做狀態(tài)行。由http版本、狀態(tài)碼和原因三部分組成。

          值得注意的是,在起始行中,每?jī)蓚€(gè)部分之間用空格隔開(kāi),最后一個(gè)部分后面應(yīng)該接一個(gè)換行,嚴(yán)格遵循ABNF語(yǔ)法規(guī)范。

          頭部

          展示一下請(qǐng)求頭和響應(yīng)頭在報(bào)文中的位置:

          91f71d88b7449b46f90504e53319b725.webp94a2206505e495e6f7c1b547942be6b4.webp

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


          1. 字段名不區(qū)分大小寫(xiě)

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

          1. 字段名后面必須緊接著:

          空行

          很重要,用來(lái)區(qū)分開(kāi)頭部實(shí)體。

          問(wèn): 如果說(shuō)在頭部中間故意加一個(gè)空行會(huì)怎么樣?

          那么空行后的內(nèi)容全部被視為實(shí)體。

          實(shí)體

          就是具體的數(shù)據(jù)了,也就是body部分。請(qǐng)求報(bào)文對(duì)應(yīng)請(qǐng)求體, 響應(yīng)報(bào)文對(duì)應(yīng)響應(yīng)體。

          002. 如何理解 HTTP 的請(qǐng)求方法?

          有哪些請(qǐng)求方法?

          http/1.1規(guī)定了以下請(qǐng)求方法(注意,都是大寫(xiě)):

          • GET: 通常用來(lái)獲取資源
          • HEAD: 獲取資源的元信息
          • POST: 提交數(shù)據(jù),即上傳數(shù)據(jù)
          • PUT: 修改數(shù)據(jù)
          • DELETE: 刪除資源(幾乎用不到)
          • CONNECT: 建立連接隧道,用于代理服務(wù)器
          • OPTIONS: 列出可對(duì)資源實(shí)行的請(qǐng)求方法,用來(lái)跨域請(qǐng)求
          • TRACE: 追蹤請(qǐng)求-響應(yīng)的傳輸路徑

          GET 和 POST 有什么區(qū)別?

          首先最直觀的是語(yǔ)義上的區(qū)別。

          而后又有這樣一些具體的差別:

          • 緩存的角度,GET 請(qǐng)求會(huì)被瀏覽器主動(dòng)緩存下來(lái),留下歷史記錄,而 POST 默認(rèn)不會(huì)。
          • 編碼的角度,GET 只能進(jìn)行 URL 編碼,只能接收 ASCII 字符,而 POST 沒(méi)有限制。
          • 參數(shù)的角度,GET 一般放在 URL 中,因此不安全,POST 放在請(qǐng)求體中,更適合傳輸敏感信息。
          • 冪等性的角度,GET冪等的,而POST不是。(冪等表示執(zhí)行相同的操作,結(jié)果也是相同的)
          • TCP的角度,GET 請(qǐng)求會(huì)把請(qǐng)求報(bào)文一次性發(fā)出去,而 POST 會(huì)分為兩個(gè) TCP 數(shù)據(jù)包,首先發(fā) header 部分,如果服務(wù)器響應(yīng) 100(continue), 然后發(fā) body 部分。(火狐瀏覽器除外,它的 POST 請(qǐng)求只發(fā)一個(gè) TCP 包)

          003: 如何理解 URI?

          URI, 全稱(chēng)為(Uniform Resource Identifier), 也就是統(tǒng)一資源標(biāo)識(shí)符,它的作用很簡(jiǎn)單,就是區(qū)分互聯(lián)網(wǎng)上不同的資源。

          但是,它并不是我們常說(shuō)的網(wǎng)址, 網(wǎng)址指的是URL, 實(shí)際上URI包含了URNURL兩個(gè)部分,由于 URL 過(guò)于普及,就默認(rèn)將 URI 視為 URL 了。

          URI 的結(jié)構(gòu)

          URI 真正最完整的結(jié)構(gòu)是這樣的。

          5a13a36b2ff05158309ca738827a98a3.webp

          可能你會(huì)有疑問(wèn),好像跟平時(shí)見(jiàn)到的不太一樣??!先別急,我們來(lái)一一拆解。

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

          user:passwd@ 表示登錄主機(jī)時(shí)的用戶(hù)信息,不過(guò)很不安全,不推薦使用,也不常用。

          host:port表示主機(jī)名和端口。

          path表示請(qǐng)求路徑,標(biāo)記資源所在位置。

          query表示查詢(xún)參數(shù),為key=val這種形式,多個(gè)鍵值對(duì)之間用&隔開(kāi)。

          fragment表示 URI 所定位的資源內(nèi)的一個(gè)錨點(diǎn),瀏覽器可以根據(jù)這個(gè)錨點(diǎn)跳轉(zhuǎn)到對(duì)應(yīng)的位置。

          舉個(gè)例子:

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

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

          URI 編碼

          URI 只能使用ASCII, ASCII 之外的字符是不支持顯示的,而且還有一部分符號(hào)是界定符,如果不加以處理就會(huì)導(dǎo)致解析出錯(cuò)。

          因此,URI 引入了編碼機(jī)制,將所有非 ASCII 碼字符界定符轉(zhuǎn)為十六進(jìn)制字節(jié)值,然后在前面加個(gè)%

          如,空格被轉(zhuǎn)義成了%20,三元被轉(zhuǎn)義成了%E4%B8%89%E5%85%83。

          004: 如何理解 HTTP 狀態(tài)碼?

          RFC 規(guī)定 HTTP 的狀態(tài)碼為三位數(shù),被分為五類(lèi):

          • 1xx: 表示目前是協(xié)議處理的中間狀態(tài),還需要后續(xù)操作。
          • 2xx: 表示成功狀態(tài)。
          • 3xx: 重定向狀態(tài),資源位置發(fā)生變動(dòng),需要重新請(qǐng)求。
          • 4xx: 請(qǐng)求報(bào)文有誤。
          • 5xx: 服務(wù)器端發(fā)生錯(cuò)誤。

          接下來(lái)就一一分析這里面具體的狀態(tài)碼。

          1xx

          101 Switching Protocols。在HTTP升級(jí)為WebSocket的時(shí)候,如果服務(wù)器同意變更,就會(huì)發(fā)送狀態(tài)碼 101。

          2xx

          200 OK是見(jiàn)得最多的成功狀態(tài)碼。通常在響應(yīng)體中放有數(shù)據(jù)。

          204 No Content含義與 200 相同,但響應(yīng)頭后沒(méi)有 body 數(shù)據(jù)。

          206 Partial Content顧名思義,表示部分內(nèi)容,它的使用場(chǎng)景為 HTTP 分塊下載和斷電續(xù)傳,當(dāng)然也會(huì)帶上相應(yīng)的響應(yīng)頭字段Content-Range

          3xx

          301 Moved Permanently即永久重定向,對(duì)應(yīng)著302 Found,即臨時(shí)重定向。

          比如你的網(wǎng)站從 HTTP 升級(jí)到了 HTTPS 了,以前的站點(diǎn)再也不用了,應(yīng)當(dāng)返回301,這個(gè)時(shí)候?yàn)g覽器默認(rèn)會(huì)做緩存優(yōu)化,在第二次訪問(wèn)的時(shí)候自動(dòng)訪問(wèn)重定向的那個(gè)地址。

          而如果只是暫時(shí)不可用,那么直接返回302即可,和301不同的是,瀏覽器并不會(huì)做緩存優(yōu)化。

          304 Not Modified: 當(dāng)協(xié)商緩存命中時(shí)會(huì)返回這個(gè)狀態(tài)碼。詳見(jiàn)瀏覽器緩存

          4xx

          400 Bad Request: 開(kāi)發(fā)者經(jīng)常看到一頭霧水,只是籠統(tǒng)地提示了一下錯(cuò)誤,并不知道哪里出錯(cuò)了。

          403 Forbidden: 這實(shí)際上并不是請(qǐng)求報(bào)文出錯(cuò),而是服務(wù)器禁止訪問(wèn),原因有很多,比如法律禁止、信息敏感。

          404 Not Found: 資源未找到,表示沒(méi)在服務(wù)器上找到相應(yīng)的資源。

          405 Method Not Allowed: 請(qǐng)求方法不被服務(wù)器端允許。

          406 Not Acceptable: 資源無(wú)法滿足客戶(hù)端的條件。

          408 Request Timeout: 服務(wù)器等待了太長(zhǎng)時(shí)間。

          409 Conflict: 多個(gè)請(qǐng)求發(fā)生了沖突。

          413 Request Entity Too Large: 請(qǐng)求體的數(shù)據(jù)過(guò)大。

          414 Request-URI Too Long: 請(qǐng)求行里的 URI 太大。

          429 Too Many Request: 客戶(hù)端發(fā)送的請(qǐng)求過(guò)多。

          431 Request Header Fields Too Large請(qǐng)求頭的字段內(nèi)容太大。

          5xx

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

          501 Not Implemented: 表示客戶(hù)端請(qǐng)求的功能還不支持。

          502 Bad Gateway: 服務(wù)器自身是正常的,但訪問(wèn)的時(shí)候出錯(cuò)了,啥錯(cuò)誤咱也不知道。

          503 Service Unavailable: 表示服務(wù)器當(dāng)前很忙,暫時(shí)無(wú)法響應(yīng)服務(wù)。

          005: 簡(jiǎn)要概括一下 HTTP 的特點(diǎn)?HTTP 有哪些缺點(diǎn)?

          HTTP 特點(diǎn)

          HTTP 的特點(diǎn)概括如下:

          1. 靈活可擴(kuò)展,主要體現(xiàn)在兩個(gè)方面。一個(gè)是語(yǔ)義上的自由,只規(guī)定了基本格式,比如空格分隔單詞,換行分隔字段,其他的各個(gè)部分都沒(méi)有嚴(yán)格的語(yǔ)法限制。另一個(gè)是傳輸形式的多樣性,不僅僅可以傳輸文本,還能傳輸圖片、視頻等任意數(shù)據(jù),非常方便。

          2. 可靠傳輸。HTTP 基于 TCP/IP,因此把這一特性繼承了下來(lái)。這屬于 TCP 的特性,不具體介紹了。

          3. 請(qǐng)求-應(yīng)答。也就是一發(fā)一收、有來(lái)有回, 當(dāng)然這個(gè)請(qǐng)求方和應(yīng)答方不單單指客戶(hù)端和服務(wù)器之間,如果某臺(tái)服務(wù)器作為代理來(lái)連接后端的服務(wù)端,那么這臺(tái)服務(wù)器也會(huì)扮演請(qǐng)求方的角色。

          4. 無(wú)狀態(tài)。這里的狀態(tài)是指通信過(guò)程的上下文信息,而每次 http 請(qǐng)求都是獨(dú)立、無(wú)關(guān)的,默認(rèn)不需要保留狀態(tài)信息。

          HTTP 缺點(diǎn)

          無(wú)狀態(tài)

          所謂的優(yōu)點(diǎn)和缺點(diǎn)還是要分場(chǎng)景來(lái)看的,對(duì)于 HTTP 而言,最具爭(zhēng)議的地方在于它的無(wú)狀態(tài)

          在需要長(zhǎng)連接的場(chǎng)景中,需要保存大量的上下文信息,以免傳輸大量重復(fù)的信息,那么這時(shí)候無(wú)狀態(tài)就是 http 的缺點(diǎn)了。

          但與此同時(shí),另外一些應(yīng)用僅僅只是為了獲取一些數(shù)據(jù),不需要保存連接上下文信息,無(wú)狀態(tài)反而減少了網(wǎng)絡(luò)開(kāi)銷(xiāo),成為了 http 的優(yōu)點(diǎn)。

          明文傳輸

          即協(xié)議里的報(bào)文(主要指的是頭部)不使用二進(jìn)制數(shù)據(jù),而是文本形式。

          這當(dāng)然對(duì)于調(diào)試提供了便利,但同時(shí)也讓 HTTP 的報(bào)文信息暴露給了外界,給攻擊者也提供了便利。WIFI陷阱就是利用 HTTP 明文傳輸?shù)娜秉c(diǎn),誘導(dǎo)你連上熱點(diǎn),然后瘋狂抓你所有的流量,從而拿到你的敏感信息。

          隊(duì)頭阻塞問(wèn)題

          當(dāng) http 開(kāi)啟長(zhǎng)連接時(shí),共用一個(gè) TCP 連接,同一時(shí)刻只能處理一個(gè)請(qǐng)求,那么當(dāng)前請(qǐng)求耗時(shí)過(guò)長(zhǎng)的情況下,其它的請(qǐng)求只能處于阻塞狀態(tài),也就是著名的隊(duì)頭阻塞問(wèn)題。接下來(lái)會(huì)有一小節(jié)討論這個(gè)問(wèn)題。

          006: 對(duì) Accept 系列字段了解多少?

          對(duì)于Accept系列字段的介紹分為四個(gè)部分: 數(shù)據(jù)格式、壓縮方式、支持語(yǔ)言字符集

          數(shù)據(jù)格式

          上一節(jié)談到 HTTP 靈活的特性,它支持非常多的數(shù)據(jù)格式,那么這么多格式的數(shù)據(jù)一起到達(dá)客戶(hù)端,客戶(hù)端怎么知道它的格式呢?

          當(dāng)然,最低效的方式是直接猜,有沒(méi)有更好的方式呢?直接指定可以嗎?

          答案是肯定的。不過(guò)首先需要介紹一個(gè)標(biāo)準(zhǔn)——MIME(Multipurpose Internet Mail Extensions, 多用途互聯(lián)網(wǎng)郵件擴(kuò)展)。它首先用在電子郵件系統(tǒng)中,讓郵件可以發(fā)任意類(lèi)型的數(shù)據(jù),這對(duì)于 HTTP 來(lái)說(shuō)也是通用的。

          因此,HTTP 從MIME type取了一部分來(lái)標(biāo)記報(bào)文 body 部分的數(shù)據(jù)類(lèi)型,這些類(lèi)型體現(xiàn)在Content-Type這個(gè)字段,當(dāng)然這是針對(duì)于發(fā)送端而言,接收端想要收到特定類(lèi)型的數(shù)據(jù),也可以用Accept字段。

          具體而言,這兩個(gè)字段的取值可以分為下面幾類(lèi):

          • 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

          壓縮方式

          當(dāng)然一般這些數(shù)據(jù)都是會(huì)進(jìn)行編碼壓縮的,采取什么樣的壓縮方式就體現(xiàn)在了發(fā)送方的Content-Encoding字段上, 同樣的,接收什么樣的壓縮方式體現(xiàn)在了接受方的Accept-Encoding字段上。這個(gè)字段的取值有下面幾種:

          • gzip: 當(dāng)今最流行的壓縮格式
          • deflate: 另外一種著名的壓縮格式
          • br: 一種專(zhuān)門(mén)為 HTTP 發(fā)明的壓縮算法
          // 發(fā)送端
          Content-Encoding: gzip
          // 接收端
          Accept-Encoding: gizp

          支持語(yǔ)言

          對(duì)于發(fā)送方而言,還有一個(gè)Content-Language字段,在需要實(shí)現(xiàn)國(guó)際化的方案當(dāng)中,可以用來(lái)指定支持的語(yǔ)言,在接受方對(duì)應(yīng)的字段為Accept-Language。如:

          // 發(fā)送端
          Content-Language: zh-CN, zh, en
          // 接收端
          Accept-Language: zh-CN, zh, en

          字符集

          最后是一個(gè)比較特殊的字段, 在接收端對(duì)應(yīng)為Accept-Charset,指定可以接受的字符集,而在發(fā)送端并沒(méi)有對(duì)應(yīng)的Content-Charset, 而是直接放在了Content-Type中,以charset屬性指定。如:

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

          最后以一張圖來(lái)總結(jié)一下吧:

          df9d54f183ce7958874f1f2e2fe6e8e3.webp

          007: 對(duì)于定長(zhǎng)和不定長(zhǎng)的數(shù)據(jù),HTTP 是怎么傳輸?shù)模?/h2>

          定長(zhǎng)包體

          對(duì)于定長(zhǎng)包體而言,發(fā)送端在傳輸?shù)臅r(shí)候一般會(huì)帶上Content-Length, 來(lái)指明包體的長(zhǎng)度。

          我們用一個(gè)nodejs服務(wù)器來(lái)模擬一下:

          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("成功啟動(dòng)");
          })

          啟動(dòng)后訪問(wèn): localhost:8081。

          瀏覽器中顯示如下:

          helloworld

          這是長(zhǎng)度正確的情況,那不正確的情況是如何處理的呢?

          我們?cè)囍堰@個(gè)長(zhǎng)度設(shè)置的小一些:

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

          重啟服務(wù),再次訪問(wèn),現(xiàn)在瀏覽器中內(nèi)容如下:

          hellowor

          那后面的ld哪里去了呢?實(shí)際上在 http 的響應(yīng)體中直接被截去了。

          然后我們?cè)囍鴮⑦@個(gè)長(zhǎng)度設(shè)置得大一些:

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

          此時(shí)瀏覽器顯示如下:

          f54cdfd00e08e5aafd0e5b4d674bdb8c.webp

          直接無(wú)法顯示了??梢钥吹?code style="font-size:14px;color:rgb(30,107,184);background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;">Content-Length對(duì)于 http 傳輸過(guò)程起到了十分關(guān)鍵的作用,如果設(shè)置不當(dāng)可以直接導(dǎo)致傳輸失敗。

          不定長(zhǎng)包體

          上述是針對(duì)于定長(zhǎng)包體,那么對(duì)于不定長(zhǎng)包體而言是如何傳輸?shù)哪兀?/p>

          這里就必須介紹另外一個(gè) http 頭部字段了:

          Transfer-Encoding: chunked

          表示分塊傳輸數(shù)據(jù),設(shè)置這個(gè)字段后會(huì)自動(dòng)產(chǎn)生兩個(gè)效果:

          • Content-Length 字段會(huì)被忽略
          • 基于長(zhǎng)連接持續(xù)推送動(dòng)態(tài)內(nèi)容

          我們依然以一個(gè)實(shí)際的例子來(lái)模擬分塊傳輸,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("

          來(lái)啦

          "
          );
          setTimeout(() => {
          res.write("第一次傳輸
          "
          );
          }, 1000);
          setTimeout(() => {
          res.write("第二次傳輸");
          res.end()
          }, 2000);
          }
          })

          server.listen(8009, () => {
          console.log("成功啟動(dòng)");
          })

          訪問(wèn)效果入下:

          8b123f723367c371aed921d3139ed1d4.webp

          用 telnet 抓到的響應(yīng)如下:

          d4cf31bc7932da325f776583244a00f6.webp

          注意,Connection: keep-alive及之前的為響應(yīng)行和響應(yīng)頭,后面的內(nèi)容為響應(yīng)體,這兩部分用換行符隔開(kāi)。

          響應(yīng)體的結(jié)構(gòu)比較有意思,如下所示:

          chunk長(zhǎng)度(16進(jìn)制的數(shù))
          第一個(gè)chunk的內(nèi)容
          chunk長(zhǎng)度(16進(jìn)制的數(shù))
          第二個(gè)chunk的內(nèi)容
          ......
          0

          最后是留有有一個(gè)空行的,這一點(diǎn)請(qǐng)大家注意。

          以上便是 http 對(duì)于定長(zhǎng)數(shù)據(jù)不定長(zhǎng)數(shù)據(jù)的傳輸方式。

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

          對(duì)于幾百 M 甚至上 G 的大文件來(lái)說(shuō),如果要一口氣全部傳輸過(guò)來(lái)顯然是不現(xiàn)實(shí)的,會(huì)有大量的等待時(shí)間,嚴(yán)重影響用戶(hù)體驗(yàn)。因此,HTTP 針對(duì)這一場(chǎng)景,采取了范圍請(qǐng)求的解決方案,允許客戶(hù)端僅僅請(qǐng)求一個(gè)資源的一部分。

          如何支持

          當(dāng)然,前提是服務(wù)器要支持范圍請(qǐng)求,要支持這個(gè)功能,就必須加上這樣一個(gè)響應(yīng)頭:

          Accept-Ranges: none

          用來(lái)告知客戶(hù)端這邊是支持范圍請(qǐng)求的。

          Range 字段拆解

          而對(duì)于客戶(hù)端而言,它需要指定請(qǐng)求哪一部分,通過(guò)Range這個(gè)請(qǐng)求頭字段確定,格式為bytes=x-y。接下來(lái)就來(lái)討論一下這個(gè) Range 的書(shū)寫(xiě)格式:

          • 0-499表示從開(kāi)始到第 499 個(gè)字節(jié)。
          • 500- 表示從第 500 字節(jié)到文件終點(diǎn)。
          • -100表示文件的最后100個(gè)字節(jié)。

          服務(wù)器收到請(qǐng)求之后,首先驗(yàn)證范圍是否合法,如果越界了那么返回416錯(cuò)誤碼,否則讀取相應(yīng)片段,返回206狀態(tài)碼。

          同時(shí),服務(wù)器需要添加Content-Range字段,這個(gè)字段的格式根據(jù)請(qǐng)求頭中Range字段的不同而有所差異。

          具體來(lái)說(shuō),請(qǐng)求單段數(shù)據(jù)和請(qǐng)求多段數(shù)據(jù),響應(yīng)頭是不一樣的。

          舉個(gè)例子:

          // 單段數(shù)據(jù)
          Range: bytes=0-9
          // 多段數(shù)據(jù)
          Range: bytes=0-9, 30-39

          接下來(lái)我們就分別來(lái)討論著兩種情況。

          單段數(shù)據(jù)

          對(duì)于單段數(shù)據(jù)的請(qǐng)求,返回的響應(yīng)如下:

          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表示請(qǐng)求的返回,100表示資源的總大小,很好理解。

          多段數(shù)據(jù)

          接下來(lái)我們看看多段請(qǐng)求的情況。得到的響應(yīng)會(huì)是下面這個(gè)形式:

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

          這個(gè)時(shí)候出現(xiàn)了一個(gè)非常關(guān)鍵的字段Content-Type: multipart/byteranges;boundary=00000010101,它代表了信息量是這樣的:

          • 請(qǐng)求一定是多段數(shù)據(jù)請(qǐng)求
          • 響應(yīng)體中的分隔符是 00000010101

          因此,在響應(yīng)體中各段數(shù)據(jù)之間會(huì)由這里指定的分隔符分開(kāi),而且在最后的分隔末尾添上--表示結(jié)束。

          以上就是 http 針對(duì)大文件傳輸所采用的手段。

          009: HTTP 中如何處理表單數(shù)據(jù)的提交?

          在 http 中,有兩種主要的表單提交的方式,體現(xiàn)在兩種不同的Content-Type取值:

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

          由于表單提交一般是POST請(qǐng)求,很少考慮GET,因此這里我們將默認(rèn)提交的數(shù)據(jù)放在請(qǐng)求體中。

          application/x-www-form-urlencoded

          對(duì)于application/x-www-form-urlencoded格式的表單內(nèi)容,有以下特點(diǎn):

          • 其中的數(shù)據(jù)會(huì)被編碼成以&分隔的鍵值對(duì)
          • 字符以URL編碼方式編碼。

          如:

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

          multipart/form-data

          對(duì)于multipart/form-data而言:

          • 請(qǐng)求頭中的Content-Type字段會(huì)包含boundary,且boundary的值有瀏覽器默認(rèn)指定。例: Content-Type: multipart/form-data;boundary=----WebkitFormBoundaryRRJKeWfHPGrS4LKe。
          • 數(shù)據(jù)會(huì)分為多個(gè)部分,每?jī)蓚€(gè)部分之間通過(guò)分隔符來(lái)分隔,每部分表述均有 HTTP 頭部描述子包體,如Content-Type,在最后的分隔符會(huì)加上--表示結(jié)束。

          相應(yīng)的請(qǐng)求體是下面這樣:

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

          小結(jié)

          值得一提的是,multipart/form-data 格式最大的特點(diǎn)在于:每一個(gè)表單元素都是獨(dú)立的資源表述。另外,你可能在寫(xiě)業(yè)務(wù)的過(guò)程中,并沒(méi)有注意到其中還有boundary的存在,如果你打開(kāi)抓包工具,確實(shí)可以看到不同的表單元素被拆分開(kāi)了,之所以在平時(shí)感覺(jué)不到,是以為瀏覽器和 HTTP 給你封裝了這一系列操作。

          而且,在實(shí)際的場(chǎng)景中,對(duì)于圖片等文件的上傳,基本采用multipart/form-data而不用application/x-www-form-urlencoded,因?yàn)闆](méi)有必要做 URL 編碼,帶來(lái)巨大耗時(shí)的同時(shí)也占用了更多的空間。

          010: HTTP1.1 如何解決 HTTP 的隊(duì)頭阻塞問(wèn)題?

          什么是 HTTP 隊(duì)頭阻塞?

          從前面的小節(jié)可以知道,HTTP 傳輸是基于請(qǐng)求-應(yīng)答的模式進(jìn)行的,報(bào)文必須是一發(fā)一收,但值得注意的是,里面的任務(wù)被放在一個(gè)任務(wù)隊(duì)列中串行執(zhí)行,一旦隊(duì)首的請(qǐng)求處理太慢,就會(huì)阻塞后面請(qǐng)求的處理。這就是著名的HTTP隊(duì)頭阻塞問(wèn)題。

          并發(fā)連接

          對(duì)于一個(gè)域名允許分配多個(gè)長(zhǎng)連接,那么相當(dāng)于增加了任務(wù)隊(duì)列,不至于一個(gè)隊(duì)伍的任務(wù)阻塞其它所有任務(wù)。在RFC2616規(guī)定過(guò)客戶(hù)端最多并發(fā) 2 個(gè)連接,不過(guò)事實(shí)上在現(xiàn)在的瀏覽器標(biāo)準(zhǔn)中,這個(gè)上限要多很多,Chrome 中是 6 個(gè)。

          但其實(shí),即使是提高了并發(fā)連接,還是不能滿足人們對(duì)性能的需求。

          域名分片

          一個(gè)域名不是可以并發(fā) 6 個(gè)長(zhǎng)連接嗎?那我就多分幾個(gè)域名。

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

          這樣一個(gè)sanyuan.com域名下可以分出非常多的二級(jí)域名,而它們都指向同樣的一臺(tái)服務(wù)器,能夠并發(fā)的長(zhǎng)連接數(shù)更多了,事實(shí)上也更好地解決了隊(duì)頭阻塞的問(wèn)題。

          011: 對(duì) Cookie 了解多少?

          Cookie 簡(jiǎn)介

          前面說(shuō)到了 HTTP 是一個(gè)無(wú)狀態(tài)的協(xié)議,每次 http 請(qǐng)求都是獨(dú)立、無(wú)關(guān)的,默認(rèn)不需要保留狀態(tài)信息。但有時(shí)候需要保存一些狀態(tài),怎么辦呢?

          HTTP 為此引入了 Cookie。Cookie 本質(zhì)上就是瀏覽器里面存儲(chǔ)的一個(gè)很小的文本文件,內(nèi)部以鍵值對(duì)的方式來(lái)存儲(chǔ)(在chrome開(kāi)發(fā)者面板的Application這一欄可以看到)。向同一個(gè)域名下發(fā)送請(qǐng)求,都會(huì)攜帶相同的 Cookie,服務(wù)器拿到 Cookie 進(jìn)行解析,便能拿到客戶(hù)端的狀態(tài)。而服務(wù)端可以通過(guò)響應(yīng)頭中的Set-Cookie字段來(lái)對(duì)客戶(hù)端寫(xiě)入Cookie。舉例如下:

          // 請(qǐng)求頭
          Cookie: a=xxx;b=xxx
          // 響應(yīng)頭
          Set-Cookie: a=xxx
          set-Cookie: b=xxx

          Cookie 屬性

          生存周期

          Cookie 的有效期可以通過(guò)ExpiresMax-Age兩個(gè)屬性來(lái)設(shè)置。

          • Expires過(guò)期時(shí)間
          • Max-Age用的是一段時(shí)間間隔,單位是秒,從瀏覽器收到報(bào)文開(kāi)始計(jì)算。

          若 Cookie 過(guò)期,則這個(gè) Cookie 會(huì)被刪除,并不會(huì)發(fā)送給服務(wù)端。

          作用域

          關(guān)于作用域也有兩個(gè)屬性: Domainpath, 給 Cookie 綁定了域名和路徑,在發(fā)送請(qǐng)求之前,發(fā)現(xiàn)域名或者路徑和這兩個(gè)屬性不匹配,那么就不會(huì)帶上 Cookie。值得注意的是,對(duì)于路徑來(lái)說(shuō),/表示域名下的任意路徑都允許使用 Cookie。

          安全相關(guān)

          如果帶上Secure,說(shuō)明只能通過(guò) HTTPS 傳輸 cookie。

          如果 cookie 字段帶上HttpOnly,那么說(shuō)明只能通過(guò) HTTP 協(xié)議傳輸,不能通過(guò) JS 訪問(wèn),這也是預(yù)防 XSS 攻擊的重要手段。

          相應(yīng)的,對(duì)于 CSRF 攻擊的預(yù)防,也有SameSite屬性。

          SameSite可以設(shè)置為三個(gè)值,Strict、LaxNone

          a.Strict模式下,瀏覽器完全禁止第三方請(qǐng)求攜帶Cookie。比如請(qǐng)求sanyuan.com網(wǎng)站只能在sanyuan.com域名當(dāng)中請(qǐng)求才能攜帶 Cookie,在其他網(wǎng)站請(qǐng)求都不能。

          b.Lax模式,就寬松一點(diǎn)了,但是只能在 get 方法提交表單況或者a 標(biāo)簽發(fā)送 get 請(qǐng)求的情況下可以攜帶 Cookie,其他情況均不能。

          c.None模式下,也就是默認(rèn)模式,請(qǐng)求會(huì)自動(dòng)攜帶上 Cookie。

          Cookie 的缺點(diǎn)

          1. 容量缺陷。Cookie 的體積上限只有4KB,只能用來(lái)存儲(chǔ)少量的信息。

          2. 性能缺陷。Cookie 緊跟域名,不管域名下面的某一個(gè)地址需不需要這個(gè) Cookie ,請(qǐng)求都會(huì)攜帶上完整的 Cookie,這樣隨著請(qǐng)求數(shù)的增多,其實(shí)會(huì)造成巨大的性能浪費(fèi)的,因?yàn)檎?qǐng)求攜帶了很多不必要的內(nèi)容。但可以通過(guò)DomainPath指定作用域來(lái)解決。

          3. 安全缺陷。由于 Cookie 以純文本的形式在瀏覽器和服務(wù)器中傳遞,很容易被非法用戶(hù)截獲,然后進(jìn)行一系列的篡改,在 Cookie 的有效期內(nèi)重新發(fā)送給服務(wù)器,這是相當(dāng)危險(xiǎn)的。另外,在HttpOnly為 false 的情況下,Cookie 信息能直接通過(guò) JS 腳本來(lái)讀取。

          012: 如何理解 HTTP 代理?

          我們知道在 HTTP 是基于請(qǐng)求-響應(yīng)模型的協(xié)議,一般由客戶(hù)端發(fā)請(qǐng)求,服務(wù)器來(lái)進(jìn)行響應(yīng)。

          當(dāng)然,也有特殊情況,就是代理服務(wù)器的情況。引入代理之后,作為代理的服務(wù)器相當(dāng)于一個(gè)中間人的角色,對(duì)于客戶(hù)端而言,表現(xiàn)為服務(wù)器進(jìn)行響應(yīng);而對(duì)于源服務(wù)器,表現(xiàn)為客戶(hù)端發(fā)起請(qǐng)求,具有雙重身份

          那代理服務(wù)器到底是用來(lái)做什么的呢?

          功能

          1. 負(fù)載均衡??蛻?hù)端的請(qǐng)求只會(huì)先到達(dá)代理服務(wù)器,后面到底有多少源服務(wù)器,IP 都是多少,客戶(hù)端是不知道的。因此,這個(gè)代理服務(wù)器可以拿到這個(gè)請(qǐng)求之后,可以通過(guò)特定的算法分發(fā)給不同的源服務(wù)器,讓各臺(tái)源服務(wù)器的負(fù)載盡量平均。當(dāng)然,這樣的算法有很多,包括隨機(jī)算法、輪詢(xún)、一致性hashLRU(最近最少使用)等等,不過(guò)這些算法并不是本文的重點(diǎn),大家有興趣自己可以研究一下。

          2. 保障安全。利用心跳機(jī)制監(jiān)控后臺(tái)的服務(wù)器,一旦發(fā)現(xiàn)故障機(jī)就將其踢出集群。并且對(duì)于上下行的數(shù)據(jù)進(jìn)行過(guò)濾,對(duì)非法 IP 限流,這些都是代理服務(wù)器的工作。

          3. 緩存代理。將內(nèi)容緩存到代理服務(wù)器,使得客戶(hù)端可以直接從代理服務(wù)器獲得而不用到源服務(wù)器那里。下一節(jié)詳細(xì)拆解。

          相關(guān)頭部字段

          Via

          代理服務(wù)器需要標(biāo)明自己的身份,在 HTTP 傳輸中留下自己的痕跡,怎么辦呢?

          通過(guò)Via字段來(lái)記錄。舉個(gè)例子,現(xiàn)在中間有兩臺(tái)代理服務(wù)器,在客戶(hù)端發(fā)送請(qǐng)求后會(huì)經(jīng)歷這樣一個(gè)過(guò)程:

          客戶(hù)端 -> 代理1 -> 代理2 -> 源服務(wù)器

          在源服務(wù)器收到請(qǐng)求后,會(huì)在請(qǐng)求頭拿到這個(gè)字段:

          Via: proxy_server1, proxy_server2

          而源服務(wù)器響應(yīng)時(shí),最終在客戶(hù)端會(huì)拿到這樣的響應(yīng)頭:

          Via: proxy_server2, proxy_server1

          可以看到,Via中代理的順序即為在 HTTP 傳輸中報(bào)文傳達(dá)的順序。

          X-Forwarded-For

          字面意思就是為誰(shuí)轉(zhuǎn)發(fā), 它記錄的是請(qǐng)求方IP地址(注意,和Via區(qū)分開(kāi),X-Forwarded-For記錄的是請(qǐng)求方這一個(gè)IP)。

          X-Real-IP

          是一種獲取用戶(hù)真實(shí) IP 的字段,不管中間經(jīng)過(guò)多少代理,這個(gè)字段始終記錄最初的客戶(hù)端的IP。

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

          X-Forwarded-For產(chǎn)生的問(wèn)題

          前面可以看到,X-Forwarded-For這個(gè)字段記錄的是請(qǐng)求方的 IP,這意味著每經(jīng)過(guò)一個(gè)不同的代理,這個(gè)字段的名字都要變,從客戶(hù)端代理1,這個(gè)字段是客戶(hù)端的 IP,從代理1代理2,這個(gè)字段就變?yōu)榱舜?的 IP。

          但是這會(huì)產(chǎn)生兩個(gè)問(wèn)題:

          1. 意味著代理必須解析 HTTP 請(qǐng)求頭,然后修改,比直接轉(zhuǎn)發(fā)數(shù)據(jù)性能下降。

          2. 在 HTTPS 通信加密的過(guò)程中,原始報(bào)文是不允許修改的。

          由此產(chǎn)生了代理協(xié)議,一般使用明文版本,只需要在 HTTP 請(qǐng)求行上面加上這樣格式的文本即可:

          // PROXY + TCP4/TCP6 + 請(qǐng)求方地址 + 接收方地址 + 請(qǐng)求端口 + 接收端口
          PROXY TCP4 0.0.0.1 0.0.0.2 1111 2222
          GET / HTTP/1.1
          ...

          這樣就可以解決X-Forwarded-For帶來(lái)的問(wèn)題了。

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

          關(guān)于強(qiáng)緩存協(xié)商緩存的內(nèi)容,我已經(jīng)在【說(shuō)一說(shuō)瀏覽器緩存】做了詳細(xì)分析,小結(jié)如下:

          首先通過(guò) Cache-Control 驗(yàn)證強(qiáng)緩存是否可用

          • 如果強(qiáng)緩存可用,直接使用
          • 否則進(jìn)入?yún)f(xié)商緩存,即發(fā)送 HTTP 請(qǐng)求,服務(wù)器通過(guò)請(qǐng)求頭中的If-Modified-Since或者If-None-Match這些條件請(qǐng)求字段檢查資源是否更新
            • 若資源更新,返回資源和200狀態(tài)碼
            • 否則,返回304,告訴瀏覽器直接從緩存獲取資源

          這一節(jié)我們主要來(lái)說(shuō)說(shuō)另外一種緩存方式: 代理緩存

          為什么產(chǎn)生代理緩存?

          對(duì)于源服務(wù)器來(lái)說(shuō),它也是有緩存的,比如Redis, Memcache,但對(duì)于 HTTP 緩存來(lái)說(shuō),如果每次客戶(hù)端緩存失效都要到源服務(wù)器獲取,那給源服務(wù)器的壓力是很大的。

          由此引入了緩存代理的機(jī)制。讓代理服務(wù)器接管一部分的服務(wù)端HTTP緩存,客戶(hù)端緩存過(guò)期后就近到代理緩存中獲取,代理緩存過(guò)期了才請(qǐng)求源服務(wù)器,這樣流量巨大的時(shí)候能明顯降低源服務(wù)器的壓力。

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

          總的來(lái)說(shuō),緩存代理的控制分為兩部分,一部分是源服務(wù)器端的控制,一部分是客戶(hù)端的控制。

          源服務(wù)器的緩存控制

          private 和 public

          在源服務(wù)器的響應(yīng)頭中,會(huì)加上Cache-Control這個(gè)字段進(jìn)行緩存控制字段,那么它的值當(dāng)中可以加入private或者public表示是否允許代理服務(wù)器緩存,前者禁止,后者為允許。

          比如對(duì)于一些非常私密的數(shù)據(jù),如果緩存到代理服務(wù)器,別人直接訪問(wèn)代理就可以拿到這些數(shù)據(jù),是非常危險(xiǎn)的,因此對(duì)于這些數(shù)據(jù)一般是不會(huì)允許代理服務(wù)器進(jìn)行緩存的,將響應(yīng)頭部的Cache-Control設(shè)為private,而不是public

          proxy-revalidate

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

          s-maxage

          sshare的意思,限定了緩存在代理服務(wù)器中可以存放多久,和限制客戶(hù)端緩存時(shí)間的max-age并不沖突。

          講了這幾個(gè)字段,我們不妨來(lái)舉個(gè)小例子,源服務(wù)器在響應(yīng)頭中加入這樣一個(gè)字段:

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

          相當(dāng)于源服務(wù)器說(shuō): 我這個(gè)響應(yīng)是允許代理服務(wù)器緩存的,客戶(hù)端緩存過(guò)期了到代理中拿,并且在客戶(hù)端的緩存時(shí)間為 1000 秒,在代理服務(wù)器中的緩存時(shí)間為 2000 s。

          客戶(hù)端的緩存控制

          max-stale 和 min-fresh

          在客戶(hù)端的請(qǐng)求頭中,可以加入這兩個(gè)字段,來(lái)對(duì)代理服務(wù)器上的緩存進(jìn)行寬容限制操作。比如:

          max-stale: 5

          表示客戶(hù)端到代理服務(wù)器上拿緩存的時(shí)候,即使代理緩存過(guò)期了也不要緊,只要過(guò)期時(shí)間在5秒之內(nèi),還是可以從代理中獲取的。

          又比如:

          min-fresh: 5

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

          only-if-cached

          這個(gè)字段加上后表示客戶(hù)端只會(huì)接受代理緩存,而不會(huì)接受源服務(wù)器的響應(yīng)。如果代理緩存無(wú)效,則直接返回504(Gateway Timeout)。

          以上便是緩存代理的內(nèi)容,涉及的字段比較多,希望能好好回顧一下,加深理解。

          014: 什么是跨域?瀏覽器如何攔截響應(yīng)?如何解決?

          在前后端分離的開(kāi)發(fā)模式中,經(jīng)常會(huì)遇到跨域問(wèn)題,即 Ajax 請(qǐng)求發(fā)出去了,服務(wù)器也成功響應(yīng)了,前端就是拿不到這個(gè)響應(yīng)。接下來(lái)我們就來(lái)好好討論一下這個(gè)問(wèn)題。

          什么是跨域

          回顧一下 URI 的組成:

          5a13a36b2ff05158309ca738827a98a3.webp

          瀏覽器遵循同源政策(scheme(協(xié)議)、host(主機(jī))port(端口)都相同則為同源)。非同源站點(diǎn)有這樣一些限制:

          • 不能讀取和修改對(duì)方的 DOM
          • 不讀訪問(wèn)對(duì)方的 Cookie、IndexDB 和 LocalStorage
          • 限制 XMLHttpRequest 請(qǐng)求。(后面的話題著重圍繞這個(gè))

          當(dāng)瀏覽器向目標(biāo) URI 發(fā) Ajax 請(qǐng)求時(shí),只要當(dāng)前 URL 和目標(biāo) URL 不同源,則產(chǎn)生跨域,被稱(chēng)為跨域請(qǐng)求。

          跨域請(qǐng)求的響應(yīng)一般會(huì)被瀏覽器所攔截,注意,是被瀏覽器攔截,響應(yīng)其實(shí)是成功到達(dá)客戶(hù)端了。那這個(gè)攔截是如何發(fā)生呢?

          首先要知道的是,瀏覽器是多進(jìn)程的,以 Chrome 為例,進(jìn)程組成如下:

          1b5911e7076cad38d0085e2a8882d6ff.webp

          538f6bb71bf53ceec9db22f03d630817.webp

          WebKit 渲染引擎V8 引擎都在渲染進(jìn)程當(dāng)中。

          當(dāng)xhr.send被調(diào)用,即 Ajax 請(qǐng)求準(zhǔn)備發(fā)送的時(shí)候,其實(shí)還只是在渲染進(jìn)程的處理。為了防止黑客通過(guò)腳本觸碰到系統(tǒng)資源,瀏覽器將每一個(gè)渲染進(jìn)程裝進(jìn)了沙箱,并且為了防止 CPU 芯片一直存在的SpectreMeltdown漏洞,采取了站點(diǎn)隔離的手段,給每一個(gè)不同的站點(diǎn)(一級(jí)域名不同)分配了沙箱,互不干擾。具體見(jiàn)YouTube上Chromium安全團(tuán)隊(duì)的演講視頻。

          在沙箱當(dāng)中的渲染進(jìn)程是沒(méi)有辦法發(fā)送網(wǎng)絡(luò)請(qǐng)求的,那怎么辦?只能通過(guò)網(wǎng)絡(luò)進(jìn)程來(lái)發(fā)送。那這樣就涉及到進(jìn)程間通信(IPC,Inter Process Communication)了。接下來(lái)我們看看 chromium 當(dāng)中進(jìn)程間通信是如何完成的,在 chromium 源碼中調(diào)用順序如下:

          703075426e97cacc6945aa55dd858fcb.webp

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

          總的來(lái)說(shuō)就是利用Unix Domain Socket套接字,配合事件驅(qū)動(dòng)的高性能網(wǎng)絡(luò)并發(fā)庫(kù)libevent完成進(jìn)程的 IPC 過(guò)程。

          好,現(xiàn)在數(shù)據(jù)傳遞給了瀏覽器主進(jìn)程,主進(jìn)程接收到后,才真正地發(fā)出相應(yīng)的網(wǎng)絡(luò)請(qǐng)求。

          在服務(wù)端處理完數(shù)據(jù)后,將響應(yīng)返回,主進(jìn)程檢查到跨域,且沒(méi)有cors(后面會(huì)詳細(xì)說(shuō))響應(yīng)頭,將響應(yīng)體全部丟掉,并不會(huì)發(fā)送給渲染進(jìn)程。這就達(dá)到了攔截?cái)?shù)據(jù)的目的。

          接下來(lái)我們來(lái)說(shuō)一說(shuō)解決跨域問(wèn)題的幾種方案。

          CORS

          CORS 其實(shí)是 W3C 的一個(gè)標(biāo)準(zhǔn),全稱(chēng)是跨域資源共享。它需要瀏覽器和服務(wù)器的共同支持,具體來(lái)說(shuō),非 IE 和 IE10 以上支持CORS,服務(wù)器需要附加特定的響應(yīng)頭,后面具體拆解。不過(guò)在弄清楚 CORS 的原理之前,我們需要清楚兩個(gè)概念: 簡(jiǎn)單請(qǐng)求非簡(jiǎn)單請(qǐng)求。

          瀏覽器根據(jù)請(qǐng)求方法和請(qǐng)求頭的特定字段,將請(qǐng)求做了一下分類(lèi),具體來(lái)說(shuō)規(guī)則是這樣,凡是滿足下面條件的屬于簡(jiǎn)單請(qǐng)求:

          • 請(qǐng)求方法為 GET、POST 或者 HEAD
          • 請(qǐng)求頭的取值范圍: Accept、Accept-Language、Content-Language、Content-Type(只限于三個(gè)值application/x-www-form-urlencodedmultipart/form-data、text/plain)

          瀏覽器畫(huà)了這樣一個(gè)圈,在這個(gè)圈里面的就是簡(jiǎn)單請(qǐng)求, 圈外面的就是非簡(jiǎn)單請(qǐng)求,然后針對(duì)這兩種不同的請(qǐng)求進(jìn)行不同的處理。

          簡(jiǎn)單請(qǐng)求

          請(qǐng)求發(fā)出去之前,瀏覽器做了什么?

          它會(huì)自動(dòng)在請(qǐng)求頭當(dāng)中,添加一個(gè)Origin字段,用來(lái)說(shuō)明請(qǐng)求來(lái)自哪個(gè)。服務(wù)器拿到請(qǐng)求之后,在回應(yīng)時(shí)對(duì)應(yīng)地添加Access-Control-Allow-Origin字段,如果Origin不在這個(gè)字段的范圍中,那么瀏覽器就會(huì)將響應(yīng)攔截。

          因此,Access-Control-Allow-Origin字段是服務(wù)器用來(lái)決定瀏覽器是否攔截這個(gè)響應(yīng),這是必需的字段。與此同時(shí),其它一些可選的功能性的字段,用來(lái)描述如果不會(huì)攔截,這些字段將會(huì)發(fā)揮各自的作用。

          Access-Control-Allow-Credentials。這個(gè)字段是一個(gè)布爾值,表示是否允許發(fā)送 Cookie,對(duì)于跨域請(qǐng)求,瀏覽器對(duì)這個(gè)字段默認(rèn)值設(shè)為 false,而如果需要拿到瀏覽器的 Cookie,需要添加這個(gè)響應(yīng)頭并設(shè)為true, 并且在前端也需要設(shè)置withCredentials屬性:

          let xhr = new XMLHttpRequest();
          xhr.withCredentials = true;

          Access-Control-Expose-Headers。這個(gè)字段是給 XMLHttpRequest 對(duì)象賦能,讓它不僅可以拿到基本的 6 個(gè)響應(yīng)頭字段(包括Cache-Control、Content-LanguageContent-Type、Expires、Last-ModifiedPragma), 還能拿到這個(gè)字段聲明的響應(yīng)頭字段。比如這樣設(shè)置:

          Access-Control-Expose-Headers: aaa

          那么在前端可以通過(guò) XMLHttpRequest.getResponseHeader('aaa') 拿到 aaa 這個(gè)字段的值。

          非簡(jiǎn)單請(qǐng)求

          非簡(jiǎn)單請(qǐng)求相對(duì)而言會(huì)有些不同,體現(xiàn)在兩個(gè)方面: 預(yù)檢請(qǐng)求響應(yīng)字段。

          我們以 PUT 方法為例。

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

          當(dāng)這段代碼執(zhí)行后,首先會(huì)發(fā)送預(yù)檢請(qǐng)求。這個(gè)預(yù)檢請(qǐng)求的請(qǐng)求行和請(qǐng)求體是下面這個(gè)格式:

          OPTIONS / HTTP/1.1
          Origin: 當(dāng)前地址
          Host: xxx.com
          Access-Control-Request-Method: PUT
          Access-Control-Request-Headers: X-Custom-Header

          預(yù)檢請(qǐng)求的方法是OPTIONS,同時(shí)會(huì)加上Origin源地址和Host目標(biāo)地址,這很簡(jiǎn)單。同時(shí)也會(huì)加上兩個(gè)關(guān)鍵的字段:

          • Access-Control-Request-Method, 列出 CORS 請(qǐng)求用到哪個(gè)HTTP方法
          • Access-Control-Request-Headers,指定 CORS 請(qǐng)求將要加上什么請(qǐng)求頭

          這是預(yù)檢請(qǐng)求。接下來(lái)是響應(yīng)字段,響應(yīng)字段也分為兩部分,一部分是對(duì)于預(yù)檢請(qǐng)求的響應(yīng),一部分是對(duì)于 CORS 請(qǐng)求的響應(yīng)。

          預(yù)檢請(qǐng)求的響應(yīng)。如下面的格式:

          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

          其中有這樣幾個(gè)關(guān)鍵的響應(yīng)頭字段:

          • Access-Control-Allow-Origin: 表示可以允許請(qǐng)求的源,可以填具體的源名,也可以填*表示允許任意源請(qǐng)求。
          • Access-Control-Allow-Methods: 表示允許的請(qǐng)求方法列表。
          • Access-Control-Allow-Credentials: 簡(jiǎn)單請(qǐng)求中已經(jīng)介紹。
          • Access-Control-Allow-Headers: 表示允許發(fā)送的請(qǐng)求頭字段
          • Access-Control-Max-Age: 預(yù)檢請(qǐng)求的有效期,在此期間,不用發(fā)出另外一條預(yù)檢請(qǐng)求。

          在預(yù)檢請(qǐng)求的響應(yīng)返回后,如果請(qǐng)求不滿足響應(yīng)頭的條件,則觸發(fā)XMLHttpRequestonerror方法,當(dāng)然后面真正的CORS請(qǐng)求也不會(huì)發(fā)出去了。

          CORS 請(qǐng)求的響應(yīng)。繞了這么一大轉(zhuǎn),到了真正的 CORS 請(qǐng)求就容易多了,現(xiàn)在它和簡(jiǎn)單請(qǐng)求的情況是一樣的。瀏覽器自動(dòng)加上Origin字段,服務(wù)端響應(yīng)頭返回Access-Control-Allow-Origin。可以參考以上簡(jiǎn)單請(qǐng)求部分的內(nèi)容。

          JSONP

          雖然XMLHttpRequest對(duì)象遵循同源政策,但是script標(biāo)簽不一樣,它可以通過(guò) src 填上目標(biāo)地址從而發(fā)出 GET 請(qǐng)求,實(shí)現(xiàn)跨域請(qǐng)求并拿到響應(yīng)。這也就是 JSONP 的原理,接下來(lái)我們就來(lái)封裝一個(gè) 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) => {
          // 初始化回調(diào)函數(shù)名稱(chēng)
          callbackName = callbackName || Math.random().toString.replace(',', '');
          // 創(chuàng)建 script 元素并加入到當(dāng)前文檔中
          let scriptEle = document.createElement('script');
          scriptEle.src = generateURL();
          document.body.appendChild(scriptEle);
          // 綁定到 window 上,為了后面調(diào)用
          window[callbackName] = (data) => {
          resolve(data);
          // script 執(zhí)行完了,成為無(wú)用元素,需要清除
          document.body.removeChild(scriptEle);
          }
          });
          }

          當(dāng)然在服務(wù)端也會(huì)有響應(yīng)的操作, 以 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標(biāo)簽,瀏覽器直接把這部分字符串執(zhí)行
          res.end(`${callback}('數(shù)據(jù)包')`);
          })
          app.listen(3000)

          前端這樣簡(jiǎn)單地調(diào)用一下就好了:

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

          CORS相比,JSONP 最大的優(yōu)勢(shì)在于兼容性好,IE 低版本不能使用 CORS 但可以使用 JSONP,缺點(diǎn)也很明顯,請(qǐng)求方法單一,只支持 GET 請(qǐng)求。

          Nginx

          Nginx 是一種高性能的反向代理服務(wù)器,可以用來(lái)輕松解決跨域問(wèn)題。

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

          20c81cfb6ec98776d6138fa4fc94f12a.webp

          正向代理幫助客戶(hù)端訪問(wèn)客戶(hù)端自己訪問(wèn)不到的服務(wù)器,然后將結(jié)果返回給客戶(hù)端。

          反向代理拿到客戶(hù)端的請(qǐng)求,將請(qǐng)求轉(zhuǎn)發(fā)給其他的服務(wù)器,主要的場(chǎng)景是維持服務(wù)器集群的負(fù)載均衡,換句話說(shuō),反向代理幫其它的服務(wù)器拿到請(qǐng)求,然后選擇一個(gè)合適的服務(wù)器,將請(qǐng)求轉(zhuǎn)交給它。

          因此,兩者的區(qū)別就很明顯了,正向代理服務(wù)器是幫客戶(hù)端做事情,而反向代理服務(wù)器是幫其它的服務(wù)器做事情。

          好了,那 Nginx 是如何來(lái)解決跨域的呢?

          比如說(shuō)現(xiàn)在客戶(hù)端的域名為client.com,服務(wù)器的域名為server.com,客戶(hù)端向服務(wù)器發(fā)送 Ajax 請(qǐng)求,當(dāng)然會(huì)跨域了,那這個(gè)時(shí)候讓 Nginx 登場(chǎng)了,通過(guò)下面這個(gè)配置:

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

          Nginx 相當(dāng)于起了一個(gè)跳板機(jī),這個(gè)跳板機(jī)的域名也是client.com,讓客戶(hù)端首先訪問(wèn) client.com/api,這當(dāng)然沒(méi)有跨域,然后 Nginx 服務(wù)器作為反向代理,將請(qǐng)求轉(zhuǎn)發(fā)給server.com,當(dāng)響應(yīng)返回時(shí)又將響應(yīng)給到客戶(hù)端,這就完成整個(gè)跨域請(qǐng)求的過(guò)程。

          其實(shí)還有一些不太常用的方式,大家了解即可,比如postMessage,當(dāng)然WebSocket也是一種方式,但是已經(jīng)不屬于 HTTP 的范疇,另外一些奇技淫巧就不建議大家去死記硬背了,一方面從來(lái)不用,名字都難得記住,另一方面臨時(shí)背下來(lái),面試官也不會(huì)對(duì)你印象加分,因?yàn)榭吹贸鰜?lái)是背的。當(dāng)然沒(méi)有背并不代表減分,把跨域原理和前面三種主要的跨域方式理解清楚,經(jīng)得起更深一步的推敲,反而會(huì)讓別人覺(jué)得你是一個(gè)靠譜的人。

          015: TLS1.2 握手的過(guò)程是怎樣的?

          之前談到了 HTTP 是明文傳輸?shù)膮f(xié)議,傳輸保文對(duì)外完全透明,非常不安全,那如何進(jìn)一步保證安全性呢?

          由此產(chǎn)生了 HTTPS,其實(shí)它并不是一個(gè)新的協(xié)議,而是在 HTTP 下面增加了一層 SSL/TLS 協(xié)議,簡(jiǎn)單的講,HTTPS = HTTP + SSL/TLS。

          那什么是 SSL/TLS 呢?

          SSL 即安全套接層(Secure Sockets Layer),在 OSI 七層模型中處于會(huì)話層(第 5 層)。之前 SSL 出過(guò)三個(gè)大版本,當(dāng)它發(fā)展到第三個(gè)大版本的時(shí)候才被標(biāo)準(zhǔn)化,成為 TLS(傳輸層安全,Transport Layer Security),并被當(dāng)做 TLS1.0 的版本,準(zhǔn)確地說(shuō),TLS1.0 = SSL3.1。

          現(xiàn)在主流的版本是 TLS/1.2, 之前的 TLS1.0、TLS1.1 都被認(rèn)為是不安全的,在不久的將來(lái)會(huì)被完全淘汰。因此我們接下來(lái)主要討論的是 TLS1.2, 當(dāng)然在 2018 年推出了更加優(yōu)秀的 TLS1.3,大大優(yōu)化了 TLS 握手過(guò)程,這個(gè)我們放在下一節(jié)再去說(shuō)。

          TLS 握手的過(guò)程比較復(fù)雜,寫(xiě)文章之前我查閱了大量的資料,發(fā)現(xiàn)對(duì) TLS 初學(xué)者非常不友好,也有很多知識(shí)點(diǎn)說(shuō)的含糊不清,可以說(shuō)這個(gè)整理的過(guò)程是相當(dāng)痛苦了。希望我下面的拆解能夠幫你理解得更順暢些吧 : )

          傳統(tǒng) RSA 握手

          先來(lái)說(shuō)說(shuō)傳統(tǒng)的 TLS 握手,也是大家在網(wǎng)上經(jīng)??吹降摹N抑耙矊?xiě)過(guò)這樣的文章,(傳統(tǒng)RSA版本)HTTPS為什么讓數(shù)據(jù)傳輸更安全,其中也介紹到了對(duì)稱(chēng)加密非對(duì)稱(chēng)加密的概念,建議大家去讀一讀,不再贅述。之所以稱(chēng)它為 RSA 版本,是因?yàn)樗诩咏饷?code style="font-size:14px;color:rgb(30,107,184);background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;">pre_random的時(shí)候采用的是 RSA 算法。

          TLS 1.2 握手過(guò)程

          現(xiàn)在我們來(lái)講講主流的 TLS 1.2 版本所采用的方式。

          8e474f9ec7cfa56b6ee593a36520f5ea.webp

          剛開(kāi)始你可能會(huì)比較懵,先別著急,過(guò)一遍下面的流程再來(lái)看會(huì)豁然開(kāi)朗。

          step 1: Client Hello

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

          client_random 是什么?用來(lái)最終 secret 的一個(gè)參數(shù)。

          加密套件列表是什么?我舉個(gè)例子,加密套件列表一般張這樣:

          TLS_ECDHE_WITH_AES_128_GCM_SHA256

          意思是TLS握手過(guò)程中,使用ECDHE算法生成pre_random(這個(gè)數(shù)后面會(huì)介紹),128位的AES算法進(jìn)行對(duì)稱(chēng)加密,在對(duì)稱(chēng)加密的過(guò)程中使用主流的GCM分組模式,因?yàn)閷?duì)稱(chēng)加密中很重要的一個(gè)問(wèn)題就是如何分組。最后一個(gè)是哈希摘要算法,采用SHA256算法。

          其中值得解釋一下的是這個(gè)哈希摘要算法,試想一個(gè)這樣的場(chǎng)景,服務(wù)端現(xiàn)在給客戶(hù)端發(fā)消息來(lái)了,客戶(hù)端并不知道此時(shí)的消息到底是服務(wù)端發(fā)的,還是中間人偽造的消息呢?現(xiàn)在引入這個(gè)哈希摘要算法,將服務(wù)端的證書(shū)信息通過(guò)這個(gè)算法生成一個(gè)摘要(可以理解為比較短的字符串),用來(lái)標(biāo)識(shí)這個(gè)服務(wù)端的身份,用私鑰加密后把加密后的標(biāo)識(shí)自己的公鑰傳給客戶(hù)端??蛻?hù)端拿到這個(gè)公鑰來(lái)解密,生成另外一份摘要。兩個(gè)摘要進(jìn)行對(duì)比,如果相同則能確認(rèn)服務(wù)端的身份。這也就是所謂數(shù)字簽名的原理。其中除了哈希算法,最重要的過(guò)程是私鑰加密,公鑰解密。

          step 2: Server Hello

          可以看到服務(wù)器一口氣給客戶(hù)端回復(fù)了非常多的內(nèi)容。

          server_random也是最后生成secret的一個(gè)參數(shù), 同時(shí)確認(rèn) TLS 版本、需要使用的加密套件和自己的證書(shū),這都不難理解。那剩下的server_params是干嘛的呢?

          我們先埋個(gè)伏筆,現(xiàn)在你只需要知道,server_random到達(dá)了客戶(hù)端。

          step 3: Client 驗(yàn)證證書(shū),生成secret

          客戶(hù)端驗(yàn)證服務(wù)端傳來(lái)的證書(shū)簽名是否通過(guò),如果驗(yàn)證通過(guò),則傳遞client_params這個(gè)參數(shù)給服務(wù)器。

          接著客戶(hù)端通過(guò)ECDHE算法計(jì)算出pre_random,其中傳入兩個(gè)參數(shù):server_paramsclient_params?,F(xiàn)在你應(yīng)該清楚這個(gè)兩個(gè)參數(shù)的作用了吧,由于ECDHE基于橢圓曲線離散對(duì)數(shù),這兩個(gè)參數(shù)也稱(chēng)作橢圓曲線的公鑰。

          客戶(hù)端現(xiàn)在擁有了client_random、server_randompre_random,接下來(lái)將這三個(gè)數(shù)通過(guò)一個(gè)偽隨機(jī)數(shù)函數(shù)來(lái)計(jì)算出最終的secret。

          step4: Server 生成 secret

          剛剛客戶(hù)端不是傳了client_params過(guò)來(lái)了嗎?

          現(xiàn)在服務(wù)端開(kāi)始用ECDHE算法生成pre_random,接著用和客戶(hù)端同樣的偽隨機(jī)數(shù)函數(shù)生成最后的secret。

          注意事項(xiàng)

          TLS的過(guò)程基本上講完了,但還有兩點(diǎn)需要注意。

          第一、實(shí)際上 TLS 握手是一個(gè)雙向認(rèn)證的過(guò)程,從 step1 中可以看到,客戶(hù)端有能力驗(yàn)證服務(wù)器的身份,那服務(wù)器能不能驗(yàn)證客戶(hù)端的身份呢?

          當(dāng)然是可以的。具體來(lái)說(shuō),在 step3中,客戶(hù)端傳送client_params,實(shí)際上給服務(wù)器傳一個(gè)驗(yàn)證消息,讓服務(wù)器將相同的驗(yàn)證流程(哈希摘要 + 私鑰加密 + 公鑰解密)走一遍,確認(rèn)客戶(hù)端的身份。

          第二、當(dāng)客戶(hù)端生成secret后,會(huì)給服務(wù)端發(fā)送一個(gè)收尾的消息,告訴服務(wù)器之后的都用對(duì)稱(chēng)加密,對(duì)稱(chēng)加密的算法就用第一次約定的。服務(wù)器生成完secret也會(huì)向客戶(hù)端發(fā)送一個(gè)收尾的消息,告訴客戶(hù)端以后就直接用對(duì)稱(chēng)加密來(lái)通信。

          這個(gè)收尾的消息包括兩部分,一部分是Change Cipher Spec,意味著后面加密傳輸了,另一個(gè)是Finished消息,這個(gè)消息是對(duì)之前所有發(fā)送的數(shù)據(jù)做的摘要,對(duì)摘要進(jìn)行加密,讓對(duì)方驗(yàn)證一下。

          當(dāng)雙方都驗(yàn)證通過(guò)之后,握手才正式結(jié)束。后面的 HTTP 正式開(kāi)始傳輸加密報(bào)文。

          RSA 和 ECDHE 握手過(guò)程的區(qū)別

          1. ECDHE 握手,也就是主流的 TLS1.2 握手中,使用ECDHE實(shí)現(xiàn)pre_random的加密解密,沒(méi)有用到 RSA。

          2. 使用 ECDHE 還有一個(gè)特點(diǎn),就是客戶(hù)端發(fā)送完收尾消息后可以提前搶跑,直接發(fā)送 HTTP 報(bào)文,節(jié)省了一個(gè) RTT,不必等到收尾消息到達(dá)服務(wù)器,然后等服務(wù)器返回收尾消息給自己,直接開(kāi)始發(fā)請(qǐng)求。這也叫TLS False Start

          016: TLS 1.3 做了哪些改進(jìn)?

          TLS 1.2 雖然存在了 10 多年,經(jīng)歷了無(wú)數(shù)的考驗(yàn),但歷史的車(chē)輪總是不斷向前的,為了獲得更強(qiáng)的安全、更優(yōu)秀的性能,在2018年就推出了 TLS1.3,對(duì)于TLS1.2做了一系列的改進(jìn),主要分為這幾個(gè)部分:強(qiáng)化安全、提高性能。

          強(qiáng)化安全

          在 TLS1.3 中廢除了非常多的加密算法,最后只保留五個(gè)加密套件:

          • 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

          可以看到,最后剩下的對(duì)稱(chēng)加密算法只有 AESCHACHA20,之前主流的也會(huì)這兩種。分組模式也只剩下 GCMPOLY1305, 哈希摘要算法只剩下了 SHA256SHA384 了。

          那你可能會(huì)問(wèn)了, 之前RSA這么重要的非對(duì)稱(chēng)加密算法怎么不在了?

          我覺(jué)得有兩方面的原因:

          第一、2015年發(fā)現(xiàn)了FREAK攻擊,即已經(jīng)有人發(fā)現(xiàn)了 RSA 的漏洞,能夠進(jìn)行破解了。

          第二、一旦私鑰泄露,那么中間人可以通過(guò)私鑰計(jì)算出之前所有報(bào)文的secret,破解之前所有的密文。

          為什么?回到 RSA 握手的過(guò)程中,客戶(hù)端拿到服務(wù)器的證書(shū)后,提取出服務(wù)器的公鑰,然后生成pre_random并用公鑰加密傳給服務(wù)器,服務(wù)器通過(guò)私鑰解密,從而拿到真實(shí)的pre_random。當(dāng)中間人拿到了服務(wù)器私鑰,并且截獲之前所有報(bào)文的時(shí)候,那么就能拿到pre_random、server_randomclient_random并根據(jù)對(duì)應(yīng)的隨機(jī)數(shù)函數(shù)生成secret,也就是拿到了 TLS 最終的會(huì)話密鑰,每一個(gè)歷史報(bào)文都能通過(guò)這樣的方式進(jìn)行破解。

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

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

          提升性能

          握手改進(jìn)

          流程如下:

          bfb4b12995fd3997ed68a5c448f4f4e5.webp

          大體的方式和 TLS1.2 差不多,不過(guò)和 TLS 1.2 相比少了一個(gè) RTT, 服務(wù)端不必等待對(duì)方驗(yàn)證證書(shū)之后才拿到client_params,而是直接在第一次握手的時(shí)候就能夠拿到, 拿到之后立即計(jì)算secret,節(jié)省了之前不必要的等待時(shí)間。同時(shí),這也意味著在第一次握手的時(shí)候客戶(hù)端需要傳送更多的信息,一口氣給傳完。

          這種 TLS 1.3 握手方式也被叫做1-RTT握手。但其實(shí)這種1-RTT的握手方式還是有一些優(yōu)化的空間的,接下來(lái)我們來(lái)一一介紹這些優(yōu)化方式。

          會(huì)話復(fù)用

          會(huì)話復(fù)用有兩種方式: Session IDSession Ticket。

          先說(shuō)說(shuō)最早出現(xiàn)的Seesion ID,具體做法是客戶(hù)端和服務(wù)器首次連接后各自保存會(huì)話的 ID,并存儲(chǔ)會(huì)話密鑰,當(dāng)再次連接時(shí),客戶(hù)端發(fā)送ID過(guò)來(lái),服務(wù)器查找這個(gè) ID 是否存在,如果找到了就直接復(fù)用之前的會(huì)話狀態(tài),會(huì)話密鑰不用重新生成,直接用原來(lái)的那份。

          但這種方式也存在一個(gè)弊端,就是當(dāng)客戶(hù)端數(shù)量龐大的時(shí)候,對(duì)服務(wù)端的存儲(chǔ)壓力非常大。

          因而出現(xiàn)了第二種方式——Session Ticket。它的思路就是: 服務(wù)端的壓力大,那就把壓力分?jǐn)偨o客戶(hù)端唄。具體來(lái)說(shuō),雙方連接成功后,服務(wù)器加密會(huì)話信息,用Session Ticket消息發(fā)給客戶(hù)端,讓客戶(hù)端保存下來(lái)。下次重連的時(shí)候,就把這個(gè) Ticket 進(jìn)行解密,驗(yàn)證它過(guò)沒(méi)過(guò)期,如果沒(méi)過(guò)期那就直接恢復(fù)之前的會(huì)話狀態(tài)。

          這種方式雖然減小了服務(wù)端的存儲(chǔ)壓力,但與帶來(lái)了安全問(wèn)題,即每次用一個(gè)固定的密鑰來(lái)解密 Ticket 數(shù)據(jù),一旦黑客拿到這個(gè)密鑰,之前所有的歷史記錄也被破解了。因此為了盡量避免這樣的問(wèn)題,密鑰需要定期進(jìn)行更換。

          總的來(lái)說(shuō),這些會(huì)話復(fù)用的技術(shù)在保證1-RTT的同時(shí),也節(jié)省了生成會(huì)話密鑰這些算法所消耗的時(shí)間,是一筆可觀的性能提升。

          PSK

          剛剛說(shuō)的都是1-RTT情況下的優(yōu)化,那能不能優(yōu)化到0-RTT呢?

          答案是可以的。做法其實(shí)也很簡(jiǎn)單,在發(fā)送Session Ticket的同時(shí)帶上應(yīng)用數(shù)據(jù),不用等到服務(wù)端確認(rèn),這種方式被稱(chēng)為Pre-Shared Key,即 PSK。

          這種方式雖然方便,但也帶來(lái)了安全問(wèn)題。中間人截獲PSK的數(shù)據(jù),不斷向服務(wù)器重復(fù)發(fā),類(lèi)似于 TCP 第一次握手?jǐn)y帶數(shù)據(jù),增加了服務(wù)器被攻擊的風(fēng)險(xiǎn)。

          總結(jié)

          TLS1.3 在 TLS1.2 的基礎(chǔ)上廢除了大量的算法,提升了安全性。同時(shí)利用會(huì)話復(fù)用節(jié)省了重新生成密鑰的時(shí)間,利用 PSK 做到了0-RTT連接。

          017: HTTP/2 有哪些改進(jìn)?

          由于 HTTPS 在安全方面已經(jīng)做的非常好了,HTTP 改進(jìn)的關(guān)注點(diǎn)放在了性能方面。對(duì)于 HTTP/2 而言,它對(duì)于性能的提升主要在于兩點(diǎn):

          • 頭部壓縮
          • 多路復(fù)用

          當(dāng)然還有一些顛覆性的功能實(shí)現(xiàn):

          • 設(shè)置請(qǐng)求優(yōu)先級(jí)
          • 服務(wù)器推送

          這些重大的提升本質(zhì)上也是為了解決 HTTP 本身的問(wèn)題而產(chǎn)生的。接下來(lái)我們來(lái)看看 HTTP/2 解決了哪些問(wèn)題,以及解決方式具體是如何的。

          頭部壓縮

          在 HTTP/1.1 及之前的時(shí)代,請(qǐng)求體一般會(huì)有響應(yīng)的壓縮編碼過(guò)程,通過(guò)Content-Encoding頭部字段來(lái)指定,但你有沒(méi)有想過(guò)頭部字段本身的壓縮呢?當(dāng)請(qǐng)求字段非常復(fù)雜的時(shí)候,尤其對(duì)于 GET 請(qǐng)求,請(qǐng)求報(bào)文幾乎全是請(qǐng)求頭,這個(gè)時(shí)候還是存在非常大的優(yōu)化空間的。HTTP/2 針對(duì)頭部字段,也采用了對(duì)應(yīng)的壓縮算法——HPACK,對(duì)請(qǐng)求頭進(jìn)行壓縮。

          HPACK 算法是專(zhuān)門(mén)為 HTTP/2 服務(wù)的,它主要的亮點(diǎn)有兩個(gè):

          • 首先是在服務(wù)器和客戶(hù)端之間建立哈希表,將用到的字段存放在這張表中,那么在傳輸?shù)臅r(shí)候?qū)τ谥俺霈F(xiàn)過(guò)的值,只需要把索引(比如0,1,2,...)傳給對(duì)方即可,對(duì)方拿到索引查表就行了。這種傳索引的方式,可以說(shuō)讓請(qǐng)求頭字段得到極大程度的精簡(jiǎn)和復(fù)用。
          63b9a737f2d01357c23ba0033d1af594.webp
          HTTP/2 當(dāng)中廢除了起始行的概念,將起始行中的請(qǐng)求方法、URI、狀態(tài)碼轉(zhuǎn)換成了頭字段,不過(guò)這些字段都有一個(gè)":"前綴,用來(lái)和其它請(qǐng)求頭區(qū)分開(kāi)。
          • 其次是對(duì)于整數(shù)和字符串進(jìn)行哈夫曼編碼,哈夫曼編碼的原理就是先將所有出現(xiàn)的字符建立一張索引表,然后讓出現(xiàn)次數(shù)多的字符對(duì)應(yīng)的索引盡可能短,傳輸?shù)臅r(shí)候也是傳輸這樣的索引序列,可以達(dá)到非常高的壓縮率。

          多路復(fù)用

          HTTP 隊(duì)頭阻塞

          我們之前討論了 HTTP 隊(duì)頭阻塞的問(wèn)題,其根本原因在于HTTP 基于請(qǐng)求-響應(yīng)的模型,在同一個(gè) TCP 長(zhǎng)連接中,前面的請(qǐng)求沒(méi)有得到響應(yīng),后面的請(qǐng)求就會(huì)被阻塞。

          后面我們又討論到用并發(fā)連接域名分片的方式來(lái)解決這個(gè)問(wèn)題,但這并沒(méi)有真正從 HTTP 本身的層面解決問(wèn)題,只是增加了 TCP 連接,分?jǐn)傦L(fēng)險(xiǎn)而已。而且這么做也有弊端,多條 TCP 連接會(huì)競(jìng)爭(zhēng)有限的帶寬,讓真正優(yōu)先級(jí)高的請(qǐng)求不能優(yōu)先處理。

          而 HTTP/2 便從 HTTP 協(xié)議本身解決了隊(duì)頭阻塞問(wèn)題。注意,這里并不是指的TCP隊(duì)頭阻塞,而是HTTP隊(duì)頭阻塞,兩者并不是一回事。TCP 的隊(duì)頭阻塞是在數(shù)據(jù)包層面,單位是數(shù)據(jù)包,前一個(gè)報(bào)文沒(méi)有收到便不會(huì)將后面收到的報(bào)文上傳給 HTTP,而HTTP 的隊(duì)頭阻塞是在 HTTP 請(qǐng)求-響應(yīng)層面,前一個(gè)請(qǐng)求沒(méi)處理完,后面的請(qǐng)求就要阻塞住。兩者所在的層次不一樣。

          那么 HTTP/2 如何來(lái)解決所謂的隊(duì)頭阻塞呢?

          二進(jìn)制分幀

          首先,HTTP/2 認(rèn)為明文傳輸對(duì)機(jī)器而言太麻煩了,不方便計(jì)算機(jī)的解析,因?yàn)閷?duì)于文本而言會(huì)有多義性的字符,比如回車(chē)換行到底是內(nèi)容還是分隔符,在內(nèi)部需要用到狀態(tài)機(jī)去識(shí)別,效率比較低。于是 HTTP/2 干脆把報(bào)文全部換成二進(jìn)制格式,全部傳輸01串,方便了機(jī)器的解析。

          原來(lái)Headers + Body的報(bào)文格式如今被拆分成了一個(gè)個(gè)二進(jìn)制的幀,用Headers幀存放頭部字段,Data幀存放請(qǐng)求體數(shù)據(jù)。分幀之后,服務(wù)器看到的不再是一個(gè)個(gè)完整的 HTTP 請(qǐng)求報(bào)文,而是一堆亂序的二進(jìn)制幀。這些二進(jìn)制幀不存在先后關(guān)系,因此也就不會(huì)排隊(duì)等待,也就沒(méi)有了 HTTP 的隊(duì)頭阻塞問(wèn)題。

          通信雙方都可以給對(duì)方發(fā)送二進(jìn)制幀,這種二進(jìn)制幀的雙向傳輸?shù)男蛄?/strong>,也叫做(Stream)。HTTP/2 用來(lái)在一個(gè) TCP 連接上來(lái)進(jìn)行多個(gè)數(shù)據(jù)幀的通信,這就是多路復(fù)用的概念。

          可能你會(huì)有一個(gè)疑問(wèn),既然是亂序首發(fā),那最后如何來(lái)處理這些亂序的數(shù)據(jù)幀呢?

          首先要聲明的是,所謂的亂序,指的是不同 ID 的 Stream 是亂序的,但同一個(gè) Stream ID 的幀一定是按順序傳輸?shù)摹6M(jìn)制幀到達(dá)后對(duì)方會(huì)將 Stream ID 相同的二進(jìn)制幀組裝成完整的請(qǐng)求報(bào)文響應(yīng)報(bào)文。當(dāng)然,在二進(jìn)制幀當(dāng)中還有其他的一些字段,實(shí)現(xiàn)了優(yōu)先級(jí)流量控制等功能,我們放到下一節(jié)再來(lái)介紹。

          服務(wù)器推送

          另外值得一說(shuō)的是 HTTP/2 的服務(wù)器推送(Server Push)。在 HTTP/2 當(dāng)中,服務(wù)器已經(jīng)不再是完全被動(dòng)地接收請(qǐng)求,響應(yīng)請(qǐng)求,它也能新建 stream 來(lái)給客戶(hù)端發(fā)送消息,當(dāng) TCP 連接建立之后,比如瀏覽器請(qǐng)求一個(gè) HTML 文件,服務(wù)器就可以在返回 HTML 的基礎(chǔ)上,將 HTML 中引用到的其他資源文件一起返回給客戶(hù)端,減少客戶(hù)端的等待。

          總結(jié)

          當(dāng)然,HTTP/2 新增那么多的特性,是不是 HTTP 的語(yǔ)法要重新學(xué)呢?不需要,HTTP/2 完全兼容之前 HTTP 的語(yǔ)法和語(yǔ)義,如請(qǐng)求頭、URI、狀態(tài)碼、頭部字段都沒(méi)有改變,完全不用擔(dān)心。同時(shí),在安全方面,HTTP 也支持 TLS,并且現(xiàn)在主流的瀏覽器都公開(kāi)只支持加密的 HTTP/2, 因此你現(xiàn)在能看到的 HTTP/2 也基本上都是跑在 TLS 上面的了。最后放一張分層圖給大家參考:

          453586db8ffb2dca32cd958ef364444d.webp

          018: HTTP/2 中的二進(jìn)制幀是如何設(shè)計(jì)的?

          幀結(jié)構(gòu)

          HTTP/2 中傳輸?shù)膸Y(jié)構(gòu)如下圖所示:

          9f1d29c804ce52d767859848a6fe404c.webp

          每個(gè)幀分為幀頭幀體。先是三個(gè)字節(jié)的幀長(zhǎng)度,這個(gè)長(zhǎng)度表示的是幀體的長(zhǎng)度。

          然后是幀類(lèi)型,大概可以分為數(shù)據(jù)幀控制幀兩種。數(shù)據(jù)幀用來(lái)存放 HTTP 報(bào)文,控制幀用來(lái)管理的傳輸。

          接下來(lái)的一個(gè)字節(jié)是幀標(biāo)志,里面一共有 8 個(gè)標(biāo)志位,常用的有 END_HEADERS表示頭數(shù)據(jù)結(jié)束,END_STREAM表示單方向數(shù)據(jù)發(fā)送結(jié)束。

          后 4 個(gè)字節(jié)是Stream ID, 也就是流標(biāo)識(shí)符,有了它,接收方就能從亂序的二進(jìn)制幀中選擇出 ID 相同的幀,按順序組裝成請(qǐng)求/響應(yīng)報(bào)文。

          流的狀態(tài)變化

          從前面可以知道,在 HTTP/2 中,所謂的,其實(shí)就是二進(jìn)制幀的雙向傳輸?shù)男蛄?/strong>。那么在 HTTP/2 請(qǐng)求和響應(yīng)的過(guò)程中,流的狀態(tài)是如何變化的呢?

          HTTP/2 其實(shí)也是借鑒了 TCP 狀態(tài)變化的思想,根據(jù)幀的標(biāo)志位來(lái)實(shí)現(xiàn)具體的狀態(tài)改變。這里我們以一個(gè)普通的請(qǐng)求-響應(yīng)過(guò)程為例來(lái)說(shuō)明:

          dfc37befa67ccfb5f1d6d859eb6d99ce.webp

          最開(kāi)始兩者都是空閑狀態(tài),當(dāng)客戶(hù)端發(fā)送Headers幀后,開(kāi)始分配Stream ID, 此時(shí)客戶(hù)端的打開(kāi), 服務(wù)端接收之后服務(wù)端的也打開(kāi),兩端的都打開(kāi)之后,就可以互相傳遞數(shù)據(jù)幀和控制幀了。

          當(dāng)客戶(hù)端要關(guān)閉時(shí),向服務(wù)端發(fā)送END_STREAM幀,進(jìn)入半關(guān)閉狀態(tài), 這個(gè)時(shí)候客戶(hù)端只能接收數(shù)據(jù),而不能發(fā)送數(shù)據(jù)。

          服務(wù)端收到這個(gè)END_STREAM幀后也進(jìn)入半關(guān)閉狀態(tài),不過(guò)此時(shí)服務(wù)端的情況是只能發(fā)送數(shù)據(jù),而不能接收數(shù)據(jù)。隨后服務(wù)端也向客戶(hù)端發(fā)送END_STREAM幀,表示數(shù)據(jù)發(fā)送完畢,雙方進(jìn)入關(guān)閉狀態(tài)。

          如果下次要開(kāi)啟新的,流 ID 需要自增,直到上限為止,到達(dá)上限后開(kāi)一個(gè)新的 TCP 連接重頭開(kāi)始計(jì)數(shù)。由于流 ID 字段長(zhǎng)度為 4 個(gè)字節(jié),最高位又被保留,因此范圍是 0 ~ 2的 31 次方,大約 21 億個(gè)。

          流的特性

          剛剛談到了流的狀態(tài)變化過(guò)程,這里順便就來(lái)總結(jié)一下傳輸?shù)奶匦?

          • 并發(fā)性。一個(gè) HTTP/2 連接上可以同時(shí)發(fā)多個(gè)幀,這一點(diǎn)和 HTTP/1 不同。這也是實(shí)現(xiàn)多路復(fù)用的基礎(chǔ)。
          • 自增性。流 ID 是不可重用的,而是會(huì)按順序遞增,達(dá)到上限之后又新開(kāi) TCP 連接從頭開(kāi)始。
          • 雙向性??蛻?hù)端和服務(wù)端都可以創(chuàng)建流,互不干擾,雙方都可以作為發(fā)送方或者接收方。
          • 可設(shè)置優(yōu)先級(jí)。可以設(shè)置數(shù)據(jù)幀的優(yōu)先級(jí),讓服務(wù)端先處理重要資源,優(yōu)化用戶(hù)體驗(yàn)。

          以上就是對(duì) HTTP/2 中二進(jìn)制幀的介紹,希望對(duì)你有所啟發(fā)

          參考:

          《web協(xié)議詳解與抓包實(shí)戰(zhàn)——陶輝》

          《透視 HTTP 協(xié)議》——chrono

          Chromium IPC 源碼

          前端開(kāi)發(fā)者必備的Nginx知識(shí) ——conardli




          推薦閱讀




          我的公眾號(hào)能帶來(lái)什么價(jià)值?(文末有送書(shū)規(guī)則,一定要看)

          每個(gè)前端工程師都應(yīng)該了解的圖片知識(shí)(長(zhǎng)文建議收藏)

          為什么現(xiàn)在面試總是面試造火箭?

          瀏覽 69
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                    <th id="afajh"><progress id="afajh"></progress></th>
                    五月天AV电影在线 | 狠狠操B 麻豆传媒肏逼视频 | 久久黄色精品视频 | 刘玥一级婬片A片AAA | 波多野结衣视频网站 |