ASP.NET Core应用针对静态文件请求的处理[2]: 条件请求与区间请求


通过调用ApplicationBuilder的扩展方法UseStaticFiles注册的StaticFileMiddleware中间件帮助我们处理针对文件的请求。对于StaticFileMiddleware处理请求的逻辑,大部分读者都应该想得到:它根据请求的地址找到目标文件的路径,然后利用注册的ContentTypeProvider根据路径解析出与文件内容相匹配的媒体类型,默认情况下得到的媒体类型是根据目标文件的扩展名解析出来的。解析出来的媒体类型将作为响应报头Content-Type的值。StaticFileMiddleware中间件最终利用FileProvider读取文件的内容作为响应消息的主体。实际上,这个中间件在处理请求时比我们想象的要多得多,针对条件请求(Conditional Request)区间请求(Range Request)的处理就没有在上面演示的实例中体现出来。 [本文已经同步到《ASP.NET Core框架揭秘》之中]

目录
一、条件请求
    HTTP条件请求
    针对静态文件的条件请求
二、 区间请求
    HTTP区间请求
    针对静态文件的区间请求

一、条件请求

所谓的条件请求就是客户端在发送GET请求获取某种资源的时候,会利用请求报头携带一些条件。服务端处理器在接受到这样的请求之后,会提取这些条件并验证目标资源的当前的状态是否满足客户端指定的条件。在有在这些条件满足的情况下,目标资源的内容才会真正响应给客户端。

HTTP条件请求

HTTP条件请求作为一项标准记录在HTTP规范中。一般来说,一个GET请求在目标资源存在的情况下总是会返回一个状态为“200 OK”的响应,目标资源的内容将直接存放在响应消息的主体部分。如果资源的内容不会轻易改变,我们希望客户端(比如浏览器)在本地缓存获取的资源。对于由它发送的针对同一资源的后续请求,如果资源内容不曾改变,那么资源的内容则无需再次作为网络负载予以响应。这就是条件请求需要解决的一个典型场景。

确定资源是否发生变化可以采用两种策略。第一种就是让资源的提供者记录下最后一次更新资源的时间,资源的负载和这个时间戳将一并作为响应提供给作为请求发送者的客户端。客户端在缓存资源自身内容的同时也会保存这个时间戳。等到下次针对同一资源发送请求,它会将这个时间戳一并发送出去,那么服务端就可以根据这个时间戳判断目标资源在上次响应之后是否被修改过。除了采用记录资源最后修改时间的方式外,我们还可以针对资源的内容生成一个“签名”,签名的一致性体现了资源内容的一致性,在HTTP规范中将这个签名成为ETag(Entity Tag)。

接下来我们从HTTP请求和响应报文的层面对条件请求进行详细介绍。对于HTTP请求来说,缓存资源携带的最后修改时间戳ETag分别保存在名为If-Modified-SinceIf-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

如果客户端想确定目标文件是否被修改,但是希望在未被修改的情况下才返回目标文件的内容,这样的请求需要使用If-Unmidified-Since和If-Match报头来承载基准时间戳和签名。比如对于如下两个请求携带的If-Unmidified-Since和If-Match报头,服务段都将作出文件尚未被修改的判断,所以文件的内容通过一个状态为“200 OK”的响应返回。  
   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规范之中。

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中间件如何显示默认页面