C#使用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(stringaddress, 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 = newSystem.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, stringfileNamePath, 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 = newHttpWebRequestReturn(); 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(EventFileUploadArge); 其参数有点特别,不是常规的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 += newFilUploadHandler(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的方式,只有我亲自试成功了,我才会写出来。 最后,由于个人技术水平和写作能力的限制,文章有不足之处再所难免,还望大家批评指正,如果发现问题,我也将会尽快修改。知错、认错、改错。 该文章在 2017/3/22 0:13:01 编辑过 |
关键字查询
相关文章
正在查询... |