Web 文件传输系列:web文件上传(一)

来源:互联网 发布:excel为防止数据丢失 编辑:程序博客网 时间:2024/06/10 03:04

          基于WEB的文件上传可以使用FTP和HTTP两种协议,用FTP的话虽然传输稳定,但安全性是个严重的问题,而且FTP服务器读用户库获取权限,这样对于用户使用来说还是不太方便。剩下只有HTTP。在HTTP中有3种方式,PUT、 WEBDAV、RFC1867,前2种方法不适合大文件上传,目前我们使用的web上传都是基于 RFC1867标准的HTML中基于表单的文件上传。

  一、先简要介绍一下RFC1867(Form-based File Upload in HTML)标准:
          带有文件提交功能的HTML表单
         现有的HTML规范为INPUT元素的TYPE属性定义了八种可能的值,分别是:CHECKBOX, HIDDEN, IMAGE, PASSWORD, RADIO, RESET, SUBMIT, TEXT. 另外,当表单 采用POST方式的时候,表单默认的具有"application/x-www-form-urlencoded" 的ENCTYPE属性。RFC1867标准对HTML做出了两处修改:

ASP.NET文件上传:

利用HTTP协议上传文件的方式非常有限,一般使用〈input type="file" / 〉标签来进行上传。

这种上传方式会将内容使用“multipart/form-data”进行编码(multipart/form-data规范原文),并将内容POST到服务器端,然后进行处理。“multipart/form-data”相对于默认的“application/x-url-encoded”,在大数据量提交时效率要高很多。使用〈input type="file" /〉标签上传文件最大的好处在于各种服务器技术都对其最好了封装,开发起来能够很直观的对上传的文件进行处理。不过总体来说,这个协议并不适合做文件传输,解析数据流内容的代价相对较高,并且没有一些例如断点续传的机制来辅助,导致在上传大文件时经常会力不从心。

ASP.NET的封装

ASP.NET 1.x 提供了一个HtmlInputFile控件,而2.0却出现了一个新的控件FileUpload,但是它生成的html却仍然是一样的,在页面中是用时需要这样来定义:

[ASP.NET 1.x]

  1. 〈input id="MyFile" type="file" runat="server" /〉  

[ASP.NET 2.0]

  1. <ASP:FILEUPLOAD&NBSP;ID=< SPAN>"FileUpload1" runat="server" />  

ASP.NET大文件上传可以说是低效率的代名词,公平的所,IIS却是它的幕后黑手。当你选择一个文件并且按下提交按钮,IIS需要解析文件的所有内容后,你才能读取上传的文件属性,IIS5.X和IIS6都是这样做的,好消息是IIS7.0将会使用Apache的方式。除非你现在就换到7.0,否则你只有长时间等待直到上传完毕,其余一无他法。你也不可以显示进度条因为你压根不知道在这段时间里到底上传了多少。

使用过FileUpload控件的都知道,它真是一把双刃剑——既可能成为我们的救世主(a savior),也能是我们的敌人(an enemy ),其中一个很常见的问题就是如何处理超过4MB的大文件上传。不过我们应该了解的是,之所以默认的文件大小上限为4MB,并不是因为设计人员想当然,而是为了避免潜在DOS攻击危险。

为了避免这个限制,需要修改配置文件的httpRuntime节的maxRequestLength属性,文件越大,处理的时间也就越长,所以意味着通常你都要修改executionTimeout属性,这个属性1.X默认是90s,2.0默认是110s,大家可以看下machine.config文件,还有个shutdownTimeout属性,不过我不知道它的用处。

IIS流程

