网站导航网学 原创论文 原创专题 网站设计 最新系统 原创论文 论文降重 发表论文 论文发表 UI设计定制 论文答辩PPT格式排版 期刊发表 论文专题
返回网学首页
网学原创论文
最新论文 推荐专题 热门论文 论文专题
当前位置: 网学 > 设计资源 > .Net编程 > 正文

详解ASP.NETMVC实现大文件异步上传的实例

论文降重修改服务、格式排版等 获取论文 论文降重及排版 论文发表 相关服务

在ASP.NET中通过HTTP上传大文件是一个由来已久的挑战,还经常被要求要显示出文件上传的进度,当你需要直接控制从浏览器上传数据流时,你会四处碰壁。

绝大多数人认为在ASP.NET中上传大文件有以下这些解决方案:

◆不要这样做。你最好是在页面中嵌入一个Silverlight或Flash进程上传文件。
◆不要这样做。因为HTTP本身设计就不是为了上传大文件,重新思考你要的功能。
◆不要这样做。ASP.NET本身设计最大也就能处理2GB大小的文件。
◆购买商业产品,如SlickUpload,它使用了一个HttpModule实现了文件流分块。
◆使用开源产品,如NeatUpload,它使用了一个HttpModule实现了文件流分块。
最近我接到一个任务,需构建一个上传工具实现以下功能:
◆必须工作在HTTP协议
◆必须允许非常大的文件上传(会大于2GB)
◆必须允许断点续传
◆必须允许并行上传

因此前三个解决方案都不适应我的需求,其它解决方案对于我而言又太笨重了,因此我开始着手解决在ASP.NET MVC中的这个问题,如果有这方面的开发背景,你一定了解大部分问题最终都归结于对ASP.NET输入流和连锁请求过程的控制,网上的资料一般都是这样描述的,只要你的代码访问了HttpRequest的InputStream属性,在你访问流之前,ASP.NET就会缓存整个上传的文件,这就意味着当我向云服务上传文件时,我必须等待整个大文件抵达服务器,然后才能将其传输到预定目的地,这意味着需要两倍的时间。
首先,我们推荐你阅读一下Scott Hanselman的有关ASP.NET MVC文件上传文章,地址/uploadfile/201101/20/0A193543154.jpg" />

图 1 :通过缓存整个文件,然后另存为的方式会使内存消耗突然上升
那么在ASP.NET MVC中通过直接访问流,不触发任何缓存机制,上传大文件该如何实现呢?解决办法就是尽量远离ASP.NET,我们先来看一看UploadController,它有三个行为方法,一个是索引我们上传的文件,一个是前面讨论的缓存逻辑,另一个是基于实时流的方法。

  1. public class UploadController : Controller   
  2. {   
  3.     [AcceptVerbs(HttpVerbs.Get)]   
  4.     [Authorize]   
  5.     public ActionResult Index()   
  6.     {   
  7.         return View();   
  8.     }   
  9.   
  10.     [AcceptVerbs(HttpVerbs.Post)]   
  11.     public ActionResult BufferToDisk()   
  12.     {   
  13.         var path = Server.MapPath("~/Uploads");   
  14.   
  15.         foreach (string file in Request.Files)   
  16.         {   
  17.             var fileBase = Request.Files[file];   
  18.   
  19.             try  
  20.             {   
  21.                 if (fileBase.ContentLength > 0)   
  22.                 {   
  23.                     fileBase.SaveAs(Path.Combine(path, fileBase.FileName));   
  24.                 }   
  25.             }   
  26.             catch (IOException)   
  27.             {   
  28.   
  29.             }   
  30.         }   
  31.   
  32.         return RedirectToAction("Index""Upload");   
  33.     }   
  34.   
  35.     //[AcceptVerbs(HttpVerbs.Post)]   
  36.     //[Authorize]   
  37.     public void LiveStream()   
  38.     {   
  39.         var path = Server.MapPath("~/Uploads");   
  40.   
  41.         var context = ControllerContext.HttpContext;   
  42.   
  43.         var provider = (IServiceProvider)context;   
  44.   
  45.         var workerRequest = (HttpWorkerRequest)provider.GetService(typeof(HttpWorkerRequest));   
  46.   
  47.         //[AcceptVerbs(HttpVerbs.Post)]   
  48.         var verb = workerRequest.GetHttpVerbName();   
  49.         if(!verb.Equals("POST"))   
  50.         {   
  51.             Response.StatusCode = (int)HttpStatusCode.NotFound;   
  52.             Response.SuppressContent = true;   
  53.             return;   
  54.         }   
  55.   
  56.         //[Authorize]   
  57.         if(!context.User.Identity.IsAuthenticated)   
  58.         {   
  59.             Response.StatusCode = (int)HttpStatusCode.Unauthorized;   
  60.             Response.SuppressContent = true;   
  61.             return;   
  62.         }   
  63.   
  64.         var encoding = context.Request.ContentEncoding;   
  65.   
  66.         var processor = new UploadProcessor(workerRequest);   
  67.   
  68.         processor.StreamToDisk(context, encoding, path);   
  69.   
  70.         //return RedirectToAction("Index", "Upload");   
  71.         Response.Redirect(Url.Action("Index""Upload"));   
  72.     }   
  73. }   

