asp.net core流式上传大文件

时间:2022-02-19 08:45:18

asp.net core流式上传大文件

首先需要明确一点就是使用流式上传和使用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的内容进行判断来获取文件流的方式来处理的。

下面来从客户端和服务端两个方面来解释asp.net core中的文件上传功能

客户端配置

文件是从客户端上传的到服务器的,所以在客户端需要一些配置。 我的客户端是HTML,使用form表单的方式来对文件进行上传,所以这里只介绍这种客户端方式。首先上传文件的话form的enctype属性必须为multipart/form-data的格式:

 <form  enctype="multipart/form-data">
....
</form>

注:关于multipart/form-data这部分内容可以参考https://www.jianshu.com/p/29e38bcc8a1d。

enctype有三种可选类型:

  • application/x-www-urlencoded 默认情况下是 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" 格式。

    如果采用这种格式来对表单的内容进行请求,那么Content-Type就是application/x-www-form-urlencoded。

  • multipart/form-data 采用这种方式提交的表单其content-type的格式就是multipart/form-data了。例如:发送一个这样的表单:
    <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-;q=0.7,*;q=0.7
    Connection: keep-alive
    Content-Length:
    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-
    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参数,值就是文件名。

    比如:

    Content-Disposition: form-data; name="user"; filename="hello.txt"

    上面的 "user" 就是表单中的控件的名字,后面的参数 filename 则是点选的文件名。
    对于可选的 Content-Type(如果没有的话),默认就是 text/plain

    注意:

    如果文件内容是通过填充表单来获得,那么上传的时候,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产生。

  • text-plain 这个不做解释了。

服务器配置

服务器采用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 = ;
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的意思就是不限制了。

asp.net core流式上传大文件的更多相关文章

  1. Asp&period;net MVC利用WebUploader上传大文件出现404解决办法。

    刚开始我上传小文件都是比较顺利的,但是上传了一个大文件大约有200M的压缩包就不行了.在chrome里面监视发现网络状态是404,我分析可能不是WebUploader的限制,应该是WebConfig限 ...

  2. &period;net core IIS&sol;Kestrel上传大文件的解决方法

    大文件,就是内容的大小超过了一定数量的文件,比如1个GB的文件. 站点一般会限制上传文件的大小,如果超过了一定限制,则会报错误. 在处理大文件上传的方式上,IIS代理和Kestrel宿主服务器的处理方 ...

  3. asp&period;net中使用swfupload上传大文件

    转载:http://www.cnblogs.com/niunan/archive/2012/01/12/2320705.html 花了一天多时间研究出来的,其实也就是网上下别人的代码然后再自己修修改改 ...

  4. ASP&period;NET Core MVC如何上传文件及处理大文件上传

    用文件模型绑定接口:IFormFile (小文件上传) 当你使用IFormFile接口来上传文件的时候,一定要注意,IFormFile会将一个Http请求中的所有文件都读取到服务器内存后,才会触发AS ...

  5. PHP流式上传和表单上传(美图秀秀)

    最近需要开发一个头像上传的功能,找了很多都需要授权的,后来找到了美图秀秀,功能非常好用. <?php /** * Note:for octet-stream upload * 这个是流式上传PH ...

  6. golang gin框架中实现大文件的流式上传

    一般来说,通过c.Request.FormFile()获取文件的时候,所有内容都全部读到了内存.如果是个巨大的文件,则可能内存会爆掉:且,有的时候我们需要一边上传一边处理. 以下的代码实现了大文件流式 ...

  7. ASP&period;NET 使用 plupload 上传大文件时出现&OpenCurlyDoubleQuote;blob”文件的Bug

    最近在一个ASP.NET 项目中使用了plupload来上传文件,结果几天后客户发邮件说上传的文件不对,说是文件无法打开 在进入系统进行查看后发现上传的文件竟然没有后缀,经过一番测试发现如果文件上传的 ...

  8. asp dotnet core 支持客户端上传文件

    本文告诉大家如何在 asp dotnet core 支持客户端上传文件 新建一个 asp dotnet core 程序,创建一个新的类,用于给客户端上传文件的信息 public class Kanaj ...

  9. &lbrack;Asp&period;net&rsqb;Uploadify上传大文件,Http error 404 解决方案

    引言 之前使用Uploadify做了一个上传图片并预览的功能,今天在项目中,要使用该插件上传大文件.之前弄过上传图片的demo,就使用该demo进行测试.可以查看我的这篇文章:[Asp.net]Upl ...

随机推荐

  1. 经典贪心算法uva11729

    uva11729 这个题的题意是 你有n个部下,每个部下需要完成一项任务.第i个部下需要你花Bi分钟交代任务,然后他会立刻独立地.无间断地执行Ji分钟后完成任务. 你需要选择交待任务的顺序,使得所有任 ...

  2. Html巩固

    1.html元素都支持全局属性(通用的属性)和事件属性.     a标签:定义一个超文本链接,链接到另一个超文本文件的! button:定义一个点击按钮,按钮是出发当前页面的事件.

  3. POJ 1442 Black Box

    第k大数维护,我推荐Treap..谁用谁知道....                                                           Black Box Time ...

  4. php常用单词语法

    header("Content-type:text/html;charset=utf-8"); 加入数组array_push($ratings_store_cop,$value); ...

  5. webstom 如何获取github上面的项目工程

    需要你配好webstorm的github相关的配置,安装好git.exe; 如何配置请参考: webstorm 如何配置git 这个点击github后 会有个提示框 如下图: 如果没有成功,会弹出下面 ...

  6. openStack telemetry&sol;ceilometry 云平台资源监控度量

  7. hdu 5654 xiaoxin and his watermelon candy 莫队

    题目链接 求给出的区间中有多少个三元组满足i+1=j=k-1 && a[i]<=a[j]<=a[k] 如果两个三元组的a[i], a[j], a[k]都相等, 那么这两个三 ...

  8. Java解惑七:很多其它类之谜

    谜题66 继承的问题. 对于实例方法:命名同样时,子类会覆写父类的方法,且訪问权限至少和父类一样大. 对于域:命名同样时,子类会隐藏父类的域,且訪问权限随意. 谜题67 不要重用库中的类名. 谜题68 ...

  9. EMM386和UMBPCI 区别

    EMM386和UMBPCI 区别 1,SupportCD-ROM[HIMEM+EMM386NOEMS].支持光驱(EMM386模式)2,SupportCD-ROM[HIMEM+UMBPCI].支持光驱 ...

  10. P3507 GRA-The Minima Game

    题目大意: 给出N个正整数,AB两个人轮流取数,A先取.每次可以取任意多个数,直到N个数都被取走.每次获得的得分为取的数中的最小值,A和B的策略都是尽可能使得自己的得分减去对手的得分更大.在这样的情况 ...