现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

类似迅雷下载实现大文件断点续传

2015-11-11 14:17 工业·编程 ⁄ 共 17582字 ⁄ 字号 暂无评论

不论是网页开发还是客户端程序开发,都有可能遇到文件下载的实现,最简单的办法好像是说使用WebClient.DownLoadFile()实现,但是如果遇到大文件需要做到断点续传,怎么办?我们看看做到断点续传需要满足的条件:

1.用户指定下载文件路径和本地保存路径(废话!)

2.用户点击开始,程序进入文件下载阶段;

3.在下载过程中,用户可以进行暂停、取消、退出程序后下次接着下载;

以上是从用户角度分析,那么看看针对这些需求,程序里面需要做些什么?这里首先需要知道,如果需要进行暂停等操作,我们需要使用到.Net下的HttpWebRequest和HttpWebResponse等类,在下载过程中,我们把文件看做一段路程,将这段路程划分为几个小路程(运动员在长跑中经常做的中途目标),这几个小路程分别使用不同的线程进行下载,下载完成后将得到的流写入本地磁盘,在将流写入本地磁盘时,我们就需要知道这段流在原始文件中的起始点,写入磁盘时,根据起始点,写入已下载的流;

下面我们看看程序需要做的事情:

1、下载文件路径和本地保存路径;

2、用户点击开始后,程序首先获取下载文件的流,通过保存文件名称和流大小确定磁盘中以前是否下载过该文件(这里有一个风险,第一次下载文件后程序退出了,第二次下载不同的文件,刚好该文件的文件名称和流大小都与第一次下载的一模一样,这里就会出现流写入问题,但是这种几率比较少,目前暂时没找到很好的解决办法,只能通过流的头文件进行对比检查是否同一个文件),如果用户以前没有下载过该文件,首先在磁盘中建立一个空文件,大小与源文件相同;代码如下:

/// <summary> 
/// 检查文件是否存在 
/// </summary> 
private void CheckFileOrCreateFile() 

    lock (locker) 
    { 
        //检查文件是否存在,需要重设计业务逻辑 
        if (File.Exists(localAdress)) 
            return; 
        using (FileStream fileStream = File.Create(localAdress)) 
        { 
            long createdSize = 0; 
            byte[] buffer = new byte[4096]; 
            while (createdSize < _file.FileSize) 
            { 
                int bufferSize = (_file.FileSize - createdSize) < 4096 ? (int)(_file.FileSize - createdSize) : 4096; 
                fileStream.Write(buffer, 0, bufferSize); 
                createdSize += bufferSize; 
            } 
        } 
    } 

3、在下载过程中,需要记录已下载时间,已下载流大小、当前下载速度等参数,供前台界面查看当前的下载状态;
需要维护的变量和下载类构造函数如下:
#region 变量 
//准备下载的文件 
private IDownLoadFile _file; 
 
//准备下载的文件转换的流 
private Stream stream = null; 
 
//下载状态 
private DownLoadStatus status; 
 
//下载文件在本机的保存位置 
private string localAdress; 
 
//lock锁对象 
static object locker = new object(); 
 
//下载文件在内存中缓存的大小 
private int cacheSize; 
 
//读取下载文件流使用的buffer大小 
private int bufferSize; 
 
//已下载大小 
private long downLoadSize; 
 
//已下载大小复制标记 
private long downLoadSizeFlag; 
 
//上一秒时已下载总大小 
private long BeforSecondDownLoadSize; 
 
//下载已耗时 
private TimeSpan useTime;  
 
//最后一次下载时间 
private DateTime lastStartTime; 
 
//预计下载总耗时 
private TimeSpan allTime; 
 
//当前下载速度 
private double speed; 
 
#endregion 
 
#region 构造方法 
 
/// <summary> 
/// 构造方法 
/// </summary> 
/// <param name="file"></param> 
public DownLoad(IDownLoadFile file, string localAdress) 
    : this(file, localAdress, 1024, 1048576) 


 
