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, ProgressBarprogressBar)
{
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(" --" + strBoundary + " ");
//请求头部信息
StringBuilder sb = new StringBuilder();
sb.Append("--");
sb.Append(strBoundary);
sb.Append(" ");
sb.Append("Content-Disposition: form-data; name="");
sb.Append("file");
sb.Append(""; filename="");
sb.Append(saveName);
sb.Append(""");
sb.Append(" ");
sb.Append("Content-Type: ");
sb.Append("application/octet-stream");
sb.Append(" ");
sb.Append(" ");
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>
<httpRuntime maxRequestLength="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的方式,只有我亲自试成功了,我才会写出来。
最后,由于个人技术水平和写作能力的限制,文章有不足之处再所难免,还望大家批评指正,如果发现问题,我也将会尽快修改。知错、认错、改错。