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 編輯過 |
關鍵字查詢
相關文章
正在查詢... |