上一篇中我们讨论了ASP.Net文件上传的方法,但是这种方法还不够完善,当Form表单提交的数据超过maxRequestLength(.Net默认为4M)时就会出现“The page cannot be displayed”的错误页面,而且这个页面非常的不友好,普通用户可能会认为是系统出了什么故障,根本无从知晓是因为自己上传的文件超过了规定的要求,所以必须得想办法解决这个问题。
大家都知道通过增大Web.config文件里maxRequestLength的值可以改变.Net默认4M的限制,让我们可以提交更大的Form表单,也就意味着可以上传更大的文件,这个maxRequestLength最大可以设置为2197151,也就是说理论上能提交的Form表单的最大数据量为2197151KB,约为2.095GB,如果忽略表单中的其他表单项,那就可以上传2GB的文件(以95M的零头来存放除File以外的其他表单项问题应该不大吧!),但是请注意,在改变maxRequestLength解决“The page cannot be displayed”错误的同时在Form表单里的其他表单项数据固定的情况下,用户能上传的文件也在变大,但是在很多时候我们并不希望用户上传太大的文件,如果把maxRequestLength设小,出现“The page cannot be displayed”的几率又会增大,那么究竟要如何限制用户上传文件的大小同时不出现“The page cannot be displayed”错误呢?
一般的做法是通过HttpRequest对象的ContentLength属性来取得Form表单提交的数据的大小,然后判断是否超过了限额,如果超过限额就给出一个出错提示,或者是通过HttpRequest对象的Files属性取得Form表单提交的所有文件,然后遍历这些文件通过ContentLength取得每个文件的大小,如果有文件超过了限额就给出一个出错提示。这两种方法都可以,但是这两种方法都比较占用服务器的资源,原因是在使用HttpRequest对象时Form表单里的所有数据都已经缓存到服务器上了,甚至都写到服务器的磁盘上了,(按照微软的说法,默认情况下Form表单提交的的数据超过256KB就会被缓存到磁盘上,原文如下:“Files are uploaded in MIME multipart/form-data format. By default, all requests, including form fields and uploaded files, larger than 256 KB are buffered to disk, rather than held in server memory”,详见:http://msdn.microsoft.com/en-us/library/system.web.httppostedfile.aspx
另一个办法就是在HttpHandler处理Request之前先判断Form表单的数据是否超过了限额,如果超过了限额那就忽略Form表单提交的数据,直接返回出错提示。具体代码如下:
1、修改web.config文件,在appsetting增加以下内容:
- <!--自定义Form表单大小限制 单位:KB(10240表示10M )-->
- <add key="customMaxRequestLength" value="10240"/>
2、修改web.config文件,在system.web中修改一下内容:(没有就添加)
- <httpRuntime maxRequestLength="2097151"
- useFullyQualifiedRedirectUrl="true"
- executionTimeout="6000"
- minFreeThreads="8"
- minLocalRequestFreeThreads="4"
- appRequestQueueLimit="100"
- enableVersionHeader="true"
- />
3、在网站根目录下增加Global.asax文件
- <%@ Application Language="VB" %>
- <script runat="server">
- Protected Sub Application_BeginRequest(ByVal sender As Object, ByVal e As System.EventArgs)
- Dim application As HttpApplication = CType(sender, HttpApplication)
- Dim context As HttpContext = application.Context
- Dim wr As HttpWorkerRequest = GetWorkerRequest()
- Dim mybindingflags As System.Reflection.BindingFlags = Reflection.BindingFlags.Instance Or Reflection.BindingFlags.NonPublic
- '取content_type
- Dim content_type As String = application.Request.ContentType
- '只处理enctype为"multipart/form-data" 的Form提交的请求
- If (Not content_type.ToLower().StartsWith("multipart/form-data")) Then Return
- '只处理包含body数据的Request
- If (Not wr.HasEntityBody()) Then Return
- '取Request的总长度
- Dim contentlen As Int32 = wr.GetTotalEntityBodyLength()
- If contentlen < (System.Configuration.ConfigurationManager.AppSettings("customMaxRequestLength") * 1024) Then Return
- Dim hct As Type = wr.GetType
- hct.GetMethod("SkipAllPostedContent", mybindingflags).Invoke(wr, Nothing)
- hct.GetField("_preloadedContent", mybindingflags).SetValue(wr, Nothing)
- hct.GetField("_contentLength", mybindingflags).SetValue(wr, 0)
- context.Response.Redirect("~/uploaderror.aspx")
- End Sub
- Private Function GetWorkerRequest() As HttpWorkerRequest
- Dim provider As IServiceProvider = HttpContext.Current
- Return CType(provider.GetService(GetType(HttpWorkerRequest)), HttpWorkerRequest)
- End Function
- </script>
4、index.aspx文件,用来提交表单
- <html>
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
- <title>无标题文档</title>
- </head>
- <body>
- <form action="save.aspx" method="post" enctype="multipart/form-data" name="form1" id="form1">
- <div><input type="file" name="file1" /></div>
- <div><input type="submit" name="Submit" value="提交" /></div>
- </form>
- </body>
- </html>
5、save.aspx文件,用来处理index.aspx提交的表单
- <html>
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
- <title>无标题文档</title>
- </head>
- <body>
- <%
- '取客户端上传的全部文件的集合
- Dim Files As HttpFileCollection
- Files = Request.Files
- '取当前日期时间
- dim currentdate as Date,strcurrentdate as string
- currentdate=now
- strcurrentdate=currentdate.Millisecond.toString()
- strcurrentdate="000" & strcurrentdate
- strcurrentdate=strcurrentdate.Substring(strcurrentdate.length()-3)
- strcurrentdate=currentdate.toString("yyyyMMddHHmmss") & strcurrentdate
- '遍历客户端上传的全部文件
- dim loop1 as Integer=0
- for loop1 = 0 To (Files.count-1)
- dim FileName as string '文件名
- dim FileType as string '文件类型
- dim FileSize as long '文件大小
- FileName=Files.item(loop1).FileName
- FileSize=Files.item(loop1).ContentLength
- FileType=Files.item(loop1).ContentType
- if FileSize<>0 then '客户端上传了文件
- FileName=Server.MapPath("~/") & strcurrentdate & "_" & (loop1+1) & FileName.Substring(FileName.lastindexof("."))
- Files.item(loop1).SaveAs(FileName)
- end if
- response.write(Filename & "<br>")
- Next loop1
- %>
- </body>
- </html>
6、UploadError.aspx文件 用来显示上传文件超额错误
- <html>
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
- <title>无标题文档</title>
- </head>
- <body>
- <div>出错了,表单不能超过<%=System.Configuration.ConfigurationManager.AppSettings("customMaxRequestLength")%>KB</div>
- <div>[ <a href="index.aspx">返回</a> ]</div>
- </body>
- </html>
以上代码的关键思路是利用HttpWorkerRequest对象通过GetTotalEntityBodyLength方法先取到整个 HTTP 请求正文的长度,然后进行判断,如果超过了限定值再利用.Net反射调用HttpWorkerRequest的SkipAllPostedContent方法忽略掉HTTP请求的正文,这样服务器就不会缓存Form表单提交的数据,最后再利用.Net反射清除preloadedContent里的数据并将contentLength设置为0,设置完成后跳转到UploadError.aspx页面,显示出错信息,并提示用户系统上传文件的大小限制。经测试此方法与前面利用HttpRequest对象相比处理速度有了很大的提升,而且文件越大效果越明显,在本机上上传50M左右的文件时,处理速度提升了差不多有50%。以下是测试时所使用的Global.ascs代码,有兴趣的可以试试:
- <%@ Application Language="VB" %>
- <script runat="server">
- Protected Sub Application_BeginRequest(ByVal sender As Object, ByVal e As System.EventArgs)
- Dim mysw As New System.Diagnostics.Stopwatch
- mysw.Start()
- '方法1,测试方法1,请将方法2至方法2结束中的代码注释掉
- Dim application As HttpApplication = CType(sender, HttpApplication)
- Dim context As HttpContext = application.Context
- Dim wr As HttpWorkerRequest = GetWorkerRequest()
- Dim mybindingflags As System.Reflection.BindingFlags = Reflection.BindingFlags.Instance Or Reflection.BindingFlags.NonPublic
- '取content_type
- Dim content_type As String = application.Request.ContentType
- '只处理enctype为"multipart/form-data" 的Form提交的请求
- If (Not content_type.ToLower().StartsWith("multipart/form-data")) Then Return
- '只处理包含body数据的Request
- If (Not wr.HasEntityBody()) Then Return
- '取Request的总长度
- Dim contentlen As Int32 = wr.GetTotalEntityBodyLength()
- If contentlen < (System.Configuration.ConfigurationManager.AppSettings("customMaxRequestLength") * 1024) Then Return
- Dim hct As Type = wr.GetType
- hct.GetMethod("SkipAllPostedContent", mybindingflags).Invoke(wr, Nothing)
- hct.GetField("_preloadedContent", mybindingflags).SetValue(wr, Nothing)
- hct.GetField("_contentLength", mybindingflags).SetValue(wr, 0)
- '方法1结束
- '方法2,测试方法2,请将方法1至方法2结束部分的代码注释掉
- 'If Request.ContentLength > (System.Configuration.ConfigurationManager.AppSettings("customMaxRequestLength") * 1024) Then
- 'End If
- '方法2结束
- mysw.Stop()
- context.Response.Write(mysw.ElapsedTicks)
- 'context.Response.Redirect("~/uploaderror.aspx")
- End Sub
- Private Function GetWorkerRequest() As HttpWorkerRequest
- Dim provider As IServiceProvider = HttpContext.Current
- Return CType(provider.GetService(GetType(HttpWorkerRequest)), HttpWorkerRequest)
- End Function
- </script>
很可惜的是利用这个方法限制上传文件的大小还有一点不是很完善的地方,那就是这种方法限制的是整个Form表单数据的大小,并不是实际的文件大小,这两者之间还是有一点点差距的,不过这可以在设置限额的时候留出一定的余量来解决(比如限制10MB,可以将customMaxRequestLength设置为10250,这样就可以留出1MB的余量给Form中的其他表单项),另外就是这种方法还是不能解决"The page cannot be displayed"的问题,不过如果把maxRequest设置为最大的话出现这个问题的概率会小了很多,毕竟没有谁会经常去上传2G以上的文件。
还有就是在上传大文件的时候,由于网络速度的问题,可能会出现网络连接超时等错误而导致上传的不成功,怎么解决这个问题呢,下一篇中我们会继续讨论。
本文档中的部分代码参考了 2bno1 及 widebright 的相关文章
(补充:http://www.codeplex.com/DevServer/SourceControl/FileView.aspx?itemId=70032&changeSetId=2991这里有HttpRequest对象的源代码,大家可以去看看,另外.Net2005的调试模式下没问题,放到IIS里面"SkipAllPostedContent"出错,再用反射一查HttpWorkerRequest里面没有这个函数了,郁闷……)