现在的位置: 首页 > 综合 > 正文

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

2013年01月22日 ⁄ 综合 ⁄ 共 12972字 ⁄ 字号 评论关闭

不论是网页开发还是客户端程序开发,都有可能遇到文件下载的实现,最简单的办法好像是说使用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个线程分别对应,然后进行下载,最后得到的文件与文件相同!

抱歉!评论已关闭.