一、实验目的
- 掌握Windows Forms应用程序的基本开发流程。
- 实现一个能够读取文件列表并从网络批量下载文件的系统。
- 了解多线程下载的基本原理和实现方法。
二、实验内容
- 创建一个Windows Forms应用程序,包含一个窗体
Form1
。 - 在
Form1
上添加一个按钮btnTest
用于触发下载操作,以及一个ListView
控件listView1
用于显示下载信息。 - 编写代码实现以下功能:
- 读取一个文本文件,该文件包含要下载的文件URL和文件名(以“|”分隔)。
- 将读取到的文件信息添加到
ListView
控件中。 - 使用自定义的
DownLoadFile
类进行文件的下载,支持多线程下载。 - 在下载过程中,通过事件更新
ListView
控件中的下载状态信息。
三、实验步骤
- 创建一个新的Windows Forms应用程序项目。
- 设计窗体界面,添加按钮和ListView控件。
- 编写代码实现上述功能。
- 调试并测试程序,确保能够正确读取文件信息、添加下载任务、显示下载状态。
四、实验代码解析
1.字段与属性
定义了DownLoadFile
类的实例dlf
,用于处理下载操作。
DownLoadFile dlf = new DownLoadFile();
2.btnTest_Click事件处理器
这段代码是一个按钮点击事件的处理函数,当点击btnTest
按钮时,触发此事件处理器,主要功能是读取一个文本文件(软件下载1.txt
),解析该文件的内容,并基于解析的结果在ListView
控件中添加项目,同时启动下载任务。以下是对这段代码的详细分析:
读取文件:
string[] lines = File.ReadAllLines("软件下载1.txt");
读取名为 "软件下载1.txt" 的文本文件的所有行,并将它们存储在字符串数组 lines
中。
读取文件内容:使用File.ReadAllLines
方法读取名为软件下载1.txt
的文件的所有行,并返回一个字符串数组lines
。
private void btnTest_Click(object sender, EventArgs e)
{
string[] lines = File.ReadAllLines("软件下载1.txt");
for (int i = 0; i < lines.Length; i++)
{
string[] line = lines[i].Split(new string[] { "|" }, StringSplitOptions.RemoveEmptyEntries);
if (line.Length == 2)
{
string path = Uri.EscapeUriString(line[1]);
string filename = Path.GetFileName(path);
string dir = @"D:\test";
ListViewItem item = listView1.Items.Add(new ListViewItem(new string[] { (listView1.Items.Count + 1).ToString(), filename, "0", "0", "0%", "0", "0", DateTime.Now.ToString(), "等待中", line[1] }));
int id = item.Index;
dlf.AddDown(path, dir, id, id.ToString());
}
}
dlf.StartDown();
}
3.Form1_Load事件处理器
窗体加载时设置下载线程数和绑定消息处理事件。
设置线程数:dlf.ThreadNum = 3;
这行代码设置了一个名为 dlf
的对象的 ThreadNum
属性值为3。在这里,它被设置为3,意味着当进行文件下载时,将使用3个线程来并行处理下载任务
private void Form1_Load(object sender, EventArgs e)
{
dlf.ThreadNum = 3; // 设置线程数
dlf.doSendMsg += SendMsgHander; // 绑定下载消息处理事件
}
4.SendMsgHander事件处理器
处理来自DownLoadFile
类的消息,更新ListView
中的下载状态。
private void SendMsgHander(DownMsg msg)
{
switch (msg.Tag)
{
case DownStatus.Start:
this.Invoke((MethodInvoker)delegate ()
{
//不过多赘述,省略相关代码...
});
break;
case DownStatus.GetLength:
this.Invoke((MethodInvoker)delegate ()
{
//不过多赘述,省略相关代码...
});
break;
case DownStatus.End:
case DownStatus.DownLoad:
this.Invoke(new MethodInvoker(() =>
{
//不过多赘述,省略相关代码...
}));
break;
case DownStatus.Error:
this.Invoke((MethodInvoker)delegate ()
{
//不过多赘述,省略相关代码...
});
break;
}
}
五、引用
1.DownLoadFile
从以下的代码片段来看,这是一个名为 DownLoadFile
的类,该类属于 Gac
命名空间,并且似乎用于处理文件的下载任务。以下是每个部分的作用:
-
类成员变量
ThreadNum
: 设定了默认的线程数量,但在这段代码中它似乎并未被用作限制同时运行的线程数。list
: 这是一个List<Thread>
类型的变量,用于存储创建的下载线程。 -
构造函数
DownLoadFile()
: 构造函数中,将Change
方法绑定到doSendMsg
事件上。这意味着当doSendMsg
事件被触发时,Change
方法将被调用。 -
Change 方法
这是一个私有方法,用于处理接收到的DownMsg
消息。如果消息标记为Error
或End
(下载出错或完成),则调用StartDown
方法开始新的下载任务。 -
AddDown 方法
此方法接受下载URL、目标目录、ID(可选)和文件名(可选)作为参数,并创建一个新的线程来执行download
方法。新创建的线程被添加到list
列表中。 -
StartDown 方法
该方法用于启动列表中的线程进行下载。默认情况下,它会启动StartNum
个线程,但如果列表中没有处于Unstarted
或Suspended
状态的线程,它将不会启动任何新线程。这里使用了lock
关键字来确保线程安全地访问list
。 -
事件和委托
dlgSendMsg
: 这是一个委托,定义了事件处理程序的签名。doSendMsg
: 这是一个事件,当某些操作(如下载开始、结束或出错)发生时,可以触发此事件并传递一个DownMsg
消息。 -
download 方法
首先创建一个DownMsg
对象并触发doSendMsg
事件来通知下载开始。然后,创建了一个FileDownloader
对象来执行实际的下载操作。
代码实现:
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace Gac
{
public class DownLoadFile
{
public int ThreadNum = 3;
List<Thread> list = new List<Thread>();
public DownLoadFile()
{
doSendMsg += Change;
}
private void Change(DownMsg msg)
{
if (msg.Tag==DownStatus.Error||msg.Tag==DownStatus.End)
{
StartDown(1);
}
}
public void AddDown(string DownUrl,string Dir, int Id = 0,string FileName="")
{
Thread tsk = new Thread(() =>
{
download(DownUrl, Dir, FileName,Id);
});
list.Add(tsk);
}
public void StartDown(int StartNum=3)
{
for (int i2 = 0; i2 < StartNum; i2++)
{
lock (list)
{
for (int i = 0; i < list.Count; i++)
{
if (list[i].ThreadState == System.Threading.ThreadState.Unstarted || list[i].ThreadState == ThreadState.Suspended)
{
list[i].Start();
break;
}
}
}
}
}
public delegate void dlgSendMsg(DownMsg msg);
public event dlgSendMsg doSendMsg;
private void download(string path, string dir,string filename,int id = 0)
{
try
{
DownMsg msg = new DownMsg();
msg.Id = id;
msg.Tag = 0;
doSendMsg(msg);
FileDownloader loader = new FileDownloader(path, dir, filename, ThreadNum);
//不过多赘述,省略...
}
catch (Exception ex)
{
DownMsg msg = new DownMsg();
msg.Id = id;
//不过多赘述,省略...
Console.WriteLine(ex.Message);
}
}
}
}
2.DownloadProgressListener
- 类成员变量:
private long presize = 0;
:这个变量被初始化为0,但在给定的代码片段中并未使用。DownMsg downMsg = null;
:这是一个DownMsg
类型的实例,用于存储和更新与下载相关的状态信息。 - 构造函数:
public DownloadProgressListener(DownMsg downmsg)
:构造函数接受一个DownMsg
类型的参数,并将其赋值给类的成员变量downMsg
。 - 事件和委托:
public delegate void dlgSendMsg(DownMsg msg);
:定义了一个名为dlgSendMsg
的委托,该委托接受一个DownMsg
类型的参数。public dlgSendMsg doSendMsg = null;
:声明了一个dlgSendMsg
类型的公开事件doSendMsg
,并将其初始化为null
。这个事件可以用于通知其他类关于下载进度的更新。 - OnDownloadSize 方法:
- 这个方法用于处理下载过程中的大小更新。它接受一个
long
类型的参数size
,表示当前已下载的字节数。 - 方法内部首先检查
downMsg
是否为null
,但即使downMsg
是null
,它也没有执行任何有意义的操作(它创建了一个局部变量DownMsg downMsg
,但没有将其赋值给类的成员变量)。 - 接下来,它计算并更新下载速度
Speed
、剩余时间Surplus
和剩余时间信息SurplusInfo
。 - 它还会更新
downMsg
的Size
属性以反映当前已下载的总量。 - 如果已下载的字节数等于
downMsg
中的Length
属性,表示下载已完成,它将设置相应的标签和状态信息。 - 最后,如果
doSendMsg
事件不为null
,它将触发该事件,并传递更新后的downMsg
对象作为参数,以通知其他类关于下载进度的更新。
- 这个方法用于处理下载过程中的大小更新。它接受一个
- DownStatus 枚举:定义了一个名为
DownStatus
的枚举,它包含了表示下载过程中不同状态的常量,如开始、获取长度、下载中、完成和错误。 - DownMsg 类:定义了一个名为
DownMsg
的类,用于封装与下载相关的状态信息。该类包含多个私有字段,如_Length
(文件总长度)、_Size
(已下载大小)和_Tag
(下载状态)等。尽管这些字段是私有的,但从给定的代码片段中无法看到相应的公共访问器(getter 和 setter)。这意味着外部类可能无法直接访问或修改这些字段的值(除非通过其他方式,如反射)。
代码实现:
using System;
using System.Collections.Generic;
using System.Text;
namespace Gac
{
public class DownloadProgressListener : IDownloadProgressListener
{
private long presize=0;
DownMsg downMsg = null;
public DownloadProgressListener(DownMsg downmsg)
{
this.downMsg = downmsg;
//this.id = id;
//this.Length = Length;
}
public delegate void dlgSendMsg(DownMsg msg);
public dlgSendMsg doSendMsg = null;
public void OnDownloadSize(long size)
{
if (downMsg==null)
{
DownMsg downMsg = new DownMsg();
}
//下载速度
if (downMsg.Size == 0)
{
downMsg.Speed = size;
}
else
{
downMsg.Speed = (float)(size - downMsg.Size);
}
if (downMsg.Speed == 0)
{
downMsg.Surplus = -1;
downMsg.SurplusInfo = "未知";
}
else
{
downMsg.Surplus = ((downMsg.Length - downMsg.Size) / downMsg.Speed);
}
downMsg.Size = size; //下载总量
if (size == downMsg.Length)
{
//下载完成
downMsg.Tag = DownStatus.End;
downMsg.SpeedInfo = "0 K";
downMsg.SurplusInfo = "已完成";
}
else
{
//下载中
downMsg.Tag = DownStatus.DownLoad;
}
if (doSendMsg != null) doSendMsg(downMsg);//通知具体调用者下载进度
}
}
public enum DownStatus
{
Start,
GetLength,
DownLoad,
End,
Error
}
public class DownMsg
{
private int _Length = 0;
private string _LengthInfo = "";
private int _Id = 0;
private DownStatus _Tag = 0;
private long _Size = 0;
private string _SizeInfo = "";
private float _Speed = 0;
private float _Surplus = 0;
private string _SurplusInfo ="";
private string _ErrMessage = "";
private string _SpeedInfo = "";
private double _Progress = 0;
public int Length
{
get
{
return _Length;
}
set
{
_Length = value;
LengthInfo = GetFileSize(value);
}
}
public int Id
{
get
{
return _Id;
}
set
{
_Id = value;
}
}
/// </summary>
public DownStatus Tag
{
get
{
return _Tag;
}
set
{
_Tag = value;
}
}
public long Size
{
get
{
return _Size;
}
set
{
_Size = value;
SizeInfo = GetFileSize(value);
if (Length >= value)
{
Progress = Math.Round((double)value / Length * 100, 2);
}
else
{
Progress = -1;
}
}
}
public float Speed
{
}
//不过多久赘述,省略下方public代码...
}
}
3.DownloadThread
这段代码定义了一个名为DownloadThread
的类,它代表了用于多线程文件下载的单个线程。我会逐步分析这段代码,并解释每个部分的作用。
1.成员变量
- saveFilePath:保存下载文件的路径。
- downUrl:要下载文件的URL。
- block:通常用于指定每个线程下载的文件块大小(尽管在这段代码中它似乎被用作整个文件的总块数,而不是块大小)。
- threadId:线程的ID,用于标识不同的下载线程。
- downLength:已经下载的字节数(对于当前线程)。
- finish:一个布尔值,指示线程是否已完成下载。
- downloader:一个
FileDownloader
类型的对象,可能是用于协调多个DownloadThread
的类。
2.构造函数
- DownloadThread(FileDownloader downloader, string downUrl, string saveFile, long block, long downLength, int threadId):初始化
DownloadThread
对象的构造函数,它接受多个参数来设置对象的初始状态。
3.ThreadRun方法
if (downLength < block):检查是否已经下载了完整的文件块(对于当前线程)
代码实现:
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.IO;
using System.Threading;
namespace Gac
{
public class DownloadThread
{
private string saveFilePath;
private string downUrl;
private long block;
private int threadId = -1;
private long downLength;
private bool finish = false;
private FileDownloader downloader;
public DownloadThread(FileDownloader downloader, string downUrl, string saveFile, long block, long downLength, int threadId)
{
this.downUrl = downUrl;
this.saveFilePath = saveFile;
this.block = block;
this.downloader = downloader;
this.threadId = threadId;
this.downLength = downLength;
}
public void ThreadRun()
{
//task
Thread td = new Thread(new ThreadStart(() =>
{
if (downLength < block)//未下载完成
{
try
{
//省略...
}
catch (Exception e)
{
this.downLength = -1;
Console.WriteLine("Thread " + this.threadId + ":" + e.Message);
}
}
}));
td.IsBackground = true;
td.Start();
}
/// <summary>
/// 下载是否完成
/// </summary>
/// <returns></returns>
public bool isFinish()
{
return finish;
}
/// <summary>
/// 已经下载的内容大小
/// </summary>
/// <returns>如果返回值为-1,代表下载失败</returns>
public long getDownLength()
{
return downLength;
}
}
}
4.FileDownloader
从以下的FileDownloader
类代码片段中,可以看到这是一个用于多线程文件下载的类的部分实现。以下是对您提供的代码的分析:
- 成员变量:
downloadSize
:用于跟踪已下载的总数据量(但在此代码片段中并未完全使用)。fileSize
:用于存储要下载的文件的大小。threads
:一个DownloadThread
类型的数组,用于存储和管理下载线程。saveFile
:保存文件的完整路径。data
:一个字典,用于存储每个线程下载的进度或位置(但在当前代码片段中仅用于更新位置,未看到如何用于进度追踪或合并文件)。block
:此变量在代码片段中未定义或使用,可能是一个用于表示单个线程下载块大小的变量,但在当前代码中未实现。downloadUrl
:要下载的文件的URL。
- 构造函数:
- 在构造函数中,首先处理文件名。如果未提供文件名,则从URL中提取文件名,并进行URI解码以处理可能的特殊字符或中文字符。
- 检查文件保存目录是否存在,如果不存在则创建。
- 初始化
threads
数组以存储下载线程。 - 创建一个
HttpWebRequest
对象来设置HTTP请求的详细信息,如Referer、Method等。但此代码片段并未实际发送请求或获取响应。
- 方法:
getThreadSize
:返回线程数组的长度,即下载线程的数量。getFileSize
:返回fileSize
变量,即要下载的文件的大小。但在此代码片段中,我们并未看到fileSize
是如何被设置的。append
:此方法使用lock
关键字来确保线程安全地更新downloadSize
。然而,在当前的代码片段中,我们并未看到downloadSize
在其他地方被使用或更新,因此这个方法目前看来并没有实际作用。update
:此方法更新data
字典中指定线程ID的下载位置。如果线程ID不存在于字典中,则将其添加到字典中。
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.IO;
using System.Net;
namespace Gac
{
public class FileDownloader
{
private long downloadSize = 0;
private long fileSize = 0;
private DownloadThread[] threads;
private string saveFile;
public Dictionary<int, long> data = new Dictionary<int, long>();
private long block;
private String downloadUrl;
public int getThreadSize()
{
return threads.Length;
}
public long getFileSize()
{
return fileSize;
}
public void append(long size)
{
lock (this) //锁定同步..........
{
downloadSize += size;
}
}
public void update(int threadId, long pos)
{
if (data.ContainsKey(threadId))
{
this.data[threadId] = pos;
}
else
{
this.data.Add(threadId, pos);
}
}
public FileDownloader(string downloadUrl, string fileSaveDir,string filename="", int threadNum=3)
{
try
{
if (string.IsNullOrEmpty(filename))
{
//省略...
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
throw new Exception("无法连接下载地址");
}
}
public long download(IDownloadProgressListener listener)
{
try
{
using (FileStream fstream = new FileStream(this.saveFile, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite))
{
if (this.fileSize > 0) fstream.SetLength(this.fileSize);
fstream.Close();
}
if (this.data.Count != this.threads.Length)
{
this.data.Clear();
for (int i = 0; i < this.threads.Length; i++)
{
this.data.Add(i + 1, 0);//初始化每条线程已经下载的数据长度为0
}
}
for (int i = 0; i < this.threads.Length; i++)
{//开启线程进行下载
long downLength = this.data[i + 1];
if (downLength < this.block && this.downloadSize < this.fileSize)
{
//判断线程是否已经完成下载,否则继续下载
}
else
{
this.threads[i] = null;
}
}
bool notFinish = true;//下载未完成
while (notFinish)
{// 循环判断所有线程是否完成下载
Thread.Sleep(900);
notFinish = false;//假定全部线程下载完成
for (int i = 0; i < this.threads.Length; i++)
{
if (this.threads[i] != null && !this.threads[i].isFinish())
{//如果发现线程未完成下载
notFinish = true;//设置标志为下载没有完成
if (this.threads[i].getDownLength() == -1)
{//如果下载失败,再重新下载
this.threads[i] = new DownloadThread(this, downloadUrl, this.saveFile, this.block, this.data[i + 1], i + 1);
this.threads[i].ThreadRun();
}
}
}
if (listener != null)
{
listener.OnDownloadSize(this.downloadSize);//通知目前已经下载完成的数据长度
Console.WriteLine(this.downloadSize);
}
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
throw new Exception("下载文件失败");
}
return this.downloadSize;
}
}
}
5.IDownloadProgressListener
using System;
using System.Collections.Generic;
using System.Text;
namespace Gac
{
public interface IDownloadProgressListener
{
void OnDownloadSize(long size);
}
}
六、实验结果
七、实验小结
本次实验通过Windows Forms应用程序实现了文件的并发下载功能。通过DownLoadFile
类管理下载任务,并使用多线程技术提高下载效率。用户点击按钮后,程序从指定文件中读取下载链接,并将任务添加到下载队列中。程序开始并发下载,并通过ListView
控件实时更新下载进度和状态。
实验结果验证了并发下载技术的有效性,显著提高了文件下载的效率。用户可以通过界面直观地了解每个下载任务的进度和状态。