在Web程序中上传文件是很常见的需求。利用HTTP协议上传文件的方式非常有限,最常见的莫过于使用元素进行上传。这种上传方式会将内容使用multipart/form-data方案进行编码,并将内容POST到服务器端。使用 multipart/form-data编码方式与默认的application/x-url-encoded编码方式相比,在大数据量情况下效率要高很多。
使用上传文件最大的优势在于编程方便,几乎各种服务器端技术都对这种上传方式做了良好的封装,使得程序员能够直观地对客户端上传的文件进行处理。不过总体来说,这个协议并不适合做文件传输,解析数据流内容的代价相对较高,并且没有一些例如断点续传的机制来辅助,导致在上传大文件时经常会力不从心。
有朋友认为使用上传文件最大的问题在于内存占用太高,由于需要将整个文件载入内存进行处理,导致如果用户上传文件太大,或者同时上传的用户太多,会造成服务器端内存耗尽。这个观点其实是错误的。对于某些服务器端的技术,例如Spring Framework,或者早期ASP.NET 1.1时,为了供程序处理,都会将用户上传的内容完全载入内存,这的确会带来问题。但是其实协议本身并没有规定服务器端应该使用何种方式来处理上传的文件。例如在现在的ASP.NET 2.0中就已经会在用户上传数据超过一定数量之后将其存在硬盘中的临时文件中,而这点对于开发人员完全透明,也就是说,开发人员可以像以前一样进行数据流的处理。
swfupload也是个开源组件,顾名思义是使用Flash进行上传。不过对于swfupload来说,Flash的作用主要是“控制”,而不是“展示 ”,这无疑给了开发人员更大的灵活性。swfupload的实现方式自然是利用了FileReference和 FileReferenceList组件所提供的功能,通过Flash与JavaScript的交互能力,使得开发文件上传功能变得非常优雅和容易。有了 swfupload,开发人员可以使用JavaScript来实现各种显示方式,开发像Flicker一样酷酷的上传界面也不再是非常困难的事情了。
swfupload是个客户端组件,它对于服务器端来说完全透明,也就是说,服务器端只需要使用对待普通form的方式来处理即可。例如在 ASP.NET中我们可以使用Generic Handler来处理客户端的文件上传。如下,fileCollection变量即为客户端Post至服务器端所有文件的集合,我们可以使用name或下标的方式来获得其中的HttpPostedFile对象。
ASP.NET 2.0启用硬盘临时文件的阈值(threshold)是可配置的:
- <system.web>
- <httpRuntime
- maxRequestLength="Int32"
- requestLengthDiskThreshold="Int32" />
- system.web>
maxRequestLength自不必说,刚接触ASP.NET的朋友总会发现上传文件不能超过4M,这就是因为 maxRequestLength的大小默认为4096,这就限制着每个请求的大小不得超过4096KB。这么做的目的是为了保护应用程序不受恶意请求的危害。当请求超过maxRequestLength之后,ASP.NET处理程序将不会处理该请求。这里和ASP.NET抛出一个异常是不同的,这就是为什么如果用户上传文件太大,看到的并非是ASP.NET应用程序中指定的错误页面(或者默认的),因为ASP.NET还没有对这个请求进行处理。 requestLengthDiskThreshold就是刚才所提到的阈值,其默认值为256,即一个请求内容超过256KB时就会启用硬盘作为缓存。这个阈值理论上和客户端是否是在上传内容无关,只要客户端发来的请求大于这个值即可。因此,在ASP.NET 2.0中服务器的内存不会因为客户端的异常请求而耗尽。
既然Flash提供了文件上传功能,Silverlight作为微软主推的RIA技术也不会缺了这项功能。这篇文章源自Silverlight 2.0的Quick Starts,展示了如何使用Silverlight 2.0开发文件上传的功能,感兴趣的朋友可以一读。
围绕着ASP.NET中上传文件这个话题也讨论了不少了,还有什么没有涉及到的吗?个人认为其实至少还有一个非常重要问题是没有讨论过,那就是在处理上传文件时占用ASP.NET处理线程的问题。众所周知,ASP.NET处理请求时会用到线程池中的线程,当线程池中的线程被用完之后没有被处理的请求只能排队了。因此增大ASP.NET应用程序吞吐量的一个重要手段,就是为一些耗时的操作使用异步处理方式(事实上这一命题可以在大部分应用中成立)。例如一个数据库查询操作需要3秒钟,如果不使用异步操作,处理线程就会被阻塞,直至查询完成。如果使用异步方式来执行数据库查询,在这3秒钟内线程就可以用户处理其他请求,当异步操作结束之后,ASP.NET就会使用另一个线程来继续处理这个请求。
上传大文件也是一个长时间占用处理线程的工作,而且遗憾的是,这无法使用异步操作来完成(通过异步操作来释放处理线程需要操作系统的支持,因此只有少量功能可以使用异步操作)。如果一个文件上传需要3分钟时间,那么在这3分钟内就会独占一个处理线程,如果上传文件的连接一多,就会大大影响应用程序的性能——就像遭受了某种方式的DOS攻击一样。因此,即使使用了像NeatUpload和swfupload这样的组件,也无法解决上传连接过多造成可用线程减少的问题。要解决这个问题并不容易,以下是两种思路(欢迎大家就此问题进行讨论):
◆扩展IIS,使上传文件或处理文件的过程不经ASP.NET处理,以减少ASP.NET应用程序线程的消耗。现在有了IIS 7,如果使用集成管道模式,应该也可以使用托管代码进行扩展。
◆使用额外的ASP.NET应用程序处理文件上传,以节省上传文件的线程对原ASP.NET应用程序线程的消耗。
【编辑推荐】