前面的話
每天都有各種媒體對象經由HTTP傳送,如圖像、文本、影片以及軟件程序等。HTTP要確保它的報文被正確傳送,識別、提取以及適當處理。為了實現這些目標,HTTP使用了完善的標簽來描述承載內容的實體。本文將詳細介紹HTTP的實體和編碼
實體介紹
如果把HTTP報文想象成因特網貨運系統中的箱子,那么HTTP實體就是報文中實際的貨物。下圖展示了一個簡單的實體,裝在HTTP響應報文中

實體首部指出這是一個純文本文檔(Content-Type : text/plain),它只有18個字節長(Content-Length: 18)。和往常一樣,一個空白行(CRLF)把首部字段同主體的開始部分分隔開來
HTTP實體首部描述了HTTP報文的內容。HTTP/1.1版定義了以下10個基本字體首部字段
Content-Type 實體中所承載對象的類型 Content-Length 所傳送實體主體的長度或大小 Content-Language 與所傳送對象最相配的語言 Content-Encoding 對象數據所做的任意變換(比如,壓縮) Content-Location 一個備用位置,請求時可通過它獲得對象 Content-Range 如果這是部分實體,這個首部說明它是整體的哪部分 Content-MD5 實體主體內容的校驗和 Last-Modified 所傳輸內容在服務器上創建或最后修改的日期時間 Expires 實體數據將要失效的日期時間 Allow 該資源所允許的各種請求方法,如GET和HEAD ETag 這份文檔特定實例的唯一驗證碼 Cache-Control 指出應該如何緩存該文檔
[注意]ETag和Cache-Control首部沒有正式定義為實體首部,但它對許多涉及實體的操作來說,是很重要的
【實體主體】
實體主體中就是原始貨物。任何其他描述性的信息都包含在首部中。因為貨物(也就是實體主體)只是原始數據,所以需要實體首部來描述數據的意義。例如,Content-Type實體首部告訴我們如何去解釋數據(是圖像還是文本等),而Content-Encoding實體首部告訴我們數據是不是已被壓縮或者重編碼
首部字段以一個空白的CRLF行結束,隨后就是實體主體的原始內容。不管內容是什么,文本或二進制的、文檔或圖像、壓縮的或未壓縮的、英語、法語或日語,都緊隨這個CRLF之后
下圖展示了兩個實際的HTTP報文的例子。一個攜帶着文本實體,另一個承載的是圖像實體。十六進制的數值中展示的是報文的實際內容

