Author:xuzhihongweb
Create Date:2011-06-03數據庫
Descriptions: WinForm程序使用HttpWebRequest實現大文件上傳緩存
一般在WinForm程序中都是採用WebClient方式實現文件上傳功能,自己這個方式沒有問題,可是當須要上傳大文件好比說(300+M)的時候,那麼WebClient將會報內存不足異常(Out of Memory Exceptions),究其緣由是由於WebClient方式是一次性將整個文件所有讀取到本地內存中,而後再以數據流形式發送至服務器。本文將講述如何採用HttpWebRequest方式每次讀取固定大小數據片斷(如4KB)發送至服務器,爲大文件上傳提供解決方案,本文還將詳細講述將如何將「文件上傳」功能作爲用戶自定義控件,實現模塊重用。服務器
開始我在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方式來實現文件分塊上傳數據流至服務器。app
按照慣例仍是先預覽一下文件上傳最後的效果吧,以下圖所示:ide
界面分爲兩部分,上面是文件基本信息,下面是文件上傳自定義控件,我這裏實現的是一個案件上傳多個監控視頻功能。如下是詳細步驟:佈局
文件上傳是一個很是經常使用的功能,爲了所寫的程序能很是方便地屢次重複使用,我決定將其處理爲一個用戶自定義控件(UserControl)。post
咱們先在項目中建立一個FileUpload文件夾,在其目錄下新建一個WPF自定義控件文件命名爲BigFileUpload.xaml,這樣就表示文件上傳是一個獨立的小模塊使用。之因此用WPF自定義控件是由於WPF頁面效果好看點,並且我想之後可能大部分C/S程序都會漸漸的由WinForm轉向WPF吧,固然建立Window Forms用戶控件也是沒有問題的。而後咱們須要作一個下圖效果的頁面佈局:測試
前臺設計代碼以下:ui
<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("\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(點擊查看定義)類,裏面記錄了文件服務器反饋的綜合信息。
很顯然文件上傳至服務器後須要有個對應的響應程序。那麼咱們再建立一個單獨的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的方式,只有我親自試成功了,我纔會寫出來。
最後,因爲我的技術水平和寫做能力的限制,文章有不足之處再所不免,還望你們批評指正,若是發現問題,我也將會盡快修改。知錯、認錯、改錯。