实现自己的音乐搜索软件(三)
2010-10-24 01:50
169 查看
实现自己的音乐搜索软件(一)
实现自己的音乐搜索软件(二)
前面介绍了实现自己的搜索模块,我们已经能够从互联网上搜索到自己想要的音乐的地址了,所以这一篇文章主要介绍实现自己的下载的模块。对于下载模块,也没有做的很复杂,毕竟对于象迅雷那种很强劲的下载工具也不是很了解,所以只能在功能上模仿一个形似。
上图右侧显示了下载模块的基本功能,支持多任务下载,并且支持断点续传。所以可以进行暂停,取消等控制。而下载并没有使用多线程下载,因为相对要复杂一些,所以目前只是单线程下载。对于MP3这种小文件来说速度还是不错的。
对于下载模块多任务实现,没有使用异步方法,或者是建立线程池去控制。而是使用了基于事件的异步模型。在.NET中基于此模型实现的控件就是BackGroundWork,他能很好的解决子线程和UI主线程间的通信,并且使用起来非常方便。但是他最大的问题是,他是以一个控件实现的,并且不支持多任务,所以我们想要使用他的话,每一个任务都需要使用一个控件,并且如果像编写一个与UI无关的下载模块,却引人一个UI控件,总是不太好,所以我们下载的核心就是自己实现一个可以支持多任务的BackGroundWork模块,用来管理下载。除了下载管理模块之外,我们还需要具体的下载功能以及下载后的文件管理功能。
.
.
如上图,下载模块一共有3个文件,其中DownloadCore.cs是对HttpWebRequest的包装主要负责下载,支持断线续传;DownloadManagement.cs是模块的核心,负责下载任务的调度,控制和管理,整个功能是模仿BackGroundWork进行,但是支持多任务控制,而且支持调度控制;FileManager.cs文件是对下载文件的管理,负责文件重名,下载临时文件等处理。
DownloadManagement在使用方面和BackGroundWork一样,使用时注册DoWork,ProgressChanged,RunWorkerCompleted3个事件。在DoWork事件对应的方法中,调用DownloadCore对象进行下载;在ProgressChanged事件对应的方法中更新下载进度和状态,而在RunWorkerCompleted3事件触发时,调用FileManager对象,管理文件。
.
.
以上是类中定义的一些变量,可以看到,我们定义了3个哈希表,用来保存等待下载和正在下载的任务,而另一个管理同步操作的列表。同时定义了3个同步锁,保证多个任务能同时正确的操作哈希表。而哈希表的key使用的是GUID,这样就能保证唯一性,每个下载任务都有一个GUID。
前面两个哈希表比较好理解,存放的对象是DownloadMusicTask,而后一个用来管理同步操作的哈希表存放的是AsyncOperation对象。在前面的BackgroundWork的内部实现的文章中有介绍过这个对象,他是BackGroundWork的核心,用来和UI线程进行通信的。他具体的实现细节前面文章介绍过了,这就就不在说了。但是BackGroundWork是单任务的,所以只存在一个AsyncOperation对象,而我们这里支持多任务,所以必须有多个AsyncOperation对象。
您的类应该在每个任务开始时调用 AsyncOperationManager.CreateOperation,为每个异步任务获取 AsyncOperation 对象。为了使客户端能够区分不同的异步任务,AsyncOperationManager.CreateOperation 采用一个参数作为客户端提供的独一无二的标记,这个就是 UserSuppliedState 属性。然后客户端代码便可使用该属性来标识当前引发进度或完成事件的特定异步任务。
以上是MSDN对他使用的解释,所以我们在创建对象时,把任务的GUID传递给了他,保证了唯一性,而在BackGroudWork中传递的是null,因为只有一个对象。
我们的的多任务下载就是以此为基础进行的。每个任务都有一个GUID,当需要与UI交互时,我们根据GUID从哈希表中查找到相应的AsyncOperation对象,然后调用Post或者PostOperationCompleted方法和UI进行通信。
.
参考:《使用基于事件的异步模式进行多线程编程》http://msdn.microsoft.com/zh-cn/library/hkasytyf(v=VS.90).aspx
基于事件的异步模式可以采用多种形式,具体取决于某个特定类支持的操作的复杂程度。最简单的类可能只有一个方法名称Async 方法和一个对应的方法名称Completed 事件。更复杂的类可能有若干个方法名称Async 方法(每种方法都有一个对应的方法名称Completed 事件),以及这些方法的同步版本。这些类分别支持各种异步方法的取消、进度报告和增量结果。
以上是我们的DownloadManagement 类中定义的事件成员。为了简便,我们使用了BackgroundWork使用的委托原型。当然也可以自己定义事件的委托类型以及参数类型。随后我们定义了3个事件发生时通知订阅者的虚方法。在这里我们并没有显示的去实现事件的订阅与注销,因为我们这里事件很少,而且也不太关注CLR实现的事件潜在的同步问题,以为使用对象自己作为同步锁,对于大多数程序并不是一个问题。
最后我们就需要定义三个触发事件的方法,在开始下载,下载中以及下载完成时触发。但是我们要注意到一个问题。我们这里定义了三个事件,注册这些事件的方法应该都是UI中的方法。而我们DownloadManagement本质是使用多线程。所以就存在一个子线程修改UI控件的问题,这是不允许的,所以我们需要使用前面提到的AsyncOperation对象,把这些方法发送到UI线程执行。而在我们这里,就是要把这3个触发事件的方法,发送到UI线程执行。所以我们需要利用委托来包装这3个方法。
所以我们定义了和这3个方法有关的委托,并在构造函数中和相应的方法进行绑定。其中WorkerThreadStartDelegate是我们自定义的委托类型,因为开始下载的方法需要在一个新的线程中执行,使用了委托的Invoke方法。而另外两个方法和SendOrPostCallback委托绑定,此委托是系统定义的:表示在消息即将被调度到同步上下文时要调用的方法。
.
整个方法比较简单,传入一个下载任务对象,检查次任务是否已经存在,如果不存在就新建一个对应的AsyncOperation对象,并添加到哈希表中。根据目前任务数量决定是进入下载队列还是等待队列。如果可以下载,我们使用了threadStart.BeginInvoke 方法让这个任务在一个新的线程中运行,这也是我们这个管理类的本质,仍旧是使用多线程。
以上就是接受到任务后,在新的任务线程中执行的方法。他的作用是管理整个下载过程。首先是触发了注册的DoWork时间的方法,我们这里也就是实际的下载方法。总的来说,这里就是让DownloadCore对象的下载方法在一个新的线程运行。当下载完成后,需要触发RunWorkerCompleted事件,并把注册的方法发送到UI线程执行。这里可以看到使用了AsyncOperation对象的PostOperationCompleted方法,并把operationCompleted委托作为参数传递了过去。
接下来就是进度报告的异步方法。使用了AsyncOperation对象的Post方法,并把progressReporter委托传递了过去。
.
在实现上,是对于每个任务都有两个标志IsCancle 和 IsStop ,我们的工作也就是设置这两个标志的状态。这里有一个问题,我们这个模块很难用在下载以外的地方,因为很难知道如何去结束一个异步操作。这也是为什么BackgroundWork不支持取消的原因。所以我们这里设置的标志位,在下载时会使用到,如果标志位为true,则从正在运行的程序中退出。从前面可以知道,下载结束时,会触发RunWorkerCompleted事件。 对于没有在下载的任务,取消时直接触发RunWorkerCompleted事件。
.
.
.
以上就是下载的实现方式,主要有几点,一个是断点续传,一个是报告进度,另一个就是计算下载速度。
.
当继续下载时,首先从硬盘上读取这个文件,如果存在,则获得文件大小,并设置到HttpWebRequest的AddRange方法中,这样就能从指定的位置请求数据,实现断点续传。另外要注意的是,断点续传时,HttpWebResponse.ContentLength返回的只是剩余数据的大小,而不是全部数据的大小。如果弄错会导致下载的文件变小,播放的MP3就是乱的了。
.
对于计算速度,就是用下载字节数除下载的时间。但是问题就是,如果读取1024个字节就计算一次,可能很不准确,因为读1024自己的时间很短,而且这样更新太快,用户也看不清楚。所以这里我采用了每400ms计算一次速度,实际的效果还是不错的。
.
这里也考虑了在下载是使用一个后缀为mus的文件存放数据,另一个后缀为cfg的文件存放下载信息,比如把DownloadMusicTask对象序列化存入到文件中,这样,即便在下载暂停,程序关闭之后,在下一次打开程序是,还能再次进行下载。而且对于多线程下载时,每个下载块的管理也是很有必要的。但是目前并没有这样做。不过实现起来没有什么难度。
.
.
在后面计划尝试进行歌词的实时显示功能,不过不知道是否有时间。因为进去工作比较忙,在开发一个手机平台上的msn客户端,也打算马上写一系列MSN协议分析的文章。所以时间就很难说了。
源代码SVN:http://code.google.com/p/cmusicsearch/
实现自己的音乐搜索软件(二)
前面介绍了实现自己的搜索模块,我们已经能够从互联网上搜索到自己想要的音乐的地址了,所以这一篇文章主要介绍实现自己的下载的模块。对于下载模块,也没有做的很复杂,毕竟对于象迅雷那种很强劲的下载工具也不是很了解,所以只能在功能上模仿一个形似。
一 下载模块功能
上图右侧显示了下载模块的基本功能,支持多任务下载,并且支持断点续传。所以可以进行暂停,取消等控制。而下载并没有使用多线程下载,因为相对要复杂一些,所以目前只是单线程下载。对于MP3这种小文件来说速度还是不错的。
对于下载模块多任务实现,没有使用异步方法,或者是建立线程池去控制。而是使用了基于事件的异步模型。在.NET中基于此模型实现的控件就是BackGroundWork,他能很好的解决子线程和UI主线程间的通信,并且使用起来非常方便。但是他最大的问题是,他是以一个控件实现的,并且不支持多任务,所以我们想要使用他的话,每一个任务都需要使用一个控件,并且如果像编写一个与UI无关的下载模块,却引人一个UI控件,总是不太好,所以我们下载的核心就是自己实现一个可以支持多任务的BackGroundWork模块,用来管理下载。除了下载管理模块之外,我们还需要具体的下载功能以及下载后的文件管理功能。
.
.
二 下载模块结构
如上图,下载模块一共有3个文件,其中DownloadCore.cs是对HttpWebRequest的包装主要负责下载,支持断线续传;DownloadManagement.cs是模块的核心,负责下载任务的调度,控制和管理,整个功能是模仿BackGroundWork进行,但是支持多任务控制,而且支持调度控制;FileManager.cs文件是对下载文件的管理,负责文件重名,下载临时文件等处理。
DownloadManagement在使用方面和BackGroundWork一样,使用时注册DoWork,ProgressChanged,RunWorkerCompleted3个事件。在DoWork事件对应的方法中,调用DownloadCore对象进行下载;在ProgressChanged事件对应的方法中更新下载进度和状态,而在RunWorkerCompleted3事件触发时,调用FileManager对象,管理文件。
.
.
三 下载管理的实现
对于下载管理DownloadManagement,是参照BackGroundWork实现的,不过他可以支持多任务,并且可以对多任务之间进行管理。在了解本对象实现之前,最好能对BackGroundWork的实现有一定了解,可以参考我之前的文章BackgroundWork的内部实现1 多任务的支持
相对于BackGroundWork,我们支持了多任务,所以必须在内部有一张表来维护这些任务。从而能对这些任务进行调度,而且还必须有一个位置的值作为Task的key,来唯一标识一个任务。/// <summary> /// 等待下载的任务列表 /// </summary> private Dictionary<Guid, DownloadMusicTask> waitDownloadTable; /// <summary> /// 下载中的任务列表 /// </summary> private Dictionary<Guid, DownloadMusicTask> downloadTable; /// <summary> /// 管理任务同步的列表 /// </summary> private Dictionary<Guid, AsyncOperation> asyncOperationtTable; //同步锁 private object waitTableLock; private object downloadTableLock; private object asyncOperationLock;
以上是类中定义的一些变量,可以看到,我们定义了3个哈希表,用来保存等待下载和正在下载的任务,而另一个管理同步操作的列表。同时定义了3个同步锁,保证多个任务能同时正确的操作哈希表。而哈希表的key使用的是GUID,这样就能保证唯一性,每个下载任务都有一个GUID。
前面两个哈希表比较好理解,存放的对象是DownloadMusicTask,而后一个用来管理同步操作的哈希表存放的是AsyncOperation对象。在前面的BackgroundWork的内部实现的文章中有介绍过这个对象,他是BackGroundWork的核心,用来和UI线程进行通信的。他具体的实现细节前面文章介绍过了,这就就不在说了。但是BackGroundWork是单任务的,所以只存在一个AsyncOperation对象,而我们这里支持多任务,所以必须有多个AsyncOperation对象。
您的类应该在每个任务开始时调用 AsyncOperationManager.CreateOperation,为每个异步任务获取 AsyncOperation 对象。为了使客户端能够区分不同的异步任务,AsyncOperationManager.CreateOperation 采用一个参数作为客户端提供的独一无二的标记,这个就是 UserSuppliedState 属性。然后客户端代码便可使用该属性来标识当前引发进度或完成事件的特定异步任务。
以上是MSDN对他使用的解释,所以我们在创建对象时,把任务的GUID传递给了他,保证了唯一性,而在BackGroudWork中传递的是null,因为只有一个对象。
AsyncOperation asyncOperation = AsyncOperationManager.CreateOperation(downloadItem.DownloadTaskID); lock (asyncOperationLock) { asyncOperationtTable.Add(downloadItem.DownloadTaskID, asyncOperation); }
我们的的多任务下载就是以此为基础进行的。每个任务都有一个GUID,当需要与UI交互时,我们根据GUID从哈希表中查找到相应的AsyncOperation对象,然后调用Post或者PostOperationCompleted方法和UI进行通信。
.
2 基于事件的异步实现
我们这里的实现,基本和BackGroudWork一致,不过相对于多任务增加了一些功能。微软也建议对于更复杂的异步应用程序,请考虑实现一个符合基于事件的异步模式的类。参考:《使用基于事件的异步模式进行多线程编程》http://msdn.microsoft.com/zh-cn/library/hkasytyf(v=VS.90).aspx
基于事件的异步模式可以采用多种形式,具体取决于某个特定类支持的操作的复杂程度。最简单的类可能只有一个方法名称Async 方法和一个对应的方法名称Completed 事件。更复杂的类可能有若干个方法名称Async 方法(每种方法都有一个对应的方法名称Completed 事件),以及这些方法的同步版本。这些类分别支持各种异步方法的取消、进度报告和增量结果。
/// <summary> /// 开始下载事件 /// </summary> public event DoWorkEventHandler DoWork; /// <summary> /// 下载中事件 /// </summary> public event ProgressChangedEventHandler ProgressChanged; /// <summary> /// 下载完成事件 /// </summary> public event RunWorkerCompletedEventHandler RunWorkerCompleted; /// <summary> /// 引发开始下载事件的方法 /// </summary> protected virtual void OnDoWork(DoWorkEventArgs e) { if (DoWork != null) { DoWork(this, e); } } /// <summary> /// 引发下载进度变化事件的方法 /// </summary> protected virtual void OnProgressChanged(ProgressChangedEventArgs e) { if (ProgressChanged != null) { ProgressChanged(this, e); } } /// <summary> /// 引发下载结束事件的方法 /// </summary> protected virtual void OnRunWorkerCompleted(RunWorkerCompletedEventArgs e) { if (RunWorkerCompleted != null) { RunWorkerCompleted(this, e); } } /// <summary> /// 开始下载时调用,触发DoWork事件 /// </summary> /// <param name="argument"></param> private void WorkerThreadStart(object argument) { //执行DoWork委托绑定的事件 try { DoWorkEventArgs e = new DoWorkEventArgs(argument); this.OnDoWork(e); } } /// <summary> /// 下载进行时调用,触发ProgressChanged事件 /// </summary> /// <param name="arg"></param> private void ProgressReporter(object arg) { this.OnProgressChanged((ProgressChangedEventArgs)arg); } /// <summary> /// 下载完成时调用,触发RunWorkerCompleted事件 /// </summary> /// <param name="arg"></param> private void AsyncOperationCompleted(object arg) { this.OnRunWorkerCompleted((RunWorkerCompletedEventArgs)arg); }
以上是我们的DownloadManagement 类中定义的事件成员。为了简便,我们使用了BackgroundWork使用的委托原型。当然也可以自己定义事件的委托类型以及参数类型。随后我们定义了3个事件发生时通知订阅者的虚方法。在这里我们并没有显示的去实现事件的订阅与注销,因为我们这里事件很少,而且也不太关注CLR实现的事件潜在的同步问题,以为使用对象自己作为同步锁,对于大多数程序并不是一个问题。
最后我们就需要定义三个触发事件的方法,在开始下载,下载中以及下载完成时触发。但是我们要注意到一个问题。我们这里定义了三个事件,注册这些事件的方法应该都是UI中的方法。而我们DownloadManagement本质是使用多线程。所以就存在一个子线程修改UI控件的问题,这是不允许的,所以我们需要使用前面提到的AsyncOperation对象,把这些方法发送到UI线程执行。而在我们这里,就是要把这3个触发事件的方法,发送到UI线程执行。所以我们需要利用委托来包装这3个方法。
//定义这些委托和相关事件的触发方法相绑定是为了工作线程和UI线程间通信 private delegate void WorkerThreadStartDelegate(object argument); private readonly WorkerThreadStartDelegate threadStart; private readonly SendOrPostCallback progressReporter; private readonly SendOrPostCallback operationCompleted; public DownloadManagement() { //绑定委托方法 this.threadStart = new WorkerThreadStartDelegate(this.WorkerThreadStart); this.progressReporter = new SendOrPostCallback(this.ProgressReporter); this.operationCompleted = new SendOrPostCallback(this.AsyncOperationCompleted); }
所以我们定义了和这3个方法有关的委托,并在构造函数中和相应的方法进行绑定。其中WorkerThreadStartDelegate是我们自定义的委托类型,因为开始下载的方法需要在一个新的线程中执行,使用了委托的Invoke方法。而另外两个方法和SendOrPostCallback委托绑定,此委托是系统定义的:表示在消息即将被调度到同步上下文时要调用的方法。
.
3 实现异步方法
我们这里只实现了异步方法,而没有实现同步方法,因为单线程下载是没有意义的。对于我们来说,这里有开始下载,停止下载,取消下载,报告下载进度这几个异步的方法。并且这几个方法是public的,共外部调用。先看看整个下载功能开始的方法/// <summary>
/// 开始下载
/// </summary>
/// <param name="argument">要下载的音乐信息</param>
public void RunWorkerAsync(DownloadMusicTask downloadItem)
{
//如果目前下载数量小于5条,则通过委托启动新的下载任务
//设置下载状态
downloadItem.IsStop = false;
downloadItem.DownloadStatus = DownloadStatus.ST_READY_DOWNLOAD;
// 对于新增的任务,添加到同步管理表
if (!asyncOperationtTable.ContainsKey(downloadItem.DownloadTaskID))
{
AsyncOperation asyncOperation = AsyncOperationManager.CreateOperation(downloadItem.DownloadTaskID); lock (asyncOperationLock) { asyncOperationtTable.Add(downloadItem.DownloadTaskID, asyncOperation); }
}
if (downloadTable.Count < 5)
{
// 如果开始的是等待任务,则从等待列表删除
if (waitDownloadTable.ContainsKey(downloadItem.DownloadTaskID))
{
lock (waitTableLock)
{
waitDownloadTable.Remove(downloadItem.DownloadTaskID);
}
}
//加入到下载列表
lock (downloadTableLock)
{
downloadTable.Add(downloadItem.DownloadTaskID, downloadItem);
}
//新启一个线程开始下载
this.threadStart.BeginInvoke(downloadItem, null, null);
}
else //如果目前下载数量大于5则进入等待下载队列
{
//设置下载状态
downloadItem.IsStop = true;
downloadItem.DownloadStatus = DownloadStatus.ST_WAIT_DOWNLOAD;
//添加到等待列表
lock (waitTableLock)
{
waitDownloadTable.Add(downloadItem.DownloadTaskID, downloadItem);
}
ReportProgress(downloadItem); //报道下载进度
}
}
整个方法比较简单,传入一个下载任务对象,检查次任务是否已经存在,如果不存在就新建一个对应的AsyncOperation对象,并添加到哈希表中。根据目前任务数量决定是进入下载队列还是等待队列。如果可以下载,我们使用了threadStart.BeginInvoke 方法让这个任务在一个新的线程中运行,这也是我们这个管理类的本质,仍旧是使用多线程。
/// <summary> /// 开始下载时调用,触发DoWork事件 /// </summary> /// <param name="argument"></param> private void WorkerThreadStart(object argument) { //执行DoWork委托绑定的事件 try { DoWorkEventArgs e = new DoWorkEventArgs(argument); this.OnDoWork(e); } catch { //因为OnDoWork是FileDownload方法,里面已经处理了异常 //所以这里实际捕获不到下载引发的异常 } //执行完获得下载任务的实体类 Guid taskID = (argument as DownloadMusicTask).DownloadTaskID; DownloadMusicTask item = downloadTable[taskID]; //如果是暂停或错误状态,把当前任务放入等待队列,而从等待队列中选一个开始下载 if (item.DownloadStatus == DownloadStatus.ST_STOP_DOWNLOAD || item.DownloadStatus == DownloadStatus.ST_ERROR_DOWNLOAD) { // 从下载队列删除 lock (downloadTableLock) { downloadTable.Remove(item.DownloadTaskID); } // 加入到等待队列,并寻找等待队列中等待的任务,移动到下载列表 lock (waitTableLock) { waitDownloadTable.Add(item.DownloadTaskID, item); if (waitDownloadTable.Count > 0 && downloadTable.Count < 5) { foreach (DownloadMusicTask task in waitDownloadTable.Values) { if (task.DownloadStatus == DownloadStatus.ST_WAIT_DOWNLOAD) { RunWorkerAsync(task); //开始等待的任务 waitDownloadTable.Remove(task.DownloadTaskID); //把这个任务从等待列表移除 break; } } } } return; } // 触发RunWorkerCompleted事件,发送到UI线程操作(cancle设为false,true会抛出异常,item.Cancle可以查看) RunWorkerCompletedEventArgs arg = new RunWorkerCompletedEventArgs(item, item.Error, false); if (asyncOperationtTable[item.DownloadTaskID] != null) { asyncOperationtTable[item.DownloadTaskID].PostOperationCompleted(this.operationCompleted, arg); } // 从同步管理表中删除已经完成(或取消)下载的任务 lock (asyncOperationLock) { asyncOperationtTable[item.DownloadTaskID] = null; asyncOperationtTable.Remove(item.DownloadTaskID); } //从下载队列中移除已经下载完(或取消)的文件, lock (downloadTableLock) { downloadTable.Remove(item.DownloadTaskID); } // 寻找等待队列中等待的任务,移动到下载列表 if (waitDownloadTable.Count > 0 && downloadTable.Count < 5) { foreach (DownloadMusicTask waitTask in waitDownloadTable.Values) { if (waitTask.DownloadStatus == DownloadStatus.ST_WAIT_DOWNLOAD) { RunWorkerAsync(waitTask); //开始等待的任务 lock (waitTableLock) { waitDownloadTable.Remove(waitTask.DownloadTaskID); //把这个任务从等待列表移除 } break; } } } }
以上就是接受到任务后,在新的任务线程中执行的方法。他的作用是管理整个下载过程。首先是触发了注册的DoWork时间的方法,我们这里也就是实际的下载方法。总的来说,这里就是让DownloadCore对象的下载方法在一个新的线程运行。当下载完成后,需要触发RunWorkerCompleted事件,并把注册的方法发送到UI线程执行。这里可以看到使用了AsyncOperation对象的PostOperationCompleted方法,并把operationCompleted委托作为参数传递了过去。
/// <summary> /// 开始回报下载进度 /// </summary> /// <param name="userState">下载任务信息</param> public void ReportProgress(object taskState) { DownloadMusicTask item = taskState as DownloadMusicTask; //通知UI,进度和状态发生变化(进度百分比客户端从taskState取得自己计算,这里返回0) ProgressChangedEventArgs arg = new ProgressChangedEventArgs(0, taskState); if (this.asyncOperationtTable[item.DownloadTaskID] != null) { this.asyncOperationtTable[item.DownloadTaskID].Post(this.progressReporter, arg); } else { //this.progressReporter(arg); //非UI线程修改UI控件会报错 } }
接下来就是进度报告的异步方法。使用了AsyncOperation对象的Post方法,并把progressReporter委托传递了过去。
.
4 异步方法的暂停和取消
在多线程和基于APM的异步模型中,最麻烦的就是暂停一个异步操作。对于基于事件的异步模型来说,应该尽量支持取消操作,而且在取消时要出发RunWorkerCompleted事件,这是微软的建议。/// <summary> /// 暂停正在进行的任务 /// </summary> /// <param name="taskID">任务ID</param> public void StopAsync(Guid taskID) { // 设置下载的任务为停止 if (downloadTable.ContainsKey(taskID)) downloadTable[taskID].IsStop = true; } /// <summary> /// 取消任务 /// </summary> /// <param name="taskID">任务ID</param> public void CancleAsync(Guid taskID) { //如果正在下载,设置为取消状态 if (downloadTable.ContainsKey(taskID)) { downloadTable[taskID].IsCancle = true; } else //如果没有在下载 { // 获得要取消的任务对象 DownloadMusicTask cancelTask = null; foreach (DownloadMusicTask waitTask in waitDownloadTable.Values) { if (waitTask.DownloadTaskID == taskID) { cancelTask = waitTask; break; } } // 触发RunWorkerCompleted事件,发送到UI线程操作 if (cancelTask != null) { cancelTask.DownloadStatus = DownloadStatus.ST_CANCEL_DOWNLOAD; //状态设置为取消 RunWorkerCompletedEventArgs arg = new RunWorkerCompletedEventArgs(cancelTask, null, false); if (asyncOperationtTable[taskID] != null) { asyncOperationtTable[taskID].PostOperationCompleted(this.operationCompleted, arg); } } // 从同步管理表中删除 lock (asyncOperationLock) { asyncOperationtTable[taskID] = null; asyncOperationtTable.Remove(taskID); } //如果存在则从等待队列中删除 if (cancelTask != null) { lock (waitTableLock) { waitDownloadTable.Remove(cancelTask.DownloadTaskID); } } } }
在实现上,是对于每个任务都有两个标志IsCancle 和 IsStop ,我们的工作也就是设置这两个标志的状态。这里有一个问题,我们这个模块很难用在下载以外的地方,因为很难知道如何去结束一个异步操作。这也是为什么BackgroundWork不支持取消的原因。所以我们这里设置的标志位,在下载时会使用到,如果标志位为true,则从正在运行的程序中退出。从前面可以知道,下载结束时,会触发RunWorkerCompleted事件。 对于没有在下载的任务,取消时直接触发RunWorkerCompleted事件。
.
5 总结
以上就是实现的基于事件的异步模型,从整个模型可以看出,他的功能就是利用委托,启动一个新的线程,运行注册到类事件上的方法,而事件并不需要知道运行的是什么方法。对于报道进度,这里只是负责把数据和注册到时间的方法发送到正确的线程中,结束时也是一样。所以基于事件的异步模型更像是一个邮差。接受数据和方法,并投递到订阅者那里,如果处理这些数据,用什么方法处理,都是订阅者的事情。这样以来,就简化了我们使用多线程时一切都要自己控制,代码也更清晰,逻辑中基本看不到多线程处理的恒基。.
.
四 文件下载和文件管理
文件下载,使用的是HttpWebRequest对象。整个过程和网页请求是一样的,不同的是相对于网页,MP3文件较大,所以我们在读取请求的流时是循环读取的。而且在保存下载数据时也要考虑到暂停和同名等情况。/// <summary> /// 下载指定路径的文件 /// </summary> /// <param name="downloadItem">下载任务信息</param> /// <returns>下载结果</returns> public PageRequestResults FileDownload(DownloadMusicTask downloadTask) { if (!string.IsNullOrEmpty(downloadTask.DownloadUrl)) { // 设置请求信息,使用GET方式获得数据 HttpWebRequest musicFileReq = (HttpWebRequest)WebRequest.Create(downloadTask.DownloadUrl); musicFileReq.AllowAutoRedirect = true; musicFileReq.Method = "GET"; //设置超时时间 musicFileReq.Timeout = SearchConfig.TIME_OUT; //获取代理 IWebProxy proxy = SearchConfig.GetConfiguredWebProxy(); //判断代理是否有效 if (proxy != null) { //代理有效时设置代理 musicFileReq.Proxy = proxy; } // 判断是否断点续传() FileStream downloadStream,configStream; if (!File.Exists(downloadTask.MusicSavePath)) { //不是断点续传时,创建下载文件和配置文件 downloadStream = new FileStream(downloadTask.MusicSavePath, FileMode.Create, FileAccess.Write); //configStream = new FileStream(downloadTask.MusicConfigPath, FileMode.Create, FileAccess.Write); downloadTask.DownloadSize = 0; } else { //断点续传时读入文件,并且从已下载的位置开始下载 downloadStream = System.IO.File.OpenWrite(downloadTask.MusicSavePath); downloadTask.DownloadSize = downloadStream.Length; downloadStream.Seek(downloadTask.DownloadSize, SeekOrigin.Current); //移动文件流中的当前指针 musicFileReq.AddRange((int)downloadTask.DownloadSize); //设置请求头的Range值 } try { // 获取HTTP请求响应 using (HttpWebResponse musicFileRes = (HttpWebResponse)musicFileReq.GetResponse()) { // 如果HTTP返回200--正常,返回206表示是断点续传 // if (musicFileRes.StatusCode == HttpStatusCode.OK || musicFileRes.StatusCode == HttpStatusCode.PartialContent || musicFileRes.StatusCode == HttpStatusCode.Moved || musicFileRes.StatusCode == HttpStatusCode.MovedPermanently) { // 获取响应的页面流 using (Stream remoteStream = musicFileRes.GetResponseStream()) { using (downloadStream) { //设置下载状态和下载大小,状态为准备下载,并汇报进度 downloadTask.DownloadStatus = DownloadStatus.ST_READY_DOWNLOAD; //此处要加上已下载的大小,因为断线续传时返回的ContentLength是从Range出到结束的大小 downloadTask.FileSize = musicFileRes.ContentLength + downloadTask.DownloadSize; downloadManager.ReportProgress(downloadTask); //汇报当前下载进度 //用户统计速度 TimeSpan totalTimeSpan = TimeSpan.Zero; int totalTimeSize = 0; //开始下载数据,检查是否下载完成 while (downloadTask.DownloadSize < downloadTask.FileSize) { // 检查是否被取消 if (downloadManager.TaskCanCancle(downloadTask.DownloadTaskID)) { //如果任务被取消退出 downloadTask.DownloadStatus = DownloadStatus.ST_CANCEL_DOWNLOAD; break; } // 检查是否被暂停 if (downloadManager.TaskCanStop(downloadTask.DownloadTaskID)) { //汇报暂停时下载进度 downloadTask.DownloadStatus = DownloadStatus.ST_STOP_DOWNLOAD; downloadManager.ReportProgress(downloadTask); break; } // 正常下载,从流中读取到文件流中 byte[] buffer = new byte[1024]; TimeSpan readStart = new TimeSpan(DateTime.Now.Ticks); int readSize = remoteStream.Read(buffer, 0, buffer.Length); TimeSpan readEnd = new TimeSpan(DateTime.Now.Ticks); TimeSpan ts = readEnd.Subtract(readStart).Duration(); totalTimeSpan += ts; totalTimeSize += readSize; //写入文件 downloadStream.Write(buffer, 0, readSize); downloadTask.DownloadSize += readSize; //计算速度,400ms刷新一次 if (totalTimeSpan.Milliseconds > 400) { downloadTask.DownloadSpeed = (totalTimeSize / totalTimeSpan.Milliseconds) * 1000; totalTimeSpan = TimeSpan.Zero; totalTimeSize = 0; } //汇报当前下载进度 downloadTask.DownloadStatus = DownloadStatus.ST_IS_DOWNLOAD; downloadManager.ReportProgress(downloadTask); } // 如果完成,设置状态 if (downloadTask.DownloadSize == downloadTask.FileSize) { downloadTask.DownloadStatus = DownloadStatus.ST_OVER_DOWNLOAD; } //完成下载,程序退出 return PageRequestResults.Success; } } } else { //汇报下载进度 downloadTask.DownloadStatus = DownloadStatus.ST_ERROR_DOWNLOAD; downloadManager.ReportProgress(downloadTask); return PageRequestResults.UnknowException; } } } catch (WebException webEx) { //出错时汇报下载进度和错误信息 downloadTask.Error = webEx; downloadTask.DownloadStatus = DownloadStatus.ST_ERROR_DOWNLOAD; downloadManager.ReportProgress(downloadTask); // 异常时返回异常的原因 if (webEx.Status == WebExceptionStatus.Timeout) { return PageRequestResults.TimeOut; } else if (webEx.Status == WebExceptionStatus.SendFailure) { return PageRequestResults.SendFailure; } else if (webEx.Status == WebExceptionStatus.ConnectFailure) { return PageRequestResults.ConnectFailure; } else if (webEx.Status == WebExceptionStatus.ReceiveFailure) { return PageRequestResults.ReceiveFailure; } else if (webEx.Status == WebExceptionStatus.NameResolutionFailure) { return PageRequestResults.DNSFailure; } else if (webEx.Status == WebExceptionStatus.RequestProhibitedByProxy) { return PageRequestResults.ProxyFailure; } else { return PageRequestResults.UnknowException; } } catch (Exception ex) { //出错时汇报下载进度和错误信息 downloadTask.Error = ex; downloadTask.DownloadStatus = DownloadStatus.ST_ERROR_DOWNLOAD; downloadManager.ReportProgress(downloadTask); return PageRequestResults.UnknowException; } finally { downloadStream.Close(); //configStream.Close(); } } return PageRequestResults.UrlIsNull; }
以上就是下载的实现方式,主要有几点,一个是断点续传,一个是报告进度,另一个就是计算下载速度。
.
1 断点续传
断点续传的功能实际是利用了HttpWebRequest的AddRange方法来实现断点续传。下载时我们把文件存入到一个.mus后缀的临时文件中。当暂停一个下载任务时,这个任务并没有从前面的DownloadManagement的哈希表中删除,只是标志位设置为了stop。当继续下载时,首先从硬盘上读取这个文件,如果存在,则获得文件大小,并设置到HttpWebRequest的AddRange方法中,这样就能从指定的位置请求数据,实现断点续传。另外要注意的是,断点续传时,HttpWebResponse.ContentLength返回的只是剩余数据的大小,而不是全部数据的大小。如果弄错会导致下载的文件变小,播放的MP3就是乱的了。
.
2 进度报告和速度
在HttpWebResponse请求成功之后就马上进行了报告,这样做是为了让UI获得要下载的文件的大小数据,以及准备下载的状态。在下载中,利用已下载大小和文件大小来进行循环,每次从流中读取1024个字节。没读取一次回报一下进度,当然可以自己去控制回报进度的频率。在下载的过程中可能会出现错误,在出现错误时,也进行了进度汇报,这样前台就能知道此时下载的状态。对于计算速度,就是用下载字节数除下载的时间。但是问题就是,如果读取1024个字节就计算一次,可能很不准确,因为读1024自己的时间很短,而且这样更新太快,用户也看不清楚。所以这里我采用了每400ms计算一次速度,实际的效果还是不错的。
.
3 文件管理
我们在下载时,文件名为歌曲的名字,所以下载多个同名音乐时很可能出现同名的状况。通用的情况就是给后面的文件加上(1),(2)这样的后缀。而在下载过程中,文件是存放到扩展名为.mus的文件中。当下载完成后,重名为对应的音乐格式后缀。而在文件取消和下载失败时,也需要对文件进行删除等操作。这里也考虑了在下载是使用一个后缀为mus的文件存放数据,另一个后缀为cfg的文件存放下载信息,比如把DownloadMusicTask对象序列化存入到文件中,这样,即便在下载暂停,程序关闭之后,在下一次打开程序是,还能再次进行下载。而且对于多线程下载时,每个下载块的管理也是很有必要的。但是目前并没有这样做。不过实现起来没有什么难度。
.
.
五 总结
这一篇主要介绍了实现的下载模块,实际我想介绍的核心就是基于事件的异步模型。所以其他和下载相关的一些地方就介绍的很少或者没有提及。目前比较忙,没有时间继续写,程序实现的功能也基本就是目前介绍的这些。 当然还是用了WMP的插件,实现了在线播放。并且还写了一个网络状态实时检查的功能,但这里都没有做更多的介绍。在后面计划尝试进行歌词的实时显示功能,不过不知道是否有时间。因为进去工作比较忙,在开发一个手机平台上的msn客户端,也打算马上写一系列MSN协议分析的文章。所以时间就很难说了。
源代码SVN:http://code.google.com/p/cmusicsearch/
相关文章推荐
- 实现自己的音乐搜索软件(一)
- 实现自己的音乐搜索软件(二)
- 自己编程实现更改电脑桌面背景并同时播放音乐
- jsp实现的数据库模糊搜索(可以自己设定匹配字符个数)
- 如何实现android清理后台时,自己的软件不被清理
- 让自己的软件实现双击打开文件(修改注册表设置关联)
- 基于postgres的应用软件的拼音搜索实现
- 如何实现android清理后台时,自己的软件不被清理
- 用 TimingLaba(时方校园定时广播系统)软件,实现学校、办公室定时播放体操、上下课(班)铃声、休息音乐
- HTML5+JavaScript+CSS实现音乐播放器——难点二:自己设计一个控制音乐播放的控制器
- 怎么样把百度搜索引入自己的网站JS实现(附源代码)
- 实现自己的软件做的编辑保存成特定后缀,可以用自己开发的软件打开
- 以亲身经历浅谈软件实现前“凡事三问”的重要性---欢迎大家分享自己的经历和感悟!
- C#软件开发实例.私人订制自己的屏幕截图工具(四)基本截图功能实现
- 如何在网页实现自己的划词搜索
- HTML5+JavaScript+CSS实现音乐播放器——难点二:自己设计一个控制音乐播放的控制器
- 怎么样把百度搜索引入自己的网站JS实现(附源代码)
- 基于OkHttpUtils自己实现一个检查升级软件功能
- android 调用Market 搜索自己的软件
- 让自己的软件实现拖拽打开文件(使用WM_DROPFILES消息和DragQueryFile函数)