小述ASP.NET大文件上传
前段时间有一同事加哥们问我文件上传问题,虽然工作不是很忙,但人一旦松懈下来要想恢复却真的不是很容易,前段时间准备写一个控件开发的专题也半途而废了。这段时间心里有了压力,遂把这个问题研究了下。
做web开发的都知道,在Web程序中上传文件是很常见的需求。利用HTTP协议上传文件的方式非常有限,一般使用<input type="file" />标签来进行上传。这种上传方式会将内容使用“multipart/form-data”进行编码(multipart/form-data规范原文),并将内容POST到服务器端,然后进行处理。“multipart/form-data”相对于默认的“application/x-url-encoded”,在大数据量提交时效率要高很多。使用<input type="file" />标签上传文件最大的好处在于各种服务器技术都对其最好了封装,开发起来能够很直观的对上传的文件进行处理。不过总体来说,这个协议并不适合做文件传输,解析数据流内容的代价相对较高,并且没有一些例如断点续传的机制来辅助,导致在上传大文件时经常会力不从心。
Web上传文件的原理及实现
银河使者的这篇文章从java语言介绍了通过<input type="file" />标签把多文件从客户端提交到服务器后进行解析处理,.net版本大致相似。这样就可以自己完成文件上传的操作。
ASP.NET的封装
ASP.NET 1.x 提供了一个HtmlInputFile控件,而2.0却出现了一个新的控件FileUpload,但是它生成的html却仍然是一样的,在页面中是用时需要这样来定义:
[ASP.NET 1.x]
[ASP.NET 2.0]
在ASP.NET里文件上传可以说是低效率的代名词,公平的所,IIS却是它的幕后黑手。当你选择一个文件并且按下提交按钮,IIS需要解析文件的所有内容后,你才能读取上传的文件属性,IIS5.X和IIS6都是这样做的,好消息是IIS7.0将会使用Apache的方式。除非你现在就换到7.0,否则你只有长时间等待直到上传完毕,其余一无他法。你也不可以显示进度条因为你压根不知道在这段时间里到底上传了多少。
使用过FileUpload控件的都知道,它真是一把双刃剑——既可能成为我们的救世主(a savior),也能是我们的敌人(an enemy ),其中一个很常见的问题就是如何处理超过4MB的大文件上传。不过我们应该了解的是,之所以默认的文件大小上限为4MB,并不是因为设计人员想当然,而是为了避免潜在DOS攻击危险。
为了避免这个限制,需要修改配置文件的httpRuntime节的maxRequestLength属性,文件越大,处理的时间也就越长,所以意味着通常你都要修改executionTimeout属性,这个属性1.X默认是90s,2.0默认是110s,大家可以看下machine.config文件,还有个shutdownTimeout属性,不过我不知道它的用处。
IIS流程
当上传了一个大文件到服务器上,不管你的maxRequestLength设置得多大,IIS都会提取完文件,然后ASP.NET根据system.web/httpRuntime节来判断大小,超过了就会抛出一个异常,这个过程是由HttpRequest的GetEntireRawContent()方法,你可以看到:
理论上说可以使文件不是全部加载,但是能配置IIS让它分块读取文件吗?至少我还不知道!
ASP.NET的弊端
ASP.NET处理文件上传的最大的问题在于内存占用太高,由于将整个文件载入内存进行处理,导致如果用户上传文件太大,或者同时上传的用户太多,会造成服务器端内存耗尽。这个观点其实是片面的,对于早期ASP.NET 1.X,为了供程序处理,会将用户上传的内容完全载入内存,这的确会带来问题,但在ASP.NET 2.0中就已经会在用户上传数据超过一定数量之后将其存在硬盘中的临时文件中,而这点对于开发人员完全透明,也就是说,开发人员可以像以前一样进行数据流的处理,这个也在httpRuntime里通过requestLengthDiskThreshold属性来设置阈值(threshold),其默认值为256,即一个请求内容超过256KB时就会启用硬盘作为缓存,这个阈值和客户端是否是在上传内容无关,只关心客户端发来的请求大于这个值。因此,在ASP.NET 2.0中服务器的内存不会因为客户端的异常请求而耗尽。
另外一个弊端就是当请求超过maxRequestLength(默认4M)之后,ASP.NET处理程序将不会处理该请求。这和ASP.NET抛出一个异常完全不同,这就是为什么如果用户上传文件太大,看到的并不是ASP.NET应用程序中指定的错误页面(或者默认的),因为ASP.NET还没有对这个请求进行处理。还有一个问题就是处理超时。这个其实可以通过在运行时读取web.config中的httpRuntime节,并转化为HttpRuntimeSection对象或者重写Page.OnError
()来检测HTTP Code(相应代码)是否为400来处理,这里不再赘述,代码如下:
对于文件上传的功能需要较为特别的需求——例如进度条提示,ASP.NET封装的控件<asp:FileUpload />就无能为力了。
好的解决方案
Robert Bazinet建议,最好的解决方案是使用RIA,大多数情况下,建议用Silverlight或Flash的上传组件来替代传统的FileUpload组件,这类组件不只是提供了更好的上传体验,也比<input type="file">标签在页面上的文本框、按钮漂亮,这个<input type="file">标签并不能够通过CSS添加样式,不过也有人尝试去解决了。至今为止并没有什么商业上传组件使用了Silverlight,不过这里有演示了用Silverlight进行多文件上传的示例程序。
当然使用Silverlight就可以很轻松的实现多线程上传,断点续传这种功能了,这些都不是我要详细讨论的内容,如果有需要可以自己去看下。
可选择的解决方案
使用<input type="file" />标签所能提供的支持非常有限,一些特殊需求我们不能实现——或者说是无法轻易地、直接地实现。所以为了实现这样的功能我们每次都要绕一个大大的弯。为了避免每次实现相同功能时都要费神费时地走一遍弯路,市面上或者开源界出现了各种上传组件,上传组件提供了封装好的功能,使得我们在实现文件上传功能时变得轻松了很多。例如几乎所有的上传组件都直接或间接地提供了进度提示的功能,有的提供了当前的百分比数值,有的则直接提供了一套UI;有的组件只提供了简单的UI,有的却提供了一整套上传、删除的管理界面。此外,有的组件还提供了防止客户端恶意上传的能力。
我觉得最好的办法是在HttpModule里分块读取文件并且保持页面激活的状态,这样就不会超时,同时也可以跟踪进度或者取消上传,或者通过HttpHandler实现,在通过进度条给用户充分提示的同时,也让开发人员能够更好地控制文件大小以及上传过程中可能出现的异常。上传组件都是用这些办法的,我们的选择有:
NeatUpload是在ASP.NET Pipeline的BeginRequest事件中截获当前的HttpWorkerRequest对象,然后直接调用其ReadEntityBody等方法获取客户端传递过来的数据流,并加以分析和处理。并通过使用新的请求进行轮询来获取当前上传的状态。关于NeatUpload和其他开源组件的介绍可以参看JeffreyZhao的在ASP.NET应用程序中上传文件,当然他还说了Memba Velodoc XP Edition和swfupload,写的非常棒!
HttpWorkerRequest实现介绍
利用隐含的HttpWorkerRequest,用它的GetPreloadedEntityBody和ReadEntityBody方法从IIS为ASP.NET建立的pipe里分块读取数据可以实现文件上传。实现方法如下:
结论
ASP.NET的文件上传是一个不完善和有缺陷的领域,相信在不久会得到提高和发展,如果你已经解决了,说明你在一个好公司,否则你可以考虑使用第三方产品来解决了。文件上传的问题,我们都能够找到很多种不同的方法来解决,挑战在于找出不同做法的利弊然后找到一个适用于自己项目的方案,这不仅仅是在文件上传这一个方面!
参考资料:
1.随便说说:在ASP.NET应用程序中上传文件
2.Handling Large File Uploads in ASP.NET
3.Uploading Files in ASP.NET 2.0
4.The Dark Side of File Uploads
5.ASP.NET Custom Error Pages
PS:写一篇博客太麻烦了,发布了n次!