一, 介绍
在介绍静态文件中间件之前, 先介绍 ContentRoot 和 webRoot 概念.
ContentRoot: 指 Web 的项目的文件夹, 包括 bin 和 webroot 文件夹.
WebRoot: 一般指 ContentRoot 路径下的 wwwroot 文件夹.
介绍这个两个概念是因为静态资源文件一般存放在 WebRoot 路径下, 也就是 wwwroot. 下面为这两个路径的配置, 如下所示:
- public static void Main(string[] args)
- {var host = new WebHostBuilder()
- .UseKestrel()
- .UseStartup<Startup>()
- .UseContentRoot(Directory.GetCurrentDirectory())
- .UseWebRoot(Directory.GetCurrentDirectory() + @"\wwwroot\")
- .UseEnvironment(EnvironmentName.Development)
- .Build();
- host.Run();
- }
上面的代码将 ContentRoot 路径和 WebRoot 路径都配置了, 其实只需配置 ContentRoot 路径, WebRoot 默认为 ContentRoot 路径下的 wwwroot 文件夹路径.
在了解静态文件中间件前, 还需要了解 HTTP 中关于静态文件缓存的机制. 跟静态文件相关的 HTTP 头部主要有 Etag 和 If-None-Match.
下面为访问静态文件服务器端和客户端的流程:
1, 客户端第一次向客户端请求一个静态文件.
2, 服务器收到客户端访问静态文件的请求, 服务器端会根据静态文件最后的修改时间和文件内容的长度生成一个 Hash 值, 并将这个值放到请求头 ETag 中.
3, 客户端第二次发起同一个请求时, 因为之前请求过此文件, 所以本地会有缓存. 在请求时会在请求头中加上 If-Nono-Match, 其值为服务器返回的 ETag 的值.
4, 服务器端比对发送的来的 If-None-Match 的值和本地计算的 ETag 的值是否相同. 如果相同, 返回 304 状态码, 客户端继续使用本地缓存. 如果不相同, 返回 200 状态码, 客户端重新解析服务器返回的数据, 不使用本地缓存.
具体看下面例子.
二, 简单使用
2.1 最简单的使用
最简单的使用就是在 Configure 中加入下面一句话, 然后将静态文件放到 webRoot 的路径下, 我没有修改 webRoot 指定的路径, 所以就是 wwwroot 文件夹.
- public void Configure(IApplicationBuilder App, IHostingEnvironment env)
- {
- App.UseStaticFiles();
- App.UseMvc();
- }
在 wwwroot 文件夹下放一个名称为 1.txt 的测试文本, 然后通过地址访问.
这种有一个缺点, 暴露这个文件的路径在 wwwroot 下.
2.2 指定请求地址
- public void Configure(IApplicationBuilder App, IHostingEnvironment env)
- {
- App.UseMvc();
- App.UseStaticFiles(new StaticFileOptions()
- {
- FileProvider = new PhysicalFileProvider(@"C:\Users\Administrator\Desktop"),
- RequestPath = new PathString("/Static")
- });
- //App.UseStaticFiles("/Static");
- }
这种指定了静态文件存放的路径为: C:\Users\Administrator\Desktop, 不是使用默认的 wwwroot 路径, 就隐藏了文件的真实路径, 并且需要在地址中加上 static 才能访问.
当然也可以不指明静态文件的路径, 只写请求路径, 如上面代码中的注释的例子. 这样静态文件就必须存储到 WebRoot 对应的目录下了. 如果 WebRoot 的目录对应的是 wwwroot, 静态文件就放到 wwwroot 文件夹中.
下面通过例子看一下静态文件的缓存, 如果你想做这个例子, 别忘记先清空缓存.
(第一次请求)
(第二次请求 文件相对第一次请求没有修改的情况)
(第三次请求 文件相对第一次请求有修改的情况)
三, 源码分析
源码在 https://github.com/aspnet/StaticFiles , 这个项目还包含有其他中间件. 既然是中间件最重要的就是参数为 HttpContext 的 Invoke 方法了, 因为每一个请求都要经过其处理, 然后再交给下一个中间件处理. 下面为处理流程.
- public async Task Invoke(HttpContext context)
- {
- var fileContext = new StaticFileContext(context, _options, _matchUrl, _logger, _fileProvider, _contentTypeProvider);
- if (!fileContext.ValidateMethod())// 静态文件的请求方式只能是 Get 或者 Head
- {
- _logger.LogRequestMethodNotSupported(context.Request.Method);
- }
- // 判断请求的路径和配置的请求路径是否匹配. 如请求路径为 http://localhost:5000/static/1.txt
- // 配置为 RequestPath = new PathString("/Static")
- // 则匹配, 并将文件路径赋值给 StaticFileContext 中点的_subPath
- else if (!fileContext.ValidatePath())
- {
- _logger.LogPathMismatch(fileContext.SubPath);
- }
- // 通过获取要访问文件的扩展名, 获取此文件对应的 MIME 类型,
- // 如果找到文件对应的 MIME, 返回 True, 并将 MIME 类型赋值给 StaticFileContext 中的_contextType
- // 没有找到返回 False.
- else if (!fileContext.LookupContentType())
- {
- _logger.LogFileTypeNotSupported(fileContext.SubPath);
- }
- // 判断访问的文件是否存在.
- // 如果存在返回 True, 并根据文件的最后修改时间和文件的长度, 生成 Hash 值, 并将值赋值给_etag, 也就是相应头中的 Etag.
- // 如果不存在 返回 False, 进入下一个中间件中处理
- else if (!fileContext.LookupFileInfo())
- {
- _logger.LogFileNotFound(fileContext.SubPath);
- }
- else
- {
- fileContext.ComprehendRequestHeaders();
- // 根据 StaticFileContext 中的值, 加上对应的相应头, 并发送响应. 具体调用方法在下面
- switch (fileContext.GetPreconditionState())
- {
- case StaticFileContext.PreconditionState.Unspecified:
- case StaticFileContext.PreconditionState.ShouldProcess:
- if (fileContext.IsHeadMethod)
- {
- await fileContext.SendStatusAsync(Constants.Status200Ok);
- return;
- }
- try
- {
- if (fileContext.IsRangeRequest)
- {
- await fileContext.SendRangeAsync();
- return;
- }
- await fileContext.SendAsync();
- _logger.LogFileServed(fileContext.SubPath, fileContext.PhysicalPath);
- return;
- }
- catch (FileNotFoundException)
- {
- context.Response.Clear();
- }
- break;
- case StaticFileContext.PreconditionState.NotModified:
- _logger.LogPathNotModified(fileContext.SubPath);
- await fileContext.SendStatusAsync(Constants.Status304NotModified);
- return;
- case StaticFileContext.PreconditionState.PreconditionFailed:
- _logger.LogPreconditionFailed(fileContext.SubPath);
- await fileContext.SendStatusAsync(Constants.Status412PreconditionFailed);
- return;
- default:
- var exception = new NotImplementedException(fileContext.GetPreconditionState().ToString());
- Debug.Fail(exception.ToString());
- throw exception;
- }
- }
- // 进入下一个中间件中处理
- await _next(context);
- }
添加响应头的方法:
public void ApplyResponseHeaders(int statusCode) { _response.StatusCode = statusCode; if (statusCode < 400) { if (!string.IsNullOrEmpty(_contentType)) { _response.ContentType = _contentType; } // 设置响应头中最后修改时间, ETag 和 accept-ranges _responseHeaders.LastModified = _lastModified; _responseHeaders.ETag = _etag; _responseHeaders.Headers[HeaderNames.AcceptRanges] = "bytes"; } if (statusCode == Constants.Status200Ok) { _response.ContentLength = _length; } _options.OnPrepareResponse(new StaticFileResponseContext() { Context = _context, File = _fileInfo, }); }
校验文件是否修改的方法:
public bool LookupFileInfo() { _fileInfo = _fileProvider.GetFileInfo(_subPath.Value); if (_fileInfo.Exists) { _length = _fileInfo.Length; DateTimeOffset last = _fileInfo.LastModified; _lastModified = new DateTimeOffset(last.Year, last.Month, last.Day, last.Hour, last.Minute, last.Second, last.Offset).ToUniversalTime(); // 通过修改时间和文件长度, 得到 ETag 的值 long etagHash = _lastModified.ToFileTime() ^ _length; _etag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"'); } return _fileInfo.Exists; }
来源: http://www.bubuko.com/infodetail-3073076.html