首先須要明確一點就是使用流式上傳和使用IFormFile在效率上沒有太大的差別,IFormFile的缺點主要是客戶端上傳過來的文件首先會緩存在服務器內存中,任何超過 64KB 的單個緩衝文件會從 RAM 移動到服務器磁盤上的臨時文件中。 文件上傳所用的資源(磁盤、RAM)取決於併發文件上傳的數量和大小。 流式處理與性能沒有太大的關係,而是與規模有關。 若是嘗試緩衝過多上傳,站點就會在內存或磁盤空間不足時崩潰(以上解釋來自官網https://docs.microsoft.com/zh-cn/aspnet/core/mvc/models/file-uploads?view=aspnetcore-2.2)。也就是說若是同時有不少客戶端上傳文件時,若是採用IFormFile的方式來上傳的話,上傳的文件首先會在你的服務器內存中進行緩存,還有可能從內存中導入到你的磁盤臨時文件中,那麼必然會有兩個問題,一個是內存佔用太高,另外一個問題就是磁盤空間不足,因此,採用流式上傳的緣由就在於解決這兩個問題。可是流式上傳須要比IFormFile複雜的多的配置,IFormFile上傳是在服務器進行模型綁定的操做,而流式上傳是要讀取Request的流並對boundary的內容進行判斷來獲取文件流的方式來處理的。html
下面來從客戶端和服務端兩個方面來解釋asp.net core中的文件上傳功能jquery
文件是從客戶端上傳的到服務器的,因此在客戶端須要一些配置。 個人客戶端是HTML,使用form表單的方式來對文件進行上傳,因此這裏只介紹這種客戶端方式。首先上傳文件的話form的enctype屬性必須爲multipart/form-data的格式:web
<form enctype="multipart/form-data"> .... </form>
注:關於multipart/form-data這部份內容能夠參考https://www.jianshu.com/p/29e38bcc8a1d。數據庫
enctype有三種可選類型:瀏覽器
application/x-www-urlencoded
,當表單使用 POST 請求時,數據會被以 x-www-urlencoded 方式編碼到 Body 中來傳送,而若是 GET 請求,則是附在 url 連接後面來發送。
GET 請求只支持 ASCII 字符集,所以,若是咱們要發送更大字符集的內容,咱們應使用 POST 請求。緩存
若是要發送大量的二進制數據(non-ASCII),"application/x-www-form-urlencoded"
顯然是低效的,由於它須要用 3 個字符來表示一個 non-ASCII 的字符。所以,這種狀況下,應該使用 "multipart/form-data"
格式。服務器
application/x-www-form-urlencoded。
<FORM method="POST" action="http://w.sohu.com/t2/upload.do" enctype="multipart/form-data"> <INPUT type="text" name="city" value="Santa colo"> <INPUT type="text" name="desc"> <INPUT type="file" name="pic"> </FORM>
POST /t2/upload.do HTTP/1.1 User-Agent: SOHUWapRebot Accept-Language: zh-cn,zh;q=0.5 Accept-Charset: GBK,utf-8;q=0.7,*;q=0.7 Connection: keep-alive Content-Length: 60408 Content-Type:multipart/form-data; boundary=ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC Host: w.sohu.com --ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC Content-Disposition: form-data; name="city" Santa colo --ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC Content-Disposition: form-data;name="desc" Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ... --ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC Content-Disposition: form-data;name="pic"; filename="photo.jpg" Content-Type: application/octet-stream Content-Transfer-Encoding: binary ... binary data of the jpg ... --ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC--
從上面的 multipart/form-data
格式發送的請求的樣式來看,它包含了多個 Parts,每一個 Part 都包含頭信息部分,
Part 頭信息中必須包含一個 Content-Disposition
頭,其餘的頭信息則爲可選項, 好比 Content-Type
等。併發
Content-Disposition
包含了 type 和 一個名字爲 name 的 parameter,type 是 form-data,name 參數的值則爲表單控件(也即 field)的名字,若是是文件,那麼還有一個 filename 參數,或者fileNameStar參數,值就是文件名。mvc
Content-Disposition: form-data; name="user"; filename="hello.txt"
上面的 "user" 就是表單中的控件的名字,後面的參數 filename 則是點選的文件名。
對於可選的 Content-Type(若是沒有的話),默認就是 text/plain
。app
注意:
若是文件內容是經過填充表單來得到,那麼上傳的時候,Content-Type 會被自動設置(識別)成相應的格式,若是無法識別,那麼就會被設置成 "application/octet-stream"
若是多個文件被填充成單個表單項,那麼它們的請求格式則會是 multipart/mixed。
若是 Part 的內容跟默認的 encoding 方式不一樣,那麼會有一個 "content-transfer-encoding"
頭信息來指定。
下面,咱們填充兩個文件到一個表單項中,行程的請求信息以下:
Content-Type: multipart/form-data; boundary=AaB03x --AaB03x Content-Disposition: form-data; name="submit-name" Larry --AaB03x Content-Disposition: form-data; name="files" Content-Type: multipart/mixed; boundary=BbC04y --BbC04y Content-Disposition: file; filename="file1.txt" Content-Type: text/plain ... contents of file1.txt ... --BbC04y Content-Disposition: file; filename="file2.gif" Content-Type: image/gif Content-Transfer-Encoding: binary ...contents of file2.gif... --BbC04y-- --AaB03x--
能夠看到一個input type="file"同時上傳兩個文件時會有一個子boundary產生。
服務器採用asp.net core。
參考https://www.cnblogs.com/liuxiaoji/p/10266609.html
參考的這篇文章中已經比較舊了,在asp.net core2.2中,已經有了一些便捷的擴展方法方法來更清晰的表示這些邏輯,可是遺憾的是asp.net core的官方文檔尚未更新這些。
此外,有關與文件斷點續傳/上傳的一個協議/規範,在這裏:https://www.cnblogs.com/850391642c/p/tus-Protocol.html;我也在考慮後續要不要使用這個協議和實現來應用到個人項目中。
下面進入正題:
使用流式上傳的方式的缺點就是配置比較複雜,你沒法使用IFormFile那種可以採用模型綁定的方式來將上傳的文件反序列化成對象,須要咱們進行配置,配置的步驟爲:
①首先要判斷content-type是不是multipart
②從HttpRequest中拿到boundary
③將拿到的boundary和HttpRequest的body組合成一個MultipartReader對象
④從組合成的MultipartReader對象中讀取有boundary分隔的每一個section,這個section有多是一個form表單的鍵值對,也有多是一個文件。
⑤逐項取出每個section,而後對每一個section進行判斷是form表單鍵值對仍是一個文件,並進行相應的處理。其中,若是是表單項的鍵值對,那麼將這個鍵值對存入一個對象中,若是是文件,則創建一個文件流並將文件寫入磁盤。
代碼基於asp.net core 2.2,代碼以下:
public static class FileStreamingHelper { /// <summary> /// 若是文件上傳成功,那麼message會返回一個上傳文件的路徑,若是失敗,message表明失敗的消息 /// </summary> /// <param name="request"></param> /// <param name="targetDirectory"></param> /// <param name="cancellationToken"></param> /// <returns></returns> public static async Task<(bool success, string filePath, FormValueProvider valueProvider)> StreamFile(this HttpRequest request, string targetDirectory, CancellationToken cancellationToken) { //讀取boundary var boundary = request.GetMultipartBoundary(); if (string.IsNullOrEmpty(boundary)) { return (false, "解析失敗", null); } //檢查相應目錄 if (!Directory.Exists(targetDirectory)) { Directory.CreateDirectory(targetDirectory); } //準備文件保存路徑 var filePath = string.Empty; //準備viewmodel緩衝 var accumulator = new KeyValueAccumulator(); //建立section reader var reader = new MultipartReader(boundary, request.Body); try { var section = await reader.ReadNextSectionAsync(cancellationToken); while (section != null) { ContentDispositionHeaderValue header = section.GetContentDispositionHeader(); if (header.FileName.HasValue || header.FileNameStar.HasValue) { var fileSection = section.AsFileSection(); var fileName = fileSection.FileName; filePath = Path.Combine(targetDirectory, fileName); if (File.Exists(filePath)) { return (false, "你以上傳過同名文件", null); } accumulator.Append("mimeType", fileSection.Section.ContentType); accumulator.Append("fileName", fileName); accumulator.Append("filePath", filePath); using (var writeStream = File.Create(filePath)) { const int bufferSize = 1024; await fileSection.FileStream.CopyToAsync(writeStream, bufferSize, cancellationToken); } } else { var formDataSection = section.AsFormDataSection(); var name = formDataSection.Name; var value = await formDataSection.GetValueAsync(); accumulator.Append(name, value); } section = await reader.ReadNextSectionAsync(cancellationToken); } } catch (OperationCanceledException) { if (File.Exists(filePath)) { File.Delete(filePath); } return (false, "用戶取消操做", null); } // Bind form data to a model var formValueProvider = new FormValueProvider( BindingSource.Form, new FormCollection(accumulator.GetResults()), CultureInfo.CurrentCulture); return (true, filePath, formValueProvider); } }
這個方法會返回一個元組,來表示一些狀態和結果,首先,方法中檢查boundary是否爲空,爲空則直接返回錯誤碼;而後,根據boundary來建立一個關鍵的MultipartReader來讀取request.body中的每一個section;而後,根據section的類型來決定將這個section看成一個filesection仍是一個formdatasection來處理。這個方法順便將CancellationToken傳入,當客戶端中斷鏈接或其餘緣由形成中斷,引起OperationCanceledException時,方法會將已接受的字節組成的文件(無用的文件)刪除。最終,方法返回一個元組,裏面有表明是否成功的布爾值,由表明消息的字符串,還有一個FormValueProvider,這個對象用於解析成最終的ViewModel。當布爾值爲true時,表明消息的字符串是一個文件路徑。用於解析ViewModel後續步驟的處理,這是由於我須要將ViewModel轉化成一條文件上傳記錄存入數據庫。
而後還須要定義一個攔截器,用於告訴mvc不要進行模型綁定,這個攔截器實現了IResourceFilter接口:
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; using System; using System.Linq; namespace MyFtp.Api.Extensions { [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter { public void OnResourceExecuting(ResourceExecutingContext context) { var formValueProviderFactory = context.ValueProviderFactories .OfType<FormValueProviderFactory>() .FirstOrDefault(); if (formValueProviderFactory != null) { context.ValueProviderFactories.Remove(formValueProviderFactory); } var jqueryFormValueProviderFactory = context.ValueProviderFactories .OfType<JQueryFormValueProviderFactory>() .FirstOrDefault(); if (jqueryFormValueProviderFactory != null) { context.ValueProviderFactories.Remove(jqueryFormValueProviderFactory); } } public void OnResourceExecuted(ResourceExecutedContext context) { } } }
asp.net core對請求body的大小以及上傳的文件的大小都有一些限制,爲了免除這些限制,咱們須要進行一些配置,若是你要是用IIS進行部署你的應用,則應該創建一個web.config文件進行相應的配置,這方面的內容在https://docs.microsoft.com/zh-cn/aspnet/core/mvc/models/file-uploads?view=aspnetcore-2.2,我使用的是kestrel,對kestrel進行配置也很是簡單,就是配置一個FormOption,在startup類中寫入:
//設置接收文件長度的最大值。 services.Configure<FormOptions>(x => { x.ValueLengthLimit = int.MaxValue; x.MultipartBodyLengthLimit = int.MaxValue; x.MultipartHeadersLengthLimit = int.MaxValue; });
上面的這個配置的單位是字節,配置了三個,這三個都是與表單相關的:一個是表單的鍵值對中的值的長度限制,一個是當表單enctype爲multipart/form-data時文件的長度限制,還有一個是multipart頭長度的限制,也就是boundary=-------------------------------Gefsgeq!34這種玩意兒的限制。
上面的配置完成後還不行,由於asp.net core還對HttpRequest的長度也作了限制,還須要對HttpRequest請求體的長度進行配置,這個配置能夠在action上面完成,有兩個attribute:
//[RequestSizeLimit()] [DisableRequestSizeLimit] public async Task<IActionResult> Post() {
.......
}
RequestSizeLimit是傳入一個表示字節的數字來對請求的大小進行限制,另外一個DisableRequestSizeLimit的意思就是不限制了。