(转)在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】
posted @ 2012-11-08 20:24  Debuggings  阅读(2300)  评论(4编辑  收藏  举报