• (转)在ASP.NET MVC中实现大文件异步上传


    在ASP.NET MVC中实现大文件异步上传(原文)

    2010-02-05 08:32 黄永兵 译 51CTO.com 我要评论(0) 字号:T | T
    一键收藏,随时查看,分享好友!

    在ASP.NET MVC中,我们使用StaticWorkerRequest建立虚假声明的方式实现大文件上传,现在你可以在ASP.NET MVC中通过直接访问数据流上传大文件,同时还可保证内存资源的消耗相对平稳。

    AD:

    【51CTO独家特稿】在ASP.NET中通过HTTP上传大文件是一个由来已久的挑战,它是许多活跃的ASP.NET论坛最常讨论 的问题之一,除了处理大文件外,用户还经常被要求要显示出文件上传的进度,当你需要直接控制从浏览器上传数据流时,你会四处碰壁。51CTO.com之前 就曾针对性的报道过《解除ASP.NET上传文件的大小限制》和《ASP.NET大文件上传开发总结》等文章。

    绝大多数人认为在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文件上传文章,地址http://www.hanselman.com/blog/CommentView.aspx?guid=bc137b6b-d8d0-47d1-9795-f8814f7d1903, 先对文件上传有一个大致的了解,但Scott Hanselman的方法是不能上传大文件的,根据Scott Hanselman的方法,你只需要修改一下web.config文件,确保ASP.NET允许最大支持2GB大小的文件上传,不要担心,这样设置并不会 吃掉你的内存,因为凡是大于256KB的数据都被缓存到磁盘上去了。

    1. ﹤system.web﹥  
    2. ﹤httpruntime requestlengthdiskthreshold="256" maxrequestlength="2097151"﹥  
    3. ﹤/httpruntime﹥﹤/system.web﹥ 

    这是一个简单的适合大多数应用的解决办法,但我的任务中不能借用这种方法,即使会将数据缓存到磁盘中,但这种类似于另存为的方法也会使用大量的内存。

    通过缓存整个文件的方式,内存消耗突然上升 
    图 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[1].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[1].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,内存消耗更平稳 
    图 2: 内存消耗更平稳

    【更多关于ASP.NET上传文件的介绍】

    1. 专访微软MVP衣明志:走进ASP.NET MVC 2框架开发
    2. ASP.NET大文件上传方法浅析
    3. ASP.NET上传文件面面观
    4. ASP.NET上传文件控件实例详解
    5. ASP.NET多附件上传和附件编辑的实现
    【责任编辑:red7 TEL:(010)68476606】
  • 相关阅读:
    Codeforces 1093D(染色+组合数学)
    Codeforces 1093C (思维+贪心)
    Codeforces 1082D (贪心)
    Codeforces 433A (背包)
    BZOJ 3262(Treap+树状数组)
    BZOJ 1588 (treap)
    Codeforces 1061C (DP+滚动数组)
    Codeforces 1080C 题解(思维+二维前缀和)
    周记 2015.07.12
    周记 2015.07.04
  • 原文地址:https://www.cnblogs.com/errorx/p/2761035.html
Copyright © 2020-2023  润新知