/// <summary> 
/// 构造方法 
/// </summary> 
/// <param name="file"></param> 
/// <param name="localAdress"></param> 
/// <param name="bufferSize"></param> 
public DownLoad(IDownLoadFile file, string localAdress, int bufferSize) 
    : this(file, localAdress, bufferSize, 1048576) 
{ } 
 
/// <summary> 
/// 构造方法 
/// </summary> 
/// <param name="file"></param> 
/// <param name="localAdress"></param> 
/// <param name="bufferSize"></param> 
/// <param name="cacheSize"></param> 
public DownLoad(IDownLoadFile file, string localAdress, int bufferSize, int cacheSize) 

    this._file = file; 
    stream = _file.GetFileStream(); 
    this.localAdress = localAdress; 
    this.status = DownLoadStatus.Idle; 
    this.cacheSize = cacheSize; 
    this.bufferSize = bufferSize; 
    this.downLoadSize = 0; 
    this.useTime = TimeSpan.Zero; 
    this.allTime = TimeSpan.Zero; 
    this.speed = 0.00; 
    System.Timers.Timer t = new System.Timers.Timer(); 
    t.Interval = 1000; 
    t.Elapsed += new System.Timers.ElapsedEventHandler(t_Elapsed); 
    t.Start(); 

 
void t_Elapsed(object sender, System.Timers.ElapsedEventArgs e) 

    OnDownLoad(); 

 
#endregion 
4、在下载过程中,需要不断想前台回显当前的下载状态,这里使用了Timer类,实现每秒对下载状态的回显,实现过程如下:
public class DownLoadEventArgs 
    { 
        /// <summary> 
        ///  与每秒下载情况相关的委托 
        /// </summary> 
        /// <param name="sender">事件发起的对象</param> 
        /// <param name="e">参数</param> 
        public delegate void SecondDownLoadEventHandler(Object sender, SecondDownLoadEventArgs e); 
 
        /// <summary> 
        /// 下载时每秒事件相关的参数 
        /// </summary> 
        public class SecondDownLoadEventArgs : EventArgs 
        { 
            //已下载大小 
            public readonly long downLoadSize; 
 
            //下载已耗时 
            public readonly TimeSpan useTime; 
 
            //预计下载总耗时 
            public readonly TimeSpan allTime; 
 
            //当前下载速度 
            public readonly double speed; 
 
            //文件总大小 
            public readonly long fileSize; 
 
            public SecondDownLoadEventArgs(long downLoadSize, TimeSpan useTime, TimeSpan allTime, double speed,long fileSize) 
            { 
                this.downLoadSize = downLoadSize; 
                this.useTime = useTime; 
                this.allTime = allTime; 
                this.speed = speed; 
                this.fileSize = fileSize; 
            } 
        } 
 
    } 
调用过程:

#region 每秒发生事件 
        public event DownLoadEventArgs.SecondDownLoadEventHandler SecondDownLoad; 
 
        public void OnDownLoad() 
        { 
            if (SecondDownLoad != null) 
            { 
                ChangeTime(); 
                this.speed = (downLoadSizeFlag - BeforSecondDownLoadSize) / 1024; 
                BeforSecondDownLoadSize = downLoadSizeFlag; 
                long temp = 0; 
                if (downLoadSizeFlag != 0) 
                    temp = this._file.FileSize / downLoadSizeFlag * (long)this.useTime.TotalSeconds * 10000000; 
                this.allTime = new TimeSpan(temp); 
                DownLoadEventArgs.SecondDownLoadEventArgs e = new DownLoadEventArgs.SecondDownLoadEventArgs(downLoadSizeFlag / 1024, useTime, allTime, speed, this._file.FileSize / 1024); 
                SecondDownLoad(this, e); 
            } 
        } 
        #endregion 

5、下来看看下载实现过程,下载过程应该是这样的,首先使用一个buffer接受服务器响应的字节流,检查内存流是否能写入该字节流,如果内存流剩余空间无法写入,将内存流数据写入本地磁盘,内存流是一个下载缓存,设定一个阈值,当内存流达到阈值后将内存流的数据写入本地磁盘,比方设置为1M;写入磁盘完成后将buffer写入内存流,不断循环,直至所有文件下载完成;下载完成的标志是已下载的流大小与源文件流大小相同;下面看看代码:

/// <summary> 
/// 具体的下载方法 
/// </summary> 
private void Download() 

    //进入下载状态 
    this.status = DownLoadStatus.Downloading; 
 
    //最近一次开始下载时间点 
    this.lastStartTime = DateTime.Now; 
 
    //读取服务器响应流缓存 
    byte[] downloadBuffer = new byte[bufferSize]; 
 
    //服务器实际相应的字节流大小 
    int bytesSize = 0; 
 
    //实际使用缓存的大小 
    long cache = 0; 
 
    //内存缓存 
    MemoryStream downloadCache = new MemoryStream(cacheSize); 
    while (true) 
    { 
        bytesSize = stream.Read(downloadBuffer, 0, downloadBuffer.Length); 
        if (this.status != DownLoadStatus.Downloading || cache + bytesSize >= cacheSize || bytesSize == 0) 
        { 
            WriteCacheToFile(downloadCache, (int)cache); 
            downLoadSize += cache; 
        } 
        if (this.status != DownLoadStatus.Downloading || downLoadSize == _file.FileSize) 
        { 
            break; 
        } 
        downloadCache.Write(downloadBuffer, 0, bytesSize); 
        cache += bytesSize; 
        downLoadSizeFlag += bytesSize; 
    } 
    //更改状态 
    ChangeStatus(); 
 
    //清理资源 
    if (stream != null) 
        stream.Close(); 
    if (downloadCache != null) 
        downloadCache.Close(); 
    Console.WriteLine("complet"); 

6、如果需要使用多线程下载文件,首先需要在外部获取源文件的总大小;在实例化下载类时将源文件分段,每段分别使用一个线程下载;
7、下面是整个下载类代码及调用代码:
//------------------------------------------------------------ 
// All Rights Reserved , Copyright (C) 2011 , lusens  
//------------------------------------------------------------ 
 
using System; 
using System.IO; 
using System.Threading; 
 
