HTTP 條件請求

HTTP 有一個條件請求的概念,其中請求的結果,甚至成功與否,都可以透過將受影響的資源與驗證器進行比較來控制。這些請求對於驗證快取內容非常有用,確保只有當它與瀏覽器已有的副本不同時才會被獲取。條件請求還可用於在恢復下載時確保文件的完整性,或在伺服器上上傳或修改文件時防止丟失更新。

原理

HTTP 條件請求是根據特定標頭的值以不同方式執行的請求。這些標頭定義了一個前置條件,如果前置條件匹配或不匹配,請求的結果將有所不同。

不同的行為由所使用的請求方法和用於前置條件的標頭集定義

  • 對於安全方法,例如通常嘗試獲取文件的GET,條件請求可用於僅在相關時才傳送迴文檔。因此,這節省了頻寬。
  • 對於不安全方法,例如通常上傳文件的PUT,條件請求可用於僅當其所基於的原始文件與伺服器上儲存的文件相同時才上傳文件。

驗證器

所有條件標頭都嘗試檢查伺服器上儲存的資源是否與特定版本匹配。為此,條件請求需要指示資源的版本。由於逐位元組比較整個資源是不切實際的,並且並非總是我們想要的,請求會傳輸一個描述版本的值。這些值稱為驗證器,分為兩種

  • 文件的上次修改日期,即last-modified日期。
  • 一個不透明的字串,唯一標識每個版本,稱為實體標籤,或ETag

比較同一資源的不同版本有點棘手:根據上下文,有兩種相等性檢查

  • 強驗證用於預期逐位元組相同的情況,例如在恢復下載時。
  • 弱驗證用於使用者代理只需要確定兩個資源是否具有相同內容的情況。即使存在微小差異,例如不同的廣告或帶有不同日期的頁尾,這些資源也可能被認為是相同的。

驗證型別與所使用的驗證器無關。Last-ModifiedETag都允許這兩種驗證型別,儘管在伺服器端實現它們的複雜性可能有所不同。HTTP 預設使用強驗證,並指定何時可以使用弱驗證。

強驗證

強驗證在於保證資源與所比較的資源逐位元組相同。這對於某些條件標頭是強制性的,對於其他標頭是預設的。強驗證非常嚴格,在伺服器級別可能難以保證,但它確實保證在任何時候都不會丟失資料,有時會犧牲效能。

使用Last-Modified進行強驗證時,很難獲得唯一識別符號。通常這是透過使用帶有資源 MD5 雜湊(或派生)的ETag來完成的。

注意:由於內容編碼的更改需要更改 ETag,因此一些伺服器在壓縮來自源伺服器的響應時(例如,反向代理)會修改 ETag。Apache 伺服器預設將壓縮方法的名稱(-gzip)附加到 ETag,但這可以透過DeflateAlterETag指令進行配置

弱驗證

弱驗證與強驗證不同,它認為文件的兩個版本在內容等效時是相同的。例如,如果一個頁面與另一個頁面僅在其頁尾中日期不同或廣告不同,則在弱驗證下將被視為相同。在使用強驗證時,這兩個相同的版本被視為不同。構建使用弱驗證的 ETag 系統對於最佳化快取效能非常有用,但可能很複雜,因為它涉及瞭解頁面不同元素的重要性。

條件標頭

幾個 HTTP 標頭,稱為條件標頭,會導致條件請求。它們是

If-Match

如果遠端資源的ETag與此標頭中列出的其中一個相等,則成功。它執行強驗證。

If-None-Match

如果遠端資源的ETag與此標頭中列出的每個都不相同,則成功。它執行弱驗證。

If-Modified-Since

如果遠端資源的Last-Modified日期比此標頭中給定的日期新,則成功。

If-Unmodified-Since

如果遠端資源的Last-Modified日期比此標頭中給定的日期舊或相同,則成功。

If-Range

類似於If-MatchIf-Unmodified-Since,但只能有一個 ETag 或一個日期。如果失敗,範圍請求將失敗,並且不會返回206 Partial Content響應,而是傳送帶有完整資源的200 OK

用例

快取更新

條件請求最常見的用例是更新快取。在空快取或沒有快取的情況下,請求的資源將以200 OK的狀態返回。

The request issued when the cache is empty triggers the resource to be downloaded, with both validator values sent as headers. The cache is then filled.

與資源一起,驗證器在標頭中傳送。在此示例中,Last-ModifiedETag都已傳送,但也可以只發送其中一個。這些驗證器與資源一起快取(像所有標頭一樣),並將在快取過時後用於建立條件請求。

只要快取未過時,就不會發出任何請求。但一旦它過時,這主要由Cache-Control標頭控制,客戶端不會直接使用快取的值,而是發出條件請求。驗證器的值用作If-Modified-SinceIf-None-Match標頭的引數。

如果資源未更改,伺服器會返回304 Not Modified響應。這使得快取再次新鮮,客戶端使用快取的資源。儘管存在消耗一些資源的響應/請求往返,但這比再次透過網路傳輸整個資源更高效。

With a stale cache, the conditional request is sent. The server can determine if the resource changed, and, as in this case, decide not to send it again as it is the same.

如果資源已更改,伺服器只會返回200 OK響應,其中包含資源的新版本(就像請求不是條件請求一樣)。客戶端使用此新資源(並將其快取)。

