前一篇中我们讨论了关于限制上传文件大小的问题,下面我们将继续讨论关于大文件上传的问题,不过在开始讨论之前首先要解释一下什么是大文件。一般人可能认为大文件指的是文件大小比较大的文件比如几十兆、上百兆甚至上千兆的文件,其实我们这里所说的“大”指的并不是文件的大小,而是指文件上传所需要的时间,这个时间不仅仅与文件的大小有关,同时与网络传输的速度有关。在过去用56K猫拨号上网的年代,上传的速度大约只有2~3KB/S左右,30分钟时间大约能上传5M左右的文件,因此相对于当时的网络速度来说5M的文件就可以算是大文件了。但是如果在千兆局域网环境下,传送一个1G的文件也花不了几分钟,在这种条件下即便是1G的文件也不能称之为大文件。在IIS网站属性的“网站”标签中有一个“连接超时”项,本文中所提到的大文件指的就是在上传过程中上传时间超过该项目设置的那些文件。
那么这个“连接超时”和我们上传的文件大小有什么关系呢,其实这要从HTTP协议的工作原理说起,在HTTP1.1中当我们在浏览器中输入URL地址访问某个网页或点击网页里的一个超链接的时候,浏览器首先会根据域名或IP地址找到对应的服务器并建立TCP/IP连接,然后浏览器再向服务器发送相关的请求,服务器响应浏览器的请求并返回相关的结果,如果IIS里没有设置保持HTTP连接选项,那么服务器在响应了浏览器的请求后就会立即关闭该TCP/IP连接,否则服务器会再等待一段时间,如果在以后的持续一段时间内没有新的请求才会关闭该TCP/IP连接。IIS中的“连接超时”指的就是这个过程中在TCP/IP连接建立以后,从浏览器发出的最后一个请求开始计时到IIS主动的中断这个TCP/IP连接的时间,因此如果我们提交请求的时间超过了这个“连接超时”设置就会因连接中断而导致请求的提交失败,也就意味着文件上传失败。所以要解决大文件传输的问题关键就是怎么解决这个连接超时的问题。
显然通过修改IIS网站属性的“连接超时”选项可以改变这个超时时间,但是在增大此时间的同时也会增大TCP/IP连接的空闲等待时间,也就是说当浏览器没有新的请求的时候服务器需要等待更长的一段时间才会关闭该TCP/IP连接,这必然会影响到服务器的性能,而且由于网络速度的不同,上传文件的大小也不固定,因此需要的时间也不确定,所以通过修改“连接超时”选项的方法并不能解决问题。
另一个办法就是在文件上传的过程中,在到达“连接超时”时间以前想办法重置那个计时器,让它重新计时那么就可以解决在大文件上传过程中因为“连接超时”而导致上传失败的问题。在前一篇中我们已经讨论了利用HttpWorkerRequest对象在.Net处理HTTP请求之前判断HTTP请求大小的方法,并用来限制上传文件的大小,在这里我们可以继续利用HttpWorkerRequest对象在.Net处理HTTP请求之前先把数据截取下来,由于客户端提交HTTP请求的时候,提交的数据并不是一次性提交的,所以我们可以在接收数据过程中,在每收到一个数据包的时候就可以重置一次“连接超时”的计时器,这样就可以解决连接超时的问题。具体实现代码如下:
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>
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>
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
- Dim contentlen As Int32 = wr.GetTotalEntityBodyLength() '取Request的总长度
- Dim totalrecv As Int32 = 0 '已读取字节数的总数
- Dim tmpBuffer As Byte() = wr.GetPreloadedEntityBody() '定义临时缓存并将预读取的内容存入临时缓存
- Dim tmpBufferSize As Int32 = wr.GetPreloadedEntityBodyLength '根据第一次预读取的内容取得系统每次可以从客户端读取的最大字节数
- Dim totalBuffer(contentlen) As Byte '定义数组缓存接收到的所有数据
- Buffer.BlockCopy(tmpBuffer, 0, totalBuffer, totalrecv, tmpBufferSize) '将预读取的数据复制到totalBuffer
- totalrecv = totalrecv + tmpBufferSize '计算总的已读取字节数
- '判断是否还需要从客户端读取数据()
- If (Not wr.IsEntireEntityBodyIsPreloaded()) Then
- '循环接收客户端发送的数据直到所有数据接收完成或客户端断开连接
- While ((totalrecv < contentlen) And wr.IsClientConnected())
- '检查剩余的字节数是否超过tmpbuffersize个字节,如果超过则以tmpbuffersize字节分块,否则以剩余字节的实际字节数分块
- If (totalrecv + tmpBufferSize) > contentlen Then
- tmpBufferSize = contentlen - totalrecv
- ReDim tmpBuffer(tmpBufferSize)
- End If
- wr.ReadEntityBody(tmpBuffer, tmpBufferSize) '读入下一个数据块
- Buffer.BlockCopy(tmpBuffer, 0, totalBuffer, totalrecv, tmpBufferSize) '将新取出的数据块复制到totalBuffer中
- totalrecv = totalrecv + tmpBufferSize '计算总的已读取字节数
- Dim hct As Type = context.GetType
- hct.GetMethod("SetStartTime", mybindingflags).Invoke(context, Nothing) '重置context的开始时间为当前时间,避免文件上传超时
- End While
- End If
- AddContentBytesToRequest(wr, totalBuffer, mybindingflags)
- End Sub
- Private Function GetWorkerRequest() As HttpWorkerRequest
- Dim provider As IServiceProvider = HttpContext.Current
- Return CType(provider.GetService(GetType(HttpWorkerRequest)), HttpWorkerRequest)
- End Function
- Private Sub AddContentBytesToRequest(ByVal wr As HttpWorkerRequest, ByVal textData As Byte(), ByVal myBindingFlags As System.Reflection.BindingFlags)
- Dim mytype As Type
- mytype = wr.GetType()
- If mytype.FullName <> "HttpWorkerRequest" Then
- mytype = mytype.BaseType
- End If
- mytype.GetField("_contentAvailLength", myBindingFlags).SetValue(wr, textData.Length)
- mytype.GetField("_contentTotalLength", myBindingFlags).SetValue(wr, textData.Length)
- mytype.GetField("_preloadedContent", myBindingFlags).SetValue(wr, textData)
- mytype.GetField("_preloadedContentRead", myBindingFlags).SetValue(wr, True)
- End Sub
- </script>
以上代码的关键思路就是利用HttpWorkerRequest在.Net处理HTTP请求之前先把数据截取出来并保存到totalBuffer这个数组里,在截取数据的同时通过反射调用HttpContext的SetStartTime方法重置连接超时的计时器(估计调用HttpContext的ResetStartTime也能达到同样的效果,不知道这两者有什么区别,大家可以试试)。
补充一点,因为上面的代码是用totalBuffer这个数组来缓存全部的HTTP请求数据,所以在实际使用过程中必然会消耗大量的内存,所以还需要对以上代码进行改造,解决的办法很简单,就是在每收到一个数据包的时候都拿去分析,把属于文件的部分直接写成文件,这样内存的使用量就会降下来,关于这部分的内容大家可以参考http://hi.baidu.com/widebright/blog/item/5b18df54aae4cf58d1090682.html这篇文章,我在这里就不再啰嗦了。至于进度条的问题,网上也有很多现成的东西,我也不在废话了,大家可以参考一下http://www.cnblogs.com/stg609/archive/2008/08/04/1259469.html这篇文章。另外就是前面关于HTTP1.1协议的内容参考了http://www.wujianrong.com/archives/2007/12/http.html这篇文章,想了解更多这方面知识的也可以去看看。