在圖a中,實體主體從第65個字節開始,緊隨首部末尾的CRLF。實體主體中包含了“Hi! I’m a message!”這句話的ASCII編碼字符
在圖b中,實體主體從第67字節開始。實體主體包含了一個GIF格式圖像的二進制內容。GIF文件以6個字節的版本標志開頭,后面是16位的寬度和16位的髙度,可以在實體主體中直接看到這3項內容
實體大小
Content-Length首部指示出報文中實體主體的字節大小。這個大小是包含了所有內容編碼的。比如,對文本文件進行了gzip壓縮的話,Content-Length首部就是壓縮后的大小,而不是原始大小
除非使用了分塊編碼,否則Content-Length首部就是帶有實體主體的報文必須使用的。使用Content-Length首部是為了能夠檢測出服務器崩潰而導致的報文截尾,並對共享持久連接的多個報文進行正確分段
HTTP的早期版本采用關閉連接的辦法來划定報文的結束。但是,沒有Content-Length的話,客戶端無法區分到底是報文結束時正常的連接關閉,還是報文傳輸中由於服務器崩潰而導致的連接關閉。客戶端需要通過Content-Length來檢測報文截尾
報文截尾的問題對緩存代理服務器來說尤其嚴重。如果緩存服務器收到被截尾的報文卻沒有識別出截尾的話,它可能會存儲不完整的內容並多次使用它來提供服務。緩存代理服務器通常不會為沒有顯式Content-Length首部的HTTP主體做緩存,以此來減小緩存已截尾報文的風險
錯誤的Content-Length比缺少Content-Length還要糟糕。因為某些早期的客戶端和服務器在Content-Length計算上存在一些眾所周知的錯誤,有些客戶端、服務器以及代理中就包含了特別的算法,用來檢測和糾正與有缺陷服務器的交互過程。HTTP/1.1規定用戶Agent代理應該在接收且檢測到無效長度時通知用戶
Content-Length首部對於持久連接是必不可少的。如果響應通過持久連接傳送,就可能有另一條HTTP響應緊隨其后。客戶端通過Content-Length首部就可以知道報文在何處結束,下一條報文從何處開始。因為連接是持久的,客戶端無法依賴連接關閉來判別報文的結束。如果沒有Content-Length首部,HTTP應用程序就不知道某個實體主體在哪里結束,下一條報文從哪里開始
有一種情況下,使用持久連接時可以沒有Content-Length首部,即采用分塊編碼(chunked encoding)時。在分塊編碼的情況下,數據是分為一系列的塊來發送的,每塊都有大小說明。哪怕服務器在生成首部的時候不知道整個實體的大小(通常是因為實體是動態生成的),仍然可以使用分塊編碼傳輸若干已知大小的塊
HTTP允許對實體主體的內容進行編碼,比如可以使之更安全或進行壓縮以節省空間。如果主體進行了內容編碼,Content-Length首部說明的就是編碼后(encoded)的主體的字節長度,而不是未編碼的原始主體的長度
某些HTTP應用程序在這方面搞錯了,發送的是數據編碼之前的大小,這會導致嚴重的錯誤,尤其是用在持久連接上。不幸的是,HTTP/1.1規范中沒有首部可以用來說明原始的、未編碼的主體的長度,這就讓客戶端難以驗證解碼過程的完整性
【確定規則】
下面列出的規則說明了在若干不同的情況下如何正確計算主體的長度和結束位置。這些規則應當按順序應用,誰先匹配就用誰
1、如果特定的HTTP報文類型中不允許帶有主體,就忽略Content-Length首部,它是對沒有實際發送出來的主體進行計算的。這種情況下,Content-Length首部是提示性的,並不說明實際的主體長度
最重要的例子就是HEAD響應。HEAD方法請求服務器發送等價的GET請求中會出現的首部,但不要包括主體。因為對GET的響應會帶有Content-Length首部,所以HEAD響應里面也有,但和GET響應不同的是,HEAD響應中不會有主體。1XX、204以及304響應也可以有提示性的Content-Length首部,但是也都沒有實體主體。那些規定不能帶有實體主體的報文,不管帶有什么首部字段,都必須在首部之后的第一個空行終止
2、如果報文中含有描述傳輸編碼的Transfer-Encoding首部(不采用默認的 HTTP“恆等”編碼),那實體就應由一個稱為“零字節塊”(zero-byte chunk)的特殊模式結束,除非報文已經因連接關閉而結束
3、如果報文中含有Content-Length首部(並且報文類型允許有實體主體),而且沒有非恆等的Transfer-Encoding首部字段,那么Content-Length的值就是主體的長度。如果收到的報文中既有Content-Length首部字段又有非恆等的Transfer-Encoding首部字段,那就必須忽略Content-Length,因為傳輸編碼會改變實體主體的表示和傳輸方式(因此可能就會改變傳輸的字節數)
4、如果報文使用了multipart/byteranges(多部分/字節范圍)媒體類型,並且沒有用Content-Length首部指出實體主體的長度,那么多部分報文中的每個部分都要說明它自己的大小。這種多部分類型是唯一的一種自定界的實體主體類型,因此除非發送方知道接收方可以解析它,否則就不能發送這種媒體類型
5、如果上面的規則都不匹配,實體就在連接關閉的時候結束。實際上,只有服務器可以使用連接關閉來指示報文的結束。客戶端不能用關閉連接來指示客戶端報文的結束,因為這樣會使服務器無法發回響應
為了和使用HTTP/1.0的應用程序兼容,任何帶有實體主體的HTTP/1.1請求都必須帶有正確的Content-Length首部字段(除非已經知道服務器兼容HTTP/1.1)
HTTP/1.1規范中建議對於帶有主體但沒有Content-Length首部的請求,服務器如果無法確定報文的長度,就應當發送400 Bad Request響應或411 Length Required響應,后一種情況表明服務器要求收到正確的Content-Length首部
實體摘要
盡管HTTP通常都是在像TCP/IP這樣的可靠傳輸協議之上實現的,但仍有很多因素會導致報文的一部分在傳輸過程中被修改,比如有不兼容的轉碼代理,或者中間代理有誤等等。為檢測實體主體的數據是否被不經意地修改,發送方可以在生成初始的主體時,生成一個數據的校驗和,這樣接收方就可以通過檢査這個校驗和來捕獲所有意外的實體修改了
服務器使用Content-MD5首部發送對實體主體運行MD5算法的結果。只有產生響應的原始服務器可以計算並發送Content-MD5首部。中間代理和緩存不應當修改或添加這個首部,否則就會與驗證端到端完整性的這個最終目的相沖突。Content-MD5首部是在對內容做了所有需要的內容編碼之后,還沒有做任何傳輸編碼之前,計算出來的。為了驗證報文的完整性,客戶端必須先進行傳輸編碼的解碼,然后計算所得到的未進行傳輸編碼的實體主體的MD5
如果一份文檔使用gzip算法進行壓縮,然后用分塊編碼發送,那么就對整個經gzip壓縮的主體進行MD5計算
除了檢査報文的完整性之外,MD5還可以當作散列表的關鍵字,用來快速定位文檔並消除不必要的重復內容存儲。除了這些可能的用法,一般不常用到Content-MD5首部
作為對HTTP的擴展,在IETF的草案中提出了其他一些摘要算法。這些擴展建議增加新的Want-Digest首部,它允許客戶端說明期望響應中使用的摘要類型,並使用質量值來建議多種摘要算法並說明優先順序
媒體類型
Content-Type首部字段說明了實體主體的MIME類型。MIME類型是標准化的名字,用以說明作為貨物運載實體的基本媒體類型(比如:HTML文件、Microsoft Word文檔或是MPEG視頻等)。客戶端應用程序使用MIME類型來解釋和處理其內容
Content-Type的值是標准化的MIME類型,都在互聯網號碼分配機構(Internet Assigned Numbers Authority,簡稱IANA)中注冊。MIME類型由一個主媒體類型(比如:text、image或audio等)后面跟一條斜線以及一個子類型組成,子類型用於進一步描述媒體類型
[注意]要訪問完整的MIME媒體類型注冊列表請移步至此
下表中列出了一些Content-Type首部中常用的MIME類型
媒體類型 描 述 text/html 實體主體是HTML文檔 text/plain 實體主體是純文本文檔 image/gif 實體主體是GIF格式的圖像 image/jpeg 實體主體是JPEG格式的圖像 audio/x-wav 實體主體包含WAV格式聲音數據 model/vrml 實體主體是三維的VRML模型 applicaiion/vnd.ms-powerpoint 實體主體是Microsoft PowerPoint演示文檔 multipart/byteranges 實體主體有若干部分,每個部分都包含了完整文檔中不同的字節范圍 message/http 實體主體包含完整的HTTP報文(參見TRACE)
要着重注意的是,Content-Type首部說明的是原始實體主體的媒體類型。如果實體經過內容編碼的話,Content-Type首部說明的仍是編碼之前的實體主體的類型
Content-Type首部還支持可選的參數來進一步說明內容的類型。charset(字符集)參數就是個例子,它說明把實體中的比特轉換為文本文件中的字符的方法:
Content-Type: text/html; charset=iso-8859-4
MIME中的multipart(多部分)電子郵件報文中包含多個報文,它們合在一起作為單一的復雜報文發送。每一部分都是獨立的,有各自的描述其內容的集,不同的部分之間用分界字符串連接在一起
HTTP也支持多部分主體。不過,通常只用在下列兩種情形之一:提交填寫好的表格,或是作為承載若干文檔片段的范圍響應
【多部分表格提交】
當提交填寫的HTTP表格時,變長的文本字段和上傳的對象都作為多部分主體里面獨立的部分發送,這樣表格中就可以填寫各種不同類型和長度的值。比如,可能選擇用昵稱和小照片來填寫詢問你的名字和介紹信息的表格,而你的朋友可能填了她的全名並在介紹信息表內抱怨了一堆大眾汽車的修理問題
HTTP使用Content-Type:multipart/form-data或Content-Type:multipart/ mixed這樣的首部以及多部分主體來發送這種請求,舉例如下:
Content-Type: multipart/form-data;boundary=[abcdefghijklmnopqrstuvwxyz]
其中的boundary參數說明了分割主體中不同部分所用的字符串
下面的例子展示了multipart/form-data編碼。假設我們有這樣的表格:
<form action="http://server.com/cgi/handle" enctype="multipart/form-data" method="post"> <p>What is your name?<input type="text" name="submit-name"><br> What files are you sending?<input type="file" name="files"></p> <input type="submit" value="Send"><input type="reset"> </form>
如果用戶在文本輸入字段中鍵入Sally,並選擇了文本文件essayfile.txt,用戶Agent代理可能會發回下面這樣的數據:

如果用戶還選了另一個(圖像)文件imagefile.gif,用戶Agent代理可能像下面這樣構造這個部分:


【多部分范圍響應】
HTTP對范圍請求的響應也可以是多部分的。這樣的響應中有Content-Type: multipart/byteranges首部和帶有不同范圍的多部分主體。下面是一個例子,展示了對文檔不同范圍的請求產生的響應:

內容編碼
HTTP應用程序有時在發送之前需要對內容進行編碼。例如,在把很大的HTML文檔發送給通過慢速連接連上來的客戶端之前,服務器可能會對它進行壓縮,這樣有助於減少傳輸實體的時間。服務器還可以把內容攪亂或加密,以此來防止未經授權的第三方看到文檔的內容
這種類型的編碼是在發送方應用到內容之上的。當內容經過內容編碼之后,編好碼的數據就放在實體主體中,像往常一樣發送給接收方
【內容編碼過程】
內容編碼的過程如下所述
1、網站服務器生成原始響應報文,其中有原始的Content-Type和Content- Length首部
2、內容編碼服務器(也可能就是原始的服務器或下行的代理)創建編碼后的報文。編碼后的報文有同樣的Content-Type但Content-Length可能不同(比如主體被壓縮了)。內容編碼服務器在編碼后的報文中增加Content-Encoding首部,這樣接收的應用程序就可以進行解碼了
3、接收程序得到編碼后的報文,進行解碼,獲得原始報文
下圖給出了內容編碼的梗概示例

