不论是网页开发还是客户端程序开发,都有可能遇到文件下载的实现,最简单的办法好像是说使用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 变量4、在下载过程中,需要不断想前台回显当前的下载状态,这里使用了Timer类,实现每秒对下载状态的回显,实现过程如下:
//准备下载的文件
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
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个线程分别对应,然后进行下载,最后得到的文件与文件相同!