当ASP.NET大文件上传到服务器上,不管你的maxRequestLength设置得多大,IIS都会提取完文件,然后ASP.NET根据system.web/httpRuntime节来判断大小,超过了就会抛出一个异常,这个过程是由HttpRequest的GetEntireRawContent()方法,你可以看到:

  1. HttpRuntimeSection httpRuntime = RuntimeConfig.  
  2. GetConfig(this._context).HttpRuntime;  
  3. int maxRequestLengthBytes = httpRuntime.  
  4. MaxRequestLengthBytes;  
  5. if (this.ContentLength  〉maxRequestLengthBytes)  
  6. {  
  7. if (!(this._wr is IIS7WorkerRequest))  
  8. {  
  9. this.Response.CloseConnectionAfterError();  
  10. }  
  11. throw new HttpException(SR.GetString  
  12. ("Max_request_length_exceeded"), null, 0xbbc);  
  13. }  

理论上说可以使文件不是全部加载,但是能配置IIS让它分块读取文件吗?至少我还不知道!

ASP.NET的弊端

ASP.NET处理文件上传的最大的问题在于内存占用太高,由于将整个文件载入内存进行处理,导致如果用户上传文件太大,或者同时上传的用户太多,会造成服务器端内存耗尽。这个观点其实是片面的,对于早期ASP.NET 1.X,为了供程序处理,会将用户上传的内容完全载入内存,这的确会带来问题,

但在ASP.NET 2.0中就已经会在用户上传数据超过一定数量之后将其存在硬盘中的临时文件中,而这点对于开发人员完全透明,也就是说,开发人员可以像以前一样进行数据流的处理,

这个也在httpRuntime里通过requestLengthDiskThreshold属性来设置阈值(threshold),其默认值为256,即一个请求内容超过256KB时就会启用硬盘作为缓存,这个阈值和客户端是否是在上传内容无关,只关心客户端发来的请求大于这个值。

因此,在ASP.NET 2.0中服务器的内存不会因为客户端的异常请求而耗尽。

另外一个弊端就是当请求超过maxRequestLength(默认4M)之后,ASP.NET处理程序将不会处理该请求。这和ASP.NET抛出一个异常完全不同,这就是为什么如果用户上传文件太大,看到的并不是ASP.NET应用程序中指定的错误页面(或者默认的),因为ASP.NET还没有对这个请求进行处理。

还有一个问题就是处理ASP.NET大文件上传的超时。

