一切福田,不離方寸,從心而覓,感無不通。

使用HttpWebRequest实现大文件上传

Author:xuzhihong

Create Date:2011-06-03

Descriptions: WinForm程序使用HttpWebRequest实现大文件上传

概述:

通常在WinForm程序中都是采用WebClient方式实现文件上传功能,本身这个方式没有问题,但是当需要上传大文件比如说(300+M)的时候,那么WebClient将会报内存不足异常(Out of Memory Exceptions),究其原因是因为WebClient方式是一次性将整个文件全部读取到本地内存中,然后再以数据流形式发送至服务器。本文将讲述如何采用HttpWebRequest方式每次读取固定大小数据片段(如4KB)发送至服务器,为大文件上传提供解决方案,本文还将详细讲述将如何将“文件上传”功能做为用户自定义控件,实现模块重用。

 

关键词:HttpWebRequest、WebClient、OutOfMemoryExceptions

 

解决方案:

开始我在WinForm项目中实现文件上传功能的时候,是采用WebClient(WebClient myWebClient = new WebClient();)方式,这大部分情况都是正确的,但有时候会出现内存不足的异常(Out of Memory Exceptions),经常测试,发现是由于上传大文件的时候才导致这问题。在网上查阅了一下其他网友的解决方案,最后找的发生异常的原因:“WebClient方式是一次性将整个文件全部读取到本地内存中,然后再以数据流形式发送至服务器”,详细请参考:http://blogs.msdn.com/b/johan/archive/2006/11/15/are-you-getting-outofmemoryexceptions-when-uploading-large-files.aspx 。按照这个解释,那么大文件上传出现内存不足的异常也就不足为奇了。下面我将讲述如何一步步使用HttpWebRequest方式来实现文件分块上传数据流至服务器。

按照惯例还是先预览一下文件上传最后的效果吧,如下图所示:

 

界面分为两部分,上面是文件基本信息,下面是文件上传自定义控件,我这里实现的是一个案件上传多个监控视频功能。以下是详细步骤:

第一步:创建用户自定义控件BigFileUpload.xaml

文件上传是一个非常常用的功能,为了所写的程序能非常方便地多次重复使用,我决定将其处理为一个用户自定义控件(UserControl)。

我们先在项目中创建一个FileUpload文件夹,在其目录下新建一个WPF自定义控件文件命名为BigFileUpload.xaml,这样就表示文件上传是一个独立的小模块使用。之所以用WPF自定义控件是因为WPF页面效果好看点,而且我想以后可能大部分C/S程序都会渐渐的由WinForm转向WPF吧,当然创建Window Forms用户控件也是没有问题的。然后我们需要做一个下图效果的页面布局:

 

前台设计代码如下:

<UserControl x:Class="CHVM.FileUpload.BigFileUpload"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

Height="160" Width="480">

<Grid Height="160" Width="480" Background="White">

<Label Height="28" HorizontalAlignment="Left" Margin="16,10,0,0" Name="label1" VerticalAlignment="Top" Width="53">文件</Label>

<Label HorizontalAlignment="Left" Margin="15,52,0,80" Name="label2" Width="54">进度</Label>

<ProgressBar Height="20" Margin="61,52,116,0" Name="progressBar1" VerticalAlignment="Top" />

<TextBox Height="23" Margin="61,12,116,0" Name="txtBoxFileName" VerticalAlignment="Top" />

<Button Height="23" HorizontalAlignment="Right" Margin="0,10,35,0" Name="BtnBrowse" VerticalAlignment="Top" Width="75" Click="BtnBrowse_Click">浏览…</Button>

<Button Height="23" HorizontalAlignment="Right" Margin="0,52,35,0" Name="BtnUpload" VerticalAlignment="Top" Width="75" Click="BtnUpload_Click">上传</Button>

<Label HorizontalAlignment="Left" Margin="16,0,0,44" Name="lblState" Width="183" Height="35" VerticalAlignment="Bottom">已上传</Label>

<Label Margin="231,0,35,44" Name="lblSize" Height="35" VerticalAlignment="Bottom">/</Label>

<Label Height="28" HorizontalAlignment="Left" Margin="16,0,0,10" Name="lblTime" VerticalAlignment="Bottom" Width="183">已用时</Label>

