前端須要瞭解的HTTP網絡協議

TCP/IP 網絡分層模型 說明 OSI 網絡分層模型 說明
應用層 (application layer) 因爲下面的三層把基礎打得很是好,因此在這一層就百花齊放了,有各類面向具體應用的協議。例如 Telnet、SSH、FTP、SMTP 等等,固然還有咱們的 HTTP。傳輸單位則是消息或報文(message)。 應用層 (application layer) 面向具體的應用傳輸數據。
- - 表示層(presentation layer) 把數據轉換爲合適、可理解的語法和語義。
- - 會話層(session layer) 維護網絡中的鏈接狀態,即保持會話和同步。
傳輸層(transport layer) 這個層次協議的職責是保證數據在 IP 地址標記的兩點之間可靠地傳輸,是 TCP 協議工做的層次,另外還有它的一個小夥伴 UDP。傳輸單位是段(segment)。 傳輸層(transport layer) 至關於 TCP/IP 裏的傳輸層。
網際層(internet layer) IP 協議就處在這一層。由於 IP 協議定義了 IP 地址 的概念,因此就能夠在 連接層 的基礎上,用 IP 地址取代 MAC 地址 ,把許許多多的局域網、廣域網鏈接成一個虛擬的巨大網絡,在這個網絡裏找設備時只要把 IP 地址再「翻譯」成 MAC 地址就能夠了。傳輸單位是包(packet)。 網絡層(network layer) 至關於 TCP/IP 裏的網際層。
連接層 (link layer/MAC) 負責在以太網、WiFi 這樣的底層網絡上發送原始數據包,工做在網卡這個層次,使用 MAC 地址來標記網絡上的設備 ,因此有時候也叫 MAC 層。傳輸單位是幀(frame)。 數據鏈路層(data link layer) 至關於 TCP/IP 的連接層。
物理層(physical layer) 網絡的物理形式,例如電纜、光纖、網卡、集線器等等。

HTTP 是什麼?

超文本傳輸協議(HTTP)是一個用於傳輸超媒體文檔(例如 HTML)的應用層協議。javascript

它是爲 Web 瀏覽器與 Web 服務器之間的通訊而設計的,但也能夠用於其餘目的。css

HTTP 遵循經典的客戶端-服務端模型,客戶端打開一個鏈接以發出請求,而後等待直到收到服務器端響應。html

HTTP 是無狀態協議,這意味着服務器不會在兩個請求之間保留任何數據(狀態)。java

HTTP報文

HTTP 協議的請求報文和響應報文的結構基本相同,由三大部分組成:git

  1. 起始行(start line):描述請求或響應的基本信息;
  2. 頭部字段集合(header):使用 key-value 形式更詳細地說明報文;
  3. 消息正文(entity):實際傳輸的數據,它不必定是純文本,能夠是圖片、視頻等二進制數據。

請求報文

請求行(request line)

請求報文的起始行叫作請求行(request line),它簡要地描述了 客戶端想要如何操做服務器端的資源github

請求方法(Method -> GET/POST)+ 請求目標(Path -> URI)+ 版本號(Version of the protocol -> HTTP 1.0 / 1.1 / 2.0)

這三個部分一般使用空格(space)來分隔,最後要用 CRLF 換行表示結束。web

請求方法

目前 HTTP/1.1 規定了八種方法,單詞 都必須是大寫的形式 ,我先簡單地列把它們列出來,後面再詳細講解。面試

  1. GET:它的含義是請求 從服務器獲取資源
  2. HEAD:獲取資源的元信息(響應頭);
  3. POST:向服務器提交數據,至關於寫入或上傳數據;
  4. PUT:向服務器提交數據,多用於更新數據;
  5. DELETE:刪除資源(基本不用);
  6. CONNECT:要求服務器爲客戶端和另外一臺遠程服務器創建特殊的鏈接隧道;
  7. OPTIONS:列出可對資源實行的操做方法,在響應頭的 Allow 字段裏返回
  8. TRACE:追蹤請求 - 響應的傳輸路徑。
GET 和 POST 的區別

做用算法

GET 用於獲取資源,而 POST 用於傳輸實體主體。數據庫

參數

GET 和 POST 的請求都能使用額外的參數,可是 GET 的參數是以查詢字符串出如今 URL 中,而 POST 的參數存儲在實體主體中。

由於 URL 只支持 ASCII 碼,所以 GET 的參數中若是存在中文等字符就須要先進行編碼。

http://www.baidu.com/?百度一下,你就知道
http://www.baidu.com/?%E7%99%BE%E5%BA%A6%E4%B8%80%E4%B8%8B%EF%BC%8C%E4%BD%A0%E5%B0%B1%E7%9F%A5%E9%81%93

安全

在 HTTP 協議裏,所謂的 安全 是指請求方法不會「破壞」服務器上的資源,即不會對服務器上的資源形成實質的修改。

GET 方法是安全的,而 POST 卻不是,由於 POST 的目的是傳送實體主體內容,這個內容多是用戶上傳的表單數據,上傳成功以後,服務器可能把這個數據存儲到數據庫中,所以狀態也就發生了改變。

冪等性

所謂的 冪等(Idempotent) 其實是一個數學用語,被借用到了 HTTP 協議裏,意思是屢次執行相同的操做,結果也都是相同的,即屢次冪後結果相等。

GET /pageX HTTP/1.1 是冪等的,連續調用屢次,客戶端接收到的結果都是同樣的:

GET /pageX HTTP/1.1
GET /pageX HTTP/1.1
GET /pageX HTTP/1.1
GET /pageX HTTP/1.1

POST /add_row HTTP/1.1 不是冪等的,若是調用屢次,就會增長多行記錄:

POST /add_row HTTP/1.1   -> Adds a 1nd row
POST /add_row HTTP/1.1   -> Adds a 2nd row
POST /add_row HTTP/1.1   -> Adds a 3rd row

緩存

GET是可緩存的,POST不可緩存。

XMLHttpRequest

XMLHttpRequest 是一個 API,它爲客戶端提供了在客戶端和服務器之間傳輸數據的功能。它提供了一個經過 URL 來獲取數據的簡單方式,而且不會使整個頁面刷新。這使得網頁只更新一部分頁面而不會打擾到用戶。XMLHttpRequest 在 AJAX 中被大量使用。
  • 在使用 XMLHttpRequest 的 POST 方法時,瀏覽器會先發送 Header 再發送 Data。但並非全部瀏覽器會這麼作,例如火狐就不會。
  • 而 GET 方法 Header 和 Data 會一塊兒發送。

URI

URI(Uniform Resource Identifier) 本質上是一個字符串,這個字符串的做用是 惟一地標記資源的位置或者名字

  • scheme 資源應該使用哪一種協議 httphttps。後面必須是 三個特定的字符 ://
  • user:passwd@ 身份信息 表示登陸主機時的用戶名和密碼,以明文形式暴露出來,不推薦
  • authority 資源所在的主機名 一般的形式是 host:port ,端口可省略。
  • path 標記資源所在位置 。URI 的 path 部分必須以 / 開始,也就是必須包含 /
  • query 查詢參數,多個 key = value 的字符串,這些 KV 值用字符 & 鏈接。
  • \#fragment 片斷標識符 它是 URI 所定位的資源內部的一個 錨點 ,瀏覽器在獲取資源後 跳轉到它指示的位置
https://search.jd.com/Search?keyword=openresty&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&wq=openresty&psort=3&click=0
// scheme -> https , authority -> search.jd.com , path -> /Search... , query -> keyword=openresty...
URI只能支持ASCII,對於ASCII 碼之外的字符集和特殊字符,URI經過百分號編碼( Percent Encoding)進行轉義,轉義規則是 直接把非 ASCII 碼或特殊字符轉換成十六進制字節值 ,而後前面再加上一個 %
http://www.baidu.com/?百度一下,你就知道
http://www.baidu.com/?%E7%99%BE%E5%BA%A6%E4%B8%80%E4%B8%8B%EF%BC%8C%E4%BD%A0%E5%B0%B1%E7%9F%A5%E9%81%93

響應報文

狀態行(status line)

響應報文的起始行叫作狀態行(status line),意思是 服務器響應的狀態

版本號(Version of the protocol -> HTTP 1.0 / 1.1 / 2.0)+ 狀態碼(Status code -> 200 / 404)+ 緣由(Status message -> 數字狀態碼補充)

這三個部分一樣使用空格(space)來分隔,最後要用 CRLF 換行表示結束。

狀態碼

狀態碼 類別 含義
1XX Informational(信息性狀態碼) 接收的請求正在處理
2XX Success(成功狀態碼) 請求正常處理完畢
3XX Redirection(重定向狀態碼) 須要進行附加操做以完成請求
4XX Client Error(客戶端錯誤狀態碼) 服務器沒法處理請求
5XX Server Error(服務器錯誤狀態碼) 服務器處理請求出錯

1XX 信息

  • 100 Continue :代表到目前爲止都很正常,客戶端能夠繼續發送請求或者忽略這個響應。

2XX 成功

  • 200 OK :是最多見的成功狀態碼,表示一切正常。若是是非 HEAD 請求,服務器返回的響應頭都會有 body 數據。
  • 204 No Content :請求已經成功處理,可是返回的響應報文不包含實體的主體部分。通常在只須要從客戶端往服務器發送信息,而不須要返回數據時使用。
  • 206 Partial Content :表示客戶端進行了範圍請求,響應報文包含由 Content-Range 指定範圍的實體內容。

3XX 重定向

  • 301 Moved Permanently永久重定向,含義是這次請求的資源已經不存在了,需改用新的 URI 再次訪問。
  • 302 Found臨時重定向,意思是請求的資源還在,但須要暫時用另外一個 URI 來訪問。

    301 和 302 都會在響應頭裏使用字段 Location,指明後續要跳轉的 URI,瀏覽器會重定向到新的 URI

  • 303 See Other :和 302 有着相同的功能,可是 303 明確要求客戶端應該採用 GET 方法獲取資源。
  • 注:雖然 HTTP 協議規定 30一、302 狀態下重定向時不容許把 POST 方法改爲 GET 方法,可是大多數瀏覽器都會在 30一、302 和 303 狀態下的重定向把 POST 方法改爲 GET 方法。
  • 304 Not Modified :若是請求報文首部包含一些條件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,若是不知足條件,則服務器會返回 304 狀態碼。

    • 這個是表示向服務器發起了請求,可是服務器響應該文件沒有變化,沒有傳回數據內容,使用瀏覽器的緩存。
  • 307 Temporary Redirect :臨時重定向,與 302 的含義相似,可是 307 要求瀏覽器不會把重定向請求的 POST 方法改爲 GET 方法。