这个其实可以通过在运行时读取web.config中的httpRuntime节,并转化为HttpRuntimeSection对象或者重写Page.OnError()来检测HTTP Code(相应代码)是否为400来处理,这里不再赘述,代码如下:

  1. System.Configuration.Configuration   
  2. config = WebConfigurationManager.  
  3. OpenWebConfiguration("~");  
  4. HttpRuntimeSection section = config.GetSection  
  5. ("system.web/httpRuntime"as HttpRuntimeSection;  
  6. double maxFileSize = Math.Round  
  7. (section.MaxRequestLength / 1024.0, 1);  
  8. string errorString = string.Format("Make sure   
  9. your file is under {0:0.#} MB.", maxFileSize);  
  1. protected override void OnError(EventArgs e)  
  2. {  
  3. HttpContext ctx = HttpContext.Current;  
  4. Exception exception = ctx.Server.GetLastError ();  
  5.  
  6. string errorString =   
  7.  "Offending URL: " + ctx.Request.Url.ToString () +  
  8.  "Source: " + exception.Source +   
  9.  "Message: " + exception.Message +  
  10.  "Stack trace: " + exception.StackTrace;  
  11.  
  12. ctx.Response.Write (errorString);  
  13.  
  14. ctx.Server.ClearError ();  
  15.  
  16. base.OnError (e);  
  17. }  

对于文件上传的功能需要较为特别的需求——例如进度条提示,ASP.NET封装的控件〈asp:FileUpload /〉就无能为力了。

好的解决方案

Robert Bazinet建议,最好的解决方案是使用RIA,大多数情况下,建议用Silverlight或Flash的上传组件来替代传统的FileUpload组件,这类组件不只是提供了更好的上传体验,也比〈input type="file"〉标签在页面上的文本框、按钮漂亮,这个〈input type="file"〉标签并不能够通过CSS添加样式,不过也有人尝试去解决了。至今为止并没有什么商业上传组件使用了Silverlight,不过这里有演示了用Silverlight进行多文件上传的示例程序。当然使用Silverlight就可以很轻松的实现多线程上传,断点续传这种功能了,这些都不是我要详细讨论的内容,如果有需要可以自己去看下。

可选择的解决方案

使用〈input type="file" /〉标签所能提供的支持非常有限,一些特殊需求我们不能实现——或者说是无法轻易地、直接地实现。所以为了实现这样的功能我们每次都要绕一个大大的弯。为了避免每次实现相同功能时都要费神费时地走一遍弯路,市面上或者开源界出现了各种上传组件,上传组件提供了封装好的功能,使得我们在实现文件上传功能时变得轻松了很多。例如几乎所有的上传组件都直接或间接地提供了进度提示的功能,有的提供了当前的百分比数值,有的则直接提供了一套UI;有的组件只提供了简单的UI,有的却提供了一整套上传、删除的管理界面。此外,有的组件还提供了防止客户端恶意上传的能力。

我觉得最好的办法是在HttpModule里分块读取文件并且保持页面激活的状态,这样就不会超时,同时也可以跟踪进度或者取消上传,或者通过HttpHandler实现,在通过进度条给用户充分提示的同时,也让开发人员能够更好地控制文件大小以及上传过程中可能出现的异常。上传组件都是用这些办法的,我们的选择有:

  1. FileUploader.NET (MediaChase公司,$310以上)   
  2. RadUpload (Telerik公司,$249)   
  3. NeatUpload (免费,遵守LGPL协议)   
  4. ······  

NeatUpload是在ASP.NET Pipeline的BeginRequest事件中截获当前的HttpWorkerRequest对象,然后直接调用其ReadEntityBody等方法获取客户端传递过来的数据流,并加以分析和处理。并通过使用新的请求进行轮询来获取当前上传的状态。关于NeatUpload和其他开源组件的介绍可以参看JeffreyZhao的在ASP.NET应用程序中上传文件,当然他还说了Memba Velodoc XP Edition和swfupload,写的非常棒!

HttpWorkerRequest实现介绍

利用隐含的HttpWorkerRequest,用它的GetPreloadedEntityBody和ReadEntityBody方法从IIS为ASP.NET建立的pipe里分块读取数据可以实现文件上传。实现方法如下:

  1. IServiceProvider provider=(IServiceProvider)  
  2. HttpContext.Current;  
  3. HttpWorkerRequest wr=(HttpWorkerRequest)  
  4. provider.GetService(typeof(HttpWorkerRequest));  
  5. byte[] bs=wr.GetPreloadedEntityBody();  
  6. if(!wr.IsEntireEntityBodyIsPreloaded())  
  7. {  
  8. int n=1024;  
  9. byte[] bs2=new byte[n];  
  10. while(wr.ReadEntityBody(bs2,n) 〉0)  
  11. {  
  12. }  
  13. }  

结论

ASP.NET大文件上传是一个不完善和有缺陷的领域,相信在不久会得到提高和发展,如果你已经解决了,说明你在一个好公司,否则你可以考虑使用第三方产品来解决了。文件上传的问题,我们都能够找到很多种不同的方法来解决,挑战在于找出不同做法的利弊然后找到一个适用于自己项目的方案,这不仅仅是在文件上传这一个方面!

 

 

 文件还是与网络状态有很大的关系,除非能自己写一个类FTP的组件,或者在服务器端用软件方法处理上传,ASP.NET默认上传为4M 。

改.net的默认配置,

<system.web>
   <httpRuntime  maxRequestLength="51200" executionTimeout="600" />
  </system.web>

这句很多,最大为50M 超时时间10分钟 ,为网站的安全性考虑,所以我想配置其中一个页面能使用该配置

<location path="shop/my/Video_add.aspx">
  <system.web>
   <httpRuntime  maxRequestLength="51200" executionTimeout="600" />
  </system.web>
</location>

注意:path="shop/  这样是对的,不要写成path="~/shop/

文章提到要进行以下配置,我的服务器是widows 2003系统,但我没进行以下操作仍然可以

所以大家看具体情况吧

 无法上传较大的文件“Request 对象 错误 ASP 0104 : 80004005”或者上传成功后,找不到文件名
  解决方案:
  先打开Internet 信息服务(IIS)管理器
  (本地计算机 )---- 属性 ----允许直接编辑配置数据库(N)

一定要勾先“允许直接编辑配置数据库(N)”
  然后在服务里关闭iis admin service服务

 找到windows\system32\inesrv下的metabase.xml,
  用计事本打开metabase.xml,找到ASPMaxRequestEntityAllowed 把他修改为需要的值,默认为204800,即200K
  把它修改为51200000(50M) ,然后重启iis admin service服务即可.

本篇文章来源于 站长资讯网 原文链接:http://www.chinahtml.com/0705/117919456513530.html

 

 

 

 

 

  二、ASP.NET大文件上传解决方案
解决的方法是利用隐含的HttpWorkerRequest,用它的 GetPreloadedEntityBody 和 ReadEntityBody方法从IIS为ASP.NET建立的pipe里分块读取数据。Chris Hynes为我们提供了这样的一个方案(用HttpModule),该方案除了允许你上传大文件外,还能实时显示上传进度。
Lion.Web.UpLoadModule和AspnetUpload 两个.NET组件都是利用的这个方案。

方案原理:
利用HttpHandler实现了类似于ISAPI Extention的功能,处理请求(Request)的信息和发送响应(Response)。

方案要点:
1. httpHandler or HttpModule
a.在asp.net进程处理request请求之前截获request对象
b.分块读取和写入数据
c.实时跟踪上传进度更新meta信息
2. 利用隐含的HttpWorkerRequest用它的GetPreloadedEntityBody 和 ReadEntityBody方法处理文件流
IServiceProvider provider = (IServiceProvider) HttpContext.Current;
HttpWorkerRequest wr = (HttpWorkerRequest) provider.GetService(typeof(HttpWorkerRequest));
byte[] bs = wr.GetPreloadedEntityBody();
....
if (!wr.IsEntireEntityBodyIsPreloaded())
{
int n = 1024;
byte[] bs2 = new byte;
while (wr.ReadEntityBody(bs2,n) >0)
{
.....
}
}
3. 自定义Multipart MIME 解析器
自动截获MIME分割符
将文件分块写如临时文件
实时更新Appliaction 状态(ReceivingData, Error, Complete)

/例子
HttpApplication application1 = sender as HttpApplication;
HttpWorkerRequest request1 = (HttpWorkerRequest) ((IServiceProvider) HttpContext.Current).GetService(typeof(HttpWorkerRequest));
try
{
if (application1.Context.Request.ContentType.IndexOf("multipart/form-data") <= -1)
{
return;
}
//Check The HasEntityBody
if (!request1.HasEntityBody())
{
return;
}
int num1 = 0;
TimeSpan span1 = DateTime.Now.Subtract(this.beginTime);

string text1 = application1.Context.Request.ContentType.ToLower();

byte[] buffer1 = Encoding.ASCII.GetBytes(("\r\n--" + text1.Substring(text1.IndexOf("boundary=") + 9)).ToCharArray());
int num2 = Convert.ToInt32(request1.GetKnownRequestHeader(11));
Progress progress1 = new Progress();

application1.Context.Items.Add("FileList", new Hashtable());

byte[] buffer2 = request1.GetPreloadedEntityBody();
num1 += buffer2.Length;

string text2 = this.AnalysePreloadedEntityBody(buffer2, "UploadGUID");
if (text2 != string.Empty)
{
application1.Context.Items.Add("LionSky_UpLoadModule_UploadGUID", text2);
}
bool flag1 = true;

if ((num2 > this.UpLoadFileLength()) && ((0 > span1.TotalHours) || (span1.TotalHours > 3)))
{
flag1 = false;
}
if ((0 > span1.TotalHours) || (span1.TotalHours > 3))
{
flag1 = false;
}
string text3 = this.AnalysePreloadedEntityBody(buffer2, "UploadFolder");
ArrayList list1 = new ArrayList();
RequestStream stream1 = new RequestStream(buffer2, buffer1, null, RequestStream.FileStatus.Close, RequestStream.ReadStatus.NoRead, text3, flag1, application1.Context, string.Empty);
list1.AddRange(stream1.ReadBody);
if (text2 != string.Empty)
{
progress1.FileLength = num2;
progress1.ReceivedLength = num1;
progress1.FileName = stream1.OriginalFileName;
progress1.FileCount = ((Hashtable) application1.Context.Items["FileList"]).Count;
application1.Application["_UploadGUID_" + text2] = progress1;
}

if (!request1.IsEntireEntityBodyIsPreloaded())
{
byte[] buffer4;
ArrayList list2;
int num3 = 204800;
byte[] buffer3 = new byte[num3];
while ((num2 - num1) >= num3)
{
if (!application1.Context.Response.IsClientConnected)
{
this.ClearApplication(application1);
}
num3 = request1.ReadEntityBody(buffer3, buffer3.Length);
num1 += num3;
list2 = stream1.ContentBody;
if (list2.Count > 0)
{
buffer4 = new byte[list2.Count + buffer3.Length];
list2.CopyTo(buffer4, 0);
buffer3.CopyTo(buffer4, list2.Count);
stream1 = new RequestStream(buffer4, buffer1, stream1.FileStream, stream1.FStatus, stream1.RStatus, text3, flag1, application1.Context, stream1.OriginalFileName);
}
else
{
stream1 = new RequestStream(buffer3, buffer1, stream1.FileStream, stream1.FStatus, stream1.RStatus, text3, flag1, application1.Context, stream1.OriginalFileName);
}
list1.AddRange(stream1.ReadBody);
if (text2 != string.Empty)
{
progress1.ReceivedLength = num1;
progress1.FileName = stream1.OriginalFileName;
progress1.FileCount = ((Hashtable) application1.Context.Items["FileList"]).Count;
application1.Application["_UploadGUID_" + text2] = progress1;
}
}
buffer3 = new byte[num2 - num1];
if (!application1.Context.Response.IsClientConnected && (stream1.FStatus == RequestStream.FileStatus.Open))
{
this.ClearApplication(application1);
}
num3 = request1.ReadEntityBody(buffer3, buffer3.Length);
list2 = stream1.ContentBody;
if (list2.Count > 0)
{
buffer4 = new byte[list2.Count + buffer3.Length];
list2.CopyTo(buffer4, 0);
buffer3.CopyTo(buffer4, list2.Count);
stream1 = new RequestStream(buffer4, buffer1, stream1.FileStream, stream1.FStatus, stream1.RStatus, text3, flag1, application1.Context, stream1.OriginalFileName);
}
else
{
stream1 = new RequestStream(buffer3, buffer1, stream1.FileStream, stream1.FStatus, stream1.RStatus, text3, flag1, application1.Context, stream1.OriginalFileName);
}
list1.AddRange(stream1.ReadBody);
if (text2 != string.Empty)
{
progress1.ReceivedLength = num1 + buffer3.Length;
progress1.FileName = stream1.OriginalFileName;
progress1.FileCount = ((Hashtable) application1.Context.Items["FileList"]).Count;
if (flag1)
{
progress1.UploadStatus = Progress.UploadStatusEnum.Uploaded;
}
else
{
application1.Application.Remove("_UploadGUID_" + text2);
}
}
}

byte[] buffer5 = new byte[list1.Count];
list1.CopyTo(buffer5);
this.PopulateRequestData(request1, buffer5);
}
catch (Exception exception1)
{
this.ClearApplication(application1);
throw exception1;
}

原创粉丝点击