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.net MVC利用WebUploader上传大文件出现404解决办法。

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

  2. .net core IIS/Kestrel上传大文件的解决方法

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

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

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

  4. ASP.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.NET 使用 plupload 上传大文件时出现“blob”文件的Bug

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

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

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

  9. [Asp.net]Uploadify上传大文件,Http error 404 解决方案

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

随机推荐

  1. docker研究-4 docker镜像制作

    这次实验以centos镜像为基础镜像进行相关docker镜像制作. 1. 下载centos镜像 [root@localhost ~]# docker pull centosUsing default ...

  2. Ubuntu环境下载程序到STM32

    1 JLink方式 1.0 下载JLink 传送门:SEGGER官网 图1.0 下载JLink 1.2 安装JLink 双击打开下载文件:JLink_Linux_V644i_x86_64.deb 1. ...

  3. conan使用(二)--创建私有仓库

    前面我们已经能够使用conan来从公共服务器上拉取C/C++包来集成进我的工程中,但是在实际开发中,我们可能需要自己封装或使用非公开的库,那么自己搭建一个私服是个很现实的需求. 搭建conan私服有几 ...

  4. JDK8在接口中引入的default

    default关键字介绍 default是在java8中引入的关键字,也可称为Virtual extension methods——虚拟扩展方法.是指,在接口内部包含了一些默认的方法实现(也就是接口中 ...

  5. 未加载opencv_world330.pdb

    根据设置下载对应的pdb文件. 无法查找或打开pdb文件

  6. k8s进入指定pod下的指定容器的命令

    访问某pod的某个容器: kubectl --namespace=default exec -it user-deployment-54469dd57-vg87g --container user - ...

  7. idea 配置 scala

    在setting 中,通过plugin 安装 Scala 然后重启idea 重启后,建一个scala文件,根据上面提示安装scala

  8. HttpRuntime应用程序运行时

    System.Web.HttpRuntime类是整个Asp.net服务器处理的入口. 这个类提供了一系列的静态属性,反映web应用程序域的设置信息,而且每个web应用程序域中存在一个System.We ...

  9. mysql使用记录

    1. 报错 10061 将mysql启动即可

  10. web服务器获取请求客户端真实地址的方法

    服务器获取客户端或者网页的请求,获取IP时需要注意,因为一个请求到达服务器之前,一般都会经过一层或者多层代理服务器,比如反向代理服务器将http://192.168.1.10:port/ 的URL反向 ...