namespace Utility.DownLoad 

    /// <summary> 
    /// 下载类 
    ///  
    /// 修改纪录 
    ///  
    ///     2011.12.12 版本:1.0 lusens 创建 
    ///  
    /// 版本:1.0 
    ///  
    /// <author> 
    ///     <name>lusens</name> 
    ///     <date>2011.12.12</date> 
    ///     <EMail>lusens@foxmail.com</EMail> 
    /// </author>  
    /// </summary> 
    public class DownLoad 
    { 
        #region 变量 
        //准备下载的文件 
        private IDownLoadFile _file; 
 
        //准备下载的文件转换的流 
        private Stream stream = null; 
 
        //下载状态 
        private DownLoadStatus status; 
 
        //下载文件在本机的保存位置 
        private string localAdress; 
 
        //lock锁对象 
        static object locker = new object(); 
 
        //下载文件在内存中缓存的大小 
        private int cacheSize; 
 
        //读取下载文件流使用的buffer大小 
        private int bufferSize; 
 
        //已下载大小 
        private long downLoadSize; 
 
        //已下载大小复制标记 
        private long downLoadSizeFlag; 
 
        //上一秒时已下载总大小 
        private long BeforSecondDownLoadSize; 
 
        //下载已耗时 
        private TimeSpan useTime;  
         
        //最后一次下载时间 
        private DateTime lastStartTime; 
 
        //预计下载总耗时 
        private TimeSpan allTime; 
 
        //当前下载速度 
        private double speed; 
 
        #endregion 
 
        #region 构造方法 
 
        /// <summary> 
        /// 构造方法 
        /// </summary> 
        /// <param name="file"></param> 
        public DownLoad(IDownLoadFile file, string localAdress) 
            : this(file, localAdress, 1024, 1048576) 
        { 
        } 
 
        /// <summary> 
        /// 构造方法 
        /// </summary> 
        /// <param name="file"></param> 
        /// <param name="localAdress"></param> 
        /// <param name="bufferSize"></param> 
        public DownLoad(IDownLoadFile file, string localAdress, int bufferSize) 
            : this(file, localAdress, bufferSize, 1048576) 
        { } 
 
        /// <summary> 
        /// 构造方法 
        /// </summary> 
        /// <param name="file"></param> 
        /// <param name="localAdress"></param> 
        /// <param name="bufferSize"></param> 
        /// <param name="cacheSize"></param> 
        public DownLoad(IDownLoadFile file, string localAdress, int bufferSize, int cacheSize) 
        { 
            this._file = file; 
            stream = _file.GetFileStream(); 
            this.localAdress = localAdress; 
            this.status = DownLoadStatus.Idle; 
            this.cacheSize = cacheSize; 
            this.bufferSize = bufferSize; 
            this.downLoadSize = 0; 
            this.useTime = TimeSpan.Zero; 
            this.allTime = TimeSpan.Zero; 
            this.speed = 0.00; 
            System.Timers.Timer t = new System.Timers.Timer(); 
            t.Interval = 1000; 
            t.Elapsed += new System.Timers.ElapsedEventHandler(t_Elapsed); 
            t.Start(); 
        } 
 
        void t_Elapsed(object sender, System.Timers.ElapsedEventArgs e) 
        { 
            OnDownLoad(); 
        } 
 
        #endregion 
 
        #region 控制下载状态 
 
        /// <summary> 
        /// 开始下载文件 
        /// </summary> 
        public void Start() 
        { 
            //检查文件是否存在 
            CheckFileOrCreateFile(); 
            // 只有空闲的下载客户端才能开始 
            if (this.status != DownLoadStatus.Idle) 
                throw new ApplicationException("只有空闲的下载客户端才能开始."); 
            // 开始在后台线程下载 
            BeginDownload(); 
        } 
 
        /// <summary> 
        /// 暂停下载 
        /// </summary> 
        public void Pause() 
        { 
            if (this.status != DownLoadStatus.Downloading) 
                throw new ApplicationException("只有正在下载的客户端才能暂停."); 
 
            // 后台线程会查看状态,如果状态时暂停的, 
            // 下载将会被暂停并且状态将随之改为暂停. 
            this.status = DownLoadStatus.Pausing; 
        } 
 
        /// <summary> 
        /// 重新开始下载. 
        /// </summary> 
        public void Resume() 
        { 
            // 只有暂停的客户端才能重新下载. 
            if (this.status != DownLoadStatus.Paused) 
                throw new ApplicationException("只有暂停的客户端才能重新下载."); 
 
            // 开始在后台线程进行下载. 
            BeginDownload(); 
        } 
 
        /// <summary> 
        /// 取消下载 
        /// </summary> 
        public void Cancel() 
        { 
            // 只有正在下载的或者是暂停的客户端才能被取消. 
            if (this.status != DownLoadStatus.Paused && this.status != DownLoadStatus.Downloading) 
                throw new ApplicationException("只有正在下载的或者是暂停的客户端才能被取消."); 
 
            // 后台线程将查看状态.如果是正在取消, 
            // 那么下载将被取消并且状态将改成已取消. 
            this.status = DownLoadStatus.Canceling; 
        } 
 
        #endregion 
 
        /// <summary> 
        /// 创建一个线程下载数据. 
        /// </summary> 
        private void BeginDownload() 
        { 
            ThreadStart threadStart = new ThreadStart(Download); 
            Thread downloadThread = new Thread(threadStart); 
            downloadThread.IsBackground = true; 
            downloadThread.Start(); 
        } 
 
        /// <summary> 
        /// 具体的下载方法 
        /// </summary> 
        private void Download() 
        { 
            //进入下载状态 
            this.status = DownLoadStatus.Downloading; 
 
            //最近一次开始下载时间点 
            this.lastStartTime = DateTime.Now; 
 
            //读取服务器响应流缓存 
            byte[] downloadBuffer = new byte[bufferSize]; 
 
            //服务器实际相应的字节流大小 
            int bytesSize = 0; 
 
            //实际使用缓存的大小 
            long cache = 0; 
 
            //内存缓存 
            MemoryStream downloadCache = new MemoryStream(cacheSize); 
            while (true) 
            { 
                bytesSize = stream.Read(downloadBuffer, 0, downloadBuffer.Length); 
                if (this.status != DownLoadStatus.Downloading || cache + bytesSize >= cacheSize || bytesSize == 0) 
                { 
                    WriteCacheToFile(downloadCache, (int)cache); 
                    downLoadSize += cache; 
                } 
                if (this.status != DownLoadStatus.Downloading || downLoadSize == _file.FileSize) 
                { 
                    break; 
                } 
                downloadCache.Write(downloadBuffer, 0, bytesSize); 
                cache += bytesSize; 
                downLoadSizeFlag += bytesSize; 
            } 
            //更改状态 
            ChangeStatus(); 
 
            //清理资源 
            if (stream != null) 
                stream.Close(); 
            if (downloadCache != null) 
                downloadCache.Close(); 
            Console.WriteLine("complet"); 
        } 
 
        /// <summary> 
        /// 检查文件是否存在 
        /// </summary> 
        private void CheckFileOrCreateFile() 
        { 
            lock (locker) 
            { 
                //检查文件是否存在,需要重设计业务逻辑 
                if (File.Exists(localAdress)) 
                    return; 
                using (FileStream fileStream = File.Create(localAdress)) 
                { 
                    long createdSize = 0; 
                    byte[] buffer = new byte[4096]; 
                    while (createdSize < _file.FileSize) 
                    { 
                        int bufferSize = (_file.FileSize - createdSize) < 4096 ? (int)(_file.FileSize - createdSize) : 4096; 
                        fileStream.Write(buffer, 0, bufferSize); 
                        createdSize += bufferSize; 
                    } 
                } 
            } 
        } 
 
        /// <summary> 
        /// 将内存流写入磁盘 
        /// </summary> 
        /// <param name="downloadCache">文件在磁盘的板寸位置</param> 
        /// <param name="cachedSize">cache的大小</param> 
        private void WriteCacheToFile(MemoryStream downloadCache, int cachedSize) 
        { 
            lock (locker) 
            { 
                using (FileStream fileStream = new FileStream(localAdress, FileMode.Open)) 
                { 
                    byte[] cacheContent = new byte[cachedSize]; 
                    downloadCache.Seek(0, SeekOrigin.Begin); 
                    downloadCache.Read(cacheContent, 0, cachedSize); 
                    fileStream.Seek(downLoadSize, SeekOrigin.Begin); 
                    fileStream.Write(cacheContent, 0, cachedSize); 
                } 
            } 
        } 
 
        /// <summary> 
        /// 更新下载状态 
        /// </summary> 
        private void ChangeStatus() 
        { 
            if (this.status == DownLoadStatus.Pausing) 
            { 
                this.status = DownLoadStatus.Paused; 
            } 
            else if (this.status == DownLoadStatus.Canceling) 
            { 
                this.status = DownLoadStatus.Canceled; 
            } 
            else 
            { 
                this.status = DownLoadStatus.Completed; 
                return; 
            } 
        } 
 
        /// <summary> 
        /// 更新下载所用时间 
        /// </summary> 
        private void ChangeTime() 
        { 
            if (this.status == DownLoadStatus.Downloading) 
            { 
                DateTime now = DateTime.Now; 
                if (now != lastStartTime) 
                { 
                    useTime = useTime.Add(now - lastStartTime); 
                    lastStartTime = now; 
                } 
            } 
        } 
 
        #region 每秒发生事件 
        public event DownLoadEventArgs.SecondDownLoadEventHandler SecondDownLoad; 
 
        public void OnDownLoad() 
        { 
            if (SecondDownLoad != null) 
            { 
                ChangeTime(); 
                this.speed = (downLoadSizeFlag - BeforSecondDownLoadSize) / 1024; 
                BeforSecondDownLoadSize = downLoadSizeFlag; 
                long temp = 0; 
                if (downLoadSizeFlag != 0) 
                    temp = this._file.FileSize / downLoadSizeFlag * (long)this.useTime.TotalSeconds * 10000000; 
                this.allTime = new TimeSpan(temp); 
                DownLoadEventArgs.SecondDownLoadEventArgs e = new DownLoadEventArgs.SecondDownLoadEventArgs(downLoadSizeFlag / 1024, useTime, allTime, speed, this._file.FileSize / 1024); 
                SecondDownLoad(this, e); 
            } 
        } 
        #endregion 
    } 

调用代码和界面:

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 WindowsFormsApplication1 

    public partial class Form1 : Form 
    { 
        public Form1() 
        { 
            InitializeComponent(); 
            Control.CheckForIllegalCrossThreadCalls = false; 
        } 
 
        void down_SecondDownLoad(object sender, Utility.DownLoad.DownLoadEventArgs.SecondDownLoadEventArgs e) 
        { 
            this.lblFileSize.Text = "文件总大小:" + e.fileSize.ToString(); 
            this.lblAllTime.Text = "预计总耗时:" + e.allTime.ToString(); 
            this.lblDownLoad.Text = "总下载:" + e.downLoadSize.ToString(); 
            this.lblSpeed.Text = "当前速率" + e.speed.ToString(); 
            this.lblUseTime.Text = "已耗时:" + ((int)e.useTime.TotalSeconds).ToString(); 
            this.progressBar1.Value = Convert.ToInt32(e.downLoadSize * 100 / e.fileSize); 
        } 
 
        private void Form1_Load(object sender, EventArgs e) 
        { 
            Utility.DownLoad.HttpDownLoadFile file = (Utility.DownLoad.HttpDownLoadFile)Utility.DownLoad.DownLoadFileFactory.CreateDownLoadFile(Utility.DownLoad.DownLoadType.HttpDownLoad, this.txtUrl.Text); 
            Utility.DownLoad.DownLoad down = new Utility.DownLoad.DownLoad(file, this.txtLocalPath.Text); 
            down.SecondDownLoad += new Utility.DownLoad.DownLoadEventArgs.SecondDownLoadEventHandler(down_SecondDownLoad); 
            down.Start(); 
        } 
    } 
}
 

以下为所有代码压缩包文件,下载地址:http://download.csdn.net/download/luxin10/3920677

备注:在后期实现下载任务导入导出等功能时,可以这么考虑:

在建立迅雷下载任务时,迅雷首先建立一个源文件的空文件,例如:稻草狗DVD中字.rmvb.td,这个文件的大小与实际文件相同,另外还建立了一个文件“稻草狗DVD中字.rmvb.td.cfg”,这个文件只有12K,我们是否可以这样考虑,如果迅雷在下载一个文件时使用了50个线程,即将文件切割为50个流文件,这个“稻草狗DVD中字.rmvb.td.cfg”文件里面是否记录了该50个线程的下载起始点,即对应程序里面的“startPoint”和“endPoint”字段,并且还记录了50个线程已下载的流大小,在导入任务时,首先读取这些数据,在下载时重建50个线程分别对应,然后进行下载,最后得到的文件与文件相同!

作者:鲁信

给我留言

留言无头像?