2012年10月7日星期日

用 C# 实现 HTTP 协议多线程下载文件

用 C# 实现 HTTP 协议多线程下载文件

本文内容

  • 环境
  • WebRequest/WebResponse 和 HttpWebRequest/HttpWebResponse
  • 演示
  • 修改记录

 

环境


  • 开发工具:VS 2010/.NET Framework 4.0
  • 系统环境:Microsoft Windows 7

 

WebRequest/WebResponse 和 HttpWebRequest/HttpWebResponse 


System.Net 命名空间为当前网络使用的多种协议提供了简单的编程接口。WebRequest 和 WebResponse 类形成了所谓的可插接式协议的基础,可插接式协议是网络服务的一种实现,它使您能够开发使用 Internet 资源的应用程序,而不必考虑各种不同协议的具体细节。

  • WebRequest 类

WebRequest 类发出对统一资源标识符(Uniform Resource Identifier,URI)的请求。它是一个抽象类。WebRequest 是 .NET Framework 的请求/响应模型的 abstract 基类,用于访问 Internet 数据。使用该请求/响应模型的应用程序可以用协议不可知的方式从 Internet 请求数据,在这种方式下,应用程序处理 WebRequest 类的实例,而协议特定的子类则执行请求的具体细节。请求从应用程序发送到某个特定的 URI,如服务器上的网页。URI 从一个为应用程序注册的 WebRequest 子代列表中确定要创建的适当子类。注册 WebRequest 子代通常是为了处理某个特定的协议(如 HTTP 或 FTP),但是也可以注册它以处理对特定服务器或服务器上路径的请求。

  • WebResponse 类

WebResponse类提供来自统一资源标识符(URI)的响应。它是一个抽象类。WebResponse 类是 abstract 基类,协议特定的响应类从该抽象基类派生。应用程序可以使用 WebResponse 类的实例以协议不可知的方式参与请求和响应事务,WebResponse 派生的协议特定的类携带请求的详细信息。客户端应用程序不直接创建 WebResponse 对象;而是调用 WebRequest 实例的 GetResponse 方法来创建。

  • HttpWebRequest 类

HttpWebRequest 类提供 WebRequest 类的 HTTP 特定的实现。HttpWebRequest 类对 WebRequest 中定义的属性和方法提供支持,也对用户直接与 HTTP 服务器交互的附加属性和方法提供支持。不要使用 HttpWebRequest 构造函数。使用 WebRequest.Create 方法初始化新的 HttpWebRequest 对象。如果 URI 的方案是 http:// 或 https://,则 Create 返回 HttpWebRequest 对象。GetResponse 方法向 RequestUri 属性中指定的资源发出同步请求,并返回包含该响应的 HttpWebResponse。可以使用 BeginGetResponse 和 EndGetResponse 方法对资源发出异步请求。当要向资源发送数据时,GetRequestStream 方法返回用于发送数据的 Stream 对象。BeginGetRequestStream 和 EndGetRequestStream 方法提供对发送数据流的异步访问。对于使用 HttpWebRequest 的客户端验证身份,客户端证书必须安装在当前用户的“我的证书”存储区中。

  • HttpWebResponse 类

HttpWebResponse 类提供 WebResponse 类的 HTTP 特定的实现。

此类包含对 WebResponse 类中的属性和方法的 HTTP 特定用法的支持。HttpWebResponse 类用于生成发送 HTTP 请求和接收 HTTP 响应的 HTTP 独立客户端应用程序。

说明:不要混淆 HttpWebResponse 和 HttpResponse 类;后者用于 ASP.NET 应用程序,而且它的方法和属性是通过 ASP.NET 的内部 Response 对象公开的。

决不要直接创建 HttpWebResponse 类的实例。而应当使用通过调用 HttpWebRequest.GetResponse 所返回的实例。您必须调用 Stream.Close 方法或 HttpWebResponse.Close 方法来关闭响应并将连接释放出来供重用。不必同时调用 Stream.Close 和 HttpWebResponse.Close,但这样做不会导致错误。

 

演示


下面演示利用 HTTP 协议编写一个多线程下载本文源代码的程序,源代码地址为 http://files.cnblogs.com/liuning8023/HttpDownload.rar。

1,新建项目 "HttpDownload";

2,Form1.Desginer.cs 代码如下所示:

namespace HttpDownload
{
    partial class Form1
    {
        /// <summary>
        /// 必需的设计器变量。
        /// </summary>
        private System.ComponentModel.IContainer components = null;
 
        /// <summary>
        /// 清理所有正在使用的资源。
        /// </summary>
        /// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }
 
        #region Windows 窗体设计器生成的代码
 
        /// <summary>
        /// 设计器支持所需的方法 - 不要
        /// 使用代码编辑器修改此方法的内容。
        /// </summary>
        private void InitializeComponent()
        {
            this.lst_processing = new System.Windows.Forms.ListBox();
            this.lbl_url = new System.Windows.Forms.Label();
            this.lbl_localFile = new System.Windows.Forms.Label();
            this.lbl_threadNum = new System.Windows.Forms.Label();
            this.txt_url = new System.Windows.Forms.TextBox();
            this.txt_localFile = new System.Windows.Forms.TextBox();
            this.txt_threadNum = new System.Windows.Forms.TextBox();
            this.btn_rec = new System.Windows.Forms.Button();
            this.txt_overTime = new System.Windows.Forms.TextBox();
            this.lbl_overTime = new System.Windows.Forms.Label();
            this.SuspendLayout();
            // 
            // lst_processing
            // 
            this.lst_processing.FormattingEnabled = true;
            this.lst_processing.ItemHeight = 12;
            this.lst_processing.Location = new System.Drawing.Point(12, 12);
            this.lst_processing.Name = "lst_processing";
            this.lst_processing.Size = new System.Drawing.Size(342, 364);
            this.lst_processing.TabIndex = 0;
            // 
            // lbl_url
            // 
            this.lbl_url.AutoSize = true;
            this.lbl_url.Location = new System.Drawing.Point(377, 17);
            this.lbl_url.Name = "lbl_url";
            this.lbl_url.Size = new System.Drawing.Size(47, 12);
            this.lbl_url.TabIndex = 1;
            this.lbl_url.Text = "文件URL";
            // 
            // lbl_localFile
            // 
            this.lbl_localFile.AutoSize = true;
            this.lbl_localFile.Location = new System.Drawing.Point(377, 42);
            this.lbl_localFile.Name = "lbl_localFile";
            this.lbl_localFile.Size = new System.Drawing.Size(53, 12);
            this.lbl_localFile.TabIndex = 2;
            this.lbl_localFile.Text = "本地地址";
            // 
            // lbl_threadNum
            // 
            this.lbl_threadNum.AutoSize = true;
            this.lbl_threadNum.Location = new System.Drawing.Point(377, 74);
            this.lbl_threadNum.Name = "lbl_threadNum";
            this.lbl_threadNum.Size = new System.Drawing.Size(41, 12);
            this.lbl_threadNum.TabIndex = 3;
            this.lbl_threadNum.Text = "线程数";
            // 
            // txt_url
            // 
            this.txt_url.Location = new System.Drawing.Point(450, 17);
            this.txt_url.Name = "txt_url";
            this.txt_url.Size = new System.Drawing.Size(353, 21);
            this.txt_url.TabIndex = 4;
            this.txt_url.Text = "http://files.cnblogs.com/liuning8023/HttpDownload.rar";
            // 
            // txt_localFile
            // 
            this.txt_localFile.Location = new System.Drawing.Point(450, 44);
            this.txt_localFile.Name = "txt_localFile";
            this.txt_localFile.Size = new System.Drawing.Size(232, 21);
            this.txt_localFile.TabIndex = 5;
            this.txt_localFile.Text = "c:////download.rar";
            // 
            // txt_threadNum
            // 
            this.txt_threadNum.Location = new System.Drawing.Point(450, 71);
            this.txt_threadNum.Name = "txt_threadNum";
            this.txt_threadNum.Size = new System.Drawing.Size(232, 21);
            this.txt_threadNum.TabIndex = 6;
            this.txt_threadNum.Text = "5";
            // 
            // btn_rec
            // 
            this.btn_rec.Location = new System.Drawing.Point(607, 137);
            this.btn_rec.Name = "btn_rec";
            this.btn_rec.Size = new System.Drawing.Size(75, 23);
            this.btn_rec.TabIndex = 7;
            this.btn_rec.Text = "接收";
            this.btn_rec.UseVisualStyleBackColor = true;
            this.btn_rec.Click += new System.EventHandler(this.btn_rec_Click);
            // 
            // txt_overTime
            // 
            this.txt_overTime.Location = new System.Drawing.Point(450, 99);
            this.txt_overTime.Name = "txt_overTime";
            this.txt_overTime.Size = new System.Drawing.Size(232, 21);
            this.txt_overTime.TabIndex = 8;
            // 
            // lbl_overTime
            // 
            this.lbl_overTime.AutoSize = true;
            this.lbl_overTime.Location = new System.Drawing.Point(377, 102);
            this.lbl_overTime.Name = "lbl_overTime";
            this.lbl_overTime.Size = new System.Drawing.Size(53, 12);
            this.lbl_overTime.TabIndex = 9;
            this.lbl_overTime.Text = "结束时间";
            // 
            // Form1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(815, 384);
            this.Controls.Add(this.lbl_overTime);
            this.Controls.Add(this.txt_overTime);
            this.Controls.Add(this.btn_rec);
            this.Controls.Add(this.txt_threadNum);
            this.Controls.Add(this.txt_localFile);
            this.Controls.Add(this.txt_url);
            this.Controls.Add(this.lbl_threadNum);
            this.Controls.Add(this.lbl_localFile);
            this.Controls.Add(this.lbl_url);
            this.Controls.Add(this.lst_processing);
            this.Name = "Form1";
            this.Text = "Form1";
            this.ResumeLayout(false);
            this.PerformLayout();
 
        }
 
        #endregion
 
        public System.Windows.Forms.ListBox lst_processing;
        private System.Windows.Forms.Label lbl_url;
        private System.Windows.Forms.Label lbl_localFile;
        private System.Windows.Forms.Label lbl_threadNum;
        private System.Windows.Forms.TextBox txt_url;
        private System.Windows.Forms.TextBox txt_localFile;
        private System.Windows.Forms.TextBox txt_threadNum;
        private System.Windows.Forms.Button btn_rec;
        private System.Windows.Forms.TextBox txt_overTime;
        private System.Windows.Forms.Label lbl_overTime;
    }
}

说明:

1)  添加四个 Lable 控件和 TextBox 控件;一个 ListBox 控件;一个 Button 控件;

2)  ListBox 控件需要将 private 属性改为 public,以便在外部使用。

3,新建 HttpMultiThreadDownload.cs 类,代码如下:

using System;
 
namespace HttpDownload
{
    /// <summary>
    /// 调用外部窗体
    /// </summary>
    /// <param name="text"></param>
    delegate void ProcessingCallback(string processing);
 
    public class HttpMultiThreadDownload
    {
        const int _bufferSize = 512;
        public Form1 frm;
        public int threadIndex;                    //线程代号                 
        public string url;                         //接收文件的URL
        public System.Net.HttpWebRequest request;
 
        public HttpMultiThreadDownload(Form1 form, int threadIdx)
        {
            frm = form;
            threadIndex = threadIdx;
            url = frm.url;
        }
        /// <summary>
        /// 析构方法
        /// </summary>
        ~HttpMultiThreadDownload()
        {
            if (!frm.InvokeRequired)
            {
                frm.Dispose();
            }
        }
        /// <summary>
        /// 接收
        /// </summary>
        public void receive()
        {
            string filename = frm.fileNames[threadIndex];  // 线程临时文件
            byte[] buffer = new byte[_bufferSize];         //接收缓冲区
            int readSize = 0;                              //接收字节数
            System.IO.FileStream fs = new System.IO.FileStream(filename, System.IO.FileMode.Create);
            System.IO.Stream ns = null;
 
            this.SetListBox("线程 " + threadIndex.ToString() + " 开始接收......");
            try
            {
                request = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(url);
                request.AddRange(frm.fileStartPos[threadIndex], frm.fileStartPos[threadIndex] + frm.fileSize[threadIndex]);
                ns = request.GetResponse().GetResponseStream();
                readSize = ns.Read(buffer, 0, _bufferSize);
                this.SetListBox("线程 " + threadIndex.ToString() + " 正在接收 " + readSize);
                while (readSize > 0)
                {
                    fs.Write(buffer, 0, readSize);
                    readSize = ns.Read(buffer, 0, _bufferSize);
                    this.SetListBox("线程 " + threadIndex.ToString() + " 正在接收 " + readSize);
                }
                fs.Close();
                ns.Close();
            }
            catch (Exception er)
            {
                System.Windows.Forms.MessageBox.Show(er.Message);
                fs.Close();
            }
            this.SetListBox("进程 " + threadIndex.ToString() + " 接收完毕!");
            frm.threadStatus[threadIndex] = true;
        }
        private void SetListBox(string processing)
        {
            if (frm.lst_processing.InvokeRequired)
            {
                ProcessingCallback d = new ProcessingCallback(SetListBox);
                frm.Invoke(d, new object[] { processing });
            }
            else
            {
                frm.lst_processing.Items.Add(processing);
            }
        }
    }
}

说明:

1)该类使用了析构函数。通常,.NET Framework 垃圾回收器会隐式地管理对象的内存分配和释放。 但是,当应用程序封装窗口、文件和网络连接这类非托管资源时,应当使用析构函数释放这些资源。 当对象符合析构时,垃圾回收器将运行对象的 Finalize 方法。

参考:http://msdn.microsoft.com/zh-cn/library/66x5fx1b.aspx

2)另外,SetListBox 方法确保以线程安全方式访问 ListBox 控件。

参考:http://msdn.microsoft.com/zh-cn/library/ms171728(v=VS.90).aspx

4,Form1.cs 代码如下所示:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
 
namespace HttpDownload
{
    public partial class Form1 : Form
    {
        public int threadNum;          // 进程
        public bool[] threadStatus;    // 每个线程结束标志
        public string[] fileNames;     // 每个线程接收文件的文件名
        public int[] fileStartPos;     // 每个线程接收文件的起始位置
        public int[] fileSize;         // 每个线程接收文件的大小
        public string url;             // 接受文件的URL
        public bool isMerge;           // 文件合并标志
 
        public Form1()
        {
            InitializeComponent();
        }
        /// <summary>
        ///
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btn_rec_Click(object sender, EventArgs e)
        {
            url = this.txt_url.Text.Trim().ToString();
 
            System.Net.HttpWebRequest request;
            long fileSizeAll = 0;
 
            request = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(url);
            fileSizeAll = request.GetResponse().ContentLength;
            request.Abort();
            threadNum = int.Parse(this.txt_threadNum.Text.Trim().ToString());
            Init(fileSizeAll);
            for (int i = 0; i < threadNum; i++)
            {
                this.lst_processing.Items.Add("线程" + i + ":" +
                    " 线程状态:" + threadStatus[i].ToString() + "-开始位置:" + fileStartPos[i].ToString() + "-大小:" + fileSize[i].ToString());
            }
            this.lst_processing.Items.Add("                                  文件总大小:" + fileSizeAll);
 
            // 定义并启动线程数组
            System.Threading.Thread[] threads = new System.Threading.Thread[threadNum];
            HttpMultiThreadDownload[] httpDownloads = new HttpMultiThreadDownload[threadNum];
            for (int i = 0; i < threadNum; i++)
            {
                httpDownloads[i] = new HttpMultiThreadDownload(this, i);
                threads[i] = new System.Threading.Thread(new System.Threading.ThreadStart(httpDownloads[i].receive));
                threads[i].Start();
            }
            System.Threading.Thread merge = new System.Threading.Thread(new System.Threading.ThreadStart(MergeFile));
            merge.Start();
            this.txt_overTime.Text = DateTime.Now.ToString();
        }
        /// <summary>
        /// 初始化
        /// </summary>
        /// <remarks>
        /// 每个线程平均分配文件大小,剩余部分由最后一个线程完成
        /// </remarks>
        /// <param name="filesize"></param>
        private void Init(long filesize)
        {
            threadStatus = new bool[threadNum];
            fileNames = new string[threadNum];
            fileStartPos = new int[threadNum];
            fileSize = new int[threadNum];
            int filethread = (int)filesize / threadNum;
            int filethreade = filethread + (int)filesize % threadNum;
            for (int i = 0; i < threadNum; i++)
            {
                threadStatus[i] = false;
                fileNames[i] = i.ToString() + ".dat";
                if (i < threadNum - 1)
                {
                    fileStartPos[i] = filethread * i;
                    fileSize[i] = filethread - 1;
                }
                else
                {
                    fileStartPos[i] = filethread * i;
                    fileSize[i] = filethreade - 1;
                }
            }
        }
        /// <summary>
        /// 合并文件
        /// </summary>
        public void MergeFile()
        {
            while (true)
            {
                isMerge = true;
                for (int i = 0; i < threadNum; i++)
                {
                    if (threadStatus[i] == false) // 若有未结束线程,则等待
                    {
                        isMerge = false;
                        System.Threading.Thread.Sleep(100);
                        break;
                    }
                }
                if (isMerge == true) // 否则,停止等待
                {
                    break;
                }
            }
 
            System.IO.FileStream fs;
            int bufferSize = 512;
            int readSize;
            string downFileNamePath = txt_localFile.Text.Trim().ToString();
            byte[] bytes = new byte[bufferSize];
            fs = new System.IO.FileStream(downFileNamePath, System.IO.FileMode.Create);
 
            for (int k = 0; k < threadNum; k++)
            {
                fs = new System.IO.FileStream(fileNames[k], System.IO.FileMode.Open);
                while (true)
                {
                    readSize = fs.Read(bytes, 0, bufferSize);
                    if (readSize > 0)
                    {
                        fs.Write(bytes, 0, readSize);
                    }
                    else
                    {
                        break;
                    }
                }
                fs.Close();
            }
            fs.Close();
            System.Windows.Forms.MessageBox.Show("接收完毕!!!");
        }
    }
}

程序运行结果如下图所示:

2012-10-07_001805

 

修改记录


  • 2012-10-07 [ADD][UPDATE]

下载 Demo


TAG: