1 HttpEntity httpEntity = httpResponse.getEntity(); 2 if (httpEntity != null) { 3 if (httpEntity.getContentEncoding() != null) { 4 if ("gzip".equalsIgnoreCase(httpEntity.getContentEncoding().getValue())) { 5 httpEntity = new GzipDecompressingEntity(httpEntity); 6 } else if ("deflate".equalsIgnoreCase(httpEntity.getContentEncoding().getValue())) { 7 httpEntity = new DeflateDecompressingEntity(httpEntity); 8 } 9 } 10 htmlByte = EntityUtils.toByteArray(httpEntity); 11 }
常常抓包看 HTTP 請求的同窗應該對 Vary 這個響應頭字段並不陌生,它有什麼用?用 PageSpeed 工具檢查頁面時,常常看到「Specify a Vary: Accept-Encoding header(請指定一個 Vary: Accept-Encoding 標頭)」這樣的建議,爲何要這樣作?本文記錄我對 Vary 的一些研究,其中就包含這些問題的答案。javascript
要了解 Vary 的做用,先得了解 HTTP 的內容協商機制。有時候,同一個 URL 能夠提供多份不一樣的文檔,這就要求服務端和客戶端之間有一個選擇最合適版本的機制,這就是內容協商。css
協商方式有兩種,一種是服務端把文檔可用版本列表發給客戶端讓用戶選,這可使用 300 Multiple Choices 狀態碼來實現。這種方案有很多問題,首先多一次網絡往返;其次服務端同一文檔的某些版本多是爲擁有某些技術特徵的客戶端準備的,而普通用戶不必定了解這些細節。舉個例子,服務端一般能夠將靜態資源輸出爲壓縮和未壓縮兩個版本,壓縮版顯然是爲支持壓縮的客戶端而準備的,但若是讓普通用戶選,極可能選擇錯誤的版本。html
因此 HTTP 的內容協商一般使用另一種方案:服務端根據客戶端發送的請求頭中某些字段自動發送最合適的版本。能夠用於這個機制的請求頭字段又分兩種:內容協商專用字段(Accept 字段)、其餘字段。java
首先來看 Accept 字段,詳見下表:nginx
請求頭字段 | 說明 | 響應頭字段 |
---|---|---|
Accept | 告知服務器發送何種媒體類型 | Content-Type |
Accept-Language | 告知服務器發送何種語言 | Content-Language |
Accept-Charset | 告知服務器發送何種字符集 | Content-Type |
Accept-Encoding | 告知服務器採用何種壓縮方式 | Content-Encoding |
例如客戶端發送如下請求頭:chrome
Accept:*/*
Accept-Encoding:
gzip
,deflate,sdch
Accept-Language:zh-CN,en-US;q=0.8,en;q=0.6
|
表示它能夠接受任何 MIME 類型的資源;支持採用 gzip、deflate 或 sdch 壓縮過的資源;能夠接受 zh-CN、en-US 和 en 三種語言,而且 zh-CN 的權重最高(q 取值 0 - 1,最高爲 1,最低爲 0,默認爲 1),服務端應該優先返回語言等於 zh-CN 的版本。瀏覽器
瀏覽器的響應頭多是這樣的:緩存
Content-Type: text
/javascript
Content-Encoding:
gzip
|
表示這個文檔確切的 MIME 類型是 text/javascript;文檔內容進行了 gzip 壓縮;響應頭沒有 Content-Language 字段,一般說明返回版本的語言正好是請求頭 Accept-Language 中權重最高的那個。bash
有時候,上面四個 Accept 字段並不夠用,例如要針對特定瀏覽器如 IE6 輸出不同的內容,就須要用到請求頭中的 User-Agent 字段。相似的,請求頭中的 Cookie 也可能被服務端用作輸出差別化內容的依據。服務器
因爲客戶端和服務端之間可能存在一個或多箇中間實體(如緩存服務器),而緩存服務最基本的要求是給用戶返回正確的文檔。若是服務端根據不一樣 User-Agent 返回不一樣內容,而緩存服務器把 IE6 用戶的響應緩存下來,並返回給使用其餘瀏覽器的用戶,確定會出問題 。
因此 HTTP 協議規定,若是服務端提供的內容取決於 User-Agent 這樣「常規 Accept 協商字段以外」的請求頭字段,那麼響應頭中必須包含 Vary 字段,且 Vary 的內容必須包含 User-Agent。同理,若是服務端同時使用請求頭中 User-Agent 和 Cookie 這兩個字段來生成內容,那麼響應中的 Vary 字段看上去應該是這樣的:
Vary: User-Agent, Cookie
|
也就是說 Vary 字段用於列出一個響應字段列表,告訴緩存服務器遇到同一個 URL 對應着不一樣版本文檔的狀況時,如何緩存和篩選合適的版本。
再來看 PageSpeed 的「Specify a Vary: Accept-Encoding header」這個提示,按照上面的說明,Accept-Encoding 屬於內容協商專用字段,服務端只須要在響應頭中增長 Content-Encoding 字段,用來指明內容壓縮格式;或者不輸出 Content-Encoding 代表內容未通過壓縮就能夠了。而緩存服務器,應該針對不一樣的 Content-Encoding 緩存不一樣內容,再根據具體請求中的 Accept-Encoding 字段返回最合適的版本。
可是有些實現得有 BUG 的緩存服務器,會忽略響應頭中的 Content-Encoding,從而可能給不支持壓縮的客戶端返回緩存的壓縮版本。有兩個方案能夠避免這種狀況發生:
一般爲了更好的利用中間實體的緩存功能,咱們都用第二種方案。
對於 css、js 這樣的靜態資源,只要客戶端支持 gzip,服務端應該老是啓用它;同時爲了不有 BUG 的緩存服務器給用戶返回錯誤的版本,還應該輸出 Vary: Accept-Encoding。
一般,上面說的這些工做,Web Server 均可以幫咱們搞定。對於 Nginx 來講,下面這個配置能夠自動給啓用了 gzip 的響應加上 Vary: Accept-Encoding:
gzip_vary on;
|
用 curl 驗證我博客的 js 文件,響應頭以下:
jerry@www:~$ curl --
head
https:
//www
.imququ.com/...
/xx
.js
HTTP
/1
.1 200 OK
Server: nginx
Date: Tue, 31 Dec 2013 16:34:48 GMT
Content-Type: application
/x-javascript
Content-Length: 66748
Last-Modified: Tue, 31 Dec 2013 14:30:52 GMT
Connection: keep-alive
Vary: Accept-Encoding
ETag:
"52c2d51c-104bc"
Expires: Fri, 29 Dec 2023 16:34:48 GMT
Cache-Control: max-age=315360000
Strict-Transport-Security: max-age=31536000
Accept-Ranges: bytes
|
能夠看到,服務端正確輸出了「Vary: Accept-Encoding」,一切正常。
可是用 Chrome 自帶抓包工具看下,這個響應頭倒是這樣:
HTTP
/1
.1 200 OK
cache-control: max-age=315360000
content-encoding:
gzip
content-
type
: application
/x-javascript
date
: Tue, 31 Dec 2013 16:35:27 GMT
expires: Fri, 29 Dec 2023 16:35:27 GMT
last-modified: Tue, 31 Dec 2013 14:30:52 GMT
server: nginx
status: 200
strict-transport-security: max-age=31536000
version: HTTP
/1
.1
|
個人博客支持 SPDY/2 協議,用 Chrome 訪問我博客會走 SPDY,因此上面的響應頭看上有點不一樣尋常,例如字段名都變成了小寫;多了 status、version 等字段,這些變化下次專門介紹(注:見「SPDY 3.1 中的請求 / 響應頭」)。神奇的是儘管服務端沒任何變化,但響應中的 Vary: Accept-Encoding 卻不見了。
SPDY 規定客戶端必須支持壓縮,這意味着 SPDY 服務器能夠直接啓用壓縮而不用關心請求頭中的 Accept-Encoding 字段。下面這段來自 Nginx 支持的 SPDY/2 協議:
User-agents are expected to support gzip and deflate compression. Regardless of the Accept-Encoding sent by the user-agent, the server may select gzip or deflate encoding at any time. [via]
因而,對於支持 SPDY 的客戶端來講,Vary: Accept-Encoding 沒有用途,Nginx 選擇直接去掉它,能夠節省一點流量。curl 或其餘不支持 SPDY 協議的客戶端仍是走 HTTP 協議,因此看到的響應頭是常規的。
Nginx 的這個作法是否合適一直有爭論,實際上並非全部支持 SPDY 的 Web Server 都會這麼作。例如即便經過 SPDY 協議訪問 Google 首頁的 js 文件,依然能夠看到 vary: Accept-Encoding:
HTTP
/1
.1 200 OK
status: 200 OK
version: HTTP
/1
.1
age: 25762
alternate-protocol: 443:quic
cache-control: public, max-age=31536000
content-encoding:
gzip
content-length: 154614
content-
type
: text
/javascript
; charset=UTF-8
date
: Tue, 31 Dec 2013 23:23:51 GMT
expires: Wed, 31 Dec 2014 23:23:51 GMT
last-modified: Mon, 16 Dec 2013 21:54:35 GMT
server: sffe
vary: Accept-Encoding
x-content-
type
-options: nosniff
x-xss-protection: 1; mode=block
|
另外,現階段 Chrome 和 Firefox 都支持 SPDY 協議,但 PageSpeed Chrome 版和 Firefox 版都沒有針對 SPDY 協議作特別處理,因此用它們測試個人博客,仍是會提示「Specify a Vary: Accept-Encoding header」,這有點讓人啼笑皆非。不過PageSpeed 在線版 已經更新規則,估計擴展版也快了。
PS:Vary 在 IE 下有不少坑,使用時要格外當心。網上這部分文章比較多,例如 hax 早年寫的 IE 與 Vary 頭,能夠點過去了解下。