虽然这里明显缺少一两个类,但基本的方法还是讲清楚了,看起来和缓存逻辑并没有太大的不同之处,我们仍然将流缓存到了磁盘,但具体处理方式却有些不同了,首先,没有与方法关联的属性,谓词和授权限制都被移除了,使用手动等值取代了,使用手工响应操作而不用ActionFilterAttribute声明的原因是这些属性涉及到了一些重要的ASP.NET管道代码,实际上在我的代码中,我还特意拦截了原生态的HttpWorkerRequest,因为它不能同时做两件事情。

HttpWorkerRequest有VIP访问传入的请求,通常它是由ASP.NET本身支持工作的,但我们绑架了请求,然后欺骗剩下的请求,让它们误以为前面的请求已经全部得到处理,为了做到这一点,我们需要上面例子中未出现的UploadProcessor类,这个类的职责是物理读取来自浏览器的每个数据块,然后将其保存到磁盘上,因为上传的内容被分解成多个部分,UploadProcessor类需要找出内容头,然后拼接成带状数据输出,这一可以在一个上传中同时上传多个文件。

  1. internal class UploadProcessor   
  2. {   
  3.     private byte _buffer;   
  4.     private byte _boundaryBytes;   
  5.     private byte _endHeaderBytes;   
  6.     private byte _endFileBytes;   
  7.     private byte _lineBreakBytes;   
  8.   
  9.     private const string _lineBreak = "\r\n";   
  10.   
  11.     private readonly Regex _filename =   
  12.         new Regex(@"Content-Disposition:\s*form-data\s*;\s*name\s*=\s*""file""\s*;\s*filename\s*=\s*""(.*)""",   
  13.                   RegexOptions.IgnoreCase | RegexOptions.Compiled);   
  14.   
  15.     private readonly HttpWorkerRequest _workerRequest;   
  16.   
  17.     public UploadProcessor(HttpWorkerRequest workerRequest)   
  18.     {   
  19.         _workerRequest = workerRequest;   
  20.     }   
  21.   
  22.     public void StreamToDisk(IServiceProvider provider, Encoding encoding, string rootPath)   
  23.     {   
  24.         var buffer = new byte[8192];   
  25.   
  26.         if (!_workerRequest.HasEntityBody())   
  27.         {   
  28.             return;   
  29.         }   
  30.   
  31.         var total = _workerRequest.GetTotalEntityBodyLength();   
  32.         var preloaded = _workerRequest.GetPreloadedEntityBodyLength();   
  33.         var loaded = preloaded;   
  34.   
  35.         SetByteMarkers(_workerRequest, encoding);   
  36.   
  37.         var body = _workerRequest.GetPreloadedEntityBody();   
  38.         if (body == null// IE normally does not preload   
  39.         {   
  40.             body = new byte[8192];   
  41.             preloaded = _workerRequest.ReadEntityBody(body, body.Length);   
  42.             loaded = preloaded;   
  43.         }   
  44.   
  45.         var text = encoding.GetString(body);   
  46.         var fileName = _filename.Matches(text)[0].Groups.Value;   
  47.         fileName = Path.GetFileName(fileName); // IE captures full user path; chop it   
  48.   
  49.         var path = Path.Combine(rootPath, fileName);   
  50.         var files = new List {fileName};   
  51.         var stream = new FileStream(path, FileMode.Create);   
  52.   
  53.         if (preloaded > 0)   
  54.         {   
  55.             stream = ProcessHeaders(body, stream, encoding, preloaded, files, rootPath);   
  56.         }   
  57.   
  58.         // Used to force further processing (i.e. redirects) to avoid buffering the files again   
  59.         var workerRequest = new StaticWorkerRequest(_workerRequest, body);   
  60.         var field = HttpContext.Current.Request.GetType().GetField("_wr", BindingFlags.NonPublic | BindingFlags.Instance);   
  61.         field.SetValue(HttpContext.Current.Request, workerRequest);   
  62.   
  63.         if (!_workerRequest.IsEntireEntityBodyIsPreloaded())   
  64.         {   
  65.             var received = preloaded;   
  66.             while (total - received >= loaded && _workerRequest.IsClientConnected())   
  67.             {   
  68.                 loaded = _workerRequest.ReadEntityBody(buffer, buffer.Length);   
  69.                 stream = ProcessHeaders(buffer, stream, encoding, loaded, files, rootPath);   
  70.   
  71.                 received += loaded;   
  72.             }   
  73.   
  74.             var remaining = total - received;   
  75.             buffer = new byte[remaining];   
  76.   
  77.             loaded = _workerRequest.ReadEntityBody(buffer, remaining);   
  78.             stream = ProcessHeaders(buffer, stream, encoding, loaded, files, rootPath);   
  79.         }   
  80.   
  81.         stream.Flush();   
  82.         stream.Close();   
  83.         stream.Dispose();   
  84.     }   
  85.   
  86.     private void SetByteMarkers(HttpWorkerRequest workerRequest, Encoding encoding)   
  87.     {   
  88.         var contentType = workerRequest.GetKnownRequestHeader(HttpWorkerRequest.HeaderContentType);   
  89.         var bufferIndex = contentType.IndexOf("boundary=") + "boundary=".Length;   
  90.         var boundary = String.Concat("--", contentType.Substring(bufferIndex));   
  91.   
  92.         _boundaryBytes = encoding.GetBytes(string.Concat(boundary, _lineBreak));   
  93.         _endHeaderBytes = encoding.GetBytes(string.Concat(_lineBreak, _lineBreak));   
  94.         _endFileBytes = encoding.GetBytes(string.Concat(_lineBreak, boundary, "--", _lineBreak));   
  95.         _lineBreakBytes = encoding.GetBytes(string.Concat(_lineBreak + boundary + _lineBreak));   
  96.     }   
  97.   
  98.     private FileStream ProcessHeaders(byte buffer, FileStream stream, Encoding encoding, int count, ICollection files, string rootPath)   
  99.     {   
  100.         buffer = AppendBuffer(buffer, count);   
  101.   
  102.         var startIndex = IndexOf(buffer, _boundaryBytes, 0);   
  103.         if (startIndex != -1)   
  104.         {   
  105.             var endFileIndex = IndexOf(buffer, _endFileBytes, 0);   
  106.             if (endFileIndex != -1)   
  107.             {   
  108.                 var precedingBreakIndex = IndexOf(buffer, _lineBreakBytes, 0);   
  109.                 if (precedingBreakIndex > -1)   
  110.                 {   
  111.                     startIndex = precedingBreakIndex;   
  112.                 }   
  113.   
  114.                 endFileIndex += _endFileBytes.Length;   
  115.   
  116.                 var modified = SkipInput(buffer, startIndex, endFileIndex, ref count);   
  117.                 stream.Write(modified, 0, count);   
  118.             }   
  119.             else  
  120.             {   
  121.                 var endHeaderIndex = IndexOf(buffer, _endHeaderBytes, 0);   
  122.                 if (endHeaderIndex != -1)   
  123.                 {   
  124.                     endHeaderIndex += _endHeaderBytes.Length;   
  125.   
  126.                     var text = encoding.GetString(buffer);   
  127.                     var match = _filename.Match(text);   
  128.   
  129.                     var fileName = match != null ? match.Groups.Value : null;   
  130.                     fileName = Path.GetFileName(fileName); // IE captures full user path; chop it   
  131.   
  132.                     if (!string.IsNullOrEmpty(fileName) && !files.Contains(fileName))   
  133.                     {   
  134.                         files.Add(fileName);   
  135.   
  136.                         var filePath = Path.Combine(rootPath, fileName);   
  137.   
  138.                         stream = ProcessNextFile(stream, buffer, count, startIndex, endHeaderIndex, filePath);   
  139.                     }   
  140.                     else  
  141.                     {   
  142.                         var modified = SkipInput(buffer, startIndex, endHeaderIndex, ref count);   
  143.                         stream.Write(modified, 0, count);   
  144.                     }   
  145.                 }   
  146.                 else  
  147.                 {   
  148.                     _buffer = buffer;   
  149.                 }   
  150.             }   
  151.         }   
  152.         else  
  153.         {   
  154.             stream.Write(buffer, 0, count);   
  155.         }   
  156.   
  157.         return stream;   
  158.     }   
  159.   
  160.     private static FileStream ProcessNextFile(FileStream stream, byte buffer, int count, int startIndex, int endIndex, string filePath)   
  161.     {   
  162.         var fullCount = count;   
  163.         var endOfFile = SkipInput(buffer, startIndex, count, ref count);   
  164.         stream.Write(endOfFile, 0, count);   
  165.   
  166.         stream.Flush();   
  167.         stream.Close();   
  168.         stream.Dispose();   
  169.   
  170.         stream = new FileStream(filePath, FileMode.Create);   
  171.   
  172.         var startOfFile = SkipInput(buffer, 0, endIndex, ref fullCount);   
  173.         stream.Write(startOfFile, 0, fullCount);   
  174.   
  175.         return stream;   
  176.     }   
  177.   
  178.     private static int IndexOf(byte array, IList value, int startIndex)   
  179.     {   
  180.         var index = 0;   
  181.         var start = Array.IndexOf(array, value[0], startIndex);   
  182.   
  183.         if (start == -1)   
  184.         {   
  185.             return -1;   
  186.         }   
  187.   
  188.         while ((start + index) < array.Length)   
  189.         {   
  190.             if (array[start + index] == value[index])   
  191.             {   
  192.                 index++;   
  193.                 if (index == value.Count)   
  194.                 {   
  195.                     return start;   
  196.                 }   
  197.             }   
  198.             else  
  199.             {   
  200.                 start = Array.IndexOf(array, value[0], start + index);   
  201.   
  202.                 if (start != -1)   
  203.                 {   
  204.                     index = 0;   
  205.                 }   
  206.                 else  
  207.                 {   
  208.                     return -1;   
  209.                 }   
  210.             }   
  211.         }   
  212.   
  213.         return -1;   
  214.     }   
  215.   
  216.     private static byte SkipInput(byte input, int startIndex, int endIndex, ref int count)   
  217.     {   
  218.         var range = endIndex - startIndex;   
  219.         var size = count - range;   
  220.   
  221.         var modified = new byte[size];   
  222.         var modifiedCount = 0;   
  223.   
  224.         for (var i = 0; i < input.Length; i++)   
  225.         {   
  226.             if (i >= startIndex && i < endIndex)   
  227.             {   
  228.                 continue;   
  229.             }   
  230.   
  231.             if (modifiedCount >= size)   
  232.             {   
  233.                 break;   
  234.             }   
  235.   
  236.             modified[modifiedCount] = input[i];   
  237.             modifiedCount++;   
  238.         }   
  239.   
  240.         input = modified;   
  241.         count = modified.Length;   
  242.         return input;   
  243.     }   
  244.   
  245.     private byte AppendBuffer(byte buffer, int count)   
  246.     {   
  247.         var input = new byte[_buffer == null ? buffer.Length : _buffer.Length + count];   
  248.         if (_buffer != null)   
  249.         {   
  250.             Buffer.BlockCopy(_buffer, 0, input, 0, _buffer.Length);   
  251.         }   
  252.         Buffer.BlockCopy(buffer, 0, input, _buffer == null ? 0 : _buffer.Length, count);   
  253.         _buffer = null;   
  254.   
  255.         return input;   
  256.     }   
  257. }  

在处理代码的中间位置,你应该注意到了另一个类StaticWorkerRequest,这个类负责欺骗ASP.NET,在点击提交按钮时,它欺骗ASP.NET,让他认为没有文件上传,这是必需的,因为当上传完毕时,如果我们要重定向到所需的页面时,ASP.NET将会检查到在HTTP实体主体中仍然有数据,然后会尝试缓存整个上传,于是我们兜了一圈又回到了原点,为了避免这种情况,我们必须欺骗HttpWorkerRequest,将它注入到HttpContext中,获得请求开始部分的StaticWorkerRequest,它是唯一有用的数据。

  1. internal class StaticWorkerRequest : HttpWorkerRequest   
  2. {   
  3.     readonly HttpWorkerRequest _request;   
  4.     private readonly byte _buffer;   
  5.   
  6.     public StaticWorkerRequest(HttpWorkerRequest request, byte buffer)   
  7.     {   
  8.         _request = request;   
  9.         _buffer = buffer;   
  10.     }   
  11.   
  12.     public override int ReadEntityBody(byte buffer, int size)   
  13.     {   
  14.         return 0;   
  15.     }   
  16.   
  17.     public override int ReadEntityBody(byte buffer, int offset, int size)   
  18.     {   
  19.         return 0;   
  20.     }   
  21.   
  22.     public override byte GetPreloadedEntityBody()   
  23.     {   
  24.         return _buffer;   
  25.     }   
  26.   
  27.     public override int GetPreloadedEntityBody(byte buffer, int offset)   
  28.     {   
  29.         Buffer.BlockCopy(_buffer, 0, buffer, offset, _buffer.Length);   
  30.         return _buffer.Length;   
  31.     }   
  32.   
  33.     public override int GetPreloadedEntityBodyLength()   
  34.     {   
  35.         return _buffer.Length;   
  36.     }   
  37.   
  38.     public override int GetTotalEntityBodyLength()   
  39.     {   
  40.         return _buffer.Length;   
  41.     }   
  42.   
  43.     public override string GetKnownRequestHeader(int index)   
  44.     {   
  45.         return index == HeaderContentLength   
  46.                    ? "0"  
  47.                    : _request.GetKnownRequestHeader(index);   
  48.     }   
  49.   
  50.     // All other methods elided, they're just passthrough   
  51. }  

使用StaticWorkerRequest建立虚假的声明,现在你可以在ASP.NET MVC中通过直接访问数据流上传大文件,使用这个代码作为开始,你可以很容易地保存过程数据,并使用Ajax调用另一个控制器行为展示其进度,将大文件缓存到一个临时区域,可以实现断点续传,不用再等待ASP.NET进程将整个文件缓存到磁盘上,同样,保存文件时也不用消耗另存为方法那么多的内存了。

使用StaticWorkerRequest,内存消耗更平稳

  • 下一篇资讯: C#MD5,RC2,DES加密类
  • 设为首页 | 加入收藏 | 网学首页 | 原创论文 | 计算机原创
    版权所有 网学网 [Myeducs.cn] 您电脑的分辨率是 像素
    Copyright 2008-2020 myeducs.Cn www.myeducs.Cn All Rights Reserved 湘ICP备09003080号 常年法律顾问:王律师