In the case where the resource was changed, it is sent back as if the request wasn't conditional.

除了在伺服器端設定驗證器外,此機制是透明的:所有瀏覽器都管理快取併發送此類條件請求,Web 開發人員無需進行任何特殊工作。

部分下載的完整性

檔案的部分下載是 HTTP 的一項功能,它允許透過保留已獲得的資訊來恢復以前的操作,從而節省頻寬和時間

A download has been stopped and only partial content has been retrieved.

支援部分下載的伺服器透過傳送Accept-Ranges標頭來廣播此功能。一旦發生這種情況,客戶端可以透過傳送帶有缺失範圍的Range標頭來恢復下載

The client resumes the requests by indicating the range he needs and preconditions checking the validators of the partially obtained request.

原理很簡單,但有一個潛在問題:如果下載的資源在兩次下載之間被修改,則獲得的範圍將對應於資源的不同版本,最終文件將損壞。

為了防止這種情況,使用條件請求。對於範圍,有兩種方法可以做到這一點。更靈活的方法是使用If-Unmodified-SinceIf-Match,如果前置條件失敗,伺服器會返回錯誤;然後客戶端從頭開始重新下載

When the partially downloaded resource has been modified, the preconditions will fail and the resource will have to be downloaded again completely.

即使此方法有效,當文件更改時,它也會增加額外的響應/請求交換。這會損害效能,HTTP 有一個特定的標頭來避免這種情況:If-Range

The If-Range headers allows the server to directly send back the complete resource if it has been modified, no need to send a 412 error and wait for the client to re-initiate the download.

此解決方案更高效,但靈活性略低,因為條件中只能使用一個 ETag。很少需要這種額外的靈活性。

透過樂觀鎖避免丟失更新問題

Web 應用程式中的常見操作是更新遠端文件。這在任何檔案系統或原始碼控制應用程式中都非常常見,但任何允許儲存遠端資源的應用程式都需要這種機制。維基百科和其他 CMS 等常見網站都有這種需求。

使用PUT方法可以實現這一點。客戶端首先讀取原始檔案,修改它們,最後將它們推送到伺服器

Updating a file with a PUT when concurrency is not involved.

不幸的是,一旦我們考慮到併發性,事情就會變得有點不準確。當一個客戶端正在本地修改其資源的新副本時,第二個客戶端可以獲取相同的資源並在其副本上執行相同的操作。接下來發生的事情非常不幸:當它們提交回伺服器時,第一個客戶端的修改會被下一個客戶端的推送丟棄,因為第二個客戶端不知道第一個客戶端對資源的更改。關於誰獲勝的決定不會傳達給另一方。保留哪個客戶端的更改將取決於它們提交的速度;這取決於客戶端、伺服器的效能,甚至是在客戶端編輯文件的人。獲勝者將每次都不同。這是一個競態條件,會導致有問題行為,這些行為難以檢測和除錯

When several clients update the same resource in parallel, we are facing a race condition: the slowest win, and the others don't even know they lost. Problematic!

沒有辦法在不打擾兩個客戶端之一的情況下解決這個問題。但是,必須避免丟失更新和競態條件。我們希望得到可預測的結果,並期望在客戶端的更改被拒絕時通知他們。

條件請求允許實現樂觀鎖演算法(大多數維基或原始碼控制系統都使用)。這個概念是允許所有客戶端獲取資源的副本,然後讓他們在本地修改它,透過成功允許第一個客戶端提交更新來控制併發性。所有後續的、基於現在過時的資源版本的更新都會被拒絕

Conditional requests allow to implement optimistic locking: now the quickest wins, and the others get an error.

這是透過使用If-MatchIf-Unmodified-Since標頭實現的。如果 ETag 與原始檔案不匹配,或者檔案自獲取以來已被修改,則更改將被拒絕並顯示412 Precondition Failed錯誤。然後由客戶端處理錯誤:要麼通知使用者重新開始(這次是在最新版本上),要麼向用戶顯示兩個版本的差異,幫助他們決定要保留哪些更改。

處理資源的首次上傳

資源的首次上傳是前一個的特例。像資源的任何更新一樣,如果兩個客戶端試圖在相似的時間執行,它也會受到競態條件的影響。為了防止這種情況,可以使用條件請求:透過新增If-None-Match並使用特殊值*,表示任何 ETag。請求將成功,僅當資源以前不存在時

Like for a regular upload, the first upload of a resource is subject to a race condition: If-None-Match can prevent it.

If-None-Match僅適用於符合 HTTP/1.1(及更高版本)的伺服器。如果不確定伺服器是否符合,您需要首先向資源發出HEAD請求以進行檢查。

總結

條件請求是 HTTP 的一個關鍵特性,允許構建高效且複雜的應用程式。對於快取或恢復下載,網站管理員唯一需要做的工作是正確配置伺服器;在某些環境中設定正確的 ETag 可能很棘手。一旦完成,瀏覽器將提供預期的條件請求。

對於鎖定機制,情況正好相反:Web 開發人員需要使用適當的標頭髮出請求,而網站管理員主要可以依靠應用程式來為他們執行檢查。

在兩種情況下都很清楚,條件請求是 Web 背後的一個基本功能。

另見