Asp.NET大文件上传开发总结

来源: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大文件上传组件开发总结(二)---提取文件内容

不知地震什么时候结束,为了给老婆小孩守夜,看来还不能睡,那就把第二篇也写了吧,只是不知对大家有没有用哟。

为了提供文件内容,我们需要首先需确定客户请求中发送的有文件内容,然后确定文件内容的位置。这部分对应的代码如下:

  HttpApplication app  =  sender  as  HttpApplication;
        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
        {
            
//  这里是对文件内容的处理
        }
上面的代码中,我们先获取Asp.NET对客户端请求的处理对象HttpWorkerRequest,然后根据这个对象的ContentType属性是否为multipart/form-data来确定对应的请求是否有上传文件,如果没有上传文件,就不处理此请求,以提高处理效率。这样处理的依据是,在有文件上传的HTML Form中,对应的enctype属性为multipart/form-data。这样就解决了确定客户端请求中是否发送有文件内容。
要确定文件的内容位置,为此我们需要使用Reflector工具反编译System.Web.dll中HttpRequest的代码,可以发现有一个GetEntireRawContent方法,这个方法里,调用了HttpWorkerRequest对的GetPreloadedEntityBody方法来获取数据,可喜的是,这个方法是Public的,所以我们也可以直接调此方法来获取客户端发送的数据。
获取数据后,我们可以将这些数据写入到一个文件中,这些数据是byte[]类型的,在写入文件时,因为Http协议是基于文本的,所以我们可以采用System.Text.Encoding.GetString方法,将这些字节数组编码成字符串,我这里的编码采用ASCII,只是这样请求中的中文就成了乱码。这样就可以获取请求的内容的文体格式,以方便我们分析(在代码中,这个写文件的功能我已去除,因为这里写全部请求内容只是用于分析使用,请参见第一篇文章中我列出的请求内容示例)。
通过分析这些请求内容,我们可以发现:每个页面控件,均可以在这里找到对应的内容,两个控件内容间是用字符串“-----------------------------7d81e441d025c”分隔的。同时在我们反编译System.Web.dll中HttpRequest的代码时,也可以发有GetMultipartBoundary这个方法,这个方法我们可以从字面意思得知是取分隔标识。我们把这个方法提取出来:
  private   byte [] GetMultipartBoundary()
    {
        
string  attributeFromHeader  =  GetAttributeFromHeader( this .ContentType,  " boundary " );
        
if  (attributeFromHeader  ==   null )
        {
            
return   null ;
        }
        attributeFromHeader 
=   " -- "   +  attributeFromHeader;
        
return  Encoding.ASCII.GetBytes(attributeFromHeader.ToCharArray());
    }
然后我们采用这个方法,就可以从请求的内容中提取出类似“-----------------------------7d81e441d025c”这样的字符串。这样,我们就可以分隔页面内不同控件的内容了。
最后,我们分析文件上传控件的内容:
Content - Disposition: form - data; name = " file1 " ; filename = " C:Documents and SettingsAdministrator??????componentart.web.ui.rar "
Content
- Type: application / x - rar - compressed

Rar
!    ?? s
我们可以发现,文件上传控件有一个filename属性,然后,下一行指定文件的MIME类型,随后这个空白行,在空白行下面,直到下一处类似“-----------------------------7d81e441d025c”标志字符串间,就是客户端发送的文件内容。我们只需把这些内容写入到服务器文件中即可。

这里,文件内容的提取的问题就可以解决了。在提取文件内容时,对处理标志字符串时需要特别处理,防止标志字符分别读取到两个缓冲区中,所以这部分代码还是比较多的,具体请参看源代码中的DataReader类。


Asp.NET大文件上传组件开发总结(三)---处理文件内容

这一篇相对就要简单一些了,只需要确定对文件的处理思路和与Asp.NET页面信息的传递的问题。

        .NET1.1中,Asp.NET将获取的请求内容放入到一个Byte[]类型中,但在Asp.NET2.0中,却将数据封装到一个类型为System.Web.HttpRawUploadedContent的对象中,在这个新对象中,有一个重要的属性:_file,这个属性的类型为TempFile。正好是这个类型提供了将请求内容保存到临时文件的功能。这些信息都可能通过Reflector工具反编译获得(所以啊,做.NET开发,这个工具是必备的)。

        在HttpRequest类中,我们可以发现如下用于分析页面各控件值的代码:

private  MultipartContentElement[] GetMultipartContent()
{
    
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事件中,采用如下语句来初始化一个会话,并指定存放文件的目录。

UploadContext context  =  UploadContextFactory.InitUploadContext( this @" c:myupload " );
在需要获取文件信息的地方:采用如果语句来获取:
 UploadContext context  =  UploadContextFactory.GetUploadContext();
        
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对象认为没有有效数据而没有分析数据。导致通过HttpRequestParams属性和Forms数据不能访问到请求内容。给这两个数据赋值后,哈!哈!一切顺利。上传过程全部结束!
由于NET2.0代码只是初步完成,等我在作进一步测试后,将在写完这部分总结后,提供给大家下载。

private   void  InjectTextParts(HttpRequest request,  byte [] textParts)
    {
        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);
        }
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值