HTTP 條件請求

HTTP 有一個條件請求的概念,其中請求的結果,甚至成功的請求,可以透過將受影響的資源與驗證器的值進行比較來改變。這種請求對於驗證快取的內容很有用,並避免無用的控制,以驗證文件的完整性,例如在恢復下載時,或在將文件上傳或修改到伺服器時防止更新丟失。

原則

HTTP 條件請求是根據特定標頭的值以不同方式執行的請求。這些標頭定義了先決條件,如果先決條件匹配或不匹配,則請求的結果將不同。

不同的行為由使用的請求方法和用於先決條件的標頭集定義。

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

驗證器

所有條件標頭都嘗試檢查儲存在伺服器上的資源是否與特定版本匹配。為此,條件請求需要指示資源的版本。由於逐位元組比較整個資源是不可行的,而且並不總是想要的,因此請求傳輸一個描述版本的 value。此類 value 稱為驗證器,分為兩種。

  • 文件的最後修改日期,即最後修改日期。
  • 一個不透明字串,唯一地標識每個版本,稱為實體標籤etag

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

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

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

強驗證

強驗證包括確保資源與它所比較的資源在位元組上完全相同。對於某些條件標頭而言,這是必需的,對於其他條件標頭而言,這是預設的。強驗證非常嚴格,可能難以在伺服器級別保證,但它確實保證任何時候都不會丟失資料,有時會以效能為代價。

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

弱驗證

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

條件頭

幾個 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.

與資源一起,驗證器在標頭中傳送。在此示例中,both 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 開發人員做任何特殊工作。

部分下載的完整性

檔案的 partial downloading 是 HTTP 的一項功能,它允許恢復之前的操作,透過保留已獲得的資訊來節省頻寬和時間。

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

支援 partial downloads 的伺服器透過傳送 Accept-Ranges 標頭來廣播這一點。一旦發生這種情況,客戶端可以透過傳送帶有丟失範圍的 Ranges 標頭來恢復下載。

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 is very simple 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 錯誤拒絕。然後由客戶端來處理錯誤:要麼通知使用者重新開始(這次使用最新的版本),要麼向用戶顯示兩個版本的diff,幫助他們決定要保留哪些更改。

處理資源的第一次上傳

資源的第一次上傳是前面提到的邊緣情況。與任何更新資源一樣,如果兩個客戶端嘗試在類似的時間執行,它也會受到競爭條件的影響。為了防止這種情況,可以使用條件請求:透過新增 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 的一項關鍵功能,它允許構建高效且複雜的應用程式。 對於快取或恢復下載,網站管理員只需要正確配置伺服器;在某些環境中設定正確的 etags 可能很棘手。 一旦實現,瀏覽器將提供預期的條件請求。

對於鎖定機制,情況恰恰相反:Web 開發人員需要發出包含正確標頭的請求,而網站管理員則可以主要依賴應用程式來執行檢查。

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