来源:http://www.cnblogs.com/bashan
以前开发过一个用Asp.NET上传大文件的代码,由于是在NET1.1下开发的,发现不能在NET.20下运行,前几天将这个问题解决了,现在把整个开发过程整理一下,供大家参考。由于内容较多,所以我打算分成六篇来写,第一篇写一下需要解决的问题和大致解决思路,再用四篇具体写一下开发中需要解决的四个主要问题,最后再写一个组件的使用方法。
大家可以点击这里下载NET1.1的源代码。
- 1、 问题提出:
使用Asp.NET在上传文件时,IIS接收到请求内容后,发送到Asp.NET的管道中,然后Asp.NET的ISAPI将读取的内容是全部装入服务器内存(NET2.0在请求内容较大时,会存放到临时文件中,见后面的开发说明),在上传文件时,消耗服务器资源较多,所以Asp.NET对上传文件的大小会有限制,且不能提供上传进度显示。而我们在实际应用中,往往会上传较大的文件的现象,这时,可以通过修改配置文件将最大将求长度配置设置得很大,但这样,服务器将会消耗大量内存。
为此,如果可以在IIS将客户端发送的内容发送到Asp.NET的管道时,随着服务器接收内容的同时,将文件内容写到服务器磁盘中,然后在Asp.NET的管道中只放入请求的对应的Form内容。这样可以避开Asp.NET上传文件的大小限制,同时提供服务器接收进度显示。
- 2、 解决方案分析:
Asp.NET提供了HttpModule功能,提供了让用户处理客户请求的手段,要实现HttpModule功能,用户只需实现System.Web.IHttpModule接口。System.Web.IHttpModule定义了两个方法void Init (HttpApplication context)和void Dispose (),Init方法给我们提供了一个类型为HttpApplication的参数,这个HttpApplication类型中,给我们提供了处理Asp.NET页面生命周期中发生的各种事件(具体请参照MSDN),我们只需要在最早的事件BeginRequest中,处理数据接收,就可以避开Asp.NET的最大请求长度限制。
为了不改变Asp.NET对页面处理逻辑,我们需要在提供文件内容后,将其它请求内容正确封装给Asp.NET,为此我们需要分析客户端发送到服务器端内容的具体格式,我们可以通过其它工具,提供这些内容,这些内容大致如下:
-----------------------------7d81e441d025c
Content-Disposition: form-data; name="UploadID"
de858d87-e8b8-4f7a-a832-357da2efdf13
-----------------------------7d81e441d025c
Content-Disposition: form-data; name="__VIEWSTATE"
/wEPDwULLTIwODE1OTkzNTUPZBYCAgMPFgIeB2VuY3R5cGUFE211bHRpcGFydC9mb3JtLWRhdGFkZAQ0pcd9kiSZ7/MOe0enKOTGFxMk
-----------------------------7d81e441d025c
Content-Disposition: form-data; name="TextBox1"
-----------------------------7d81e441d025c
Content-Disposition: form-data; name="TextBox2"
-----------------------------7d81e441d025c
Content-Disposition: form-data; name="file1"; filename="C:/Documents and Settings/Administrator/??????/componentart.web.ui.rar"
Content-Type: application/x-rar-compressed
Rar! ??s这里我把文件具体内容去掉了,我们提取文件后,封送给Asp.NET也就是去掉这部分后的内容。
-----------------------------7d81e441d025c
Content-Disposition: form-data; name="Button1"
开始上传
-----------------------------7d81e441d025c
Content-Disposition: form-data; name="__EVENTVALIDATION"
/wEWBAL1w7VGAuzRsusGAuzR9tkMAoznisYGMJIxZyapejeOQaIIwcOkPrWK6nE=
-----------------------------7d81e441d025c--
请注意红色注释部分,那里原来是上传的文件内容,我们封送给Asp.NET就应当时去掉文件内容后内容,这样,页面控件的值、页面事件就会正确的发送到Asp.NET页面。只是页面不能正确获取上传文件的内容。
这里又出现了三个问题:一是:我们如何获取文件内容,如何确定那部分内容是上传的文件内容而不是页面其它控件内容;二是:我们如何在提取文件内容后让Asp.NET页面正确获取文件;三是:我们如何将改写后的内容封送给Asp.NET,让它感觉不到我们提取了文件内容。
只要解决了上面三个问题,那大文件上传的问题就解决了,最后就是提供进度显示,由于内容较多,我分四部分来介绍这三个问题和进度显示。
Asp.NET大文件上传组件开发总结(二)---提取文件内容
不知地震什么时候结束,为了给老婆小孩守夜,看来还不能睡,那就把第二篇也写了吧,只是不知对大家有没有用哟。
为了提供文件内容,我们需要首先需确定客户请求中发送的有文件内容,然后确定文件内容的位置。这部分对应的代码如下:
HttpWorkerRequest request = GetWorkerRequest(app.Context);
if ( ! IsUploadRequest(app.Request)) return ; // 不是文件上传,则退出
string sContentType = app.Request.ContentType.ToLower();
byte [] arrBoundary = GetMultipartBoundary(sContentType);
int ContentLength = app.Request.ContentLength; // 信息体的总长度
DataReader dataReader = new DataReader(app.Context.Request.ContentEncoding, arrBoundary);
DateTime startDate = DateTime.Now;
byte [] arrBuffer = request.GetPreloadedEntityBody();
if (arrBuffer == null )
{
arrBuffer = new Byte[ 0 ];
tempFile.Close();
return ; // 没有读取到信息体
}
else
{
// 这里是对文件内容的处理
}
要确定文件的内容位置,为此我们需要使用Reflector工具反编译System.Web.dll中HttpRequest的代码,可以发现有一个GetEntireRawContent方法,这个方法里,调用了HttpWorkerRequest对的GetPreloadedEntityBody方法来获取数据,可喜的是,这个方法是Public的,所以我们也可以直接调此方法来获取客户端发送的数据。
获取数据后,我们可以将这些数据写入到一个文件中,这些数据是byte[]类型的,在写入文件时,因为Http协议是基于文本的,所以我们可以采用System.Text.Encoding.GetString方法,将这些字节数组编码成字符串,我这里的编码采用ASCII,只是这样请求中的中文就成了乱码。这样就可以获取请求的内容的文体格式,以方便我们分析(在代码中,这个写文件的功能我已去除,因为这里写全部请求内容只是用于分析使用,请参见第一篇文章中我列出的请求内容示例)。
通过分析这些请求内容,我们可以发现:每个页面控件,均可以在这里找到对应的内容,两个控件内容间是用字符串“-----------------------------7d81e441d025c”分隔的。同时在我们反编译System.Web.dll中HttpRequest的代码时,也可以发有GetMultipartBoundary这个方法,这个方法我们可以从字面意思得知是取分隔标识。我们把这个方法提取出来:
{
string attributeFromHeader = GetAttributeFromHeader( this .ContentType, " boundary " );
if (attributeFromHeader == null )
{
return null ;
}
attributeFromHeader = " -- " + attributeFromHeader;
return Encoding.ASCII.GetBytes(attributeFromHeader.ToCharArray());
}
最后,我们分析文件上传控件的内容:
Content - Type: application / x - rar - compressed
Rar ! ?? s
这里,文件内容的提取的问题就可以解决了。在提取文件内容时,对处理标志字符串时需要特别处理,防止标志字符分别读取到两个缓冲区中,所以这部分代码还是比较多的,具体请参看源代码中的DataReader类。
Asp.NET大文件上传组件开发总结(三)---处理文件内容
这一篇相对就要简单一些了,只需要确定对文件的处理思路和与Asp.NET页面信息的传递的问题。
.NET1.1中,Asp.NET将获取的请求内容放入到一个Byte[]类型中,但在Asp.NET2.0中,却将数据封装到一个类型为System.Web.HttpRawUploadedContent的对象中,在这个新对象中,有一个重要的属性:_file,这个属性的类型为TempFile。正好是这个类型提供了将请求内容保存到临时文件的功能。这些信息都可能通过Reflector工具反编译获得(所以啊,做.NET开发,这个工具是必备的)。
在HttpRequest类中,我们可以发现如下用于分析页面各控件值的代码:
{
if ( this ._multipartContentElements == null )
{
byte [] multipartBoundary = this .GetMultipartBoundary();
if (multipartBoundary == null )
{
return new MultipartContentElement[ 0 ];
}
HttpRawUploadedContent entireRawContent = this .GetEntireRawContent();
if (entireRawContent == null )
{
return new MultipartContentElement[ 0 ];
}
this ._multipartContentElements = HttpMultipartContentTemplateParser.Parse(entireRawContent, entireRawContent.Length, multipartBoundary, this .ContentEncoding);
}
return this ._multipartContentElements;
}
说了这么多,只是说明了一个问题:通过改变Asp.NET页面对象的属性值来向页面传送是比较麻烦的。所以我采取单件模式的设计思路。具体是,将获取的文件内容写入到磁盘文件;每次上传文件过程中,初始化一个会话标志,页面通过这个会话标志来提取这些文件信息。在系统中,定义了两个类用来处理这项工作:
类UploadContext用于封装文件信息。
类UploadContextFactory用于会话标志的管理。
在上传页面里,组件将在页面中注册一个名为UploadID的Hidden字段。存放一个GUID作为会话标识。这个标志会在文件上传时,出现在请求内容中,组件通过获取这个会话标志和文件内容,正确将文件保存到临时目录中,并将提取文件信息供页面使用。页面则根据Request["UploadID"]字段在类UploadContextFactory提取上传文件对应的会话标志,并获取文件信息。这样就实现了页面对文件内容的提取。
这样,要求在上传页面的Page_Load事件中,采用如下语句来初始化一个会话,并指定存放文件的目录。
if ((context != null ) && (context.Status == uploadStatus.Complete))
... {
context.SaveFile(file1.ClientID, Request.MapPath("/myupload/"));
}
当然这里就存在一个问题是:会话标志过期及没有初始化标志上传文件时的处理(如不请求页面,而直接发送文件,这可能会在用户使用其它工具发送时出现)。这个问题暂时没有处理。
现在,上传的文件可以上传到服务器了,页面也可以获取上传的文件内容以作进一步处理,如限制文件类型、存放到数据库等。
感觉思路有点乱了,那就先写出来,我随后再来整理。
Asp.NET大文件上传组件开发总结(四)---封送数据给Asp.NET页面
这个功能主要是为了不影响Asp.NET的处理模型,将请求中除上传的文件内容外的其它正常请求内容继续发送到Asp.NET页面处理。
我们通过分析HttpRequest对象的GetEntireRawContent()方法在读取数据,并且发现这个方法在读取数据时,是将数据赋值到了_rawContent属性,所以我们只要能把处理后的数据赋值给HttpRequest对象的_rawContent属性,即可封送数据给Asp.NET页面。同时,由于我们改写了请求内容,所以HttpRequest的_contentLength也应当改写。
由于这些属性方式是私有的,我们不能直接访问,所以我们必须采用反射的方法给属性赋值。代码如下:在这里,在将代码用到NET2.0时,遇到了问题。因为在NET1.1时,HttpRequest的_rawContent属性是一个byte[]类型,但到了NET2.0,这个属性变成了HttpRawUploadedContent类型的对象,出现了赋值失败。通过查看HttpRawUploadedContent反射代码,发现原来这个类是为了将过大的请求内容写到磁盘文件中。没办法,我只有创建这个类的实例后再赋值给HttpRequest的_rawContent属性。这下能上传文件了,可是,Asp.NET页面的控件值全部丢失了,数据没有有效的封送到页面。嘿嘿,这里正好发挥我刚学会的一项技术---调试框架源码。于是从GetEntireRawContent()方法一路跟踪下去,发现是由于HttpRawUploadedContent对象的_length属性为零,导致HttpRequest对象认为没有有效数据而没有分析数据。导致通过HttpRequest的Params属性和Forms数据不能访问到请求内容。给这两个数据赋值后,哈!哈!一切顺利。上传过程全部结束!
由于NET2.0代码只是初步完成,等我在作进一步测试后,将在写完这部分总结后,提供给大家下载。
{
BindingFlags flags1 = BindingFlags.NonPublic | BindingFlags.Instance;
Type type1 = request.GetType();
FieldInfo info1 = type1.GetField( " _rawContent " , flags1);
FieldInfo info2 = type1.GetField( " _contentLength " , flags1);
if ((info1 != null ) && (info2 != null ))
{
Assembly web = Assembly.GetAssembly( typeof (HttpRequest));
Type hraw = web.GetType( " System.Web.HttpRawUploadedContent " );
object [] argList = new object [ 2 ];
argList[ 0 ] = textParts.Length + 1024 ;
argList[ 1 ] = textParts.Length;
CultureInfo currCulture = CultureInfo.CurrentCulture;
object httpRawUploadedContent = Activator.CreateInstance(hraw,
BindingFlags.NonPublic | BindingFlags.Instance,
null ,
argList,
currCulture,
null );
Type contentType = httpRawUploadedContent.GetType();
FieldInfo dataField = contentType.GetField( " _data " , flags1);
dataField.SetValue(httpRawUploadedContent, textParts);
FieldInfo lengthField = contentType.GetField( " _length " , flags1);
lengthField.SetValue(httpRawUploadedContent, textParts.Length);
FieldInfo fileThresholdField = contentType.GetField( " _fileThreshold " , flags1);
fileThresholdField.SetValue(httpRawUploadedContent, textParts.Length + 1024 );
info1.SetValue(request, httpRawUploadedContent);
info2.SetValue(request, textParts.Length);
}
}