4XX 客戶端錯誤

  • 400 Bad Request :請求報文中存在語法錯誤。
  • 401 Unauthorized :該狀態碼錶示發送的請求須要有認證信息(BASIC 認證、DIGEST 認證)。若是以前已進行過一次請求,則表示用戶認證失敗。
  • 403 Forbidden :請求被拒絕。
  • 404 Not Found :表示請求的資源在本服務器上未找到,因此沒法提供給客戶端。

剩下的錯誤代碼較明確地說明了錯誤的緣由:

  • 405 Method Not Allowed:不容許使用某些方法操做資源,例如不容許 POST 只能 GET;
  • 406 Not Acceptable:資源沒法知足客戶端請求的條件,例如請求中文但只有英文;
  • 408 Request Timeout:請求超時,服務器等待了過長的時間;
  • 409 Conflict:多個請求發生了衝突,能夠理解爲多線程併發時的競態;
  • 413 Request Entity Too Large:請求報文裏的 body 太大;
  • 414 Request-URI Too Long:請求行裏的 URI 太大;
  • 429 Too Many Requests:客戶端發送了太多的請求,一般是因爲服務器的限連策略;
  • 431 Request Header Fields Too Large:請求頭某個字段或整體太大;

5XX 服務器錯誤

  • 500 Internal Server Error :服務器正在執行請求時發生錯誤。
  • 501 Not Implemented :表示客戶端請求的功能還不支持。
  • 502 Bad Gateway:一般是服務器做爲網關或者代理時返回的錯誤碼,表示服務器自身工做正常,訪問後端服務器時發生了錯誤,但具體的錯誤緣由也是不知道的。
  • 503 Service Unavailable :服務器暫時處於超負載或正在進行停機維護,如今沒法處理請求。

Headers

請求行 + 頭部字段 = 請求頭 ; 狀態行 + 頭部字段 = 響應頭。

請求頭和響應頭的結構是基本同樣的,惟一的區別是起始行,因此請求頭和響應頭裏的字段能夠放在一塊兒介紹。

頭部字段是 key-value 的形式,keyvalue 之間用 : 分隔,最後用 CRLF 換行表示字段結束。

HTTP 頭字段很是靈活,不只能夠使用標準裏的 Host、Connection 等已有頭,也能夠 任意添加自定義頭 ,這就給 HTTP 協議帶來了無限的擴展可能。

不過使用頭字段須要注意下面幾點:

  1. 字段名不區分大小寫,例如 Host 也能夠寫成 host ,但首字母大寫的可讀性更好;
  2. 字段名裏不容許出現空格,能夠使用連字符 - ,但不能使用下劃線 _ 。例如,test-name 是合法的字段名,而 test nametest_name 是不正確的字段名;
  3. 字段名後面必須緊接着 :,不能有空格,而 : 後的字段值前能夠有多個空格;
  4. 字段的順序是沒有意義的,能夠任意排列不影響語義;
  5. 字段原則上不能重複,除非這個字段自己的語義容許,例如 Set-Cookie

通用頭部字段

頭部字段名 說明
Cache-Control 控制緩存的行爲
Connection 控制再也不轉發給代理的首部字段、管理持久鏈接
Date 建立報文的日期時間
Pragma 報文指令
Trailer 報文末端的首部一覽
Transfer-Encoding 指定報文主體的傳輸編碼方式
Upgrade 升級爲其餘協議
Via 代理服務器的相關信息
Warning 錯誤通知

請求頭部字段

頭部字段名 說明
Accept 用戶代理可處理的媒體類型
Accept-Charset 優先的字符集
Accept-Encoding 優先的內容編碼
Accept-Language 優先的語言(天然語言)
Authorization Web 認證信息
Expect 期待服務器的特定行爲
From 用戶的電子郵箱地址
Host 請求資源所在服務器
If-Match 比較實體標記(ETag)
If-Modified-Since 比較資源的更新時間
If-None-Match 比較實體標記(與 If-Match 相反)
If-Range 資源未更新時發送實體 Byte 的範圍請求
If-Unmodified-Since 比較資源的更新時間(與 If-Modified-Since 相反)
Max-Forwards 最大傳輸逐跳數
Proxy-Authorization 代理服務器要求客戶端的認證信息
Range 實體的字節範圍請求
Referer 對請求中 URI 的原始獲取方
TE 傳輸編碼的優先級
User-Agent HTTP 客戶端程序的信息

響應頭部字段

頭部字段名 說明
Accept-Ranges 是否接受字節範圍請求
Age 推算資源建立通過時間
ETag 資源的匹配信息
Location 令客戶端重定向至指定 URI
Proxy-Authenticate 代理服務器對客戶端的認證信息
Retry-After 對再次發起請求的時機要求
Server HTTP 服務器的安裝信息
Vary 代理服務器緩存的管理信息
WWW-Authenticate 服務器對客戶端的認證信息

實體頭部字段

頭部字段名 說明
Allow 資源可支持的 HTTP 方法
Content-Encoding 實體主體適用的編碼方式
Content-Language 實體主體的天然語言
Content-Length 實體主體的大小
Content-Location 替代對應資源的 URI
Content-MD5 實體主體的報文摘要
Content-Range 實體主體的位置範圍
Content-Type 實體主體的媒體類型
Expires 實體主體過時的日期時間
Last-Modified 資源的最後修改日期時間

空行(CRLF)

空行,也就是 「CRLF」,十六進制的「0D0A」。

entity/body

實際傳輸的數據,它不必定是純文本,能夠是圖片、視頻等二進制數據。與 header 對應,被稱爲 body 。

  1. 數據類型表示實體數據的內容是什麼,使用的是 MIME type,相關的頭字段是 Accept 和 Content-Type;
  2. 數據編碼表示實體數據的壓縮方式,相關的頭字段是 Accept-Encoding 和 Content-Encoding;
  3. 語言類型表示實體數據的天然語言,相關的頭字段是 Accept-Language 和 Content-Language(一般不會發送);
  4. 字符集表示實體數據的編碼方式,相關的頭字段是 Accept-Charset(一般不會發送) 和 Content-Type;

MIME type

多用途互聯網郵件擴展(Multipurpose Internet Mail Extensions),簡稱爲 MIME。HTTP 取了其中的一部分,用來標記 body 的數據類型 ,這就是咱們日常總能聽到的 MIME type

經常使用的MIME type

  1. text:即文本格式的可讀數據,text/html(超文本文檔)、text/plain(純文本)、text/css(樣式表) 等。
  2. image:即圖像文件,有 image/gifimage/jpegimage/png 等。
  3. audio/video:音頻和視頻數據,例如 audio/mpegvideo/mp4 等。
  4. application:數據格式不固定,多是文本也多是二進制,必須由上層應用程序來解釋。常見的有 application/jsonapplication/javascriptapplication/pdf 等,另外,若是實在是不知道數據是什麼類型,像剛纔說的黑盒,就會是 application/octet-stream即不透明的二進制數據
Accept: text/html,application/xml,image/webp,image/png //客戶端可接收類型,","分隔符列出多個類型。
Content-Type: text/html //實體數據的真實類型。

Encoding type

HTTP 在傳輸時爲了節約帶寬,有時候還會 壓縮數據 ,經過Encoding type解壓縮。

經常使用的Encoding type只有下面三種:

  1. gzip:GNU zip 壓縮格式,也是互聯網上最流行的壓縮格式;
  2. deflate:zlib(deflate)壓縮格式,流行程度僅次於 gzip;
  3. br:一種專門爲 HTTP 優化的新壓縮算法(Brotli)。
Accept-Encoding: gzip, deflate, br //客戶端支持的壓縮格式,若是沒有就表示客戶端不支持壓縮數據。
Content-Encoding: gzip //實體數據使用的壓縮格式,若是沒有就表示相應數據沒被壓縮。

語言類型和字符集

爲了解決語言文字國際化的問題,又引入了兩個概念:語言類型字符集

Accept-Language: zh-CN, zh, en //請求頭字段,表示客戶端可理解的天然語言,用`,` 作分隔符列出多個類型。
Content-Language: zh-CN //實體頭字段,告訴客戶端實體數據使用的語言類型。(不會發送)

Accept-Charset: gbk, utf-8 //請求頭字段,表示瀏覽器請求的字符集。(不會發送)
Content-Type: text/html; charset=utf-8 //通用頭字段裏面包含服務器返回的實體數據字符集。

瀏覽器都支持多種字符集,一般不會發送 Accept-Charset,而服務器也不會發送 Content-Language,由於使用的語言徹底能夠由字符集推斷出來,因此在請求頭裏通常只會有 Accept-Language 字段,響應頭裏只會有 Content-Type 字段。

內容協商的質量值和結果

