目錄
1. HTTP傳輸大文件的方法
1.1. 數據壓縮
1.2. 分塊傳輸
1.3. 范圍請求
1.4. 多段數據
2. HTTP的連接管理
2.1. 短連接
2.2. 長連接
2.3. 隊頭阻塞
3. HTTP的重定向和跳轉
3.1. 重定向的過程
3.2. 重定向狀態碼
3.3. 重定向的應用場景
3.4. 重定向的相關問題
1. HTTP傳輸大文件的方法
如何在有限的帶寬下高效快捷地傳輸這些大文件:
- 壓縮 HTML 等文本文件是傳輸大文件最基本的方法;
- 分塊傳輸可以流式收發數據,節約內存和帶寬,使用響應頭字段“Transfer-Encoding: chunked”來表示,分塊的格式是 16 進制長度頭 + 數據塊;
- 范圍請求可以只獲取部分數據,即“分塊請求”,實現視頻拖拽或者斷點續傳,使用請求頭字段“Range”和響應頭字段“Content-Range”,響應狀態碼必須是 206;
- 也可以一次請求多個范圍,這時候響應報文的數據類型是“multipart/byteranges”,body 里的多個部分會用 boundary 字符串分隔。
1.1. 數據壓縮
通常瀏覽器在發送請求時都會帶著“Accept-Encoding”頭字段,里面是瀏覽器支持的壓縮格式列表,例如 gzip、deflate、br 等,這樣服務器就可以從中選擇一種壓縮算法,放進“Content-Encoding”響應頭里,再把原數據壓縮后發給瀏覽器。
不過這個解決方法也有個缺點,gzip 等壓縮算法通常只對文本文件有較好的壓縮率,而圖片、音頻視頻等多媒體數據本身就已經是高度壓縮的,再用 gzip 處理也不會變小(甚至還有可能會增大一點),所以它就失效了。
不過數據壓縮在處理文本的時候效果還是很好的,所以各大網站的服務器都會使用這個手段作為“保底”。例如,在 Nginx 里就會使用“gzip on”指令,啟用對“text/html”的壓縮。
1.2. 分塊傳輸
把大文件“拆開”,分解成多個小塊,把這些小塊分批發給瀏覽器,瀏覽器收到后再組裝復原。
這種“化整為零”的思路在 HTTP 協議里就是“chunked”分塊傳輸編碼,在響應報文里用頭字段“Transfer-Encoding: chunked”來表示,意思是報文里的 body 部分不是一次性發過來的,而是分成了許多的塊(chunk)逐個發送。
分塊傳輸也可以用于“流式數據”,例如由數據庫動態生成的表單頁面,這種情況下 body 數據的長度是未知的,無法在頭字段“Content-Length”里給出確切的長度,所以也只能用 chunked 方式分塊發送。
“Transfer-Encoding: chunked”和“Content-Length”這兩個字段是互斥的,也就是說響應報文里這兩個字段不能同時出現,一個響應報文的傳輸要么是長度已知,要么是長度未知。
分塊傳輸的編碼規則:
- 每個分塊包含兩個部分,長度頭和數據塊;
- 長度頭是以 CRLF(回車換行,即\r\n)結尾的一行明文,用 16 進制數字表示長度;
- 數據塊緊跟在長度頭后,最后也用 CRLF 結尾,但數據不包含 CRLF;
- 最后用一個長度為 0 的塊表示結束,即“0\r\n\r\n”。
分塊傳輸示例:
不過瀏覽器在收到分塊傳輸的數據后會自動按照規則去掉分塊編碼,重新組裝出內容,所以想要看到服務器發出的原始報文形態就得用 Telnet 手工發送請求(或者用 Wireshark 抓包):
16進制(0123456789abcdef)
1.3. 范圍請求
如果想獲取一個大文件其中的片段數據,需要使用“范圍請求”(range requests)。
范圍請求允許客戶端在請求頭里使用專用字段來表示只獲取文件的一部分,相當于是客戶端的“化整為零”。
范圍請求不是 Web 服務器必備的功能,可以實現也可以不實現,所以服務器必須在響應頭里使用字段“Accept-Ranges: bytes”明確告知客戶端:“我是支持范圍請求的”。
如果不支持的話服務器可以發送“Accept-Ranges: none”,或者干脆不發送“Accept-Ranges”字段,這樣客戶端就認為服務器沒有實現范圍請求功能,只能收發整塊文件。
請求頭Range是 HTTP 范圍請求的專用字段,格式是“bytes=x-y”,其中的 x 和 y 是以字節為單位的數據范圍。
范圍請求示例:
1.4. 多段數據
范圍請求還支持在 Range 頭里使用多個“x-y”,一次性獲取多個片段數據。
這種情況需要使用一種特殊的 MIME 類型:“multipart/byteranges”,表示報文的 body 是由多段字節序列組成的,并且還要用一個參數“boundary=xxx”給出段之間的分隔標記。
多段數據的格式與分塊傳輸也比較類似,但它需要用分隔標記 boundary 來區分不同的片段。
范圍請求多段數據示例:
2. HTTP的連接管理
- 早期的 HTTP 協議使用短連接,收到響應后就立即關閉連接,效率很低;
- HTTP/1.1 默認啟用長連接,在一個連接上收發多個請求響應,提高了傳輸效率;
- 服務器會發送“Connection: keep-alive”字段表示啟用了長連接;
- 報文頭里如果有“Connection: close”就意味著長連接即將關閉;
- 過多的長連接會占用服務器資源,所以服務器會用一些策略有選擇地關閉長連接;
- “隊頭阻塞”問題會導致性能下降,可以用“并發連接”和“域名分片”技術緩解。
2.1. 短連接
HTTP 協議最初(0.9/1.0)是個非常簡單的協議,通信過程也采用了簡單的“請求 - 應答”方式。
它底層的數據傳輸基于 TCP/IP,每次發送請求前需要先與服務器建立連接,收到響應報文后會立即關閉連接。因為客戶端與服務器的整個連接過程很短暫,不會與服務器保持長時間的連接狀態,所以就被稱為“短連接”(short-lived connections)。早期的 HTTP 協議也被稱為是“無連接”的協議。
在短連接中, TCP 協議頻繁的建立連接和關閉連接,時間浪費嚴重,效率很低。
2.2. 長連接
“長連接”,也叫“持久連接”(persistent connections)、“連接保活”(keep alive)、“連接復用”(connection reuse),可以有效解決短連接的缺點。
在短連接里發送了三次 HTTP“請求 - 應答”,每次都會浪費 60% 的 RTT 時間。而在長連接的情況下,同樣發送三次請求,因為只在第一次時建立連接,在最后一次時關閉連接,所以浪費率就是“3÷9≈33%”,降低了差不多一半的時間損耗。
HTTP/1.1 中的連接都會默認啟用長連接。不需要用什么特殊的頭字段指定,只要向服務器發送了第一次請求,后續的請求都會重復利用第一次打開的 TCP 連接,也就是長連接,在這個連接上收發數據。
不過不管客戶端是否顯式要求長連接,如果服務器支持長連接,它總會在響應報文里放一個“Connection: keep-alive”字段,告訴客戶端:“我是支持長連接的,接下來就用這個 TCP 一直收發數據吧”。
長連接的缺點:
TCP 連接長時間不關閉,服務器必須在內存里保存它的狀態,這就占用了服務器的資源。如果有大量的空閑長連接只連不發,就會很快耗盡服務器的資源,導致服務器無法為真正有需要的用戶提供服務。
長連接缺點的解決方法:
- 在客戶端,可以在請求頭里加上“Connection: close”字段,告訴服務器:“這次通信后就關閉連接”。服務器看到這個字段,就知道客戶端要主動關閉連接,于是在響應報文里也加上這個字段,發送之后就調用 Socket API 關閉 TCP 連接。
- 服務器端通常不會主動關閉連接,但也可以使用一些策略。以 Nginx 為例,它有兩種方式:
- 使用“keepalive_timeout”指令,設置長連接的超時時間,如果在一段時間內連接上沒有任何數據收發就主動斷開連接,避免空閑連接占用系統資源。
- 使用“keepalive_requests”指令,設置長連接上可發送的最大請求次數。比如設置成 1000,那么當 Nginx 在這個連接上處理了 1000 個請求后,也會主動斷開連接。
2.3. 隊頭阻塞
“隊頭阻塞”與短連接和長連接無關,而是由 HTTP 基本的“請求 - 應答”模型所導致的。
因為 HTTP 規定報文必須是“一發一收”,這就形成了一個先進先出的“串行”隊列。隊列里的請求沒有輕重緩急的優先級,只有入隊的先后順序,排在最前面的請求被最優先處理。
如果隊首的請求因為處理的太慢耽誤了時間,那么隊列里后面的所有請求也不得不跟著一起等待,結果就是其他的請求承擔了不應有的時間成本。
性能優化:
1. 并發連接:對一個域名發起多個長連接,用數量來解決質量的問題。
但這種方式也存在缺陷。如果每個客戶端都想自己快,建立很多個連接,用戶數×并發數就會是個天文數字。服務器的資源根本就扛不住,或者被服務器認為是惡意攻擊,反而會造成“拒絕服務”。
2. 域名分片:多開幾個域名,比如 shard1.chrono.com、shard2.chrono.com,而這些域名都指向同一臺服務器 www.chrono.com,這樣實際長連接的數量就又上去了。
3. HTTP的重定向和跳轉
- 重定向是服務器發起的跳轉,要求客戶端改用新的 URI 重新發送請求,通常會自動進行,用戶是無感知的;
- 301/302 是最常用的重定向狀態碼,分別是“永久重定向”和“臨時重定向”;
- 響應頭字段 Location 指示了要跳轉的 URI,可以用絕對或相對的形式;
- 重定向可以把一個 URI 指向另一個 URI,也可以把多個 URI 指向同一個 URI,用途很多;
- 使用重定向時需要當心性能損耗,還要避免出現循環跳轉。
“超文本”里含有“超鏈接”,可以從一個“超文本”跳躍到另一個“超文本”,用戶可以在查看時隨意點擊鏈接、轉換頁面。點擊頁面“鏈接”時,瀏覽器首先要解析鏈接文字里的 URI,再用這個 URI 發起一個新的 HTTP 請求,獲取響應報文后就會切換顯示內容,渲染出新 URI 指向的頁面。
這樣的跳轉動作是由瀏覽器的使用者主動發起的,可以稱為“主動跳轉”,但還有一類跳轉是由服務器發起的,瀏覽器使用者無法控制,相對地就可以稱為“被動跳轉”,這在 HTTP 協議里有個專門的名詞,叫做“重定向”(Redirection)。
3.1. 重定向的過程
在實驗環境下用Chrome瀏覽器打開“http://www.chrono.com/18-1”。
“Location”字段屬于響應字段,必須出現在響應報文里。但只有配合 301/302 狀態碼才有意義,它標記了服務器要求重定向的 URI,這里就是要求瀏覽器跳轉到“index.html”。
瀏覽器收到 301/302 報文,會檢查響應頭里有沒有“Location”。如果有,就從字段值里提取出 URI,發出新的 HTTP 請求,相當于自動替我們點擊了這個鏈接。
在“Location”里的 URI 既可以使用絕對 URI,也可以使用相對 URI。所謂“絕對 URI”,就是完整形式的 URI,包括 scheme、host:port、path 等。所謂“相對 URI”,就是省略了 scheme 和 host:port,只有 path 和 query 部分,是不完整的,但可以從請求上下文里計算得到。
實驗環境的 URI“/18-1”還支持使用 query 參數“dst=xxx”,指明重定向的 URI。
在重定向時如果只是在站內跳轉,你可以放心地使用相對 URI。但如果要跳轉到站外,就必須用絕對 URI。
例如,如果想跳轉到 Nginx 官網,就必須在“nginx.org”前把“http://”都寫出來,否則瀏覽器會按照相對 URI 去理解,得到的就會是一個不存在的 URI“http://www.chrono.com/nginx.org”
http://www.chrono.com/18-1?dst=nginx.org # 錯誤
http://www.chrono.com/18-1?dst=http://nginx.org # 正確
3.2. 重定向狀態碼
301俗稱“永久重定向”(Moved Permanently),意思是原 URI 已經“永久”性地不存在了,今后的所有請求都必須改用新的 URI。
瀏覽器看到 301,就知道原來的 URI“過時”了,就會做適當的優化。比如歷史記錄、更新書簽,下次可能就會直接用新的 URI 訪問,省去了再次跳轉的成本。搜索引擎的爬蟲看到 301,也會更新索引庫,不再使用老的 URI。
302俗稱“臨時重定向”(“Moved Temporarily”),意思是原 URI 處于“臨時維護”狀態,新的 URI 是起“頂包”作用的“臨時工”。
瀏覽器或者爬蟲看到 302,會認為原來的 URI 仍然有效,但暫時不可用,所以只會執行簡單的跳轉頁面,不記錄新的 URI,也不會有其他的多余動作,下次訪問還是用原 URI。
301/302 是最常用的重定向狀態碼,在 3××里還有:
- 303 See Other:類似 302,但要求重定向后的請求改為 GET 方法,訪問一個結果頁面,避免 POST/PUT 重復操作;
- 307 Temporary Redirect:類似 302,但重定向后請求里的方法和實體不允許變動,含義比 302 更明確;
- 308 Permanent Redirect:類似 307,不允許重定向后的請求變動,但它是 301“永久重定向”的含義。
不過這三個狀態碼的接受程度較低,有的瀏覽器和服務器可能不支持,開發時應當慎重,測試確認瀏覽器的實際效果后才能使用。
3.3. 重定向的應用場景
什么時候需要重定向:
1. 資源不可用”,需要用另一個新的 URI 來代替。
例如域名變更、服務器變更、網站改版、系統維護,這些都會導致原 URI 指向的資源無法訪問,為了避免出現 404,就需要用重定向跳轉到新的 URI,繼續為網民提供服務。
2. “避免重復”,讓多個網址都跳轉到一個 URI,增加訪問入口。
例如,有的網站都會申請多個名稱類似的域名,然后把它們再重定向到主站上。
重定向狀態碼如何選擇:
1. 301 的含義是“永久”的。
如果域名、服務器、網站架構發生了大幅度的改變,比如啟用了新域名、服務器切換到了新機房、網站目錄層次重構,這些都算是“永久性”的改變。原來的 URI 已經不能用了,必須用 301“永久重定向”,通知瀏覽器和搜索引擎更新到新地址,這也是搜索引擎優化(SEO)要考慮的因素之一。
2. 302 的含義是“臨時”的。
原來的 URI 在將來的某個時間點還會恢復正常,常見的應用場景就是系統維護,把網站重定向到一個通知頁面,告訴用戶過一會兒再來訪問。另一種用法就是“服務降級”,比如在雙十一促銷的時候,把訂單查詢、領積分等不重要的功能入口暫時關閉,保證核心服務能夠正常運行。
3.4. 重定向的相關問題
1. 性能損耗
重定向的機制決定了一個跳轉會有兩次請求 - 應答,比正常的訪問多了一次。
雖然 301/302 報文很小,但大量的跳轉對服務器的影響也是不可忽視的。站內重定向還好說,可以長連接復用,站外重定向就要開兩個連接,如果網絡連接質量差,那成本可就高多了,會嚴重影響用戶的體驗。
所以重定向應當適度使用,決不能濫用。
2. 循環跳轉
如果重定向的策略設置欠考慮,可能會出現“A=>B=>C=>A”的無限循環,不停地在這個鏈路里轉圈圈。
所以 HTTP 協議特別規定,瀏覽器必須具有檢測“循環跳轉”的能力,在發現這種情況時應當停止發送請求并給出錯誤提示。
實驗環境的 URI“/18-2”就模擬了這樣的一個“循環跳轉”,它跳轉到“/18-1”,并用參數“dst=/18-2”再跳回自己,實現了兩個 URI 的無限循環。結果如下: