使用HttpWebRequest实现大文件上传

Author:xuzhihong

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方式来实现文件分块上传数据流至服务器。

按照惯例还是先预览一下文件上传最后的效果吧,如下图所示:

 

使用HttpWebRequest实现大文件上传 - 飞天心宏 - 飞天心宏的博客

 

界面分为两部分,上面是文件基本信息,下面是文件上传自定义控件,我这里实现的是一个案件上传多个监控视频功能。以下是详细步骤:

第一步:创建用户自定义控件BigFileUpload.xaml

文件上传是一个非常常用的功能,为了所写的程序能非常方便地多次重复使用,我决定将其处理为一个用户自定义控件(UserControl)。

我们先在项目中创建一个FileUpload文件夹,在其目录下新建一个WPF自定义控件文件命名为BigFileUpload.xaml,这样就表示文件上传是一个独立的小模块使用。之所以用WPF自定义控件是因为WPF页面效果好看点,而且我想以后可能大部分C/S程序都会渐渐的由WinForm转向WPF吧,当然创建Window Forms用户控件也是没有问题的。然后我们需要做一个下图效果的页面布局:

 

使用HttpWebRequest实现大文件上传 - 飞天心宏 - 飞天心宏的博客

 

前台设计代码如下:

<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(点击查看定义)类,里面记录了文件服务器反馈的综合信息。

 

第二步:创建服务器响应程序BigFileUploadServerApp

很显然文件上传至服务器后需要有个对应的响应程序。那么我们再创建一个单独的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,而是自定义继承自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.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实现大文件上传 - 飞天心宏 - 飞天心宏的博客

 

这里有个提示框提示用户是否继续上传,如果是那么程序将刷新一下用户控件,但是上面的文件案件基本信息仍然保留,这样就做到了我所希望的一个案件对应上传多个视频的效果。

 

 

说在最后

该解决方案成功实现了基于HttpWebRequest的方式实现大文件上传,相对来说这个界面还是挺好看的。对应大文件上传有人也许会说用FTP的方式处理,听人说配置有点复杂,由于我个人比较懒,所以没去亲自试验,以后有机会再试试FTP的方式,只有我亲自试成功了,我才会写出来。

最后,由于个人技术水平和写作能力的限制,文章有不足之处再所难免,还望大家批评指正,如果发现问题,我也将会尽快修改。知错、认错、改错。

 

转载于:https://www.cnblogs.com/zhangxiaozhong/p/3232177.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值