通過調用ApplicationBuilder的擴展方法UseStaticFiles注冊的StaticFileMiddleware中間件幫助我們處理針對文件的請求。對於StaticFileMiddleware處理請求的邏輯,大部分讀者都應該想得到:它根據請求的地址找到目標文件的路徑,然後利用注冊的ContentTypeProvider根據路徑解析出與文件內容相匹配的媒體類型,默認情況下得到的媒體類型是根據目標文件的擴展名解析出來的。解析出來的媒體類型將作為響應報頭Content-Type的值。StaticFileMiddleware中間件最終利用FileProvider讀取文件的內容作為響應消息的主體。實際上,這個中間件在處理請求時比我們想象的要多得多,針對條件請求(Conditional Request)和區間請求(Range Request)的處理就沒有在上面演示的實例中體現出來。 [本文已經同步到《ASP.NET Core框架揭秘》之中]
目錄
一、條件請求
HTTP條件請求
針對靜態文件的條件請求
二、 區間請求
HTTP區間請求
針對靜態文件的區間請求
所謂的條件請求就是客戶端在發送GET請求獲取某種資源的時候,會利用請求報頭攜帶一些條件。服務端處理器在接受到這樣的請求之後,會提取這些條件並驗證目標資源的當前的狀態是否滿足客戶端指定的條件。在有在這些條件滿足的情況下,目標資源的內容才會真正響應給客戶端。
HTTP條件請求作為一項標准記錄在HTTP規范中。一般來說,一個GET請求在目標資源存在的情況下總是會返回一個狀態為“200 OK”的響應,目標資源的內容將直接存放在響應消息的主體部分。如果資源的內容不會輕易改變,我們希望客戶端(比如浏覽器)在本地緩存獲取的資源。對於由它發送的針對同一資源的後續請求,如果資源內容不曾改變,那麼資源的內容則無需再次作為網絡負載予以響應。這就是條件請求需要解決的一個典型場景。
確定資源是否發生變化可以采用兩種策略。第一種就是讓資源的提供者記錄下最後一次更新資源的時間,資源的負載和這個時間戳將一並作為響應提供給作為請求發送者的客戶端。客戶端在緩存資源自身內容的同時也會保存這個時間戳。等到下次針對同一資源發送請求,它會將這個時間戳一並發送出去,那麼服務端就可以根據這個時間戳判斷目標資源在上次響應之後是否被修改過。除了采用記錄資源最後修改時間的方式外,我們還可以針對資源的內容生成一個“簽名”,簽名的一致性體現了資源內容的一致性,在HTTP規范中將這個簽名成為ETag(Entity Tag)。
接下來我們從HTTP請求和響應報文的層面對條件請求進行詳細介紹。對於HTTP請求來說,緩存資源攜帶的最後修改時間戳和ETag分別保存在名為If-Modified-Since和If-None-Match的報頭中。報頭名稱體現的意思是如果目標資源在指定的時間之後被修過(If-Modified-Since)或者目前資源的狀態與提供ETag的不匹配(If-None-Match)才將目標資源的內容作為響應負載返回。
當服務端接收到針對某個資源的GET請求,如果請求不具有上述這兩個報頭或者根據這兩個報頭攜帶的信息判斷資源已經發生改變,在的情況下會返回一個狀態碼為“200 OK”的響應。除了將資源內容作為響應主體之外,如果能夠獲取到該資源最後一次修改的時間(一般精確到秒),格式化的時間戳將保存到一個名為Last-Modified的報頭中。至於針對資源自身內容生成的簽名,對應的報頭名稱就是ETag。反之,如果做出相反的判斷,服務端會響應一個狀態碼為“304 Not Modified”的響應,這個響應不具有主體。一般來說,這樣的響應也會攜帶Last-Modified和ETag報頭。
與條件請求相關的請求報頭還具有額外兩個,即If-Unmodified-Since和If -Match,它們具有與If-Modified-Since和If-None-Match完全相反的語義,分別表示如果目標資源在指定時間之後沒有被修改(If-Unmodified-Since)或者目標資源目前的ETag與提供的ETag匹配(If-Match)的請求下才將資源作為響應負載返回。針對這樣的請求,如果根據攜帶的這兩個報頭判斷出目標資源並不曾發生變化,服務端會返回一個將資源內容作為主體的“200 OK”響應,這樣的響應也會攜帶Last-Modified和If-Match報頭。反之,如果做出了相反的判斷,服務端會響應一個狀態碼為“412 Precondition Failed”的響應。
接下來我們通過實例演示的形式來介紹StaticFileMiddleware中間件在針對條件請求方面做了些什麼。假設我們在ASP.NET Core應用中發布一個文本文件(foobar.txt),內容為“abcdefghijklmnopqrstuvwxyz0123456789”(26個字母+10個數字),目標地址為“http://localhost:5000/foobar.txt”。現在我們直接利用Fiddler針對這個地址發送一個普通的GET請求,看看會得到怎樣的響應。
1: GET http://localhost:5000/foobar.txt HTTP/1.1
2: Host: localhost:5000
3:
4: HTTP/1.1 200 OK
5: Date: Thu, 10 Nov 2016 13:01:59 GMT
6: Content-Length: 39
7: Content-Type: text/plain
8: Server: Kestrel
9: Last-Modified: Thu, 10 Nov 2016 01:43:37 GMT
10: Accept-Ranges: bytes
11: ETag: "1d23af3dad4aaa7"
12:
13: abcdefghijklmnopqrstuvwxyz0123456789從上面給出的請求與響應報文的內容可以看出,對於一個針對物理文件的GET請求,如果目標文件存在,服務器會返回一個狀態碼為“200 OK”的響應。除了承載著文件內容的主體外,響應報文還具有兩個額外的報頭,它們分別是表示目標文件最後一次修改時間的Last-Modified和作為文件簽名的ETag。
現在客戶端不但獲得了目標文件的內容,還得到了該文件最後被修改的時間戳和簽名,如果它只想確定這個文件是否被更新,並在在更新之後返回新的內容,那麼它可以針對這個文件所在的地址再次發送一個GET請求,並將這個時間戳和簽名通過相應的請求報頭發送給服務端,我們知道這兩個報頭的名稱分別是If-Modified-Since和If-None-Match。由於我們沒有修改文件的內容,所以服務器返回如下一個狀態為“304 Not Modified”的響應,這個不包括主體的響應同樣具有相同的Last-Modified和ETag報頭。
1: GET http://localhost:5000/foobar.txt HTTP/1.1
2: Host: localhost:5000
3: If-Modified-Since: Thu, 10 Nov 2016 01:43:37 GMT
4: If-None-Match: "1d23af3dad4aaa7"
5:
6: HTTP/1.1 304 Not Modified
7: Date: Thu, 10 Nov 2016 13:23:04 GMT
8: Content-Type: text/plain
9: Server: Kestrel
10: Last-Modified: Thu, 10 Nov 2016 01:43:37 GMT
11: Accept-Ranges: bytes
12: ETag: "1d23af3dad4aaa7"
如果我們將If-None-Match報頭修改成一個較早的時間戳,或者改變了If-None-Match報頭的簽名,服務端都將做出文件已經被修改的判斷。在這種情況下,最初那個狀態碼為“200 OK”的響應又會再次被返回,具體請求和對應的響應體現在如下所示的代碼片段中。
1: GET http://localhost:5000/foobar.txt HTTP/1.1
2: Host: localhost:5000
3: If-Modified-Since: Wed, 09 Nov 2016 01:43:37 GMT
4: If-None-Match: "1d23af3dad4aaa7"
5:
6: HTTP/1.1 200 OK
7: Date: Thu, 10 Nov 2016 13:30:25 GMT
8: Content-Length: 39
9: Content-Type: text/plain
10: Server: Kestrel
11: Last-Modified: Thu, 10 Nov 2016 01:43:37 GMT
12: Accept-Ranges: bytes
13: ETag: "1d23af3dad4aaa7"
14:
15: GET http://localhost:5000/foobar.txt HTTP/1.1
16: Host: localhost:5000
17: If-Modified-Since: Thu, 10 Nov 2016 01:43:37 GMT
18: If-None-Match: "abc123xyz456"
19:
20: HTTP/1.1 200 OK
21: Date: Thu, 10 Nov 2016 13:31:49 GMT
22: Content-Length: 39
23: Content-Type: text/plain
24: Server: Kestrel
25: Last-Modified: Thu, 10 Nov 2016 01:43:37 GMT
26: Accept-Ranges: bytes
27: ETag: "1d23af3dad4aaa7"
28:
29: abcdefghijklmnopqrstuvwxyz0123456789
1: GET http://localhost:5000/foobar.txt HTTP/1.1
2: Host: localhost:5000
3: If-Unmodified-Since: Fri, 11 Nov 2016 01:43:37 GMT
4:
5: HTTP/1.1 200 OK
6: Date: Thu, 10 Nov 2016 13:46:32 GMT
7: Content-Length: 39
8: Content-Type: text/plain
9: Server: Kestrel
10: Last-Modified: Thu, 10 Nov 2016 01:43:37 GMT
11: Accept-Ranges: bytes
12: ETag: "1d23af3dad4aaa7"
13:
14: abcdefghijklmnopqrstuvwxyz0123456789
15:
16: GET http://localhost:5000/foobar.txt HTTP/1.1
17: Host: localhost:5000
18: If-Match: "1d23af3dad4aaa7"
19:
20: HTTP/1.1 200 OK
21: Date: Thu, 10 Nov 2016 13:47:42 GMT
22: Content-Length: 39
23: Content-Type: text/plain
24: Server: Kestrel
25: Last-Modified: Thu, 10 Nov 2016 01:43:37 GMT
26: Accept-Ranges: bytes
27: ETag: "1d23af3dad4aaa7"
28:
29: abcdefghijklmnopqrstuvwxyz0123456789
如果目標文件當前的狀態不能滿足If-Unmidified-Since或者If-Match報頭體現的條件,那麼返回的將是一個狀態為“412 Preconception Failed”的響應,如下所示的就是兩條這樣的請求和對應響應的內容。
1: GET http://localhost:5000/foobar.txt HTTP/1.1
2: Host: localhost:5000
3: If-Unmodified-Since: Wed, 9 Nov 2016 01:43:37 GMT
4:
5: HTTP/1.1 412 Precondition Failed
6: Date: Thu, 10 Nov 2016 13:54:09 GMT
7: Content-Length: 0
8: Server: Kestrel
9:
10: GET http://localhost:5000/foobar.txt HTTP/1.1
11: Host: localhost:5000
12: If-Match: "abc123xyz456"
13:
14: HTTP/1.1 412 Precondition Failed
15: Date: Thu, 10 Nov 2016 13:55:31 GMT
16: Content-Length: 0
17: Server: Kestrel
大部分針對物理文件的請求都是希望獲取整個文件的內容,區間請求則使我們可以獲取某個文件部分區間的內容。區間請求使我們可以通過多次請求來獲取某個較大文件的內容,並實現斷點續傳。如果同一個文件同時存放到多台服務器,我們可以利用區間請求同時下載不同部分的內容。和條件請求一樣,區間請求也是作為標准定義在HTTP規范之中。
如果我們下希望通過一個GET請求獲取目標資源的某個區間的內容,那麼我們會將這個區間存放到一個名為Range的報頭中。雖然HTTP規范允許指定多個區間,但是StaticFileMiddleware中間件只支持單一區間。至於分區所采用的的計量單位,HTTP規范並未作強制的規定,但是StaticFileMiddleware支持的代碼為“bytes”,也就是說它是以字節為單位對文件內容進行分區的。
Range報頭攜帶的分區信息采用的格式為“bytes={from}-{to}”({from}和{to}分別表示區間開始和結束的位置),比如“bytes=1000-1999”表示獲取目標資源從1001到2000共計1000個字節(第1個字節的位置為0)。如果{to}大於整個資源的長度,這樣的區間被認為是有效的,意味著區間從{from}到資源的最後一個字節。如果區間被定義成“bytes={from}-”這種形式,同樣表示區間從{from}到資源的最後一個字節。采用“bytes=-{n}”這種格式定義的區間則表示資源的最後n個字節。不論采用何種形式,如果{from}大於整個資源的總長度,這樣的定義都是不合法的定義了。
如果請求的Range報頭攜帶一個不合法的區間,服務端回返回一個狀態碼為“416 Range Not Satisfiable”的響應,否則會返回一個狀態為“206 Partial Content”的響應,響應的主體將只包含指定區間的內容。返回的內容在整個資源的位置通過響應報頭Content-Range表示,采用的格式為“{from}-{to}/{length}”。除此之外,還有一個與區間請求相關的響應報頭“Accept-Ranges”,它表示服務端能夠接受區間類型。比如前面針對條件請求的響應都具有這樣一個報頭“Accept-Ranges: bytes”,表示服務支持針對資源的區間劃分,該報頭的值為“none”,則意味著服務端不支持區間請求。
區間請求在某些時候也會去驗證資源內容是否發生改變。在這種情況下,請求會利用一個名為If-Range的報頭攜帶一個基礎時間戳或者整個資源(不是當前請求的區間)的簽名。服務端在接收到請求之後會根據這個報頭判斷請求的整個資源是否發生變換,如果判斷的結果是已經發生改變,它會返回一個狀態碼為“200 OK”的響應,響應主體將會包含整個資源的內容。只有在判斷資源並未發生變化的前提下,服務端再會返回指定區間的內容。
接下來我們照理從HTTP請求和響應報文的角度來探討StaticFileMiddleware中間件針對區間請求的支持。我們依然沿用前面演示條件請求的那個例子,這個例子中作為目標文件的foobar.txt包含26個字母和10個數字,加上UTF文本文件初始的三個字符(EF BB BF),所以總長度為39。我們利用Fiddler發送如下兩個請求分別獲取前面26個字母(3-28)和後面10個數字(-10)。
1: GET http://localhost:5000/foobar.txt HTTP/1.1
2: Host: localhost:5000
3: Range: bytes=3-28
4:
5: HTTP/1.1 206 Partial Content
6: Date: Thu, 10 Nov 2016 15:15:54 GMT
7: Content-Length: 26
8: Content-Type: text/plain
9: Server: Kestrel
10: Content-Range: bytes 3-28/39
11: Last-Modified: Thu, 10 Nov 2016 01:43:37 GMT
12: Accept-Ranges: bytes
13: ETag: "1d23af3dad4aaa7"
14:
15: Abcdefghijklmnopqrstuvwxyz
16:
17: GET http://localhost:5000/foobar.txt HTTP/1.1
18: Host: localhost:5000
19: Range: bytes=-10
20:
21: HTTP/1.1 206 Partial Content
22: Date: Thu, 10 Nov 2016 15:17:02 GMT
23: Content-Length: 10
24: Content-Type: text/plain
25: Server: Kestrel
26: Content-Range: bytes 29-38/39
27: Last-Modified: Thu, 10 Nov 2016 01:43:37 GMT
28: Accept-Ranges: bytes
29: ETag: "1d23af3dad4aaa7"
30:
31: 0123456789
由於請求中指定了正確的區間,所以我們會得到兩個狀態碼為“206 Partial Content”的響應,響應的主體僅僅包含目標區間的內容。除此之外,響應報頭“Content-Range” (“bytes 3-28/39”和“bytes 29-38/39”)指明了返回內容的區間范圍和整個文件總長度。目標文件最後修改時間戳和簽名同樣會存在於響應報頭Last-Modified和ETag之中。
接下來我們如下一個區間請求,並刻意指定一個不合法的區間(“50-”)。正如HTTP規范所描述的那樣,這種情況下我們得到的是一個狀態碼為“416 Range Not Satisfiable”的響應。
1: GET http://localhost:5000/foobar.txt HTTP/1.1
2: Host: localhost:5000
3: Range: bytes=50-
4:
5: HTTP/1.1 416 Range Not Satisfiable
6: Date: Thu, 10 Nov 2016 15:27:00 GMT
7: Content-Length: 0
8: Server: Kestrel
9: Content-Range: bytes */39
為了驗證區間請求針對文件更新狀態的檢驗,我們使用了請求報頭If-Range。在如下所示的這兩個請求中,我們分別將一個基准時間戳和文件簽名作為這個報頭值,很明顯服務端針對這兩個報頭的值都將做出“文件已經更新”的判斷。根據HTTP規范的約定,這種請求將會返回一個狀態碼“200 OK”的響應,響應的主體將會包含整個文件的內容,如下所示的響應消息證實了這一點。
1: GET http://localhost:5000/foobar.txt HTTP/1.1
2: Host: localhost:5000
3: Range: bytes=-10
4: If-Range: Wed, 09 Nov 2016 01:43:37 GMT
5:
6: HTTP/1.1 200 OK
7: Date: Thu, 10 Nov 2016 15:35:59 GMT
8: Content-Length: 39
9: Content-Type: text/plain
10: Server: Kestrel
11: Last-Modified: Thu, 10 Nov 2016 01:43:37 GMT
12: Accept-Ranges: bytes
13: ETag: "1d23af3dad4aaa7"
14:
15: abcdefghijklmnopqrstuvwxyz0123456789
16:
17: GET http://localhost:5000/foobar.txt HTTP/1.1
18: Host: localhost:5000
19: Range: bytes=-10
20: If-Range: "123abc456"
21:
22: HTTP/1.1 200 OK
23: Date: Thu, 10 Nov 2016 15:36:54 GMT
24: Content-Length: 39
25: Content-Type: text/plain
26: Server: Kestrel
27: Last-Modified: Thu, 10 Nov 2016 01:43:37 GMT
28: Accept-Ranges: bytes
29: ETag: "1d23af3dad4aaa7"
30:
31: abcdefghijklmnopqrstuvwxyz0123456789
ASP.NET Core應用針對靜態文件請求的處理[1]: 以Web的形式發布靜態文件
ASP.NET Core應用針對靜態文件請求的處理[2]: 條件請求與區間請求
ASP.NET Core應用針對靜態文件請求的處理[3]: StaticFileMiddleware中間件如何處理針對文件請求
ASP.NET Core應用針對靜態文件請求的處理[4]: DirectoryBrowserMiddleware中間件如何呈現目錄結構
ASP.NET Core應用針對靜態文件請求的處理[5]: DefaultFilesMiddleware中間件如何顯示默認頁面