<Label Height="28" Margin="230,0,35,10" Name="lblSpeed" VerticalAlignment="Bottom">平均速度</Label>

</Grid>

</UserControl>

 

 

后台CS代码:

public delegate void FilUploadHandler(EventFileUploadArg e);

///<summary>

///自定义事件数据参数类

///</summary>

public class EventFileUploadArg : EventArgs

{

private HttpWebRequestReturn hwr;

///<summary>

///文件上传服务器返回类

///</summary>

public HttpWebRequestReturn HwrReturn

{

get

{

return hwr;

}

set

{

hwr = value;

}

}

public EventFileUploadArg()

{

hwr = new HttpWebRequestReturn();

}

public EventFileUploadArg(HttpWebRequestReturn hwrReturn)

{

hwr = hwrReturn;

}

}

 

///<summary>

/// BigFileUpload.xaml 的交互逻辑

///</summary>

public partial class BigFileUpload : UserControl

{

public BigFileUpload()

{

InitializeComponent();

}

 

public event FilUploadHandler EventFileUpload;

 

///<summary>

///服务器接收的地址 如:http://192.168.0.105:8078/Default.aspx

///</summary>

public string ServerAddress

{

get;

set;

}

///<summary>

///状态标识是否上传成功

///</summary>

private bool IsSuccess

{

get;

set;

}

///<summary>

///将本地文件上传到指定的服务器(HttpWebRequest方法)

///</summary>

///<param name="address">文件上传到的服务器</param>

///<param name="fileNamePath">要上传的本地文件(全路径)</param>

///<param name="saveName">文件上传后的名称</param>

///<param name="progressBar">上传进度条</param>

///<returns>服务器反馈信息</returns>

private HttpWebRequestReturn Upload_Request(string address, string fileNamePath, string saveName, ProgressBar progressBar)

{

HttpWebRequestReturn hwr;

 

// 要上传的文件

FileStream fs = new FileStream(fileNamePath, FileMode.Open, FileAccess.Read);

BinaryReader r = new BinaryReader(fs);

 

//时间戳

string strBoundary = "———-" + DateTime.Now.Ticks.ToString("x");

byte[] boundaryBytes = Encoding.ASCII.GetBytes("\r\n--" + strBoundary + "\r\n");

 

//请求头部信息

StringBuilder sb = new StringBuilder();

sb.Append("--");

sb.Append(strBoundary);

sb.Append("\r\n");

sb.Append("Content-Disposition: form-data; name=\"");

sb.Append("file");

sb.Append("\"; filename=\"");

sb.Append(saveName);

sb.Append("\"");

sb.Append("\r\n");

sb.Append("Content-Type: ");

sb.Append("application/octet-stream");

sb.Append("\r\n");

sb.Append("\r\n");

 

string strPostHeader = sb.ToString();

byte[] postHeaderBytes = Encoding.UTF8.GetBytes(strPostHeader);

 

// 根据uri创建HttpWebRequest对象

HttpWebRequest httpReq = (HttpWebRequest)WebRequest.Create(new Uri(address));

httpReq.Method = "POST";

 

//对发送的数据不使用缓存【重要、关键】

httpReq.AllowWriteStreamBuffering = false;

 

//设置获得响应的超时时间(300秒)

httpReq.Timeout = 300000;

httpReq.ContentType = "multipart/form-data; boundary=" + strBoundary;

long length = fs.Length + postHeaderBytes.Length + boundaryBytes.Length;

long fileLength = fs.Length;

httpReq.ContentLength = length;

try

{

progressBar.Maximum = fileLength;//int.MaxValue;

progressBar.Minimum = 0;

progressBar.Value = 0;

 

//每次上传4k

int bufferLength = 4096;

byte[] buffer = new byte[bufferLength];

 

//已上传的字节数

long offset = 0;

 

//开始上传时间

DateTime startTime = DateTime.Now;

int size = r.Read(buffer, 0, bufferLength);

Stream postStream = httpReq.GetRequestStream();

 

//发送请求头部消息

postStream.Write(postHeaderBytes, 0, postHeaderBytes.Length);

while (size > 0)

{

postStream.Write(buffer, 0, size);

offset += size;

progressBar.Value = offset;//(int)(offset * (int.MaxValue / length));

TimeSpan span = DateTime.Now – startTime;

double second = span.TotalSeconds;

lblTime.Content = "已用时:" + second.ToString("F2") + "秒";

if (second > 0.0001)

{

lblSpeed.Content = " 平均速度:" + (offset / 1024 / second).ToString("0.00") + "KB/秒";

}

else

{

lblSpeed.Content = " 平均速度太快,系统放弃计算";

}

//lblState.Content = "已上传:" + (offset * 100.0 / length).ToString("F2") + "%";

lblState.Content = "已上传:" + (offset * 100.0 / fileLength).ToString("F2") + "%";

//1024*1024=1048576

if (fileLength > 1048576) //根据文件是否大于1M,来使用单位【处理精度】

{

lblSize.Content = (offset / 1048576.0).ToString("F2") + "M/" + (fileLength / 1048576.0).ToString("F2") + "M";

}

else

{

lblSize.Content = (offset / 1024.0).ToString("F2") + "KB/" + (fileLength / 1024.0).ToString("F2") + "KB";

}

size = r.Read(buffer, 0, bufferLength);

}

//添加尾部的时间戳

postStream.Write(boundaryBytes, 0, boundaryBytes.Length);

postStream.Close();

 

//获取服务器端的响应

WebResponse webRespon = httpReq.GetResponse();

Stream s = webRespon.GetResponseStream();

StreamReader sr = new StreamReader(s);

 

//读取服务器端返回的消息

string serverMsg = sr.ReadLine();

hwr = JSSerialize.Deserialize<HttpWebRequestReturn>(serverMsg);

s.Close();

sr.Close();

 

}

catch(Exception ex)

{

hwr = new HttpWebRequestReturn();

hwr.success = false;

hwr.errors = ex.Message;

}

finally

{

fs.Close();

r.Close();

}

 

return hwr;

}

 

///<summary>

///浏览

///</summary>

///<param name="sender"></param>

///<param name="e"></param>

private void BtnBrowse_Click(object sender, RoutedEventArgs e)

{

IsSuccess = false;

System.Windows.Forms.OpenFileDialog ofd = new System.Windows.Forms.OpenFileDialog();

ofd.Multiselect = false; //单选

ofd.Filter = "Video files (*.avi)|*.avi|All files (*.*)|*.*";

ofd.FilterIndex = 2;

ofd.RestoreDirectory = false;

if (ofd.ShowDialog() == System.Windows.Forms.DialogResult.OK)

{

txtBoxFileName.Text = ofd.FileName;

}

}

 

///<summary>

///上传

///</summary>

///<param name="sender"></param>

///<param name="e"></param>

private void BtnUpload_Click(object sender, RoutedEventArgs e)

{

BtnUpload.IsEnabled = false;

string fileNamePath = txtBoxFileName.Text; //本地欲上传文件完整路径

if (fileNamePath == "")

{

MessageBox.Show("请选择要上传的文件路径!","温馨提示");

}

else if (!File.Exists(fileNamePath))

{

MessageBox.Show("选择的文件不存在,可能已经被删除,请重新选择!", "温馨提示");

}

else

{

try

{

string fileName = fileNamePath.Substring(fileNamePath.LastIndexOf("\\") + 1); //欲上传文件名

string fileNameExt = fileName.Substring(fileName.LastIndexOf(".")); //文件后缀,包含"."

string saveName = fileName.Substring(0, fileName.Length – fileNameExt.Length) + DateTime.Now.ToString("yyMMddhhmmss") + DateTime.Now.Millisecond.ToString() + fileNameExt;

HttpWebRequestReturn hwr = Upload_Request(ServerAddress, fileNamePath, saveName, progressBar1);

if (hwr.success) //上传成功

{

if (EventFileUpload != null)

{

EventFileUploadArg arg = new EventFileUploadArg(hwr);

EventFileUpload(arg); //上传后执行文件上传的后续的自定义事件

}

}

else

{

MessageBox.Show(hwr.message);

}

}

catch (System.Exception ex)

{

MessageBox.Show(ex.Message);

}

}

BtnUpload.IsEnabled = true;

}

 

曾 经在大学的时候,记得数字图像处理老师给我们说过:“中国的书籍讲的大部分都是理论,很少有真正将完整代码写出来的”。所以我每次写文章的时候,都有个习 惯就是尽可能完整的把代码贴出来,一是怕自己文字功底太差表示不清楚,二是方便大家和自己以后理解。题外话少说,还是简单的讲述一下界面及代码结构吧。

界面相当简单,就是一个浏览按钮和一个上传按钮,以及一些用于增加友好度的Label提示。浏览按钮对应的事件BtnBrowse_Click,里面定义了一个OpenFileDialog用于选择需要上传的文件。上传按钮对应的事件BtnUpload_Click作了一些基本的验证,然后调用了最关键的Upload_Request方法,同时执行了一个委托事件EventFileUpload(arg); //上传后执行文件上传的后续的自定义事件

Uplaod_Request方法带有四个参数:

///<summary>

///将本地文件上传到指定的服务器(HttpWebRequest方法)

///</summary>

///<param name="address">文件上传到的服务器(服务器接收的地址如:http://192.168.0.105:8078/Default.aspx )</param>

///<param name="fileNamePath">要上传的本地文件(全路径)</param>

///<param name="saveName">文件上传后的名称</param>

///<param name="progressBar">上传进度条</param>

///<returns>服务器反馈信息</returns>

private HttpWebRequestReturn Upload_Request(string address, string fileNamePath, string saveName, ProgressBar progressBar){}

值得一提的是这里的返回类型HttpWebRequestReturn(点击查看定义)是为了和数据库对应自己定义的一个类,继承自统一返回类型TwiReturn(点击查看定义)类,里面记录了文件服务器反馈的综合信息。

 

第二步:创建服务器响应程序BigFileUploadServerApp

很显然文件上传至服务器后需要有个对应的响应程序。那么我们再创建一个单独的Web应用程序(命名为:BigFileUploadServerApp),发布在服务器中的IIS上,只需要一个默认的Default.aspx页面和一个FileUpload空文件夹即可,我们将FileUpload文件夹所存放的目录作为文件上传至服务器存放的目录。

Default.aspx.cs代码也相当简单:

protected void Page_Load(object sender, EventArgs e)

{

HttpWebRequestReturn hwr = new HttpWebRequestReturn();

hwr.hasRight = true;

if (Request.Files.Count > 0)

{

try

{

HttpPostedFile file = Request.Files[0];

string filePath = this.MapPath("FileUpload") + "\\" + file.FileName;

file.SaveAs(filePath);

hwr.FileName = file.FileName;

hwr.FileFullName = filePath;

hwr.ContentLength = file.ContentLength;

IPHostEntry hostInfo = Dns.GetHostEntry(Server.MachineName);

hwr.ServerIP = hostInfo.AddressList[0].ToString();

hwr.success = true;

}

catch (Exception ex)

{

hwr.errors = ex.Message;

}

}

else

{

hwr.errors = "服务器没接收到上传的文件信息,请检查上传的文件是否为空文件!";

}

string strReturn = JSSerialize.Serialize(hwr);

Response.Write(strReturn);

Response.End();

}

返回类型记录了另存为的文件名FileName,文件在服务器中的全路径FileFullName,服务器IP地址ServerIP等信息,JSSerialize.Serialize()(点击查看定义)方法是将对象序列化为字符串。最后需要说明的是:微软为了防止拒绝服务攻击,对文件上传做了一个大小限制,最大默认为4M,然后我们使用HttpWebRequest方法将会受到其影响。为了突破这个限制,那么我们需要在Web.Config文件中的system.web节点下增加一个httpRuntime配置,

<system.web>

<httpRuntimemaxRequestLength="1000000"executionTimeout="600"></httpRuntime>

</system.web>

其中MaxRequestLength单位为KB,executionTimeout单位为秒,大小自己根据实际情况进行控制。

文件上传至服务器之后,我们还需要将文件基本信息记录到对应的数据库中,那么在执行“上传”事件时我们还需要执行自定义后续操作。由于我们做的是一个通用的文件上传功能,所以不能直接将业务逻辑写在BtnUpload_Click方法中,因为每个地方上传处理的逻辑也许并不一样。这个时候当然就该是伟大的委托上场了,在此我们定义了一个FileUploadHandler委托,定义如下:

public delegate void FilUploadHandler(EventFileUploadArg e);

其参数有点特别,不是常规的EventArgs,而是自定义继承自EventArgs的EventFileUploadArg,定义如下:

///<summary>

///自定义事件数据参数类

///</summary>

public class EventFileUploadArg : EventArgs

{

private HttpWebRequestReturn hwr;

///<summary>

///文件上传服务器返回类

///</summary>

public HttpWebRequestReturn HwrReturn

{

get

{

return hwr;

}

set

{

hwr = value;

}

}

public EventFileUploadArg()

{

hwr = new HttpWebRequestReturn();

}

public EventFileUploadArg(HttpWebRequestReturn hwrReturn)

{

hwr = hwrReturn;

}

}

为什么要定义这么一个参数呢?因为我们在服务器接收文件后得到了一些反馈信息(是一个HttpWebRequestReturn类的实例),那么在处理后续的逻辑的时候,是希望了解这些信息的,所谓的了解其实就是能够访问反馈信息,那么无疑于这种方式公开出来是非常合理的。

 

第三步:应用

到这里我们已经把自定义用户控件做好了,但是还没真正使用。这么这一步我们将讨论如何使用它。为了实现前面演示的效果我们新建一个WinForm窗体页面暂且命名为(FormVideoFileUpload.cs),然后做一个简单的布局,如下图:

 

上面的都是文件基本信息,下面的是一个Panel用于承载我们前面做好的“自定义文件上传控件BigFileUpload.xaml”,后台cs代码如下:

public partial class FormVideoFileUpload : Form

{

public FormVideoFileUpload()

{

InitializeComponent();

 

AddBfuControl();

}

 

///<summary>

///增加文件上传自定义控件

///</summary>

public void AddBfuControl()

{

BigFileUpload bfu = new BigFileUpload();

bfu.EventFileUpload += new FilUploadHandler(Bfu_BtnUpload_Click);

bfu.ServerAddress = CommPar.VM_VideoFilesUrl;

ElementHost elHost = new ElementHost();

elHost.Dock = DockStyle.None;

elHost.Width = panel1.Width;

elHost.Height = panel1.Height;

elHost.Child = bfu;

panel1.Controls.Add(elHost);

}

 

///<summary>

///文件上传完成的自定义事件

///</summary>

///<param name="arg"></param>

public void Bfu_BtnUpload_Click(EventFileUploadArg arg)

{

if (arg.HwrReturn.success)

{

TMEDIAS medias = new TMEDIAS();

medias.MEDIASEED = txtMediaSeed.Text;

medias.MEDIASOURCE = txtMediaSource.Text;

medias.CASENUMBER = txtCaseNumber.Text;

medias.REMARK = txtRemark.Text;

medias.FILENAME = arg.HwrReturn.FileName;

medias.FILEFULLNAME = arg.HwrReturn.FileFullName;

medias.SERVERIP = arg.HwrReturn.ServerIP;

medias.UPDATETIME = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");

medias.OPERATORID = 6;

medias.OPERATOR = "赵精伟";

TwiReturn twi = UsingBLL.medias.Add(medias);

if (twi.success)

{

DialogResult dResult = MessageBox.Show("恭喜你文件上传成功,是否继续上传视频文件?", "恭喜", MessageBoxButtons.YesNo, MessageBoxIcon.Question);

if (dResult == DialogResult.Yes)

{

panel1.Controls.Clear();

AddBfuControl();

}

else

{

this.Hide();

}

}

else

{

MessageBox.Show(twi.message, "提示");

}

}

else

{

MessageBox.Show(arg.HwrReturn.message,"提示");

}

}

}

 

 

经过不懈的努力,和这么长时间的耐心,到这里已经完成了我们所要做的工作了,看看我们的功能界面吧,不容易呀!

 

这里有个提示框提示用户是否继续上传,如果是那么程序将刷新一下用户控件,但是上面的文件案件基本信息仍然保留,这样就做到了我所希望的一个案件对应上传多个视频的效果。

 

 

说在最后

该解决方案成功实现了基于HttpWebRequest的方式实现大文件上传,相对来说这个界面还是挺好看的。对应大文件上传有人也许会说用FTP的方式处理,听人说配置有点复杂,由于我个人比较懒,所以没去亲自试验,以后有机会再试试FTP的方式,只有我亲自试成功了,我才会写出来。

最后,由于个人技术水平和写作能力的限制,文章有不足之处再所难免,还望大家批评指正,如果发现问题,我也将会尽快修改。知错、认错、改错。

from:http://xuzhihong1987.blog.163.com/blog/static/2673158720115991432899/