C#使用HttpWebRequest實(shí)現(xiàn)大文件上傳
當(dāng)前位置:點(diǎn)晴教程→知識管理交流
→『 技術(shù)文檔交流 』
Author:xuzhihong Create Date:2011-06-03 Descriptions: WinForm程序使用HttpWebRequest實(shí)現(xiàn)大文件上傳 概述:通常在WinForm程序中都是采用WebClient方式實(shí)現(xiàn)文件上傳功能,本身這個(gè)方式?jīng)]有問題,但是當(dāng)需要上傳大文件比如說(300+M)的時(shí)候,那么WebClient將會報(bào)內(nèi)存不足異常(Out of Memory Exceptions),究其原因是因?yàn)?/span>WebClient方式是一次性將整個(gè)文件全部讀取到本地內(nèi)存中,然后再以數(shù)據(jù)流形式發(fā)送至服務(wù)器。本文將講述如何采用HttpWebRequest方式每次讀取固定大小數(shù)據(jù)片段(如4KB)發(fā)送至服務(wù)器,為大文件上傳提供解決方案,本文還將詳細(xì)講述將如何將“文件上傳”功能做為用戶自定義控件,實(shí)現(xiàn)模塊重用。
關(guān)鍵詞:HttpWebRequest、WebClient、OutOfMemoryExceptions
解決方案:開始我在WinForm項(xiàng)目中實(shí)現(xiàn)文件上傳功能的時(shí)候,是采用WebClient(WebClient myWebClient = new WebClient();)方式,這大部分情況都是正確的,但有時(shí)候會出現(xiàn)內(nèi)存不足的異常(Out of Memory Exceptions),經(jīng)常測試,發(fā)現(xiàn)是由于上傳大文件的時(shí)候才導(dǎo)致這問題。在網(wǎng)上查閱了一下其他網(wǎng)友的解決方案,最后找的發(fā)生異常的原因:“WebClient方式是一次性將整個(gè)文件全部讀取到本地內(nèi)存中,然后再以數(shù)據(jù)流形式發(fā)送至服務(wù)器”,詳細(xì)請參考:http://blogs.msdn.com/b/johan/archive/2006/11/15/are-you-getting-outofmemoryexceptions-when-uploading-large-files.aspx 。按照這個(gè)解釋,那么大文件上傳出現(xiàn)內(nèi)存不足的異常也就不足為奇了。下面我將講述如何一步步使用HttpWebRequest方式來實(shí)現(xiàn)文件分塊上傳數(shù)據(jù)流至服務(wù)器。 按照慣例還是先預(yù)覽一下文件上傳最后的效果吧,如下圖所示:
界面分為兩部分,上面是文件基本信息,下面是文件上傳自定義控件,我這里實(shí)現(xiàn)的是一個(gè)案件上傳多個(gè)監(jiān)控視頻功能。以下是詳細(xì)步驟: 第一步:創(chuàng)建用戶自定義控件BigFileUpload.xaml文件上傳是一個(gè)非常常用的功能,為了所寫的程序能非常方便地多次重復(fù)使用,我決定將其處理為一個(gè)用戶自定義控件(UserControl)。 我們先在項(xiàng)目中創(chuàng)建一個(gè)FileUpload文件夾,在其目錄下新建一個(gè)WPF自定義控件文件命名為BigFileUpload.xaml,這樣就表示文件上傳是一個(gè)獨(dú)立的小模塊使用。之所以用WPF自定義控件是因?yàn)?/span>WPF頁面效果好看點(diǎn),而且我想以后可能大部分C/S程序都會漸漸的由WinForm轉(zhuǎn)向WPF吧,當(dāng)然創(chuàng)建Window Forms用戶控件也是沒有問題的。然后我們需要做一個(gè)下圖效果的頁面布局:
前臺設(shè)計(jì)代碼如下: <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">進(jìn)度</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">已用時(shí)</Label> <Label Height="28" Margin="230,0,35,10" Name="lblSpeed"VerticalAlignment="Bottom">平均速度</Label> </Grid> </UserControl>
后臺CS代碼: public delegate void FilUploadHandler(EventFileUploadArg e); /// <summary> /// 自定義事件數(shù)據(jù)參數(shù)類 /// </summary> public class EventFileUploadArg : EventArgs { private HttpWebRequestReturn hwr; /// <summary> /// 文件上傳服務(wù)器返回類 /// </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> /// 服務(wù)器接收的地址 如:http://192.168.0.105:8078/Default.aspx /// </summary> public string ServerAddress { get; set; } /// <summary> /// 狀態(tài)標(biāo)識是否上傳成功 /// </summary> private bool IsSuccess { get; set; }
/// <summary> /// 將本地文件上傳到指定的服務(wù)器(HttpWebRequest方法) /// </summary> /// <param name="address">文件上傳到的服務(wù)器</param> /// <param name="fileNamePath">要上傳的本地文件(全路徑)</param> /// <param name="saveName">文件上傳后的名稱</param> /// <param name="progressBar">上傳進(jìn)度條</param> /// <returns>服務(wù)器反饋信息</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);
//時(shí)間戳 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);
// 根據(jù)uri創(chuàng)建HttpWebRequest對象 HttpWebRequest httpReq = (HttpWebRequest)WebRequest.Create(new Uri(address)); httpReq.Method = "POST";
//對發(fā)送的數(shù)據(jù)不使用緩存【重要、關(guān)鍵】 httpReq.AllowWriteStreamBuffering = false;
//設(shè)置獲得響應(yīng)的超時(shí)時(shí)間(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];
//已上傳的字節(jié)數(shù) long offset = 0;
//開始上傳時(shí)間 DateTime startTime = DateTime.Now; int size = r.Read(buffer, 0,bufferLength); Stream postStream =httpReq.GetRequestStream();
//發(fā)送請求頭部消息 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 = "已用時(shí):" +second.ToString("F2") + "秒"; if (second > 0.0001) { lblSpeed.Content = " 平均速度:" + (offset / 1024 / second).ToString("0.00") + "KB/秒"; } else { lblSpeed.Content = " 平均速度太快,系統(tǒng)放棄計(jì)算"; } //lblState.Content = "已上傳:" + (offset * 100.0 / length).ToString("F2") + "%"; lblState.Content = "已上傳:" + (offset * 100.0 / fileLength).ToString("F2") + "%"; //1024*1024=1048576 if (fileLength > 1048576) //根據(jù)文件是否大于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); } //添加尾部的時(shí)間戳 postStream.Write(boundaryBytes, 0,boundaryBytes.Length); postStream.Close();
//獲取服務(wù)器端的響應(yīng) WebResponse webRespon =httpReq.GetResponse(); Stream s = webRespon.GetResponseStream(); StreamReader sr = new StreamReader(s);
//讀取服務(wù)器端返回的消息 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("選擇的文件不存在,可能已經(jīng)被刪除,請重新選擇!", "溫馨提示"); } 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); //上傳后執(zhí)行文件上傳的后續(xù)的自定義事件 } } else { MessageBox.Show(hwr.message); }
} catch (System.Exception ex) { MessageBox.Show(ex.Message); } } BtnUpload.IsEnabled = true; }
曾經(jīng)在大學(xué)的時(shí)候,記得數(shù)字圖像處理老師給我們說過:“中國的書籍講的大部分都是理論,很少有真正將完整代碼寫出來的”。所以我每次寫文章的時(shí)候,都有個(gè)習(xí)慣就是盡可能完整的把代碼貼出來,一是怕自己文字功底太差表示不清楚,二是方便大家和自己以后理解。題外話少說,還是簡單的講述一下界面及代碼結(jié)構(gòu)吧。 界面相當(dāng)簡單,就是一個(gè)瀏覽按鈕和一個(gè)上傳按鈕,以及一些用于增加友好度的Label提示。瀏覽按鈕對應(yīng)的事件BtnBrowse_Click,里面定義了一個(gè)OpenFileDialog用于選擇需要上傳的文件。上傳按鈕對應(yīng)的事件BtnUpload_Click作了一些基本的驗(yàn)證,然后調(diào)用了最關(guān)鍵的Upload_Request方法,同時(shí)執(zhí)行了一個(gè)委托事件EventFileUpload(arg); //上傳后執(zhí)行文件上傳的后續(xù)的自定義事件 Uplaod_Request方法帶有四個(gè)參數(shù): /// <summary> /// 將本地文件上傳到指定的服務(wù)器(HttpWebRequest方法) /// </summary> /// <param name="address">文件上傳到的服務(wù)器(服務(wù)器接收的地址如:http://192.168.0.105:8078/Default.aspx )</param> /// <param name="fileNamePath">要上傳的本地文件(全路徑)</param> /// <param name="saveName">文件上傳后的名稱</param> /// <param name="progressBar">上傳進(jìn)度條</param> /// <returns>服務(wù)器反饋信息</returns> private HttpWebRequestReturn Upload_Request(string address, stringfileNamePath, string saveName, ProgressBar progressBar){} 值得一提的是這里的返回類型HttpWebRequestReturn(點(diǎn)擊查看定義)是為了和數(shù)據(jù)庫對應(yīng)自己定義的一個(gè)類,繼承自統(tǒng)一返回類型TwiReturn(點(diǎn)擊查看定義)類,里面記錄了文件服務(wù)器反饋的綜合信息。
第二步:創(chuàng)建服務(wù)器響應(yīng)程序BigFileUploadServerApp很顯然文件上傳至服務(wù)器后需要有個(gè)對應(yīng)的響應(yīng)程序。那么我們再創(chuàng)建一個(gè)單獨(dú)的Web應(yīng)用程序(命名為:BigFileUploadServerApp),發(fā)布在服務(wù)器中的IIS上,只需要一個(gè)默認(rèn)的Default.aspx頁面和一個(gè)FileUpload空文件夾即可,我們將FileUpload文件夾所存放的目錄作為文件上傳至服務(wù)器存放的目錄。 Default.aspx.cs代碼也相當(dāng)簡單: 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 = "服務(wù)器沒接收到上傳的文件信息,請檢查上傳的文件是否為空文件!"; } string strReturn = JSSerialize.Serialize(hwr); Response.Write(strReturn); Response.End(); } 返回類型記錄了另存為的文件名FileName,文件在服務(wù)器中的全路徑FileFullName,服務(wù)器IP地址ServerIP等信息,JSSerialize.Serialize()(點(diǎn)擊查看定義)方法是將對象序列化為字符串。最后需要說明的是:微軟為了防止拒絕服務(wù)攻擊,對文件上傳做了一個(gè)大小限制,最大默認(rèn)為4M,然后我們使用HttpWebRequest方法將會受到其影響。為了突破這個(gè)限制,那么我們需要在Web.Config文件中的system.web節(jié)點(diǎn)下增加一個(gè)httpRuntime配置, <system.web> <httpRuntime maxRequestLength="1000000" executionTimeout="600"></httpRuntime> </system.web> 其中MaxRequestLength單位為KB,executionTimeout單位為秒,大小自己根據(jù)實(shí)際情況進(jìn)行控制。 文件上傳至服務(wù)器之后,我們還需要將文件基本信息記錄到對應(yīng)的數(shù)據(jù)庫中,那么在執(zhí)行“上傳”事件時(shí)我們還需要執(zhí)行自定義后續(xù)操作。由于我們做的是一個(gè)通用的文件上傳功能,所以不能直接將業(yè)務(wù)邏輯寫在BtnUpload_Click方法中,因?yàn)槊總€(gè)地方上傳處理的邏輯也許并不一樣。這個(gè)時(shí)候當(dāng)然就該是偉大的委托上場了,在此我們定義了一個(gè)FileUploadHandler委托,定義如下: public delegate void FilUploadHandler(EventFileUploadArge); 其參數(shù)有點(diǎn)特別,不是常規(guī)的EventArgs,而是自定義繼承自EventArgs的EventFileUploadArg,定義如下: /// <summary> /// 自定義事件數(shù)據(jù)參數(shù)類 /// </summary> public class EventFileUploadArg : EventArgs { private HttpWebRequestReturn hwr; /// <summary> /// 文件上傳服務(wù)器返回類 /// </summary> public HttpWebRequestReturn HwrReturn { get { return hwr; } set { hwr = value; } } public EventFileUploadArg() { hwr = new HttpWebRequestReturn(); } public EventFileUploadArg(HttpWebRequestReturn hwrReturn) { hwr = hwrReturn; } } 為什么要定義這么一個(gè)參數(shù)呢?因?yàn)槲覀冊诜?wù)器接收文件后得到了一些反饋信息(是一個(gè)HttpWebRequestReturn類的實(shí)例),那么在處理后續(xù)的邏輯的時(shí)候,是希望了解這些信息的,所謂的了解其實(shí)就是能夠訪問反饋信息,那么無疑于這種方式公開出來是非常合理的。
第三步:應(yīng)用到這里我們已經(jīng)把自定義用戶控件做好了,但是還沒真正使用。這么這一步我們將討論如何使用它。為了實(shí)現(xiàn)前面演示的效果我們新建一個(gè)WinForm窗體頁面暫且命名為(FormVideoFileUpload.cs),然后做一個(gè)簡單的布局,如下圖:
上面的都是文件基本信息,下面的是一個(gè)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("恭喜你文件上傳成功,是否繼續(xù)上傳視頻文件?", "恭喜",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,"提示"); } } }
經(jīng)過不懈的努力,和這么長時(shí)間的耐心,到這里已經(jīng)完成了我們所要做的工作了,看看我們的功能界面吧,不容易呀!
這里有個(gè)提示框提示用戶是否繼續(xù)上傳,如果是那么程序?qū)⑺⑿乱幌掠脩艨丶巧厦娴奈募讣拘畔⑷匀槐A?,這樣就做到了我所希望的一個(gè)案件對應(yīng)上傳多個(gè)視頻的效果。
說在最后該解決方案成功實(shí)現(xiàn)了基于HttpWebRequest的方式實(shí)現(xiàn)大文件上傳,相對來說這個(gè)界面還是挺好看的。對應(yīng)大文件上傳有人也許會說用FTP的方式處理,聽人說配置有點(diǎn)復(fù)雜,由于我個(gè)人比較懶,所以沒去親自試驗(yàn),以后有機(jī)會再試試FTP的方式,只有我親自試成功了,我才會寫出來。 最后,由于個(gè)人技術(shù)水平和寫作能力的限制,文章有不足之處再所難免,還望大家批評指正,如果發(fā)現(xiàn)問題,我也將會盡快修改。知錯(cuò)、認(rèn)錯(cuò)、改錯(cuò)。 該文章在 2017/3/22 0:13:01 編輯過 |
關(guān)鍵字查詢
相關(guān)文章
正在查詢... |