在這個例子中,通過gzip內容編碼函數對HTML頁面處理之后,得到一個更小的、壓縮的主體。經過網絡發送的是壓縮的主體,並打上了gzip壓縮的標志。接收的客戶端使用gzip解碼器對實體進行解壓縮
下面給出的響應片段是另一個編碼響應的例子(一個壓縮的圖像):
HTTP/1.1 200 OK Date: Fri, 05 Nov 2016 22:35:15 GMT Server: Apache/1.2.4 Content-Length: 6096 Content-Type: image/gif Content-Encoding: gzip [...]
注意,Content-Type首部可以且還應當出現在報文中。它說明了實體的原始格式,一旦實體被解碼,要顯示的時候,可能還是需要該信息才行的。記住,Content-Length首部現在代表的是編碼之后的主體長度
【內容編碼類型】
HTTP定義了一些標准的內容編碼類型,並允許用擴展編碼的形式增添更多的編碼。 由互聯網號碼分配機構(IANA)對各種編碼進行標准化,它給每個內容編碼算法分配了唯一的代號。Content-Encoding首部就用這些標准化的代號來說明編碼時使用的算法
下表列出了一些常用的內容編碼代號
Content-Encoding值 描述
gzip 表明實體采用GNU zip編碼
compress 表明實體采用Unix的文件壓縮程序
deflate 表明實體是用zlib的格式壓縮的
identity 表明沒有對實體進行編碼。當沒有Content-Encoding首部時,就默認為這種情況
gzip、compress以及deflate編碼都是無損壓縮算法,用於減少傳輸報文的大小,不會導致信息損失。這些算法中,gzip通常是效率最高的,使用最為廣泛
【Accept-Encoding 首部】
毫無疑問,我們不希望服務器用客戶端無法解碼的方式來對內容進行編碼。為了避免服務器使用客戶端不支持的編碼方式,客戶端就把自己支持的內容編碼方式列表放在請求的Accept-Encoding首部里發出去。如果HTTP請求中沒有包含Accept-Encoding首部,服務器就可以假設客戶端能夠接受任何編碼方式(等價於發送Accept-Encoding:*)
下圖展示HTTP事務中的Accept-Encoding首部

