.Net Core 中間件之靜態文件(StaticFiles)

一、介紹

  在介紹靜態文件中間件之前,先介紹 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;
        }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章