学习Asp.net2.0上传文件组件编写笔记
by widebright http://hi.baidu.com/widebright
Asp.Net本身提供的文件上传控件有一些确定,比如不能上传大文件、不能获取进度条。不能捕获文件超时错误等。所以很多人都写了自
定义的上传组件,还听说有人拿这种组件来卖,卖到600多块钱一个。因为前段时间在做文件上传,所以就也找时间又看了一下这方面的东西。
在网上找资料,大都是采用这个方案:利用隐含的HttpWorkerRequest,用它的GetPreloadedEntityBody 和 ReadEntityBody方法从IIS为
ASP.NET建立的pipe里分块读取数据。Chris Hynes为我们提供了这样的一个方案(用HttpModule),该方案除了允许你上传大文件外,还能实时
显示上传进度。
关于这个方案的追详细讨论是在微软的Asp.net论坛上看到的,众多高手讨论,最后得出结果。这个13页的帖子的地址为:
http://forums.asp.net/1/1338742/ShowThread.aspx
其他有参考价值的资料有:
1。Wu.Country@侠缘 写的asp.net上传组件,开源的,有详细代码,也是这种方案,http://wucountry.cnblogs.com/ Wu.Country@侠缘写
的文章 “SunriseUpload.0.9.1的源码分析(七)” 也是关于原理的。
2。宝玉 (http://webuc.net/dotey/)写的asp上传文件组件源代码,也看了他的几篇文章,也是文件上传方面的,抱歉忘记网,自己上网找
应该可以找到的。
3。因为这个采用的是写自定义httpModule来做到到的,所以还看了以下学习httpModule 的资料:
ASP_NET中的Http Handles - [橫渡] - CSDNBlog
HttpModule和HttpHandler(续) - 武子的专栏 - CSDNBlog
MSDN Library > .NET Development > ASP.NET > Building ASP.NET Applications > ASP.NET Web Applications > Extending ASP.NET
Processing with HTTP Modules > How to: Create Custom HTTP Modules
了解ASP_NET底层架构 - 枫的专栏 - CSDNBlog
4 。因为上传文件是通过http协议,所以看还要看“RFC 1867 (rfc1867) - Form-based File Upload in HTML”,上传文件的协议就是在这个
文档上描述的。
我自己写的上传组件实现代码。通过写自定义的httpmodule,调用GetPreloadedEntityBody 和
ReadEntityBody方法截获文件上传数据,自己处理content,把文件写入硬盘。仅仅是学习,没有实现进度条,采用vb.net,在Asp.net2.0 windows xp上测试通过,本机测试是上传文件速率只有6.3M/s,比较低效率。懒得写原理说明了,我的注释写的很详细,因为我也是学着玩的。和前人的代码没有多大分别,我增加了一点,可以避免上传大文件时的超时错误,是通过.Net反射来做到的,还有就是以前的代码有上传文件太大的错误,也是通过反射来避免。
以下就是代码:
////////////自定义HttpModule////////////////
Imports System
Imports System.Text
Imports System.Web
Imports System.Reflection '使用了反射
Public Class widebrightHttpModule
Implements IHttpModule
Private parser As ContentBlockParser = New ContentBlockParser
Public Sub Dispose() Implements System.Web.IHttpModule.Dispose
End Sub
Public Sub Init(ByVal application As System.Web.HttpApplication) Implements System.Web.IHttpModule.Init
AddHandler application.BeginRequest, _
AddressOf Me.WB_BeginRequest
AddHandler application.EndRequest, _
AddressOf Me.WB_EndRequest
AddHandler application.Error, AddressOf Me.WB_RequestError
End Sub
Private Sub WB_BeginRequest(ByVal source As Object, ByVal e As EventArgs)
' Create HttpApplication and HttpContext objects to access
' request and response properties.
Dim application As HttpApplication = CType(source, HttpApplication)
Dim context As HttpContext = application.Context
context.Response.Write _
("<h1><font color=red>HelloWorldModule: " & "Beginning of Request</font></h1><hr>")
Dim work_request As HttpWorkerRequest = GetWorkerRequest()
'Try
'content_type获得数据是这样的: multipart/form-data; boundary=---------------------------7d6242291005ae
Dim content_type As String = application.Request.ContentType.ToLower()
If (Not content_type.StartsWith("multipart/form-data")) Then Return '只处理传送文件的request
If (Not work_request.HasEntityBody()) Then Return '只处理 包含 body 数据 的request
'可能要缓存所有的数据才能获取Request.Files.Count,所以这时数据全部在GetPreloadedEntityBody()处得到。不能用这个!!!
!
'查了资料,如果你访问了HttpRequest object,的任何属性,那么这个request的所有内容读要被预先读入内存,包括大量的文件内容
数据
'所以千万不要访问application.Request的任何属性。否则将在GetPreloadedEntityBody()一下得到全部的数据
' If application.Request.Files.Count = 0 Then Return 不能用这个!!!!
'获取边界标志
Dim strboundary As String = "\r\n--" + content_type.Substring(content_type.IndexOf("boundary=") + 9)
'边界字节标志,可用于区分客户端发送过来的request数据中各种不同content,边界标志。
Dim boundaryData As Byte() = Encoding.UTF8.GetBytes(strboundary)
'总个请求的数据长度,包括各种content和文件内容数据, 尝试上传一个2。8G的文件,结果这里得到一个负数,不知道是否http协议
不支持这么大的 文件
'web。config 中定义maxRequestLength的解释 The value for the property 'maxRequestLength' is not valid. The error is:
The value must be inside the range 0-2097151.
Dim request_total_length As Long = Convert.ToInt64(work_request.GetKnownRequestHeader
(HttpWorkerRequest.HeaderContentLength))
'保存当前的request请求内容数据已经处理了多少字节。
Dim finished_length As Long = 0
Dim max_size As Long = 524288000 '定义允许的最大的上传文件字节长度,这里默认为 500M
'仅仅在这里设置允许的文件大小,没有在web。config 文件中设置
'***********************************
'<httpRuntime maxRequestLength="809600"
' useFullyQualifiedRedirectUrl = "true"
' executionTimeout="600" />
'*********************************************
'则文件可以上传保存, 但最后还是出现 Maximum request length exceeded.的错误
'下面使用 respone。redirect或者使用反射修改request._contentlength 属性后之后就没有此问题了。
If (request_total_length > max_size) Then Return
Dim preloaded_entity_body As Byte() = work_request.GetPreloadedEntityBody()
If (Not preloaded_entity_body Is Nothing) Then
' current_point = current_point + preloaded_entity_body.Length
Dim dddddddd As String = System.Text.Encoding.ASCII.GetChars(preloaded_entity_body)
'PreloadedEntityBody也是属于整个request contents的一部分,所以这里也要设置 current_position,表示已经处理了多少字
节了。
finished_length = finished_length + preloaded_entity_body.Length
'某些时候content filename= 标志会在这里得到,所以还要处理这些数据
parser.parse_bytes(preloaded_entity_body, preloaded_entity_body.Length)
End If
'是否所有的数据都在上面的GetPreloadedEntityBody()中得到,是就没必要做下一步的处理的了
If (Not work_request.IsEntireEntityBodyIsPreloaded()) Then
'最常见的是ReadEntityBody返回8192 个字节长度的数据,所以定义太大是没有什么必要的。
Const BUFFER_LENGTH As Integer = 10240
Dim buffer(BUFFER_LENGTH) As Byte
Dim used_length As Integer = 0
While (request_total_length - finished_length > BUFFER_LENGTH)
used_length = work_request.ReadEntityBody(buffer, BUFFER_LENGTH)
parser.parse_bytes(buffer, used_length)
finished_length = finished_length + used_length
'可以正确解释中文文件名,有人说网络上中文编码都是用的UTF8
'实际调试发现使用System.Text.Encoding.UTF8来解码content header是正确得
' Dim dddddd As String = System.Text.Encoding.UTF8.GetChars(buffer) ’这一句是调试的时候查看代码的,比较
影响性能,注释掉后传送速度由 3.3m/s 升到5.8m/s。
'上传大文件时避免超时错误的解决方法*******************************
'查看资料知道编程设置application.Server.ScriptTimeout 和 在web.config文件中 设置
' executionTimeout 有一样的作用。
'****************************
' <httpRuntime maxRequestLength="809600"
'useFullyQualifiedRedirectUrl = "true"
' executionTimeout="2" />
' <!--
'只有当 compilation 元素中的调试属性为 False 时,此超时属性才适用,这时要使用Release模式编译, 若要帮助避免在
调试期间关闭应用程序,请不要将此超时属性设置为较大值
' -->
'********************************
'使用Reflector查看 发现HttpServerUtility 类型(以上application.Server的类型)的.ScriptTimeout属性代码如下:
' public int ScriptTimeout
'{
' get
' {
' if (this._context != null)
' {
' return Convert.ToInt32(this._context.Timeout.TotalSeconds, CultureInfo.InvariantCulture);
' }
' return 110;
' }
' [AspNetHostingPermission(SecurityAction.Demand, Level=AspNetHostingPermissionLevel.Medium)]
' set
' {
' if (this._context == null)
' {
' throw new HttpException(SR.GetString("Server_not_available"));
' }
' If (value <= 0) Then
' {
' throw new ArgumentOutOfRangeException("value");
' }
' this._context.Timeout = new TimeSpan(0, 0, value);
' }
'}
' 实际就是设置_context.Timeout 。
' 再一次使用Reflector查看(发现HttpContext的有一个方法为)
' internal void SetStartTime()
'{
' this._timeoutStartTime = DateTime.UtcNow; 把超时时间设置为当前时间
'}
'怀疑和超时有关,通过以下的反射代码,调用此方法,果然可以做到传送时不超时。在 IIS5。1 和》net 2。0 下测试通过
Dim ddflags As BindingFlags = (BindingFlags.NonPublic Or BindingFlags.Instance)
Dim hct As Type = context.GetType
hct.GetMethod("SetStartTime", ddflags).Invoke(context, Nothing) '重置 context当前时间,避免超时
End While
'保证最后一个buffer没有超过已知的数据长度,这样就不用给ReadEntityBody发送更多的不需要得填充请求,
'避免ReadEntityBody等待更多得数据到来,来填充我们提供得buffer,而这时数据已经获取完毕了。
'这样就可以避免超时等待,尽快得退出循环。
Dim left_size As Integer = request_total_length - finished_length
Array.Resize(buffer, left_size)
used_length = work_request.ReadEntityBody(buffer, left_size)
Dim dddddddd As String = System.Text.Encoding.UTF8.GetChars(buffer)
parser.parse_bytes(buffer, used_length)
'work_request.SendStatus(200, "ok")
'work_request.EndOfRequest()
'Response.End() 调用这个可以结束当前请求并停止触发在HTTP管道中后续的事件,然后发生将控制返回到Web服务器中,
EndRequest事件并不会被触发
End If
'通过下面这个几个语句检测到,在调试的时候这里是没有数据的 , ReadEntityBody读到的数据没有被放到preload那块内存
'但下面的 GetRequestData打印出来的却是包含整个文件的内容,确实是 ReadEntityBody中包含的数据也被缓存了。
'preloaded_entity_body = work_request.GetPreloadedEntityBody()
'If (Not preloaded_entity_body Is Nothing) Then
' Dim dddddddd As String = System.Text.Encoding.ASCII.GetChars(preloaded_entity_body)
'End If
'加上这几句后在IIS 5。1版+IE7中直接运行的时候,上传文件借宿后就没有超时等待了,但如果文件太大,错误依然存在。
'但在调试的时候有问题,AddContentBytesToRequest函数getfield那里有问题。
'估计是IIS版本不同
Dim contents_without_file_data As Byte() = parser.Get_All_Contents()
parser.Reset_Parser() 'Get_All_Contents 之后再通知parser准备处理下一个页面请求,否则可能Get_All_Contents得不到数据
Dim ddddd As String = System.Text.Encoding.UTF8.GetChars(contents_without_file_data)
GetRequestData(context, work_request)
'这时打印时候, 好像数据都还在缓存里, 全部preload了, 怀疑所有ReadEntityBody读到的数据, 也被放到preload那块内存了
AddContentBytesToRequest(work_request, contents_without_file_data) '提交content修改到IIS
GetRequestData(context, work_request) '在页面上打印request信息,调试的时候使用,看
AddContentBytesToRequest是否起作用了。
'***** 避免上传结束后 文件太大错误的 解决方法一*******************************
'把respone重新引导到一个地址,重新加载一下网页,还是打开上传文件页面,就不会出现上传保存文件结束后的,文件太大提示了
。
'这个可以避免
'[HttpException (0x80004005): Maximum request length exceeded.]
'System.Web.HttpRequest.GetEntireRawContent(+3226541)
' 错误,还没有找到其他的解决办法。
' context.Response.Redirect("default.aspx") '重定向可以避免错误页面
'*******************************************************************************
'*****避免上传结束后 文件太大错误的 解决方法二*******************************
'经过使用Reflector查看GetEntireRawContent,发现这个从错误 和Request.ContentLength 有关,
'而ContentLength被设置 为等于._wr.GetKnownRequestHeader(11),这个值是请求的总长度,所以最后比较时会发生文件太大错误,
''Dim ddd As Integer = context.Request.ContentLength '和httprequest的 属性 _contentLength 有关,考虑使用
反射修改
Dim flags As BindingFlags = (BindingFlags.NonPublic Or BindingFlags.Instance)
Dim hr As Type = context.Request.GetType
hr.GetField("_contentLength", flags).SetValue(context.Request, contents_without_file_data.Length) '修改这个也可以
避免错误页面
'*****************************************************************************
'Catch ex As Exception
'End Try
End Sub
'这里EndRequest不用写什么东西都可以
Private Sub WB_EndRequest(ByVal source As Object, ByVal e As EventArgs)
Dim application As HttpApplication = CType(source, HttpApplication)
Dim context As HttpContext = application.Context
context.Response.Write _
("<hr><h1><font color=red>HelloWorldModule: " & "End of Request</font></h1>")
End Sub
Private Sub WB_RequestError(ByVal source As Object, ByVal e As EventArgs)
End Sub
Private Function GetWorkerRequest() As HttpWorkerRequest
Dim provider As IServiceProvider = HttpContext.Current
Return CType(provider.GetService(GetType(HttpWorkerRequest)), HttpWorkerRequest)
End Function
Private Function AddContentBytesToRequest(ByVal wkr As HttpWorkerRequest, ByVal textData As Byte()) As Boolean
Dim type_unknown As Type
Dim flags As BindingFlags = (BindingFlags.NonPublic Or BindingFlags.Instance)
Dim ddd As String = HttpContext.Current.Request.ServerVariables("SERVER_SOFTWARE")
' Is there application host IIS6.0?
If (HttpContext.Current.Request.ServerVariables("SERVER_SOFTWARE").Equals("Microsoft-IIS/6.0")) Then
type_unknown = wkr.GetType().BaseType.BaseType
ElseIf (HttpContext.Current.Request.ServerVariables("SERVER_SOFTWARE").Equals("Microsoft-IIS/5.1")) Then
' 在IIS5.1中获取的是 BaseType : System.Web.Hosting.ISAPIWorkerRequest
'System.Web.Hosting.ISAPIWorkerRequest类是HttpWorkerRequest类的一个抽象子类(译注:HttpWorkerRequest和
ISAPIWorkerRequest都是抽象类, 并且ISAPIWorkerRequest继承自HttpWorkerRequest),
type_unknown = wkr.GetType().BaseType
Else '直接在VS2005的调试器中调试运行时,SERVER_SOFTWARE 变量为空,而不是上面的两个值
'在VS2005的调试调试器中调试运行时,下面一句获取的是HttpWorkerRequest类
type_unknown = wkr.GetType().BaseType.BaseType
End If
'The difference that the second line returns System.Web.Hosting.ISAPIWorkerRequest
'and the first System.Web.Hosting.ISAPIWorkerRequestInProcForIIS6. Unfortunately there's no documentation
'on these classes. I can't even use them for casting, comparing (IS operator) or whatever.
'Set values of working request
type_unknown.GetField("_contentAvailLength", flags).SetValue(wkr, textData.Length)
type_unknown.GetField("_contentTotalLength", flags).SetValue(wkr, textData.Length)
type_unknown.GetField("_preloadedContent", flags).SetValue(wkr, textData)
type_unknown.GetField("_preloadedContentRead", flags).SetValue(wkr, True)
Return True
End Function
'打印 request的各项属性,主要是content 内容长度等各项数据,便于调试
Private Sub GetRequestData(ByVal ctx As HttpContext, ByVal hwr As HttpWorkerRequest)
Dim bindingFlags As BindingFlags = bindingFlags.Instance Or bindingFlags.NonPublic
Dim type_unknown As Type = hwr.GetType().BaseType
ctx.Response.Write("SERVER_SOFTWARE: " + HttpContext.Current.Request.ServerVariables("SERVER_SOFTWARE") + "<br>")
ctx.Response.Write("BaseType : " + type_unknown.ToString() + "<br>")
ctx.Response.Write("ContentAvailLength : " + type_unknown.GetField("_contentAvailLength", bindingFlags).GetValue
(hwr).ToString() + "<br>")
ctx.Response.Write("ContentTotalLength : " + type_unknown.GetField("_contentTotalLength", bindingFlags).GetValue
(hwr).ToString() + "<br>")
ctx.Response.Write("PreloadedContentRead : " + type_unknown.GetField("_preloadedContentRead", bindingFlags).GetValue
(hwr).ToString() + "<br>")
ctx.Response.Write("ContentType : " + type_unknown.GetField("_contentType", bindingFlags).GetValue(hwr).ToString() +
"<br>")
ctx.Response.Write("ContentLengthSent : " + type_unknown.GetField("_contentLengthSent", bindingFlags).GetValue
(hwr).ToString() + "<br>")
ctx.Response.Write("PreloadedContent : <br>" + Text.Encoding.UTF8.GetString(type_unknown.GetField
("_preloadedContent", bindingFlags).GetValue(hwr)))
End Sub
End Class
'private void ClearRequestData()
'{
'BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic;
'Type type = hwr.GetType().BaseType;
'type.GetField("_contentAvailLength", bindingFlags).SetValue(hwr, 0);
'type.GetField("_contentTotalLength", bindingFlags).SetValue(hwr, 0);
'type.GetField("_preloadedContent", bindingFlags).SetValue(hwr, null);
'type.GetField("_preloadedContentRead", bindingFlags).SetValue(hwr, true);
'//type.GetField("_contentType", bindingFlags).SetValue(hwr, 0);
'type.GetField("_contentLengthSent", bindingFlags).SetValue(hwr, true);
'}
'private void SetRequestData(byte[] Data)
'{
'BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic;
'Type type = hwr.GetType().BaseType;
'type.GetField("_contentAvailLength", bindingFlags).SetValue(hwr, Data.Length);
'type.GetField("_contentTotalLength", bindingFlags).SetValue(hwr, Data.Length);
'type.GetField("_preloadedContent", bindingFlags).SetValue(hwr, Data);
'type.GetField("_preloadedContentRead", bindingFlags).SetValue(hwr, true);
'}
'private void GetRequestData()
'{
'BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic;
'Type type = hwr.GetType().BaseType;
'ctx.Response.Write("BaseType : " + type.ToString() + "<br>");
'ctx.Response.Write("ContentAvailLength : " + type.GetField("_contentAvailLength", bindingFlags).GetValue(hwr).ToString() +
"<br>");
'ctx.Response.Write("ContentTotalLength : " + type.GetField("_contentTotalLength", bindingFlags).GetValue(hwr).ToString() +
"<br>");
'ctx.Response.Write("PreloadedContentRead : " + type.GetField("_preloadedContentRead", bindingFlags).GetValue(hwr).ToString()
+ "<br>");
'ctx.Response.Write("ContentType : " + type.GetField("_contentType", bindingFlags).GetValue(hwr).ToString() + "<br>");
'ctx.Response.Write("ContentLengthSent : " + type.GetField("_contentLengthSent", bindingFlags).GetValue(hwr).ToString() +
"<br>");
'ctx.Response.Write("PreloadedContent : <br>" + DecodeToString((byte[])type.GetField("_preloadedContent",
bindingFlags).GetValue(hwr)));
'}
'private void InjectTextParts(HttpWorkerRequest request, byte[] textParts)
' {
' BindingFlags flags1 = BindingFlags.NonPublic | BindingFlags.Instance;
' Type type1 = request.GetType();
' while ((type1 != null) && (type1.FullName != "System.Web.Hosting.ISAPIWorkerRequest"))
' {
' type1 = type1.BaseType;
' }
' if (type1 != null)
' {
' type1.GetField("_contentAvailLength", flags1).SetValue(request, textParts.Length);
' type1.GetField("_contentTotalLength", flags1).SetValue(request, textParts.Length);
' type1.GetField("_preloadedContent", flags1).SetValue(request, textParts);
' type1.GetField("_preloadedContentRead", flags1).SetValue(request, true);
' }
' }
未完 待续