Accept-Encoding字段包含用逗號分隔的支持編碼的列表,下面是一些例子
Accept-Encoding: compress, gzip Accept-Encoding: * Accept-Encoding: compress;q=0.5, gzip; q=1.0 Accept-Encoding: gzip;q=l.0, identity; q=0.5, *;q=0
客戶端可以給每種編碼附帶Q(質值參數來說明編碼的優先級。Q值的范圍從0.0到1.0,0.0說明客戶端不想接受所說明的編碼,1.0則表明最希望使用的編碼。"*"表示“任何其他方法”。決定在響應中回送什么內容給客戶端是個更通用的過程,而選擇使用何種內容編碼則是此過程的一部分
identity編碼代號只能在Accept-Encoding首部中出現,客戶端用它來說明相對於其他內容編碼算法的優先級
傳輸編碼
內容編碼,是對報文的主體進行的可逆變換。內容編碼是和內容的具體格式細節緊密相關的。例如,可能會用gzip壓縮文本文件,但不是JPEG文件,因為JPEG這類東西用gzip壓縮的不夠好
傳輸編碼也是作用在實體主體上的可逆變換,但使用它們是由於架構方面的原因,同內容的格式無關。使用傳輸編碼是為了改變報文中的數據在網絡上傳輸的方式

【可靠傳輸】
長久以來,在其他一些協議中會用傳輸編碼來保證報文經過網絡時能得到“可靠傳輸”。在HTTP協議中,可靠傳輸關注的焦點有所不同,因為底層的傳輸設施已經標准化並且容錯性更好。在HTTP中,只有少數一些情況下,所傳輸的報文主體可能會引發問題,其中兩種情況如下所述
1、未知的尺寸
如果不先生成內容,某些網關應用程序和內容編碼器就無法確定報文主體的最終大小。通常,這些服務器希望在知道大小之前就開始傳輸數據。因為HTTP協議要求Content-Length首部必須在數據之前,有些服務器就使用傳輸編碼來發送數據,並用特別的結束腳注表明數據結束
2、安全性
可以用傳輸編碼來把報文內容擾亂,然后在共享的傳輸網絡上發送。不過,由於像SSL這樣的傳輸層安全體系的流行,就很少需要靠傳輸編碼來實現安全性了
【Transfer-Encoding首部】
HTTP協議中只定義了下面兩個首部來描述和控制傳輸編碼
Transfer-Encoding 告知接收方為了可靠地傳輸報文,已經對其進行了何種編碼
TE 用在請求首部中,告知服務器可以使用哪些傳輸編碼擴展
下面的例子中,請求使用了TE首部來告訴服務器它可以接受分塊編碼(如果是HTTP/1.1應用程序的話,這就是必須的)並且願意接受附在分塊編碼的報文結尾上的拖掛:
GET /new_products-html HTTP/1.1 Host: www.joes-hardware.com User-Agent: Mozilla/4.61 [en] (WinNT; I) TE: trailers, chunked
對它的響應中包含Transfer-Encoding首部,用於告訴接收方已經用分塊編碼對報文進行了傳輸編碼:
HTTP/1.1 200 OK Transfer-Encoding: chunked Server: Apache/3.0
在這個起始首部之后,報文的結構就將發生改變
傳輸編碼的值都是大小寫無關的。HTTP/1.1規定在TE首部和Transfer-Encoding首部中使用傳輸編碼值。最新的HTTP規范只定義了一種傳輸編碼,就是分塊編碼
與Accept-Encoding首部類似,TE首部也可以使用Q值來說明傳輸編碼的優先順序。不過,HTTP/1.1規范中禁止將分塊編碼關聯的Q值設為0.0
HTTP將來的擴展可能會推動對更多傳輸編碼的需求。如果真的如此,那分塊編碼仍應始終作用在其他傳輸編碼之上,這樣就保證數據可以像隧道那樣“穿透”那些只理解分塊編碼但不理解其他傳輸編碼的HTTP/1.1應用程序
【分塊編碼】
分塊編碼把報文分割為若干個大小已知的塊。塊之間是緊挨着發送的,這樣就不需要在發送之前知道整個報文的大小了
要注意的是,分塊編碼是一種傳輸編碼,因此是報文的屬性,而不是主體的屬性
1、分塊與持久連接
若客戶端和服務器之間不是持久連接,客戶端就不需要知道它正在讀取的主體的長度,而只需要讀到服務器關閉主體連接為止
當使用持久連接時,在服務器寫主體之前,必須知道它的大小並在Content-Length首部中發送。如果服務器動態創建內容,就可能在發送之前無法知道主體的長度
分塊編碼為這種困難提供了解決方案,只要允許服務器把主體逐塊發送,說明每塊的大小就可以了。因為主體是動態創建的,服務器可以緩沖它的一部分,發送其大小和相應的塊,然后在主體發送完之前重復這個過程。服務器可以用大小為0的塊作為主體結束的信號,這樣就可以繼續保持連接,為下一個響應做准備
分塊編碼是相當簡單的,下圖展示了一個分塊編碼報文的基本結構。它由起始的HTTP響應首部塊開始,隨后就是一系列分塊。每個分塊包含一個長度值和該分塊的數據。長度值是十六進制形式並將CRLF與數據分隔開。分塊中數據的大小以字節計算,不包括長度值與數據之間的CRLF序列以及分塊結尾的CRLF序列。最后一個塊有點特別,它的長度值為0,表示“主體結束”

客戶端也可以發送分塊的數據給服務器。因為客戶端事先不知道服務器是否接受分塊編碼(這是因為服務器不會在給客戶端的響應中發送TE首部),所以客戶端必須做好服務器用411 Length Required(需要Content-Length首部)響應來拒絕分塊請求的准備
2、分塊報文的拖掛
如果客戶端的TE首部中說明它可以接受拖掛的話,就可以在分塊的報文最后加上拖掛。產生原始響應的服務器也可以在分塊的報文最后加上拖掛。拖掛的內容是可選的無數據,客戶端不一定需要理解和使用,客戶端可以忽略並丟棄拖掛中的內容
拖掛中可以包含附帶的首部字段,它們的值在報文開始的時候可能是無法確定(例如,必須要先生成主體的內容)。Content-MD5首部就是一個可以在拖掛中發送的首部,因為在文檔生成之前,很難算出它的MD5。上圖中展示了拖掛的使用方式。報文首部中包含一個Trailer首部,列出了跟報文之后的首部列表。在Trailer首部中列出的首部就緊接在最后一個分塊之后
除了Transfer-Encoding、Trailer以及Content-Length首部之外,其他HTTP首部都可以作為拖掛發送
內容編碼與傳輸編碼可以同時使用。例如,下圖展示了發送方如何用內容編碼壓縮HTML文件,再使用傳輸編碼分塊發送。接收方“重構”主體的過程和發送方相反

【傳輸編碼的規則】
對報文主體使用傳輸編碼時,必須遵守以下規則:傳輸編碼集合中必須包括“分塊”。唯一的例外是使用關閉連接來結束報文;當使用分塊傳輸編碼時,它必須是最后一個作用到報文主體之上的;分塊傳輸編碼不能多次作用到一個報文主體上。這些規則使得接收方能夠確定報文的傳輸長度。
傳輸編碼是HTTP1.1版中引入的一個相對較新的特性。實現傳輸編碼的服務器必須特別注意不要把經傳輸編碼后的報文發送給非HTTP/1.1的應用程序。同樣地,如果服務器收到無法理解的經過傳輸編碼的報文,它應當用501 Unimplemented狀態碼來回復。不過,所有的HTTP/1.1應用程序至少都必須支持分塊編碼
實例操控
網站對象並不是靜態的。同樣的URL會隨着時間變化而指向對象的不同版本。以CNN的主頁為例,同一天里多次訪問http://www.cnn.com,可能每次得到的返回頁面都會略有不同
可以把CNN的主頁當作一個對象來考慮,其不同版本就可以看作這個對象的不同實例。在下圖中,客戶端多次請求同一個資源(URL),但得到的是該資源的不同實例,因為它是隨時間而變化的。在時間(a)和時間(b)具有相同的實例,而在時間(c)則是不同的實例

HTTP協議規定了稱為實例操控(instance manipulations)的一系列請求和響應操作,用以操控對象的實例。兩個主要的實例操控方法是范圍請求和差異編碼。這兩種方法都要求客戶端能夠標識它所擁有(如果有的話)的資源的特定副本,並在一定的條件下請求新的實例
【新鮮度】
現在再回顧上圖,客戶端起初沒有該資源的副本,因此它發送請求給服務器要求得到一份。服務器用該資源的版本1給以響應。客戶端現在可以緩存這份副本,但是要緩存多長時間呢?
當文檔在客戶端“過期”之后(也就是說,客戶端不再認為該副本有效),客戶端必須從服務器請求一份新的副本。不過,如果該文檔在服務器上並未發生改變,客戶端也就不需要再接收一次了——繼續使用緩存的副本即可
這種特殊的請求,稱為有條件的請求(conditional request),要求客戶端使用驗證碼(validator)來告知服務器它當前擁有的版本號,並僅當它的當前副本不再有效時才要求發送新的副本
服務器應當告知客戶端能夠將內容緩存多長時間,在這個時間之內就是新鮮的。服務器可以用這兩個首部之一來提供這種信息:Expires(過期) 和Cache-Control(緩存控制)
Expires首部規定文檔“過期”的具體時間——此后就不應當認為它還是最新的。Expires首部的語法如下:
Expires: Sun Mar 18 23:59:59 GMT 2016
客戶端和服務器為了能正確使用Expires首部,它們的時鍾必須同步。這並不總是很容易的,因為它們可能都沒有運行像NetworkTimeProtocol(網絡時間協議,NTP)這樣的時鍾同步協議。用相對時間來定義過期的機制會更有用。Cache-Control首部可以用秒數來規定文檔最長使用期——從文檔離開服務器之后算起的總計時間。使用期不與時鍾同步,因此可以給出更精確的結果
實際上,Cache-Control首部功能很強大。服務器和客戶端都可以用它來說明新鮮度,並且除了使用期或過期時間之外,還有很多指令可用。下表列出了Cache-Control首部的一些指令


【驗證碼】
當請求緩存服務器中的副本時,如果它不再新鮮,緩存服務器就需要保證它有一個新鮮的副本。緩存服務器可以向原始服務器獲取當前的副本。但在很多情況下,原始服務器上的文檔仍然與緩存中已過期的副本相同。緩存的副本或許已經過期了,但原始服務器上的內容與緩存的內容仍然相同。如果服務器上的文檔和已過期的緩存副本相同,而緩存服務器還是要從原始服務器上取文檔的話,那緩存服務器就是在浪費網絡帶寬,給緩存服務器和原始服務器增加不必要的負載,使所有事情都變慢了
為了避免這種情況,HTTP為客戶端提供了一種方法,僅當資源改變時才請求副本,這種特殊請求稱為有條件的請求。有條件的請求是標准的HTTP請求報文,但僅當某個特定條件為真時才執行。例如,某個緩存服務器可能發送下面的有條件GET報文給服務器,僅當文件/announce.html從2016年6月29日(這是緩存的文檔最后被作者修改的時間)之后發生改變的情況下才發送它:
GET /announce.html HTTP/1.0 If-Modified-Since: Sat, 29 Jun 2016, 14:30:00 GMT
有條件的請求是通過以“If-”開頭的有條件的首部來實現的。在上面的例子中,有條件的首部是If-Modified-Since(如果-從……之后-修改過)。有條件的首部使得方法僅在條件為真時才執行。如果條件不滿足,服務器就發回一個HTTP錯誤碼
每個有條件的請求都通過特定的驗證碼來發揮作用。驗證碼是文檔實例的一個特殊屬性,用它來測試條件是否為真。從概念上說,你可以把驗證碼看作文件的序列號、版本號,或者最后發生改變的日期時間
有條件的首部If-Modified-Since測試的是文檔實例最后被修改的日期時間,因此我們說最后被修改的日期時間就是驗證碼。有條件的首部If-None-Match測試的是文檔的ETag值,它是與實體相關聯的一個特殊的關鍵字,或者說是版本識別標記。Last-Modified和ETag是HTTP使用的兩種主要驗證碼。下表中列出了用於有條件請求的4種HTTP首部。每個有條件的首部之后就是這種首部所用的驗證碼類型

HTTP把驗證碼分為兩類:弱驗證碼(weak validators)和強驗證碼(strong validators)。弱驗證碼不一定能唯一標識資源的一個實例,而強驗證碼必須如此。弱驗證碼的一個例子是對象的大小字節數。有可能資源的內容改變了,而大小還保持不變,因此假想的字節計數驗證碼與改變是弱相關的。而資源內容的加密校驗和(比如MD5)就是強驗證碼,當文檔改變時它總是會改變
最后修改時間被當作弱驗證碼,因為盡管它說明了資源最后被修改的時間,但它的描述精度最大就是1秒。因為資源在1秒內可以改變很多次,而且服務器每秒可以處理數千個請求,最后修改日期時間並不總能反應變化情況。ETag首部被當作強驗證碼,因為每當資源內容改變時,服務器都可以在ETag首部放置不同的值。版本號和摘要校驗和也是很好的ETag首部候選,但它們不能帶有任意的文本。ETag首部很靈活,它可以帶上任意的文本值(以標記的形式),這樣就可以用來設計出各種各樣的客戶端和服務器驗證策略
有時候,客戶端和服務器可能需要采用不那么精確的實體標記驗證方法。例如,某服務器可能想對一個很大、被廣泛緩存的文檔進行一些美化修飾,但不想在緩存服務器再驗證時產生很大的傳輸流量。在這種情況下,該服務器可以在標記前面加上“W/”前綴來廣播一個“弱”實體標記。對於弱實體標記來說,只有當關聯的實體在語義上發生了重大改變時,標記才會變化。而強實體標記則不管關聯的實體發生了什么性質的變化,標記都一定會改變
下面的例子展示了客戶端如何用弱實體標記向服務器請求再驗證。服務器僅當文檔的內容從版本4.0算起發生了顯著變化時,才返回主體:
GET /announce.html HTTP/1.1 If-None-Match: W/"v4.0"
當客戶端多次訪問同一個資源時,首先需要判斷它當前的副本是不是仍然新鮮。如果不再新鮮,它們就必須從服務器獲取最新的版本。為了避免在資源沒有改變的情況下收到一份相同的副本,客戶端可以向服務器發送有條件的請求,說明能唯一標識客戶端當前副本的驗證碼。只在資源和客戶端的副本不同的情況下服務器才會發送其副本
【范圍請求】
關於客戶端如何要求服務器只在資源的客戶端副本不再有效的情況下才發送其副本,前面已經清楚地解釋了。HTTP還進一步錦上添花:它允許客戶端實際上只請求文檔的一部分,或者說某個范圍
假設正通過慢速的調制解調器連接下載最新的熱門軟件,已經下了四分之三,忽然因為一個網絡故障,連接中斷了。你已經為等待下載完成耽誤了很久,而現在被迫要全部重頭再來,祈禱着別再發生這樣的倒霉事了
有了范圍請求,HTTP客戶端可以通過請求曾獲取失敗的實體的一個范圍(或者說一部分),來恢復下載該實體。當然這有一個前提,那就是從客戶端上一次請求該實體到這次發出范圍請求的時段內,該對象沒有改變過
GET /bigfile.html HTTP/1.1 Host: www.joes-hardware.com Range: bytes=4000- User-Agent: Mozilla/4.61 [en] (WinNT; I)
在本例中,客戶端請求的是文檔開頭4000字節之后的部分(不必給出結尾字節數,因為請求方可能不知道文檔的大小)。在客戶端收到了開頭的4000字節之后就失敗的情況下,可以使用這種形式的范圍請求。還可以用Range首部來請求多個范圍(這些范圍可以按任意順序給出,也可以相互重疊)
例如,假設客戶端同時連接到多個服務器,為了加速下載文檔而從不同的服務器下載同一個文檔的不同部分。對於客戶端在一個請求內請求多個不同范圍的情況,返回的響應也是單個實體,它有一個多部分主體及Content-Type:multipart/byteranges首部
並不是所有服務器都接受范圍請求,但很多服務器可以。服務器可以通過在響應中包含Accept-Ranges首部的形式向客戶端說明可以接受的范圍請求。這個首部的值是計算范圍的單位,通常是以字節計算的。例如:
HTTP/1.1 200 0K Date: Fri, 05 Nov 2016 22:35:15 GMT Server: Apache/1.2.4 Accept-Ranges: bytes

Range首部在流行的點對點(Peer-to-Peer,P2P)文件共享客戶端軟件中得到廣泛應用,它們從不同的對等實體同時下載多媒體文件的不同部分
注意,范圍請求也屬於一類實例操控,因為它們是在客戶端和服務器之間針對特定的對象實例來交換信息的。也就是說,客戶端的范圍請求僅當客戶端和服務器擁有文檔的同一個版本時才有意義
【差異編碼】
我們曾把網站頁面的不同版本看作頁面的不同實例。如果客戶端有一個頁面的已過期副本,就要請求頁面的最新實例。如果服務器有該頁面更新的實例,就要把它發給客戶端,哪怕頁面上只有一小部分發生了改變,也要把完整的新頁面實例發給客戶端
若改變的地方比較少,與其發送完整的新頁面給客戶端,客戶端更願意服務器只發送頁面發生改變的部分,這樣就可以更快地得到最新的頁面。差異編碼是HTTP協議的一個擴展,它通過交換對象改變的部分而不是完整的對象來優化傳輸性能。差異編碼也是一類實例操控,因為它依賴客戶端和服務器之間針對特定的對象實例來交換信息。RFC 3229描述了差異編碼
下圖清楚地展示了差異編碼的結構,包括請求、生成、接收和裝配文檔的全過程。客戶端必須告訴服務器它有頁面的哪個版本,它願意接受頁面最新版的差異(delta),它懂得哪些將差異應用於現有版本的算法。服務器必須檢査它是否有這個頁面的客戶端現有版本,計算客戶端現有版本與最新版之間的差異(有若干算法可以計算兩個對象之間的差異)。然后服務器必須計算差異,發送給客戶端,告知客戶端所發送的是差異,並說明最新版頁面的新標識(ETag),因為客戶端將差異應用於其老版本之后就會得到這個版本

客戶端在If-None-Match首部中使用的是它所持有頁面版本的唯一標識,這個標識是服務器之前響應客戶端時在ETag首部中發送的。客戶端是在對服務器說:“如果你那里頁面的最新版本標識和這個ETag不同,就把這個頁面的最新版本發給我。”如果只有If-None-Match首部,服務器將會把該頁面的最新版本完整地發給客戶端。(假設最新版和客戶端持有的版本不同)
不過,如果客戶端想告訴服務器它願意接受該頁面的差異,只要發送A-IM首部就可以了。A-IM是Accept-Instance-Manipulation(接受實例操控)的縮寫。形象比喻的話,客戶端相當於這樣說:“哦對了,我能接受某些形式的實例操控,如果你會其中一種的話,就不用發送完整的文檔給我了。”在A-IM首部中,客戶端會說明它知道哪些算法可以把差異應用於老版本而得到最新版本。服務端發送回下面這些內容:一個特殊的響應代碼——226 IM Used,告知客戶端它正在發送的是所請求對象的實例操控,而不是那個完整的對象自身;一個IM(Instance-Manipulation的縮寫)首部,說明用於計算差異的算法,新的ETag首部和Delta-Base首部,說明用於計算差異的基線文檔的ETag(理論上,它應該和客戶端之前請求里的if-None-Match首部中的ETag相同)
下表總結了差異編碼使用的首部

客戶端可以使用A-IM首部說明可以接受的一些實例操控的類型。服務器在IM首部中說明使用的是何種實例操控。不過到底哪些實例操控類型是可接受的呢?它們又是做什么的呢?下表中列出了一些在IANA注冊的實例操控類型

上圖中,服務器側的“差異生成器”根據基線文檔和該文檔的最新實例,用客戶端在A-IM首部中指明的算法計算它們之間的差異。客戶端側的“差異應用器”得到差異,將其應用於基線文檔,得到文檔的最新實例。例如,如果產生差異的算法是Unix系統的diff-e命令,客戶端就可以用Unix系統中的文本編輯器ed提供的功能來應用差異,因為diff-e <file1> <file2>產生了一系列ed命令來把<file1>轉化為<file2>。ed是一個非常簡單的編輯器,支持一些命令。上圖的例子中,5c說明要刪除基線文檔的第5行,而chisels.<cr>.說明要添加chisels.,就這么簡單。對於更大的改動,會產生更復雜的指令。Unix系統的diff-e算法是對文件進行逐行比較的,這對於文本文件沒問題,但並不適合二進制文件。vcdiff算法更強大,對於非文本文件也適用,並且產生的差異比diff-e要小
差異編碼的規范中詳細定義了A-IM和IM首部的格式。在這里,我們只要知道這些首部中可以說明多個實例操控(並可以帶有相關的質量值)就夠了,在返回給客戶端之前,文檔可以經過多種實例操控,這樣可以獲得最大程度的壓縮。例如,用vcdiff算法產生的差異隨后可以再用gzip算法壓縮。於是服務器的響應中就含有IM:vcdiff,gzip首部。客戶端應當先對內容進行gunzip,再把得到的差異應用到自己的基線頁面上,這樣才能生成最終的文檔
差異編碼可以減少傳輸次數,但實現起來可能比較麻煩。設想一下頁面改動頻繁,而且有很多不同的人都在訪問的情形。支持差異編碼的服務器必須保存頁面隨時間變化的所有不同版本,這樣才能指出最新版本與所請求的客戶端持有的任意版本之間的差異
如果文檔變化頻繁,而且有很多客戶端都在請求文檔,那它們就會獲得文檔的不同實例。隨后當它們再向服務器發起請求時,它們將請求它們所持有的版本與最新版本之間的差異。為了能夠只向它們發送變化的部分,服務器必須保存所有客戶端曾經持有過的版本
要降低提交文檔時的延遲時間,服務器必須增加磁盤空間來保存文檔的各種舊的實例。實現差異編碼所需的額外磁盤空間可能很快就會將減少傳輸量獲得的好處抵消掉