使用HttpWebRequest實現大文件上傳

Author:xuzhihongweb

Create Date:2011-06-03數據庫

Descriptions: WinForm程序使用HttpWebRequest實現大文件上傳緩存

概述:

一般在WinForm程序中都是採用WebClient方式實現文件上傳功能,自己這個方式沒有問題,可是當須要上傳大文件好比說(300+M)的時候,那麼WebClient將會報內存不足異常(Out of Memory Exceptions),究其緣由是由於WebClient方式是一次性將整個文件所有讀取到本地內存中,而後再以數據流形式發送至服務器。本文將講述如何採用HttpWebRequest方式每次讀取固定大小數據片斷(如4KB)發送至服務器,爲大文件上傳提供解決方案,本文還將詳細講述將如何將「文件上傳」功能作爲用戶自定義控件,實現模塊重用。服務器

 

關鍵詞:HttpWebRequestWebClientOutOfMemoryExceptions

 

解決方案:

開始我在WinForm項目中實現文件上傳功能的時候,是採用WebClientWebClient 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

 

使用HttpWebRequest實現大文件上傳 - 飛天心宏 - 飛天心宏的博客

 

界面分爲兩部分,上面是文件基本信息,下面是文件上傳自定義控件,我這裏實現的是一個案件上傳多個監控視頻功能。如下是詳細步驟:佈局

第一步:建立用戶自定義控件BigFileUpload.xaml

文件上傳是一個很是經常使用的功能,爲了所寫的程序能很是方便地屢次重複使用,我決定將其處理爲一個用戶自定義控件(UserControl)。post

咱們先在項目中建立一個FileUpload文件夾,在其目錄下新建一個WPF自定義控件文件命名爲BigFileUpload.xaml,這樣就表示文件上傳是一個獨立的小模塊使用。之因此用WPF自定義控件是由於WPF頁面效果好看點,並且我想之後可能大部分C/S程序都會漸漸的由WinForm轉向WPF吧,固然建立Window Forms用戶控件也是沒有問題的。而後咱們須要作一個下圖效果的頁面佈局:測試

 

使用HttpWebRequest實現大文件上傳 - 飛天心宏 - 飛天心宏的博客

 

前臺設計代碼以下: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 addressstring fileNamePathstring saveNameProgressBarprogressBar)

        {

            HttpWebRequestReturn hwr;

 

            // 要上傳的文件

            FileStream fs = new FileStream(fileNamePathFileMode.OpenFileAccess.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 senderRoutedEventArgs 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 senderRoutedEventArgs 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(ServerAddressfileNamePathsaveName,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 addressstring fileNamePathstring saveNameProgressBar progressBar){}

值得一提的是這裏的返回類型HttpWebRequestReturn(點擊查看定義)是爲了和數據庫對應本身定義的一個類,繼承自統一返回類型TwiReturn(點擊查看定義),裏面記錄了文件服務器反饋的綜合信息。

 

第二步:建立服務器響應程序BigFileUploadServerApp

很顯然文件上傳至服務器後須要有個對應的響應程序。那麼咱們再建立一個單獨的Web應用程序(命名爲:BigFileUploadServerApp),發佈在服務器中的IIS上,只須要一個默認的Default.aspx頁面和一個FileUpload空文件夾便可,咱們將FileUpload文件夾所存放的目錄做爲文件上傳至服務器存放的目錄。

Default.aspx.cs代碼也至關簡單:

        protected void Page_Load(object senderEventArgs 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,而是自定義繼承自EventArgsEventFileUploadArg,定義以下:

    /// <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),而後作一個簡單的佈局,以下圖:

使用HttpWebRequest實現大文件上傳 - 飛天心宏 - 飛天心宏的博客

 

上面的都是文件基本信息,下面的是一個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.YesNoMessageBoxIcon.Question);

                    if (dResult == DialogResult.Yes)

                    {

                        panel1.Controls.Clear();

                        AddBfuControl();

                    }

                    else

                    {

                        this.Hide();

                    }

                }

                else

                {

                    MessageBox.Show(twi.message"提示");

                }             

               

            }

            else

            {

                MessageBox.Show(arg.HwrReturn.message,"提示");

            }

        }

}

 

 

通過不懈的努力,和這麼長時間的耐心,到這裏已經完成了咱們所要作的工做了,看看咱們的功能界面吧,不容易呀!

 

使用HttpWebRequest實現大文件上傳 - 飛天心宏 - 飛天心宏的博客

 

這裏有個提示框提示用戶是否繼續上傳,若是是那麼程序將刷新一下用戶控件,可是上面的文件案件基本信息仍然保留,這樣就作到了我所但願的一個案件對應上傳多個視頻的效果。

 

 

說在最後

該解決方案成功實現了基於HttpWebRequest的方式實現大文件上傳,相對來講這個界面仍是挺好看的。對應大文件上傳有人也許會說用FTP的方式處理,聽人說配置有點複雜,因爲我我的比較懶,因此沒去親自試驗,之後有機會再試試FTP的方式,只有我親自試成功了,我纔會寫出來。

最後,因爲我的技術水平和寫做能力的限制,文章有不足之處再所不免,還望你們批評指正,若是發現問題,我也將會盡快修改。知錯、認錯、改錯。

相關文章
相關標籤/搜索