在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
我们通过《以Web的形式发布静态文件》和《条件请求与区间请求》中的实例演示,以及上面针对条件请求和区间请求的介绍,从提供的功能和特性的角度对这个名为StaticFileMiddleware的中间进行了全面的介绍,接下来我们将更近一步,将从实现原理的角度来进一步认识这个中间件。 [本文已经同步到《ASP.NET Core框架揭秘》之中]
不过在此之前,我们先来看看StaticFileMiddleware这个类型的定义。 class StaticFileMiddleware
2: {
public StaticFileMiddleware(RequestDelegate next, IHostingEnvironment hostingEnv, IOptions<StaticFileOptions> options, ILoggerFactory loggerFactory);
public Task Invoke(HttpContext context);
5: }
如上面的代码片段所示,除了作为“下一个中间件”的next参数之前,StaticFileMiddleware的构造函数还包含三个参数。其中hostingEnv和loggerFactory这两个参数分别表示当前执行环境和用来创建Logger的工厂,最重要的options参数表示为这个中间件指定的配置选项,至于具体可以提供怎样的配置选项,我们只需要看看 StaticFileOptions这个类型提供了怎样的属性成员。 class StaticFileOptions : SharedOptionsBase
2: {
public IContentTypeProvider ContentTypeProvider { get; set; }
string DefaultContentType { get; set; }
bool ServeUnknownFileTypes { get; set; }
public Action<StaticFileResponseContext> OnPrepareResponse { get; set; }
7:
public StaticFileOptions();
public StaticFileOptions(SharedOptions sharedOptions);
10: }
11:
class SharedOptionsBase
13: {
protected SharedOptionsBase(SharedOptions sharedOptions);
public IFileProvider FileProvider { get; set; }
public PathString RequestPath { get; set; }
17: }
18:
class SharedOptions
20: {
public IFileProvider FileProvider { get; set; }
public PathString RequestPath { get; set; }
23: }
24:
class StaticFileResponseContext
26: {
public HttpContext Context { get; }
public IFileInfo File { get; }
29: }
如上面的代码片段所示,StaticFileOptions继承自抽象类型SharedOptionsBase,后者实际上体现的是两个路径之间的映射关系,一个是HTTP请求采用的路径,另一个则是文件的物理地址,后者体现为一个FileProvider对象。不过也正是因为文件的读取是通过这个FileProvider来完成的,而FileProvider未必就一定对应着具体的物理文件,所以StaticFileMiddleware并不限于针对专门处理“物理文件”。 直接定义在StaticFileOptions中的前三个类型都与媒体类型的解析有关,其中ContentTypeProvider属性返回一个根据请求相对地址进行媒体类型的ContentTypeProvider对象。如果这个ContentTypeProvider不能正确解析出目标文件的媒体类型,我们可以利用DefaultContentType设置一个默认媒体类型。但是只有将另一个名为ServeUnknownFileTypes的属性设置为True的情况下,媒体类型不能正常识别的请求采用使用这个默认设置的媒体类型。 StaticFileOptions还具有一个OnPrepareResponse属性,它返回一个Action<StaticFileResponseContext>类型的委托对象,我们可以为这属性指定的委托对象来对最终的响应进行定制。至于作为委托输入参数的是一个类型为StaticFileResponseContext的对象,我们利用它可以获得当前的HTTP上下文和目标文件。 针对StaticFileMiddleware这个中间件的注册一般都是调用针对ApplicationBuilder的UseStaticFiles扩展方法来完成的。具体来说,一共具有三个UseStaticFiles方法重载供我们选择,如下所示的代码片段展示了这三个扩展方法的实现。 class StaticFileExtensions
2: {
this IApplicationBuilder app)
4: {
return app.UseMiddleware<StaticFileMiddleware>();
6: }
7:
this IApplicationBuilder app,StaticFileOptions options)
9: {
return app.UseMiddleware<StaticFileMiddleware>(Options.Create<StaticFileOptions>(options));
11: }
12:
string requestPath)
14: {
new StaticFileOptions
16: {
new PathString(requestPath)
18: };
return app.UseStaticFiles(options);
20: }
21: }
二、ContentTypeProviderStaticFileMiddleware针对物理文件请求的处理并不仅仅限于完成文件内容的响应,还需要针对文件的格式解析出正确的媒体类型。对于客户端来说,如果无法确定媒体类型,获取的文件就像是一步无法解码的天书,毫无意义。StaticFileMiddleware利用指定的ContentTypeProvider来解析媒体类型,ContentTypeProvider是我们对实现了IContentTypeProvider接口的所有类型以及对应对象的统称。如下面的代码片段所示,IContentTypeProvider接口定义了唯一的方法TryGetContentType根据当前请求的相对路径来解析这个作为输出参数的媒体类型。 interface IContentTypeProvider
2: {
string contentType);
4: }
StaticFileMiddleware默认使用的ContentProvider是一个具有如下定义的FileExtensionContentTypeProvider对象。顾名思义,FileExtensionContentTypeProvider利用物理文件的扩展名来解析对应的媒体类型,它利用其Mappings属性表示的字典维护了一个扩展名与媒体类型之间的映射关系。我们常用的数百种标准的文件扩展名和对应的媒体类型之间的映射关系都会保存在爱这个字典中。如果我们发布的文件具有一些特殊的扩展名,或者我们需要现有的某些扩展名映射为不同的媒体类型,这些通过添加或者修改映射关系来实现。 class FileExtensionContentTypeProvider : IContentTypeProvider
2: {
string> Mappings { get; }
4:
public FileExtensionContentTypeProvider();
string> mapping);
7:
string contentType);
9: }
三、利用配置指定StaticFileOptions由于StaticFileMiddleware的构造函数用来设置相关选项的options参数类型为IOptions<StaticFileOptions>,所以我们可以根据Options模式将StaticFileOptions对象承载的部分选项定义在配置文件中。比如我们利用如下所示的一个JSON文件开启了针对未知文件类型的支持,并设置了默认使用媒体类型(“application/octet-stream”),这两个配置项对应着StaticFileOptions的同名属性。 1: {
true,
4: }
有了这个配置文件(假设文件名为“StaticFileOptions.json”),我们就可以按照如下的方式加载它并生成对应的Configuration对象,然后采用Options模式特有的编程模式实现与StaticFileOptions类型的映射。这样的配置将会自动应用到注册的StaticFileMiddleware中间件上。 class Program
2: {
void Main()
4: {
new ConfigurationBuilder()
)
7: .Build();
8:
new WebHostBuilder()
10: .UseContentRoot(Directory.GetCurrentDirectory())
11: .UseKestrel()
12: .ConfigureServices(svsc=>svsc.Configure<StaticFileOptions>(config))
13: .Configure(app=>app.UseStaticFiles())
14: .Build()
15: .Run();
16: }
17: }
对于上面这样的应用,所有未知文件类型都将自动映射为“application/octet-stream”媒体类型。如果使用浏览器请求一个未知类型的文件(比如前面演示的“~/wwwroot/img/ dophin1.img”),目标文件将以如下图所示的形式以一个附件的形式被下载。 四、实现原理为了上读者朋友们对针对静态文件的请求在StaticFileMiddleware中间件的处理具有更加深刻的认识,接下来我们会采用相对简单的代码来重新定义这个中间件。这部分作为选修内容供有兴趣的读者朋友阅读,忽略这些内容不会影响对后续内容的理解。这个模拟中间件具有与StaticFileMiddleware相同的能力,它能够将目标文件的内容采用正确的媒体类型响应给客户端,同时能够处理条件请求和区间请求。 StaticFileMiddleware中间处理针对静态文件请求的整个处理流程大体上可以划分为如上图所示的三个步骤:
接下来我们按照上述的这个流程来重新定义这个StaticFileMiddleware,不过在此之前先来了解一下我们预先定义的几个辅助性的扩展方法。如下面代码片段所示,扩展方法UseMethods用于判指定的请求是否采用指定的HTTP方法,而TryGetSubpath用于解析请求的目标文件的相对路径。TryGetContentType方法会根据指定的StaticFileOptions携带的ContentTypeProvider解析出正确的媒体类型,而TryGetFileInfo则根据指定的路径获取描述目标文件的FileInfo对象。至于最后的IsRangeRequest方法,它会根据是否携带Rang报头判断指定的请求是否是一个区间请求。 class Extensions
2: {
string[] methods)
4: {
return methods.Contains(context.Request.Method, StringComparer.OrdinalIgnoreCase);
6: }
7:
out PathString subpath)
9: {
out subpath);
11: }
12:
string contentType)
14: {
string.IsNullOrEmpty(contentType = options.DefaultContentType) && options.ServeUnknownFileTypes);
16: }
17:
out IFileInfo fileInfo)
19: {
return (fileInfo = options.FileProvider.GetFileInfo(subpath.Value)).Exists;
21: }
22:
this HttpContext context)
24: {
null;
26: }
27: }
如下所示的模拟类型 StaticFileMiddleware的定义。如果指定的StaticFileOptions没有提供FileProvider,我们会默认使用指向WebRoot目录的那个PhysicalFileProvider。如果一个具体的ContentTypeProvider没有显式指定,我们使用的则是一个FileExtensionContentTypeProvider对象。这两个默认值分别解释了两个问题,为什么请求的静态文件将WebRoot作为默认的根目录,以及为什么目标文件的扩展名决定响应的媒体类型。 class StaticFileMiddleware
2: {
private RequestDelegate _next;
private StaticFileOptions _options;
5:
public StaticFileMiddleware(RequestDelegate next, IHostingEnvironment env, IOptions<StaticFileOptions> options)
7: {
8: _next = next;
9: _options = options.Value;
10: _options.FileProvider = _options.FileProvider??env.WebRootFileProvider;
new FileExtensionContentTypeProvider();
12: }
13: ...
14: }
我们上述的三个步骤分别实现在三个对应的方法(TryGetFileInfo、ResolvePreconditionState和SendResponseAsync)中,所以StaticFileMiddleware的Invoke方法按照如下的方式先后调用这三个方法完整对整个请求的处理。 class StaticFileMiddleware
2: {
public async Task Invoke(HttpContext context)
4: {
5: IFileInfo fileInfo;
string contentType;
7: DateTimeOffset? lastModified;
8: EntityTagHeaderValue etag;
9:
out etag))
11: {
this.GetPreconditionState(context, lastModified.Value, etag);
this.SendResponseAsync(preconditionState, context, etag, lastModified.Value, contentType, fileInfo);
return;
15: }
16: await _next(context);
17: }
18: ...
19: }
接下来我们的重点就集中到上述这三个方法的实现上。我们首先看看TryGetFileInfo方法是如何根据请求的路径获得描述目标文件的FileInfo对象的。如下面的代码片段所示,如果目标文件存在,这个方法除了将目标文件的FileInfo对象作为输出参数返回之外,与这个文件相关的数据(媒体类型、最后修改时间戳和封装签名的ETag)。 class StaticFileMiddleware
2: {
out EntityTagHeaderValue etag)
4: {
null;
null;
7: PathString subpath;
8:
out fileInfo))
10: {
11: DateTimeOffset last = fileInfo.LastModified;
long etagHash = last.ToFileTime() ^ fileInfo.Length;
);
new DateTimeOffset(last.Year, last.Month, last.Day, last.Hour, last.Minute, last.Second, last.Offset).ToUniversalTime();
true;
16: |
请发表评论