寫在前面
最近抽空參加了幾場大廠的面試,突然發現一個現象,就是不論面試偏服務端的職位還是偏客戶端的職位,不論面試的 5 年以上的高級職位,還是 3 年左右的中級職位,面試官開頭所問問題必然是關於 HTTP 的。
我記得之前找工作的時候,似乎都是先考察一些職位所需技能領域的基礎知識,之後再考察關於 HTTP 的東西,現在大家都將 HTTP 的問題放到面試的開頭來問,我覺的應該是越來越多的招聘者意識到,作爲一個 Web 開發者,HTTP 真的是太重要了,必須要先考察。
回想起來,這幾年我自己對於 HTTP 的學習大多是碎片化的,很多東西無法系統地在腦海中組織起來。雖然感覺 HTTP 整體的學習難度是比較低的,但是各個知識點交雜在一起又變得很複雜很難,相信大家都會有同感。同時有些知識點,如果在實際工作中沒有采坑或者刻意深挖的話,很自然地就被忽略了。
由於在之前一次面試中,被狠狠地問了若干關於 Vary 的問題,所以想抽一些時間整理一下那些比較容易讓人忽略的知識點,算是查漏補缺吧。
內容協商
首先需要了解的是內容協商這個術語。當我們通過某個 URI 來訪問其指向的資源時,HTTP 協議可以通過內容協商機制提供資源的不同的展示形式。
如果缺少服務端開發經驗話,對於這個概念可能會感到陌生,但其實我們在工作中幾乎都會遇到它,比如在調用接口時,經常會用到 Accept: application/json
這個頭部,有時可能會用到 Accept: application/xml
,這就是內容協商,前者期望接口返回 json 格式的數據,而後者期望返回 xml 格式的數據。
一般客戶端涉及的常見頭部有以下幾個:
- Accept: 聲明客戶端可以處理的資源格式
- Accept-Charset: 聲明客戶端可以處理的字符集類型
- Accept-Language: 聲明客戶端可以理解的自然語言
- Accept-Encoding: 聲明客戶端支持的編碼格式
而服務端涉及的常見頭部包括:
- Content-Type: 指示資源的 MIME 類型
- Content-Language: 指示該資源所期望的自然語言
- Content-Encoding: 指示資源使用該編碼格式進行內容轉換
仔細觀察的話,會發現它們其實存在着一定程度的對應關係。原因也很簡單,既然是協商,那必然就會和兩個人在進行說話一樣,如果兩者之間的對話內容沒有關聯,他們還怎麼溝通呢?客戶端和服務端進行溝通同理。
如果想詳細瞭解該機制,可以參考MDN的文檔,很詳細,這裏就不多說了。
這裏順帶說明一下,對於內容協商機制中涉及的頭部,從 web 發展歷史上來看已經沒有什麼實質的用途了,原因如下(有興趣的話可以閱讀這篇wiki):
- Accept-Charset: 由於 utf-8 成爲主流的字符集類型,所以使用其他字符集類型的服務可以將其轉換爲 utf-8 類型
-
Accept-Language: 大體包含以下幾點
- 提供多種語言服務的網站往往是基於某種特定語言構建,再提供其他語言支持的,這樣每種語言類型的內容在質量上層次不齊,而訪問者可能會更傾向於內容質量更高的那一種語言,而內容協商機制無法替代用戶的主觀判斷
- 實踐中,對於切換網站語言的功能,切換方式往往更傾向於主動切換(比如提供一個切換的按鈕)而非自動切換
- 瀏覽器在用戶不提供語言相關配置的情況下,很難猜測用戶的自然語言傾向(一般可能會根據地理定位、ip等因素猜測),打個比方,比如我會經常出差去日本,但這不代表我會說日語,同時雖然我掛了加拿大的 vps,但是提供中文內容的網站,我還是傾向於看中文
- Accept: 與 Accept-Language 類似,同樣因爲內容的格式會因用戶的主觀意識而不同,還有諸多其他因素制約內容協商機制,所以最終失敗了。
唯一有些用途的是 Accept-Encoding,但鑑於如今大部分現代瀏覽器都已支持多種壓縮方式(常見的如 gzip、br),因此一定程度上已經不需要額外聲明這個頭部了,雖然大部分瀏覽器都會自動發送這個頭部,但其實這會造成額外 23 字節的浪費。
Vary 頭部
在理解(或者鞏固)了內容協商的概念後,就可以介紹 Vary 這個頭部了。直接引用 MDN 對於它的描述:
The Vary HTTP response header determines how to match future request headers to decide whether a cached response can be used rather than requesting a fresh one from the origin server.Vary 是一個HTTP響應頭部信息,它決定了對於未來的一個請求頭,應該使用一個緩存作爲響應還是向源服務器請求一個新的響應。
單純靠文檔對於 Vary 的描述來理解它其實是有些困難的,最起碼我會有這種感覺。
這個頭部的語法和其他的 HTTP 頭部類似,如下:
Vary: <header-name>, <header-name>, ...
不同的頭部之間使用逗號進行分割,同時可以指定 *
爲它的值,這樣等價於將資源視爲唯一,並不進行緩存,但這並不是最佳實踐,因此不建議這麼做。
Vary 的工作原理
一句話概括它的工作原理就是,就是它表示某個響應因某個響應頭部而不同。舉個例子,比如 Vary: Accept
的意思即爲,響應因請求資源格式頭部而不同,那麼通過相同 URI 訪問的資源就可以根據這個頭上知道其內容格式不同。
但我們已經知道,對於大部分內容協商機制中涉及的頭部,已經被看作是失敗的,那麼 Vary 和這些頭部搭配使用還有什麼意義呢?話雖如此,但 Vary 還可以與 HTTP 中其他的頭部來搭配使用,從而滿足很多應用場景下的特殊需求,比如動態服務、防止緩存錯亂等。
Vary 的應用場景
以下簡單羅列一些常用的應用場景以及採坑指南。
Vary 與 動態服務
關於動態服務,最常見的莫過於 Vary: User-Agent
。衆所周知,UA 是一段特徵字符串,通常包含區分客戶端類型、操作系統、版本號等信息,隨着移動 web 應用變得越流行,一個應用網站同時提供桌面和移動兩種版本的應用是很常見的事情。通過設置 Vary: User-Agent
頭部,對於搜索引擎,對於關鍵字的搜索結果可以提供更加準確的應用版本,對於客戶端,可以使其從緩存服務器獲取到相應應用類型的緩存版本,而不是錯誤地將桌面版緩存傳遞給移動版應用。
web 應用的性能在加載速度這一指標上,很大程度上取決於加載資源的大小,而圖片資源是所佔比例最大的一塊。爲了減少圖片的大小,除了對常見的圖片格式進行壓縮以外,chrome 推出的 WebP 格式也是不錯的選擇。但是這裏的問題是,不是所有的瀏覽器都支持 WebP 圖片格式的,所以這裏使用 Vary: Accept
來針對瀏覽器的支持情況返回相應的緩存副本,支持則返回 WebP 格式,不支持則返回縮略圖或者原圖。
還有其他關於動態服務的場景,比如要針對不同分辨率的屏幕加載不同質量的圖片(Client Hints 相關的頭部)、針對不同用戶身份提供不同的資源(Cookie頭部)等等。
Vary 與 緩存錯亂
有時候我們會發現響應中存在 Vary: Accept-Encoding
頭部信息,我原先按照內容協商機制中所描述的內容來理解,但到後來才發現,其實很大程度上是爲了防止緩存錯亂的問題。
設想一下,如果沒有這個頭部,當兩個分別支持 gzip 和 不支持 gzip 的客戶端對同一份資源進行獲取時,結果會變得十分微妙。如果不支持 gzip 的客戶端先訪問,緩存代理會緩存未壓縮的版本,那麼當支持 gzip 的客戶端再訪問時,由於命中緩存,雖然它支持 gzip 但也只能加載未壓縮的資源。反過來同樣如此,支持 gzip 客戶端先訪問,則緩存代理會緩存壓縮版本,當不支持 gzip 的客戶端再訪問時,緩存同樣命中,但是由於它無法對壓縮資源解碼,所以會呈現亂碼。
通過 Vary: Accept-Encoding
我們可以防止這種情況的發生,因爲 Vary 在這裏其實是扮演着校驗器的角色,它會進一步對命中緩存的資源進行再校驗,如果發現頭部信息不同,則會將緩存資源視爲無效,從而將請求繼續轉發至源服務器。這對於緩存代理服務器也有一定的益處,因爲可以有有依據地針對不同的 Accept-Encoding 緩存不同的資源副本。
Vary 與 緩存命中率
Vary 雖然可以防止緩存錯亂,但並不代表可以濫用,盲目的使用會適得其反,比如之前提及的 Vary: *
,這樣等價於將每個請求視爲唯一,並且不緩存其響應資源,除非有意爲之,不然沒有人會犧牲緩存帶來的性能提升。
同時對於一些 Header 的值是開放性的,比如之前提及的 User-Agent,如果單純從字面量來匹配的話,衆多桌面瀏覽器的值會因各種因素而不同的,如果僅是簡單地將 UA 作爲區分桌面端和移動端的依據,那麼緩存命中率會達到一個很低的水平。如何解決這個問題呢?可以將這些 UA 頭部的值進行標準化,比如可以通過正則匹配所有桌面瀏覽器的 UA 並重新更改爲 Desktop,之後再轉發至緩存代理和源服務器,這樣有利於提高緩存命中率,關於這部分的內容,可以參考這篇文章,其中有很細緻的講解。
所以我們要時刻留意,在使用 Vary 時,一定要根據緩存命中率作出調整,在不發生緩存錯亂的情況之下,儘可能的提高資源的緩存命中率。
Vary 與 CORS
對於跨域的有情況,Vary 也包含一些內容。HTTP 協議規定,當服務端響應包含 Access-Control-Allow-Origin 頭部,且它的值是一個具體的域名而不是通配符 *
,那麼這時必須要包含 Vary: Origin
這個頭部。
爲什麼要包含這個頭部,因爲請求頭中的 Origin 頭部代表了該請求來源的具體域名信息,那麼對於不同域名網站所發起的請求,會使用僅屬於它本身的緩存。一般而言,我們很少會遇到這種問題,因爲一般都將 Access-Control-Allow-Origin 設置爲了 *
,至少我自己是這樣的。如果想進一步瞭解 Vary 和 CORS 的內容,可以參考這篇文章。
最後
差不多就這麼多內容了,如有錯誤,還望指正。