Accept: text/html,application/xml;q=0.9,*/*;q=0.8
Vary: Accept-Encoding,User-Agent,Accept

在 HTTP 協議裏用 Accept、Accept-Encoding、Accept-Language 等請求頭字段進行內容協商的時候,還能夠用一種特殊的 q 參數表示權重來設定優先級,這裏的 qquality factor的意思。

權重的最大值是 1,最小值是 0.01,默認值是 1,若是值是 0 就表示拒絕。具體的形式是在數據類型或語言代碼後面加一個 ; ,而後是 q=value

這裏要提醒的是 ; 的用法,在大多數編程語言裏 ; 的斷句語氣要強於 , ,而在 HTTP 的內容協商裏卻剛好反了過來,; 的意義是小於 , 的。

這個 Vary 字段表示服務器依據了 Accept-Encoding、User-Agent 和 Accept 這三個頭字段,而後決定了發回的響應報文。

Vary 字段能夠認爲是響應報文的一個特殊的 版本標記 。每當 Accept 等請求頭變化時,Vary 也會隨着響應報文一塊兒變化。也就是說,同一個 URI 可能會有多個不一樣的「版本」,主要用在傳輸鏈路中間的代理服務器實現緩存服務 ,這個以後講 HTTP 緩存 時還會再提到。

HTTP 傳輸大文件的方法

數據壓縮

Accept-Encoding: gzip, deflate, br //客戶端支持的壓縮格式,若是沒有就表示客戶端不支持壓縮數據。
Content-Encoding: gzip //實體數據使用的壓縮格式,若是沒有就表示相應數據沒被壓縮。

分塊傳輸

Transfer-Encoding: chunked //通用頭字段,body 分紅了許多的塊(chunk)逐個發送。

Transfer-Encoding: chunkedContent-Length 這兩個字段是 互斥的 ,也就是說響應報文裏這兩個字段不能同時出現,一個響應報文的傳輸要麼是長度已知,要麼是長度未知(chunked)。

  1. 每一個分塊包含兩個部分,長度頭和數據塊;
  2. 長度頭是以 CRLF(回車換行,即 \r\n )結尾的一行明文,用 16 進制數字表示長度;
  3. 數據塊緊跟在長度頭後,最後也用 CRLF 結尾,但數據不包含 CRLF
  4. 最後用一個長度爲 0 的塊表示結束,即 0\r\n\r\n
HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

7\r\n //length 是以十六進制的形式表示。\r\n 是 CRLF
Mozilla\r\n
9\r\n
Developer\r\n
7\r\n
Network\r\n
0\r\n
\r\n

範圍請求(range requests)

在視頻中拖動進度條,這其實是想獲取一個大文件其中的片斷數據 ,而分塊傳輸並無這個能力。

HTTP 協議爲了知足這樣的需求,提出了 範圍請求 (range requests)的概念,容許客戶端在請求頭裏使用專用字段來表示只獲取文件的一部分 。

Accept-Ranges: none //不支持範圍請求
Accept-Ranges: bytes = x - y //支持範圍請求, x 和 y 是字節爲單位的數據範圍,表示的是偏移量。

//假設文件是100字節
Accept-Ranges: bytes = 0- //表示從文檔起點到文檔終點,至關於 0-99 ,即整個文件;
Accept-Ranges: bytes = 10- //是從第 10 個字節開始到文檔末尾,至關於 10-99;
Accept-Ranges: bytes = -1 //是文檔最後一個字節,至關於 99-99;
Accept-Ranges: bytes = -10 //是從文檔末尾倒數 10 個字節,至關於 90-99。

服務器收到 Range 字段後,須要作四件事:

  1. 檢查範圍是否合法。範圍越界返回狀態碼 416
  2. 範圍正確,根據 Range 頭計算偏移量,讀取文件片斷。返回狀態碼 206
  3. 服務器添加響應頭字段Content-Range 。格式是bytes x-y/length
  4. 發送數據。
HTTP/1.1 206 Partial Content
Date: Wed, 15 Nov 2015 06:25:24 GMT
Last-Modified: Wed, 15 Nov 2015 04:58:08 GMT
Content-Range: bytes 21010-47021/47022
Content-Length: 26012
Content-Type: image/gif

... 26012 bytes of partial image data ...

多段數據

Range: bytes=0-9, 20-29, 30-39 //請求頭

響應頭使用的MIMEmultipart/byteranges,還有個參數boundary=xxx是用來分隔字節序列的。

每一個字節序列還須要用 Content-TypeContent-Range 標記這段數據的類型和所在範圍。

HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=00001111
Content-Length: 189
Connection: keep-alive
Accept-Ranges: bytes
 
 
--00001111
Content-Type: text/plain
Content-Range: bytes 0-9/96
 
// this is
--00001111
Content-Type: text/plain
Content-Range: bytes 20-29/96
 
ext json d
--00001111--

HTTP鏈接管理

短鏈接與長鏈接

當瀏覽器訪問一個包含多張圖片的 HTML 頁面時,除了請求訪問的 HTML 頁面資源,還會請求圖片資源。若是每進行一次 HTTP 通訊就要新建一個 TCP 鏈接,那麼開銷會很大。

長鏈接只須要創建一次 TCP 鏈接就能進行屢次 HTTP 通訊。

  • 從 HTTP/1.1 開始默認是長鏈接的,若是要斷開鏈接,須要由客戶端或者服務器端提出斷開,使用 Connection : close
  • 在 HTTP/1.1 以前默認是短鏈接的,若是須要使用長鏈接,則使用 Connection : Keep-Alive

隊頭阻塞(Head-of-line blocking)

隊頭阻塞與短鏈接和長鏈接無關 ,而是由 HTTP 基本的 請求 - 應答 模型所致使的。

由於 HTTP 規定報文必須是 一發一收 ,這就造成了一個先進先出的 串行隊列 。隊列裏的請求沒有優先級,只有入隊的前後順序,排在最前面的請求被優先處理。若是隊首的請求處理的慢,後面的全部請求就要一直等待。這就致使隊頭阻塞。

性能優化

併發鏈接(concurrent connections)

同時對一個域名發起多個長鏈接,用數量來解決質量的問題 。但這種方式也存在缺陷。若是每一個客戶端都想本身快,創建不少個鏈接,用戶數×併發數就會是個天文數字。服務器的資源根本就扛不住,或者被服務器認爲是惡意攻擊,反而會形成拒絕服務。

HTTP 協議建議客戶端使用併發,但不能「濫用」併發。

域名分片(domain sharding)

HTTP 協議和瀏覽器不是限制併發鏈接數量嗎?好,那我就多開幾個域名,好比 shard1.chrono.comshard2.chrono.com,而這些域名都指向同一臺服務器 www.chrono.com ,這樣實際長鏈接的數量就又上去了,也就解決了隊頭阻塞問題。

HTTP 的重定向和跳轉

https://im.qq.com/download/

QQ 主頁點一下 下載 鏈接,會發生什麼呢?

瀏覽器解析文字裏的 URI -> 用這個 URI 發起一個新的 HTTP 請求 -> 得到響應報文,渲染出新 URI 指向的頁面。

這樣由瀏覽器使用者發起的跳轉叫 主動跳轉,由服務器發起的跳轉,被叫作 重定向(Redirection)。

Location:https://im.qq.com/download/ //絕對URI。
Location:/index.html  //相對URI,Location是響應頭部字段。

重定向的應用場景

  • 資源不可用,須要用另外一個新的URI來代替。例如域名變動、服務器變動、網站改版、系統維護等。
  • 避免重複,讓多個網址都跳到一個URI,增長訪問入口不會增長額外的工做量。例如,有的網站都會申請多個名稱相似的域名,而後把它們再重定向到主站上。

重定向的相關問題

  • 性能消耗 兩次 請求-應答,比正常訪問多一次。適當使用,不能濫用。
  • 循環跳轉 若是重定向的策略設置欠考慮,可能會出現 A=>B=>C=>A 的無限循環。瀏覽器必須具備檢測 循環跳轉 的能力,在發現這種狀況時應當中止發送請求並給出錯誤提示。

HTTP Cookie

HTTP 協議是無狀態的,主要是爲了讓 HTTP 協議儘量簡單,使得它可以處理大量事務。HTTP/1.1 引入 Cookie 來保存狀態信息。

Cookie 是服務器發送到用戶瀏覽器並保存在本地的一小塊數據,它會在瀏覽器以後向同一服務器再次發起請求時被攜帶上,用於告知服務端兩個請求是否來自同一瀏覽器。因爲以後每次請求都會須要攜帶 Cookie 數據,所以會帶來額外的性能開銷(尤爲是在移動環境下)。

Cookie 曾一度用於客戶端數據的存儲,由於當時並無其它合適的存儲辦法而做爲惟一的存儲手段,但如今隨着現代瀏覽器開始支持各類各樣的存儲方式,Cookie 漸漸被淘汰。新的瀏覽器 API 已經容許開發者直接將數據存儲到本地,如使用 Web storage API(本地存儲和會話存儲)或 IndexedDB。

Cookie 的工做過程

  1. 客戶端發送 HTTP 請求到服務器
  2. 當服務器收到 HTTP 請求時,在響應頭裏面添加一個 Set-Cookie 字段
  3. 瀏覽器收到響應後保存下 Cookie
  4. 以後對該服務器每一次請求中都經過 Cookie 字段將 Cookie 信息發送給服務器。

Cookie 的屬性

Cookie: name=value; name2=value2; name3=value3 //請求頭
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly...

Cookie 的生命週期

Set-Cookie:Expires=Wed, 21 Oct 2015 07:28:00 GMT; Max-Age=10; //Max-Age優先級更高
  • Expires 俗稱 過時時間,用的是 絕對時間點 ,能夠理解爲 截止日期(deadline)。
  • Max-Age 用的是相對時間,單位是秒,瀏覽器用收到報文的時間點再加上 Max-Age,就能夠獲得失效的絕對時間。

Cookie 的做用域

Domain 標識指定了哪些主機能夠接受 Cookie。若是不指定,默認爲當前文檔的主機(不包含子域名)。若是指定了 Domain,則通常包含子域名。例如,若是設置 Domain=mozilla.org,則 Cookie 也包含在子域名中(如 developer.mozilla.org)。

Set-Cookie: Domain = mozilla.org;

Path 標識指定了主機下的哪些路徑能夠接受 Cookie(該 URL 路徑必須存在於請求 URL 中)。以字符 %x2F ("/") 做爲路徑分隔符,子路徑也會被匹配。例如,設置 Path=/docs,則如下地址都會匹配:

/docs
/docs/Web/
/docs/Web/HTTP

大多數狀況只用一個/或者直接省略,表示域名下任意路徑都容許使用Cookie。

Cookie 的安全性

  • HttpOnly Cookie 只能經過 HTTP 協議傳輸,禁止其餘訪問方式。瀏覽器會禁用document.cookie 等一切相關的 API。這樣就會阻止 跨站腳本(XSS)
  • SameSite 能夠設置成Strict / Lax / None分別表明嚴格限定Cookie跨站 / 容許GET/HEAD跨站 / 不限制。能夠防範跨站請求僞造(XSRF)攻擊
  • Secure 表示這個 Cookie 僅能用 HTTPS 協議加密傳輸 ,明文的 HTTP 協議會禁止發送。但 Cookie 自己不是加密的,瀏覽器裏仍是以明文的形式存在。

    • 注意:非安全站點(http:)已經不能再在 cookie 中設置 secure 指令了(在Chrome 52+ and Firefox 52+ 中新引入的限制)。

Cookie 的應用

Cookie 主要用於如下三個方面:

  • 會話狀態管理(如用戶登陸狀態、購物車、遊戲分數或其它須要記錄的信息)。
  • 個性化設置(如用戶自定義設置、主題等)。
  • 瀏覽器行爲跟蹤(如跟蹤分析用戶行爲等)。

Cookie缺點

  • 長度限制 只有4KB。
  • 安全問題 因爲Cookie明文傳遞,很容易被攔截、篡改,攔截以後會暴露session信息。
  • 額外開銷 Cookie 在每次發起 HTTP 請求的時候都會被髮送給服務器,會增長開銷。
  • 某些客戶端不支持 cookie 。

HTTP 的緩存控制

緩存(cache)是一種保存資源副本並在下次請求時直接使用該副本的技術。當 web 緩存發現請求的資源已經被存儲,它會攔截請求,返回該資源的拷貝,而不會去源服務器從新下載。

強緩存

不須要發送請求到服務端,直接讀取瀏覽器本地緩存,在 Chrome 的 Network 中顯示的 HTTP 狀態碼是 200 ,在 Chrome 中,強緩存又分爲 Disk Cache(存放在硬盤中)和 Memory Cache(存放在內存中),存放的位置是由瀏覽器控制的。是否強緩存由 ExpiresCache-ControlPragma 3 個 Header 屬性共同來控制。

Pragma

Pragma:no-cache; //禁用緩存,只用於HTTP1.0的狀況。響應頭字段不支持這個屬性。

Expires

Expires所定義的緩存時間是相對服務器上的時間而言的,其定義的是資源「失效時刻」,若是客戶端上的時間跟服務器上的時間不一致(特別是用戶修改了本身電腦的系統時間),那緩存時間可能就沒啥意義了。

Expires:Wed, 21 Oct 2015 07:28:00 GMT;//http1.0 指定緩存的過時時間。實體頭字段。

Cache-Control

針對上述的「Expires時間是相對服務器而言,沒法保證和客戶端時間統一」的問題,HTTP1.1新增了 Cache-Control 來定義緩存過時時間。

//可緩存性
Cache-Control:no-cache;//至關於`max-age=0,must-revalidate`即資源被緩存,可是緩存馬上過時。同時下次訪問時強制驗證資源有效性。
Cache-Control:no-store;//請求和響應都不緩存。通用頭字段。
Cache-Control:public;//代表響應能夠被髮送請求的客戶端,代理服務器等緩存。響應頭字段。
Cache-Control:private;//代表響應只能被單個用戶緩存,不能做爲共享緩存(即代理服務器不能緩存它)。響應頭字段。
//到期
Cache-Control:max-age=<seconds>;//緩存資源,可是在指定時間(單位爲秒)後過時。時間是相對於請求的時間。通用頭字段。 
Cache-Control:s-maxage=<seconds>;//覆蓋max-age或者Expires頭,可是僅適用於共享緩存(好比各個代理),私有緩存會忽略它。響應頭字段。
Cache-Control:max-stale[=<seconds>];//指定時間內,即便緩存過時,資源依然有效。請求頭字段。
Cache-Control:min-fresh=<seconds>;//緩存的資源至少要保持指定時間的新鮮期。請求頭字段。
//從新驗證和從新加載
Cache-Control:must-revalidate;//一旦資源過時(好比已經超過max-age),在成功向原始服務器驗證以前,緩存不能用該資源響應後續請求。響應頭字段。
Cache-Control:proxy-revalidate;//與must-revalidate做用相同,但它僅適用於共享緩存(例如代理),並被私有緩存忽略。響應頭字段。
//其餘
Cache-Control:no-transform;//強制要求代理服務器不要對資源進行轉換,禁止代理服務器對 Content-Encoding、Content-Range、Content-Type 字段的修改(所以代理的 gzip 壓縮將不被容許)。請求頭字段。
Cache-Control:only-if-cached;//僅僅返回已經緩存的資源,不訪問網絡,若無緩存則返回504。請求頭字段。

假設所請求資源於4月5日緩存, 且在4月12日過時。

max-agemax-stalemin-fresh 同時使用時, 它們的設置相互之間獨立生效, 最爲保守的緩存策略老是有效。

這意味着,若是max-age = 10 daysmax-stale = 2 daysmin-fresh = 3 days, 那麼:

  • 根據max-age的設置,覆蓋原緩存週期,緩存資源將在4月15日失效( 5 + 10 = 15);
  • 根據max-stale的設置, 緩存過時後兩天依然有效,此時響應將返回110(Response is stale)狀態碼, 緩存資源將在4月14日失效( 12 + 2 = 14);
  • 根據min-fresh的設置,至少要留有3天的新鮮期,緩存資源將在4月9日失效( 12 - 3 = 9);

因爲客戶端老是採用最保守的緩存策略, 所以, 4月9往後,對於該資源的請求將從新向服務器發起驗證。

協商緩存

當客戶端上某個資源保存的緩存時間過時了,但這時候服務器並無更新過這個資源,若是這個資源很大,咱們有必要再把這個資源從新發一遍嗎?

答案是否認的。爲了讓客戶端與服務器之間能實現緩存文件是否更新的驗證、提高緩存的複用率,HTTP1.1新增了幾個首部字段來作這件事情。

Last-Modified / If-Modified-Since / If-Unmodified-Since

  1. 當客戶端發送請求後,服務器端會將資源最後的修改時間以Last-Modifie:GMT的形式加在實體頭字段上一塊兒返回給客戶端。
  2. 客戶端爲資源標記上該信息,下次再次請求時,會把該信息附帶在請求報文中一併帶給服務器去作檢查。

    • 若傳遞的時間與服務器上該資源最終修改時間是一致的,則說明該資源沒有被修改過,直接返回狀態碼304內容爲空,這樣就節省了帶寬和時間。
    • 若是兩個時間不一致,則服務器會發回該資源並返回狀態碼200,和第一次請求相似。

Last-Modified用於標記請求資源的最後一次修改時間, 格式爲GMT(格林尼治標準時間)。

If-Modified-Since緩存校驗字段, 其值爲上次響應頭的Last-Modified值,若與請求資源當前的Last-Modified值相同,那麼將返回304狀態碼的響應,反之, 將返回200狀態碼響應。請求頭字段。最多見的應用場景:

  • 最多見的應用場景是來更新沒有特定 ETag 標籤的緩存實體。

If-Unmodified-Since 緩存校驗字段,其值爲上次響應頭的Last-Modified值,表示資源在指定的時間以後未修改則正常執行更新,不然返回412(Precondition Failed)狀態碼的響應。請求頭字段。經常使用於以下兩種場景:

  • 不安全的請求,好比說使用POST請求更新Wiki文檔,文檔未修改時才執行更新。
  • If-Range 字段同時使用時, 能夠用來保證新的片斷請求來自一個未修改的文檔。
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT;//精確度比ETag低,備用機制。HTTP1.0。
If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT;//當與 If-None-Match 一同出現時,它(If-Modified-Since)會被忽略掉。
If-Unmodified-Since: Wed, 21 Oct 2015 07:28:00 GMT;

Last-Modified存在的問題:

  1. Last-Modified 標註的最後修改只能精確到秒級,若是某些文件在 1 秒鐘之內,被修改屢次的話,它將不能準確標註文件的新鮮度;
  2. 某些文件也許會週期性的更改,可是他的內容並不改變(僅僅改變的修改時間),但 Last-Modified 卻改變了,致使文件無法使用緩存;
  3. 有可能存在服務器沒有準確獲取文件修改時間,或者與代理服務器時間不一致等情形。

ETag / If-None-Match / If-Match

爲了解決上述Last-Modified可能存在的的問題,HTTP1.1還推出了 ETag 實體頭字段。

服務器經過算法,給資源計算得出一個惟一標識符,在把資源響應給客戶端的時候,會在實體頭字段加上ETag:惟一標識符一塊兒返回給客戶端。例如:

ETag: "x234dff";

客戶端會保留該ETag字段,並在下一次請求時將其一併帶過去給服務器。服務器只要對比客戶端(做爲If-None-Match字段的值一塊兒發送)傳來的ETag和本身服務器上該資源的ETag是否一致,就能很好的判斷資源相對客戶端而言是否被修改過了。

  • 若是服務器發現ETag匹配不上,那麼直接以常規GET200回包形式將新資源(固然也包括了新的ETag)發給客戶端。
  • 若是ETag一致,則直接返回304知會客戶端直接使用本地緩存便可。
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"//強緩存 ETag 要求資源在字節級別必須徹底相符。
ETag: W/"0815"//弱ETag 只要求資源在語義上沒變化,但內部可能會發生了變化(HTML裏的標籤順序調整,多出了幾個空格)。

If-None-Match: "bfc13a64729c4290ef5b2c2730249c88ca92d82d"//請求頭字段。
If-None-Match: W/"67ab43", "54ed21", "7892dd"
If-None-Match: * //星號是一個特殊值,能夠表明任意資源。它只用在進行資源上傳時,一般是採用 PUT 方法,來檢測擁有相同識別ID的資源是否已經上傳過了。
    
If-Match: "bfc13a64729c4290ef5b2c2730249c88ca92d82d"//請求頭字段。
If-Match: W/"67ab43", "54ed21", "7892dd"
If-Match: * //星號指代任意資源。

If-None-Match

經常使用於判斷緩存資源是否有效

  • 在請求方法爲 GET 和 HEAD 的狀況下

    • ETag列表不匹配,服務器返回狀態碼200的響應。
    • ETag列表匹配,服務器返回狀態碼304的響應。
  • 其餘方法,尤爲是PUT,將If-None-Match used 的值設置爲 *,用來生成事先並不知道是否存在的文件,能夠確保先前並無進行過相似的上傳操做,防止以前操做數據的丟失。

當與 If-Modified-Since 一同使用的時候,If-None-Match 優先級更高(假如服務器支持的話)。

If-Match

經常使用於判斷條件是否知足

  • 對於 GETHEAD 方法,搭配 Range頭字段 使用,能夠用來保證新請求的範圍與以前請求的範圍是對同一份資源的請求。若是 ETag 沒法匹配,那麼須要返回 416 (Range Not Satisfiable,範圍請求沒法知足) 響應。
  • 對於其餘方法來講,尤爲是 PUT, If-Match 頭字段能夠用來避免更新丟失問題。它能夠用來檢測用戶想要上傳的不會覆蓋獲取原始資源以後作出的更新。若是請求的條件不知足,那麼須要返回 412 (Precondition Failed,先決條件失敗) 響應。

緩存頭部對比

頭部 優點和特色 劣勢和問題
Expires 一、HTTP 1.0 產物,能夠在HTTP 1.0和1.1中使用,簡單易用。 二、以時刻標識失效時間。 一、時間是由服務器發送的(UTC),若是服務器時間和客戶端時間存在不一致,可能會出現問題。 二、存在版本問題,到期以前的修改客戶端是不可知的。
Cache-Control 一、HTTP 1.1 產物,以時間間隔標識失效時間,解決了Expires服務器和客戶端相對時間的問題。 二、比Expires多了不少選項設置。 一、HTTP 1.1 纔有的內容,不適用於HTTP 1.0 。 二、存在版本問題,到期以前的修改客戶端是不可知的。
Last-Modified 一、不存在版本問題,每次請求都會去服務器進行校驗。服務器對比最後修改時間若是相同則返回304,不一樣返回200以及資源內容。 一、只要資源修改,不管內容是否發生實質性的變化,都會將該資源返回客戶端。例如週期性重寫,這種狀況下該資源包含的數據實際上同樣的。 二、以時刻做爲標識,沒法識別一秒內進行屢次修改的狀況。 三、某些服務器不能精確的獲得文件的最後修改時間。
ETag 一、能夠更加精確的判斷資源是否被修改,能夠識別一秒內屢次修改的狀況。 二、不存在版本問題,每次請求都回去服務器進行校驗。 一、計算ETag值須要性能損耗。 二、分佈式服務器存儲的狀況下,計算ETag的算法若是不同,會致使瀏覽器從一臺服務器上得到頁面內容後到另一臺服務器上進行驗證時發現ETag不匹配的狀況。

緩存優先級

Pragma > Cache-Control > Expires

ETag > Last-Modified(同時存在的時候,Last-ModifiedETag 備份)。

瀏覽器能夠在內存、硬盤中開闢一個空間用於保存請求資源副本。

請求一個資源時,會按照Service Worker -> Memory Cache -> Disk Cache -> Push Cache依次查找緩存。

200 from memory cache 表示不訪問服務器,直接從內存中讀取緩存。 儲存較小的文件,關閉進程後緩存資源隨之銷燬。

200 from disk cache 表示不訪問服務器,直接從硬盤中讀取緩存。存儲大文件,關閉進程後,緩存資源依然存在。

200 from prefetch cachepreload (預加載)或 prefetch (預先載入)的資源加載時,二者也是均存儲在 http cache,當資源加載完成後,若是資源是能夠被緩存的,那麼其被存儲在 http cache 中等待後續使用;若是資源不可被緩存,那麼其在被使用前均存儲在 memory cache

緩存請求流程

用戶行爲

  • 當使用F5刷新網頁時,會跳過強緩存,可是會檢查協商緩存。
  • 當使用Ctrl+F5 強制刷新網頁時,直接從服務器下載新資源,跳過強緩存和協商緩存。

HTTP 的代理服務

代理在客戶端和服務器本來的通訊鏈路中插入的一箇中間環節 ,也是服務器,但提供的是代理服務。

所謂的代理服務是指服務自己不產生內容,而是處於中間位置 轉發上下游的請求和響應,具備雙重身份:面向下游的用戶時,表現爲服務器,表明源服務器響應客戶端的請求;而面向上游的源服務器時,又表現爲客戶端,表明客戶端發送請求。

這裏主要說的是最多見的反向代理

代理的做用

  • 負載均衡:經過算法把外部的流量合理地分散到多臺源服務器,提升系統的總體資源利用率和性能。
  • 健康檢查:使用 心跳 等機制監控後端服務器,發現有故障就及時 踢出 集羣,保證服務高可用;
  • 安全防禦:保護被代理的後端服務器,限制 IP 地址或流量,抵禦網絡攻擊和過載;
  • 加密卸載:對外網使用 SSL/TLS 加密通訊認證,而在安全的內網不加密,消除加解密成本;
  • 數據過濾:攔截上下行的數據,任意指定策略修改請求或者響應;
  • 內容緩存:暫存、複用服務器響應,這個與 緩存控制密切相關,咱們稍後再說。

代理相關頭字段

Via 代理服務器用它標明代理的身份。Via 是一個通用頭字段,請求頭或響應頭裏均可以出現。

有多個代理節點的狀況下,客戶端發送請求和服務器返回響應via頭字段順序不同:

Via 字段只解決了 客戶端和源服務器判斷是否存在代理的問題,還不能知道對方的真實信息

但服務器的 IP 地址應該是保密的,關係到企業的內網安全,因此通常不會讓客戶端知道。不過反過來,一般服務器須要知道客戶端的真實 IP 地址,方便作訪問控制、用戶畫像、統計分析

惋惜的是 HTTP 標準裏並無爲此定義頭字段 ,但已經出現了不少 事實上的標準 ,最經常使用的兩個頭字段是 X-Forwarded-ForX-Real-IP

  • X-Forwarded-For:鏈式存儲

    字面意思是爲 誰而轉發 ,形式上和 Via 差很少,也是每通過一個代理節點就會在字段裏追加一個信息,但 Via 追加的是代理主機名(或者域名),而 X-Forwarded-For 追加的是請求方的 IP 地址。因此,在字段裏最左邊的 IP 地址就客戶端的地址。

  • X-Real-IP:只有客戶端 IP 地址

    是另外一種獲取客戶端真實 IP 的手段,它的做用很簡單,就是記錄客戶端 IP 地址,沒有中間的代理信息。

    若是客戶端和源服務器之間只有一個代理,那麼這兩個字段的值就是相同的。

代理協議

X-Forwarded-For缺點:

經過 X-Forwarded-For 操做代理信息 必需要解析 HTTP 報文頭 ,會下降代理的轉發性能

另外一個問題是 X-Forwarded-For 等頭 必需要修改原始報文 ,而有些狀況下是不容許甚至不可能的(好比使用 HTTPS 通訊被加密 )。

因此就出現了一個專門的 代理協議 (The PROXY protocol) ,它由知名的代理軟件 HAProxy 所定義,也是一個 事實標準 ,被普遍採用(注意並非 RFC)。

代理協議有 v1 和 v2 兩個版本,v1 和 HTTP 差很少,也是明文,而 v2 是二進制格式。

v1版本在HTTP 報文前增長了一行 ASCII 碼文本,這一行文本其實很是簡單,開頭必須是 PROXY 五個大寫字母,而後是 TCP4 或者 TCP6 ,表示客戶端的 IP 地址類型,再後面是請求方地址、應答方地址、請求方端口號、應答方端口號,最後用一個回車換行(\r\n)結束。

PROXY TCP4 1.1.1.1 2.2.2.2 55555 80\r\n
GET / HTTP/1.1\r\n
Host: www.xxx.com\r\n
\r\n
你以爲代理有什麼缺點?實際應用時如何避免?

代理的缺點是增長鏈路長度,會增長響應耗時,應儘可能減小在代理商所作的的一些與業務無關的複雜耗時操做。

HTTP 的緩存代理

緩存控制 + 代理服務 = 緩存代理

加入了緩存後代理服務收到源服務器發來的響應數據後須要作兩件事:

  • 第一個固然是把報文轉發給客戶端。
  • 而第二個就是把報文存入本身的 Cache 裏。

下一次再有相同的請求,代理服務器就能夠直接發送 304 或者緩存數據,沒必要再從源服務器那裏獲取。這樣就下降了客戶端的等待時間,同時節約了源服務器的網絡帶寬。

源服務器的緩存控制

數據是否容許代理緩存:

  • private:表示緩存只能在客戶端保存,是用戶 私有 的。
  • public:容許

緩存失效後從新驗證:proxy-revalidate 代理緩存過時後必須驗證,對應的是客戶端的(must-revalidate)。

緩存的生存時間:s-maxage 限制在代理服務器上能緩存多久。

private, max-age = 5;
public, max-age = 5, s-maxage = 10;
max-age = 30, proxy-revalidate, no-transform;

客戶端的緩存控制

only-if-cached

表示 只接受代理緩存的數據 ,不接受源服務器的響應。若是代理上沒有緩存或者緩存過時,就應該給客戶端返回一個 504(Gateway Timeout)。

Vary: Accept-Encoding

對於服務器而言,資源文件可能不止一個版本,好比說壓縮和未壓縮,針對不一樣的客戶端,一般須要返回不一樣的資源版本。好比說老式的瀏覽器可能不支持解壓縮,這個時候,就須要返回一個未壓縮的版本; 對於新的瀏覽器,支持壓縮,返回一個壓縮的版本,有利於節省帶寬,提高體驗。 那麼怎麼區分這個版本呢, 這個時候就須要Vary了。

服務器經過指定Vary: Accept-Encoding, 告知代理服務器, 對於這個資源, 須要緩存兩個版本: 壓縮和未壓縮。這樣老式瀏覽器和新的瀏覽器,經過代理,就分別拿到了未壓縮和壓縮版本的資源,避免了都拿同一個資源的尷尬 。

Vary:Accept-Encoding,User-Agent;

Age 和 Date

出現此字段, 表示命中代理服務器的緩存。它指的是代理服務器對於請求資源的已緩存時間, 單位爲秒。以下:

Age:2383321
Date:Wed, 08 Mar 2017 16:12:42 GMT

以上指的是,代理服務器在2017年3月8日16:12:42時向源服務器發起了對該資源的請求, 目前已緩存了該資源2383321秒。

Date 指的是響應生成的時間。 請求通過代理服務器時,返回的Date未必是最新的, 一般這個時候,代理服務器將增長一個Age字段告知該資源已緩存了多久。

緩存實踐

靜態資源

永遠不會修改的內容:JS 和 CSS 文件,圖片,和任何類型的二進制文件都屬於這個類目。

永遠,我確實說的是永遠。爲靜態資源指定版本號是很通用的作法。它們不管何時改動了,它們的 URL 就改變了。

這裏是一些針對靜態資源的簡單的規則:

  • 在文件或者路徑中嵌入指紋。避免爲指紋使用查詢字符串。另外,確保生成的URL長度超過8個不一樣的字符。
<link rel = "Stylesheet" href="http://static.xxx.com/a_f02bc2.css">
<script src = "http://static.xxx.com/a_13cfa51.js"
  • 使用這些 HTTP 頭:
Cache-Control: public, max-age=31536000
Expires: (一年後的今天)
ETag: (基於內容生成)
Last-Modified: (過去某個時間)
Vary: Accept-Encoding

動態資源

針對應用程序私密性和新鮮度方面需求的不一樣,咱們應該使用不一樣的緩存控制設置。

對於非私密性和常常性變更的資源(想像一下股票信息),咱們應該使用下面這些:

Cache-Control: public, max-age=0 
Expires: (當前時間) 
ETag: (基於內容生成) 
Last-Modified: (過去某個時間) 
Vary: Accept-Encoding

這些設置的效果是:

  • 這些資源能夠被公開地(經過瀏覽器和代理服務器)緩存起來。每一次在瀏覽器使用這些資源以前,瀏覽器或者代理服務器會檢查這些資源是否有更新的版本,若是有,就把它們下載下來。
  • 這樣的設置須要注意,瀏覽器在從新檢查資源時效性方面有必定的靈活性。典型的是,當用戶點擊了「返回/前進」按鈕時,瀏覽器不會從新檢查這些資源文件,而是直接使用緩存的版本。

你若是須要更嚴格的控制,須要告知瀏覽器即便當用戶點擊了「返回/前進」按鈕,也須要從新檢查這些資源文件,那麼能夠使用:

Cache-Control: public, no-cache, no-store

不是全部的動態資源都會立刻變成過期的資源。若是它們能夠保持至少5分鐘的時效,能夠使用:

Cache-Control: public, max-age=300

通過這樣的設置,瀏覽器只會在5分鐘以後才從新檢查。在這以前,緩存的內容會被直接使用。若是在5分鐘後,這些過期的內容須要嚴格控制,你能夠添加 must-revalidate 字段:

Cache-Control: public, max-age=300, must-revalidate

對於私密或者針對用戶的內容,須要把 public 替換爲 private 以免內容被代理緩存。

Cache-Control: private, …

HTTP優缺點

優勢

  • 簡單、靈活和易於擴展;

    • 簡單 基本的報文格式就是 header+body,頭部信息也是簡單的文本格式,用的也都是常見的英文單詞。
    • 靈活和易於擴展 協議裏的請求方法、URI、狀態碼、緣由短語、頭字段等每個核心組成要素都沒有被寫死,容許開發者任意定製、擴充或解釋。
  • 擁有成熟的軟硬件環境,應用的很是普遍,是互聯網的基礎設施;
  • 無狀態,能夠輕鬆實現集羣化,擴展性能;
  • 明文傳輸 不須要藉助任何外部工具,就能夠很容易地查看或者修改。

缺點

  • 無狀態,沒有記憶能力,它就沒法支持須要連續多個步驟的事務操做,比方說網絡購物;
  • 明文傳輸,數據徹底肉眼可見,容易被竊聽;
  • 不安全,沒法驗證通訊雙方的身份,也不能判斷報文是否被竄改。

HTTPS

HTTPS 實際上是一個「很是簡單」的協議,裏面規定了 新的協議名「https」,默認端口號 443 ,至於其餘的請求 - 應答模式、報文結構、請求方法、URI、頭字段、鏈接管理等等都徹底沿用 HTTP。

HTTPS = HTTP + SSL/TLS

對稱加密和非對稱加密

對稱加密

對稱加密(Symmetric-Key Encryption),就是指加密和解密時使用的 密鑰都是同一個 ,是 對稱 的。只要保證了密鑰的安全,那整個通訊過程就能夠說具備了機密性。

目前經常使用的有:

  • AES (高級加密標準 Advanced Encryption Standard),密匙長度能夠是12八、19二、256。
  • ChaCha20 Google 設計的另外一種加密算法,密鑰長度固定爲 256 位。

加密分組模式

對稱算法還有一個 分組模式 的概念,它可讓算法用固定長度的密鑰加密任意長度的明文 ,把小祕密(即密鑰)轉化爲大祕密(即密文)。

AEAD(Authenticated Encryption with Associated Data) ,在加密的同時增長了認證的功能,經常使用的是 GCM、CCM 和 Poly1305。

優缺點

  • 優勢:運算速度快;
  • 缺點:沒法安全地將密鑰傳輸給通訊方,也就是密匙交換成了問題。

非對稱加密

它有兩個密鑰,一個叫 公鑰(public key),一個叫 私鑰(private key)。兩個密鑰是不一樣的(不對稱) ,公鑰能夠公開給任何人使用,而私鑰必須嚴格保密。

公鑰和私鑰有個特別的 單向 性,雖然均可以用來加密解密,但 公鑰加密後只能用私鑰解密 ,反過來,私鑰加密後也只能用公鑰解密

非對稱加密能夠解決 密鑰交換 的問題。

目前經常使用的有:

  • RSA 它的安全性基於 整數分解 的數學難題,使用兩個超大素數的乘積做爲生成密鑰的材料,想要從公鑰推算出私鑰是很是困難的。如今推薦密匙長度是 2048。
  • ECC(Elliptic Curve Cryptography) 它基於 橢圓曲線離散對數 的數學難題,使用特定的曲線方程和基點生成公鑰和私鑰,子算法 ECDHE 用於密鑰交換,ECDSA 用於數字簽名。

優缺點

  • 優勢:能夠更安全地將公開密鑰傳輸給通訊發送方;
  • 缺點:運算速度慢。

混合加密

雖然非對稱密匙沒有 密匙交換 問題,可是它的運行速度很慢。若是僅用非對稱加密,雖然保證了安全,但通訊速度有如蝸牛,實用性就變成了零。

因此就產生了 把對稱加密和非對稱加密結合起來混合加密,二者互相取長補短,即能高效地加密解密,又能安全地密鑰交換。

  1. 在通訊剛開始的時候使用非對稱算法 ,好比 RSA、ECDHE,首先解決密鑰交換的問題
  2. 而後用隨機數產生對稱算法使用的 會話密鑰(session key),再用 公鑰加密 。由於會話密鑰很短,一般只有 16 字節或 32 字節,因此慢一點也無所謂。
  3. 對方拿到密文後用 私鑰解密 ,取出會話密鑰。這樣,雙方就實現了對稱密鑰的安全交換,後續就再也不使用非對稱加密,全都使用對稱加密。

數字簽名與證書

咱們把消息加密了就能保證安全嗎?

考慮下面的狀況:

非對稱加密的算法都是公開的,全部人均可以本身生成一對公鑰私鑰。

當服務端向客戶端返回公鑰 A1 的時候,中間人將其替換成本身的公鑰 B1 傳送給瀏覽器。

而瀏覽器此時一無所知,傻乎乎地使用公鑰 B1 加密了密鑰 K 發送出去,又被中間人截獲,中間人利用本身的私鑰 B2 解密,獲得密鑰 K,再使用服務端的公鑰 A1 加密傳送給服務端,完成了通訊鏈路,而服務端和客戶端毫無感知。

要保證安全,咱們不只要加密,還要保證消息的完整性以及身份認證。

摘要算法

摘要算法(Digest Algorithm),也就是常說的散列函數、哈希函數(Hash Function)。把一個大空間映射到小空間,因爲對輸入具備 單向性雪崩效應,能夠用來作數據的完整性校驗。

TLS 推薦的摘要算法是 SHA-2SHA-2 其實是一系列摘要算法的統稱,總共有 6 種,經常使用的有 SHA22四、SHA25六、SHA384,分別可以生成 28 字節、32 字節、48 字節的摘要。

可是它不具有機密性,在混合加密系統裏用 會話密鑰加密消息和摘要,這個術語叫作 哈希消息認證碼(HMAC)

數字簽名

如今還有個漏洞,通訊的兩個端點(endpoint) 也就是你怎麼證實是你?服務器怎麼證實是服務器?

非對稱加密裏的 私鑰 ,使用私鑰再加上摘要算法,就可以實現 數字簽名 ,同時實現 身份認證不能否認

但又由於非對稱加密效率過低,因此私鑰只加密原文的摘要

這裏的私鑰是你本身須要有一個 私鑰 ,服務器也須要有一個 私鑰,大家互相交換公鑰,除非大家的私鑰被泄密,不然身份認證和不能否認就能保證。

數字證書和 CA

到如今,綜合使用對稱加密、非對稱加密和摘要算法,咱們已經實現了安全的四大特性,是否是已經完美了呢?

不是的,這裏還有一個 公鑰的信任 問題。由於誰均可以發佈公鑰,如何保證公鑰不是僞造的?

找一個 公認的可信第三方 ,讓它做爲「信任的起點,遞歸的終點」,構建起公鑰的信任鏈。這就是 CA(Certificate Authority,證書認證機構),使用 CA 的私鑰對你的 公鑰進行簽名(包含序列號、用途、頒發者、有效時間等等和你的公鑰打包再簽名),造成 數字證書(Certificate)

那麼 CA 怎麼證實本身呢?這仍是信任鏈的問題。小一點的 CA 可讓大 CA 簽名認證 ,但鏈條的最後,也就是 Root CA ,就只能本身證實本身了。

也就是說,個人公鑰是 CA 的私鑰簽名的,那麼我須要拿到該 CA 的公鑰進行解密,解密成功才能證實沒有被僞造,那麼最後仍是信任鏈的問題,最終解決辦法就是 Root CA,這就叫 自簽名證書(Self-Signed Certificate)或者 根證書(Root Certificate),有了這個證書體系,操做系統和瀏覽器都內置了各大 CA 的根證書

也就是說,若是你的公鑰不是 CA 頒發的,那麼想要瀏覽器認爲是安全的,就必須將它安裝到系統的根證書存儲區裏。

TLS

TLS 協議的組成

記錄協議

記錄協議(Record Protocol)規定了 TLS 收發數據的基本單位:記錄(record)。它有點像是 TCP 裏的 segment,全部的其餘子協議都須要經過記錄協議發出。但多個記錄數據能夠在一個 TCP 包裏一次性發出,也並不須要像 TCP 那樣返回 ACK。

警報協議

警報協議(Alert Protocol)的職責是向對方發出警報信息,有點像是 HTTP 協議裏的狀態碼。好比,protocol_version 就是不支持舊版本,bad_certificate 就是證書有問題,收到警報後另外一方能夠選擇繼續,也能夠當即終止鏈接。

握手協議

握手協議(Handshake Protocol)是 TLS 裏最複雜的子協議,要比 TCP 的 SYN/ACK 複雜的多,瀏覽器和服務器會在握手過程當中協商 TLS 版本號、隨機數、密碼套件等信息,而後交換證書和密鑰參數,最終雙方協商獲得會話密鑰,用於後續的混合加密系統。

變動密碼規範協議

最後一個是 變動密碼規範協議(Change Cipher Spec Protocol),它很是簡單,就是一個「通知」,告訴對方,後續的數據都將使用加密保護。那麼反過來,在它以前,數據都是明文的。 (在 TLS 1.3 中這個協議已經刪除,爲了兼容 TLS 老版本,可能還會存在)。

心跳協議

Heartbeat 擴展爲 TLS/DTLS 提供了一種新的協議,容許在不須要重協商的狀況下,使用 keep-alive 功能。Heartbeat 擴展也爲 path MTU (PMTU) 發現提供了基礎(這個是 TLS 1.3 新加的,TLS 1.3 以前的版本沒有這個協議。)。

TLS 1.2 鏈接過程解析

ECDHE握手過程

  1. 在 TCP 創建鏈接以後,瀏覽器會首先發一個 Client Hello 消息,也就是跟服務器打招呼。裏面有客戶端的TLS版本號、支持的密碼套件,還有一個 隨機數(Client Random) ,用於後續生成會話密鑰。

    • 瀏覽器和服務器在使用 TLS 創建鏈接時須要選擇一組恰當的加密算法來實現安全通訊,這些算法的組合被稱爲 密碼套件(cipher suite,也叫加密套件)
  2. 服務器接收到,當即返回 server_random,確認TLS版本號和使用的密碼套件 ECDHE。

    • 服務器爲了證實本身的身份,給客戶端發送 Server Certificate。
    • 由於服務器選擇了 ECDHE 算法,因此它會在證書後發送 Server Key Exchange 消息,裏面是 橢圓曲線的公鑰(Server Params) ,用來實現密鑰交換算法,再加上本身的私鑰簽名認證。
    • 以後是 Server Hello Done 消息。
  3. 瀏覽器接收,先驗證數字證書和簽名。

    • 客戶端按照密碼套件的要求,也生成一個 橢圓曲線的公鑰(Client Params) ,用 Client Key Exchange 消息發給服務器。
    • Client Random + Server Random 經過 ECDHE 算法獲得 Pre-Master
    • Client Random、Server Random 和 Pre-Master 生成用於加密會話的主密匙 Master Secret
    master_secret = PRF(pre_master_secret, "master secret",
                        ClientHello.random + ServerHello.random) //Master Secret 公式

這裏的 PRF 就是僞隨機數函數,它基於密碼套件裏的最後一個參數,好比此次的 SHA384,經過摘要算法來再一次強化 Master Secret 的隨機性。

主密鑰有 48 字節,但它也不是最終用於通訊的會話密鑰,還會再用 PRF 擴展出更多的密鑰,好比客戶端發送用的會話密鑰(client_write_key)、服務器發送用的會話密鑰(server_write_key)等等,避免只用一個密鑰帶來的安全隱患。

有了主密鑰和派生的會話密鑰,握手就快結束了。客戶端發一個 Change Cipher Spec ,而後再發一個 Finished 消息,把以前全部發送的數據作個摘要,再加密一下,讓服務器作個驗證。

  1. 服務器也是一樣的操做,發 Change Cipher SpecFinished 消息,雙方都驗證加密解密 OK,握手正式結束,後面就收發被加密的 HTTP 請求和響應了。
密碼套件
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 //密鑰交換算法 + 簽名算法 + 對稱加密算法 + 摘要算法
  • 密鑰協商算法使用 ECDHE;
  • 簽名算法使用 RSA;
  • 握手後的通訊使用 AES 對稱算法,密鑰長度 256 位,分組模式是 GCM;
  • 摘要算法使用 SHA384。

RSA握手過程

大致的流程沒有變,只是 Pre-Master 再也不須要用算法生成,而是客戶端直接生成隨機數,而後用服務器的公鑰加密,經過 Client Key Exchange 消息發給服務器。服務器再用私鑰解密,這樣雙方也實現了共享三個隨機數,就能夠生成主密鑰。

  1. 客戶端連上服務端。
  2. 服務端發送 CA 證書給客戶端。

    發送的是一個 CA 鏈,不包含 ROOT 證書。

  3. 客戶端驗證該證書的可靠性。

    解析 CA 鏈,用鏈條上的 CA 進行驗證,一層層地驗證,直到找到根證書,就可以肯定證書是可信的。

  4. 客戶端從 CA 證書中取出公鑰。
  5. 客戶端生成一個隨機密鑰 k,並用這個公鑰加密獲得 k*。
  6. 客戶端把 k* 發送給服務端。
  7. 服務端收到 k* 後用本身的私鑰解密獲得 k。
  8. 此時雙方都獲得了密鑰 k,協商完成。
  9. 後續使用密鑰 k* 加密信息。

RSA 和 ECDHE 握手過程的區別:

  • RSA 密鑰協商算法「不支持」前向保密,ECDHE 密鑰協商算法「支持」前向保密;
  • 使用了 RSA 密鑰協商算法,TLS 完成四次握手後,才能進行應用數據傳輸,而對於 ECDHE 算法,客戶端能夠不用等服務端的最後一次 TLS 握手,就能夠提早發出加密的 HTTP 數據,節省了一個消息的往返時間;
  • 使用 ECDHE, 在 TLS 第 2 次握手中,會出現服務器端發出的「Server Key Exchange」消息,而 RSA 握手過程沒有該消息;

TLS 1.3 特性解析

強化安全

密碼套件名 代碼
TLS_AES_128_GCM_SHA256 {0x13,0x01}
TLS_AES_256_GCM_SHA384 {0x13,0x02}
TLS_CHACHA20_POLY1305_SHA256 {0x13,0x03}
TLS_AES_128_GCM_SHA256 {0x13,0x04}
TLS_AES_128_GCM_8_SHA256 {0x13,0x05}

握手分析

  1. 瀏覽器首先仍是發一個 Client Hello

    • 由於 1.3 的消息兼容 1.2,因此開頭的版本號、支持的密碼套件和隨機數(Client Random)結構都是同樣的(不過這時的隨機數是 32 個字節)。
  2. 服務器收到 Client Hello 一樣返回 Server Hello 消息,仍是要給出一個 隨機數(Server Random)和選定密碼套件。

    • supported_versions 裏確認使用的是 TLS1.3,而後在 key_share 擴展帶上曲線和對應的公鑰參數。

這時只交換了兩條消息,客戶端和服務器就拿到了四個共享信息:Client RandomServer RandomClient ParamsServer Params ,兩邊就能夠各自用 ECDHE 算出 Pre-Master ,再用 HKDF 生成主密鑰 Master Secret ,效率比 TLS1.2 提升了一大截。

這裏 TLS1.3 還有一個安全強化措施,多了個 Certificate Verify 消息,用服務器的私鑰把前面的曲線、套件、參數等握手數據加了簽名,做用和 Finished 消息差很少。但因爲是私鑰簽名,因此強化了身份認證和和防竄改。

這兩個 Hello 消息以後,客戶端驗證服務器證書,再發 Finished 消息,就正式完成了握手,開始收發 HTTP 報文。

HTTP/2

HTTP/1.x 缺陷

HTTP/1.x 實現簡單是以犧牲性能爲代價的:

  • 客戶端須要使用多個鏈接才能實現併發和縮短延遲;
  • 不會壓縮請求和響應首部,從而致使沒必要要的網絡流量;
  • 不支持有效的資源優先級,導致底層 TCP 鏈接的利用率低下。

頭部壓縮

HTTP/1.1 的首部帶有大量信息,並且每次都要重複發送。

HTTP/2.0 要求客戶端和服務器同時維護和更新一個包含以前見過的頭字段表,從而避免了重複傳輸。

HTTP/2 並無使用傳統的壓縮算法,而是開發了專門的 HPACK 算法,在客戶端和服務器兩端創建「字典」,用索引號表示重複的字符串,還釆用 Huffman 編碼來壓縮整數和字符串,能夠達到 50%~90% 的高壓縮率。

二進制幀

HTTP/2.0 將報文分紅 HEADERS 幀和 DATA 幀,它們都是二進制格式的。

在通訊過程當中,只會有一個 TCP 鏈接存在,它承載了任意數量的雙向數據流(Stream)。

  • 一個數據流(Stream)都有一個惟一標識符和可選的優先級信息,用於承載雙向信息。
  • 消息(Message)是與邏輯請求或響應對應的完整的一系列幀。
  • 幀(Frame)是最小的通訊單位,來自不一樣數據流的幀能夠交錯發送,而後再根據每一個幀頭的數據流標識符從新組裝。

服務端推送

HTTP/2.0 在客戶端請求一個資源時,會把相關的資源一塊兒發送給客戶端,客戶端就不須要再次發起請求了。例如客戶端請求 page.html 頁面,服務端就把 script.js 和 style.css 等與之相關的資源一塊兒發給客戶端。

CORS跨域

當一個資源從與該資源自己所在的服務器不一樣的域或端口請求一個資源時,資源會發起一個跨域 HTTP 請求。

出於安全緣由,瀏覽器限制從腳本內發起的跨源HTTP請求。 例如,XMLHttpRequest和Fetch API遵循同源策略。 這意味着使用這些API的Web應用程序只能從加載應用程序的同一個域請求HTTP資源,除非使用CORS頭文件。

(譯者注:跨域並不必定是瀏覽器限制了發起跨站請求,也多是跨站請求能夠正常發起,可是返回結果被瀏覽器攔截了。最好的例子是 CSRF 跨站攻擊原理,請求是發送到了後端服務器不管是否跨域!注意:有些瀏覽器不容許從 HTTPS 的域跨域訪問 HTTP,好比 Chrome 和 Firefox,這些瀏覽器在請求還未發出的時候就會攔截請求,這是一個特例。)

隸屬於 W3C 的 Web 應用工做組推薦了一種新的機制,即跨源資源共享(Cross-Origin Resource Sharing ) CORS。這種機制讓Web應用服務器能支持跨站訪問控制,從而使得安全地進行跨站數據傳輸成爲可能。須要特別注意的是,這個規範是針對API容器的(好比說XMLHttpReques 或者 Fetch),以減輕跨域HTTP請求的風險。CORS 須要客戶端和服務器同時支持。目前,全部瀏覽器都支持該機制。

跨域資源共享標準( cross-origin sharing standard )容許在下列場景中使用跨域 HTTP 請求:

  • 前文提到的由 XMLHttpRequest 或 Fetch 發起的跨域 HTTP 請求。
  • Web 字體 (CSS 中經過 @font-face 使用跨域字體資源), 所以,網站就能夠發佈 TrueType 字體資源,並只容許已受權網站進行跨站調用。
  • WebGL 貼圖
  • 使用 drawImage 將 Images/video 畫面繪製到 canvas
  • 樣式表(使用 CSSOM)
  • Scripts (未處理的異常)

把CORS分爲:簡單請求、預請求和附帶憑證信息的請求。

1. 簡單請求

某些請求不會觸發 CORS 預檢請求。本文稱這樣的請求爲「簡單請求」,請注意,該術語並不屬於 Fetch (其中定義了 CORS)規範。若請求知足全部下述條件,則該請求可視爲「簡單請求」:

(1). 使用下列方法之一:

  • GET
  • HEAD
  • POST

(2). Fetch 規範定義了對 CORS 安全的首部字段集合,不得人爲設置該集合以外的其餘首部字段。該集合爲:

Accept
Accept-Language
Content-Language
Content-Type (須要注意額外的限制)
DPR
Downlink
Save-Data
Viewport-Width
Width

(3). Content-Type 的值僅限於下列三者之一:

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

(4). 請求中的任意XMLHttpRequestUpload 對象均沒有註冊任何事件監聽器;XMLHttpRequestUpload 對象能夠使用 XMLHttpRequest.upload 屬性訪問。

(5). 請求中沒有使用 ReadableStream 對象。

簡單來講,重點須要記住的就是兩點:

**(1)只使用 GET, HEAD 或者 POST 請求方法。若是使用 POST 向服務器端傳送數據,則數據類型(Content-Type)只能是 application/x-www-form-urlencoded, multipart/form-data 或 text/plain中的一種。
(2)不會使用自定義請求頭(相似於 X-Modified 這種)。**

舉例:

//好比說,假如站點 http://foo.example 的網頁應用想要訪問 http://bar.other 的資源。如下的 JavaScript 代 
//碼應該會在 foo.example 上執行:    
var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/public-data/';
function callOtherDomain() {
  if(invocation) {    
    invocation.open('GET', url, true);
    invocation.onreadystatechange = handler;
    invocation.send(); 
  }
}

//讓咱們看看,在這個場景中,瀏覽器會發送什麼的請求到服務器,而服務器又會返回什麼給瀏覽器:
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 
Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Referer: http://foo.example/examples/access-control/simpleXSInvocation.html
Origin: http://foo.example //該請求來自於 http://foo.exmaple。
//以上是瀏覽器發送請求

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2.0.61 
Access-Control-Allow-Origin: * //這代表服務器接受來自任何站點的跨站請求。若是設置爲http://foo.example。其它站點就不能跨站訪問 http://bar.other 的資源了。
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml
//以上是服務器返回信息給瀏覽器

如下狀況,請求會返回相關響應信息

  • 若是資源是容許公開訪問的(就像任何容許GET訪問的 HTTP資源),返回Access-Control-Allow-Origin:*頭信息就足夠了,除非是一些須要Cookies和HTTP身份驗證信息的請求。
  • 若是資源訪問被限制基於相同的域名,或者若是要訪問的資源須要憑證(或設置憑證),那麼就有必要對請求頭信息中的ORIGIN進行過濾,或者至少響應請求的來源(例如Access-Control-Allow-Origin:http://arunranga.com)。./)
    另外,將發送Access-Control-Allow-Credentials:TRUE頭信息,這在後續部分將進行討論。

2. 預請求

與前述簡單請求不一樣,「需預檢的請求」要求必須首先使用 OPTIONS 方法發起一個預檢請求到服務器,以獲知服務器是否容許該實際請求。"預檢請求「的使用,能夠避免跨域請求對服務器的用戶數據產生未預期的影響。

當請求知足下述任一條件時,即應首先發送預檢請求:

(1). 使用了下面任一 HTTP 方法:

PUT
DELETE
CONNECT
OPTIONS
TRACE
PATCH

(2). 人爲設置了對 CORS 安全的首部字段集合以外的其餘首部字段。該集合爲:

Accept
Accept-Language
Content-Language
Content-Type (but note the additional requirements below)
DPR
Downlink
Save-Data
Viewport-Width
Width

(3). Content-Type 的值不屬於下列之一:

application/x-www-form-urlencoded
multipart/form-data
text/plain

(4). 請求中的XMLHttpRequestUpload 對象註冊了任意多個事件監聽器。
(5). 請求中使用了ReadableStream對象。

不一樣於上面討論的簡單請求,「預請求」要求必須先發送一個 OPTIONS 請求給目的站點,來查明這個跨站請求對於目的站點是否是安全可接受的。這樣作,是由於跨站請求可能會對目的站點的數據形成破壞。 當請求具有如下條件,就會被當成預請求處理:

**(1)請求以 GET, HEAD 或者 POST 之外的方法發起請求。或者,使用 POST,但請求數據爲 application/x-www-form-urlencoded, multipart/form-data 或者 text/plain 之外的數據類型。好比說,用 POST 發送數據類型爲 application/xml 或者 text/xml 的 XML 數據的請求。
(2)使用自定義請求頭(好比添加諸如 X-PINGOTHER)**

舉個例子:

var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/post-here/';
var body = '{C}{C}{C}{C}{C}{C}{C}{C}{C}{C}Arun';
function callOtherDomain(){
  if(invocation){
    invocation.open('POST', url, true);
    invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
    invocation.setRequestHeader('Content-Type', 'application/xml');
    invocation.onreadystatechange = handler;
    invocation.send(body); 
  }
}

如上,以 XMLHttpRequest 建立了一個 POST 請求,爲該請求添加了一個自定義請求頭(X-PINGOTHER: pingpong),並指定數據類型爲 application/xml。因此,該請求是一個「預請求」形式的跨站請求。瀏覽器使用一個 OPTIONS 發送了一個「預請求」。Firefox 3.1 根據請求參數,決定須要發送一個「預請求」,來探明服務器端是否接受後續真正的請求。 OPTIONS 是 HTTP/1.1 裏的方法,用來獲取更多服務器端的信息,是一個不該該對服務器數據形成影響的方法。 隨同 OPTIONS 請求,如下兩個請求頭一塊兒被髮送:

Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER

假設服務器成功響應返回部分信息以下:

Access-Control-Allow-Origin: http://foo.example //代表服務器容許http://foo.example的請求
Access-Control-Allow-Methods: POST, GET, OPTIONS //代表服務器能夠接受POST, GET和 OPTIONS的請求方法
Access-Control-Allow-Headers: X-PINGOTHER //傳遞一個可接受的自定義請求頭列表。服務器也須要設置一個與瀏覽器對應。不然會報 Request header field X-Requested-With is not allowed by Access-Control-Allow-Headers in preflight response 的錯誤
Access-Control-Max-Age: 1728000 //告訴瀏覽器,本次「預請求」的響應結果有效時間是多久。在上面的例子裏,1728000秒錶明着20天內,瀏覽器在處理針對該服務器的跨站請求,均可以無需再發送「預請求」,只需根據本次結果進行判斷處理。

3. 附帶憑證信息的請求

Fetch 與 CORS 的一個有趣的特性是,能夠基於 HTTP cookies 和 HTTP 認證信息發送身份憑證。通常而言,對於跨域 XMLHttpRequest 或 Fetch 請求,瀏覽器不會發送身份憑證信息。若是要發送憑證信息,須要設置 XMLHttpRequest 的某個特殊標誌位。

本例中,http://foo.example 的某腳本向 http://bar.other 發起一個GET 請求,並設置 Cookies:

var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/credentialed-content/';
    
function callOtherDomain(){
  if(invocation) {
    invocation.open('GET', url, true);
    invocation.withCredentials = true;
    invocation.onreadystatechange = handler;
    invocation.send(); 
  }
}

第 7 行將 XMLHttpRequest 的 withCredentials 標誌設置爲 true,從而向服務器發送 Cookies。由於這是一個簡單 GET 請求,因此瀏覽器不會對其發起「預檢請求」。可是,若是服務器端的響應中未攜帶 Access-Control-Allow-Credentials: true ,瀏覽器將不會把響應內容返回給請求的發送者。

假設服務器成功響應返回部分信息以下:

Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Credentials: true
Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT

若是 bar.other 的響應頭裏沒有 Access-Control-Allow-Credentials:true,則響應會被忽略.。特別注意: 給一個帶有withCredentials的請求發送響應的時候,服務器端必須指定容許請求的域名,不能使用「」。上面這個例子中,若是響應頭是這樣的 Access-Control-Allow-Origin: ,則響應會失敗。在這個例子裏,由於Access-Control-Allow-Origin的值是 http://foo.example 這個指定的請求域名,因此客戶端把帶有憑證信息的內容被返回給了客戶端。另外注意,更多的cookie信息也被建立了。

CORS 和 JSONP 對比

  • JSONP 只能實現 GET 請求,而 CORS 支持全部類型的 HTTP 請求。
  • 使用 CORS,開發者能夠使用普通的 XMLHttpRequest 發起請求和得到數據,比起 JSONP 有更好的錯誤處理。
  • JSONP 主要被老的瀏覽器支持,它們每每不支持 CORS,而絕大多數現代瀏覽器都已經支持了 CORS)。
  • CORS 與 JSONP 相比,無疑更爲先進、方便和可靠。

參考

OSI七層模型詳解

透視HTTP協議

MDN HTTP教程

硬核!30 張圖解 HTTP 常見的面試題

HTTP

圖解HTTP緩存

HTTP緩存控制小結

瀏覽器緩存機制剖析

完全弄懂瀏覽器緩存策略

HTTP 緩存

變態的靜態資源緩存與更新

HTTPS 詳解

TLS & DTLS Heartbeat Extension

HTTP 基礎概述

相關文章
相關標籤/搜索