.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資料夾中。
下面通過例子看一下靜態檔案的快取,如果你想做這個例子,別忘記先清空快取。
(第一次請求)
(第二次請求 檔案相對第一次請求沒有修改的情況)
(第三次請求 檔案相對第一次請求有修改的情況)
三、原始碼分析
原始碼在 ofollow,noindex">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; }