一、项目说明
还在担心qq嘤乐下载速度慢?
还在烦恼xx网盘限速?
还在疑惑为什么自己的程序不能实现并发下载?
文件并发下载,程序猿们绕不过去的坎,就今天,来不及了,就这篇吧!
圆规正传,本项目旨在通过多线程并发下载文件,提高下载速度和效率。that‘s all。
二、实现思路
界面设计:我使用了 Windows 窗体框架来设计界面,这使得用户可以直观地与应用程序进行交互。界面包含一个按钮,用户点击该按钮即可触发下载操作,并有一个列表视图控件用于显示下载任务信息。
文件读取:我利用了 C# 提供的文件操作库,特别是 File.ReadAllLines() 方法,来读取指定文本文件中的所有行。这样我就能够轻松地获取到包含下载链接的文本文件内容。
下载任务添加:在读取到文本文件的内容后,我对每一行进行处理,提取出文件名和下载链接。
使用 Split() 方法按照 | 符号分割文件名和下载链接,并对下载链接进行 URL 编码,以确保特殊字符被正确处理。然后,我将每一个有效的文件名和下载链接添加到下载任务列表中,以便后续启动下载操作。
下载任务启动:当用户点击按钮时,我调用 StartDown() 方法来启动下载任务。这样就能够保证用户在点击按钮后,下载操作会立即开始。
下载状态更新:在下载过程中,我根据下载状态更新界面显示,例如显示下载进度、下载速度等信息。为了确保界面的响应性和线程安全性,我使用了委托和异步操作来更新界面,这样用户就能够实时地了解到下载任务的执行情况。
综上所述,这段代码是一个简单而功能强大的 Windows 窗体应用程序,用于从文本文件中读取下载链接并启动下载任务。它充分利用了 .NET 平台提供的基本库以及我自己编写的下载任务管理库,为用户提供了便捷的下载服务。
三、代码分析
1.windows窗体页面展示:![](https://img-blog.csdnimg.cn/direct/05dad9e404644a20bd10665c58ad9939.png)
我们从测试button开始介绍。
2.测试button:
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 = @" C:\Users\风不远万里\Desktop\Text";
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();
}
我们通过这个按钮点击事件处理函数来执行用户点击按钮后,程序应该执行的操作。
string[] lines = File.ReadAllLines("软件下载1.txt");
这句话用来读取指定文件中的所有行,我们可以把这个过程类比成我们点击下载链接的过程,而在我们的程序中就把相关链接放到这个软件下载1.txt中,然后让程序遍历文件的每一行,提取下载链接。
DownLoadFile dlf = new DownLoadFile();
dlf.AddDown(path, dir, id, id.ToString());
dlf.StartDown();
第一句是创建下载文件的实例,放在触发函数外,第二句是通过该实例添加下载任务。第三句是开始下载任务。
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] // 原始 URL
}));
ListViewItem item = listView1.Items.Add(...);
这一部分是在 listView1 控件中添加一个新的项,并将其赋值给名为 item 的 ListViewItem 对象。
new ListViewItem(new string[] { ... });
这里创建了一个新的 ListViewItem 对象,并使用一个字符串数组作为参数,该数组包含了新项中每个子项的内容。
(listView1.Items.Count + 1).ToString():这是新项的第一列,表示序号。通过 listView1.Items.Count 获取已经存在的项的数量,然后加上 1,以确保新项的序号是递增的。将结果转换为字符串形式。
filename:这是新项的第二列,表示文件名。这个值是从之前处理的文本文件中提取的。
"0":这是新项的第三列,表示初始大小。在这里设置为 0,表示初始状态下文件大小未知。
"0":这是新项的第四列,表示初始长度。同样设置为 0,表示初始状态下文件长度未知。
"0%":这是新项的第五列,表示初始进度。设置为 "0%" 表示初始状态下进度为 0%。
"0":这是新项的第六列,表示初始速度。同样设置为 0,表示初始状态下下载速度未知。
"0":这是新项的第七列,表示初始剩余时间。同样设置为 0,表示初始状态下剩余时间未知。
DateTime.Now.ToString():这是新项的第八列,表示当前时间。使用 DateTime.Now 获取当前系统时间,并将其转换为字符串形式。
"等待中":这是新项的第九列,表示初始状态。在这里设置为 "等待中",表示初始状态为等待下载。
line[1]:这是新项的第十列,表示原始 URL。这个值是从之前处理的文本文件中提取的下载链接。
这样,通过添加包含所需信息的字符串数组,我们成功地创建了一个新的 ListViewItem 对象,并将其添加到了 listView1 控件中。
// 窗体加载事件处理函数
private void Form1_Load(object sender, EventArgs e)
{
// 设置下载的线程数量,默认是3
dlf.ThreadNum = 3;
// 订阅下载过程中发送消息的事件处理函数
dlf.doSendMsg += SendMsgHander;
}
当然,在下载过程中,也缺少不了下载过程的事件处理函数 ,该函数用于处理下载过程中产生的不同状态的消息,例如下载开始、获取下载文件长度、下载结束、下载中、下载错误等。
3.过程的事件处理函数:
private void SendMsgHander(DownMsg msg)
{
switch (msg.Tag)
{
case DownStatus.Start:
this.Invoke((MethodInvoker)delegate ()
{
listView1.Items[msg.Id].SubItems[8].Text = "开始下载";
listView1.Items[msg.Id].SubItems[7].Text = DateTime.Now.ToString();
});
break;
case DownStatus.GetLength:
this.Invoke((MethodInvoker)delegate ()
{
listView1.Items[msg.Id].SubItems[3].Text = msg.LengthInfo;
listView1.Items[msg.Id].SubItems[8].Text = "连接成功";
});
break;
case DownStatus.End:
case DownStatus.DownLoad:
this.Invoke(new MethodInvoker(() =>
{
this.Invoke((MethodInvoker)delegate ()
{
listView1.Items[msg.Id].SubItems[2].Text = msg.SizeInfo;
listView1.Items[msg.Id].SubItems[4].Text = msg.Progress.ToString() + "%";
listView1.Items[msg.Id].SubItems[5].Text = msg.SpeedInfo;
listView1.Items[msg.Id].SubItems[6].Text = msg.SurplusInfo;
if (msg.Tag == DownStatus.DownLoad)
{
listView1.Items[msg.Id].SubItems[8].Text = "下载中";
}
else
{
listView1.Items[msg.Id].SubItems[8].Text = "下载完成";
}
Application.DoEvents();
});
}));
break;
case DownStatus.Error:
this.Invoke((MethodInvoker)delegate ()
{
listView1.Items[msg.Id].SubItems[6].Text = "失败";
listView1.Items[msg.Id].SubItems[8].Text = msg.ErrMessage;
Application.DoEvents();
});
break;
}
}
让我来详细解释一下这个函数的作用:
函数名和参数:数名 SendMsgHander,用于描述这个函数的功能。参数 msg 是一个类型为 DownMsg 的对象,用于传递下载过程中的消息。
处理不同的下载状态:使用 switch 语句根据 msg.Tag 的值来判断当前的下载状态,根据不同的状态执行相应的操作。
界面更新:在下载过程中,通过更新界面来显示下载状态的变化。例如,更新进度条、显示下载速度、显示剩余时间等,以便用户了解下载进度和状态。
线程安全性:使用了 Invoke 方法来确保界面更新的线程安全性。由于界面控件只能在创建它的线程上访问,而下载过程通常在后台线程执行,因此必须通过 Invoke 方法将界面更新操作委托给创建控件的线程。
错误处理:处理下载过程中可能发生的错误,例如网络连接错误、文件不存在等情况。在发生错误时,更新界面显示错误信息,并通知用户下载任务失败。
需要注意的是,这段代码使用了invoke方法,当涉及多线程编程时,确保线程安全性至关重要。在 C# 中,界面控件通常只能由创建它的 UI 线程访问和操作,如果在其他线程上直接访问界面控件,可能会导致应用程序崩溃或出现不可预料的行为。这时就需要使用 Invoke
方法来确保线程安全性。
4.Invoke 方法的作用:
-
将操作委托给 UI 线程执行:
- Invoke方法可以将操作(例如更新界面控件)委托给创建控件的 UI 线程执行。这样做可以避免在非 UI 线程上直接操作界面控件,从而保证了线程安全性。
-
阻塞调用:
- 调用 Invoke方法时,当前线程会被阻塞,直到操作完成并返回结果。这确保了在操作执行完成之前,不会执行任何其他的操作,避免了竞态条件和数据不一致问题。
5.如何使用 Invoke 方法:
-
使用匿名方法或委托:
- Invoke方法接受一个委托作为参数,通常使用匿名方法、Lambda 表达式或者命名委托来定义需要执行的操作。
-
确保在 UI 线程上执行:
- 在委托中执行的操作应该是对界面控件的访问和操作,确保在 UI 线程上执行,以避免线程安全性问题。
-
处理异常:
- 在委托中执行的操作可能会抛出异常,确保在 Invoke 方法外部处理这些异常,以避免导致应用程序崩溃。
在编写多线程应用程序时,了解并正确使用Invoke方法是确保应用程序稳定性和可靠性的重要一步。
6.创建downloadfile类:
在之前的代码中出现了’DownLoadFile dlf = new DownLoadFile();‘这条语句,这个downloadfile类是我们自己定义的下载类,现在附上相关代码。
public class DownLoadFile
{
public int ThreadNum = 3; // 默认线程数为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);
loader.data.Clear();
msg.Tag = DownStatus.Start;
msg.Length = (int)loader.getFileSize(); // 获取文件大小
doSendMsg(msg); // 发送消息通知下载进度
// 创建下载进度监听器并监听下载进度
DownloadProgressListener linstenter = new DownloadProgressListener(msg);
linstenter.doSendMsg = new DownloadProgressListener.dlgSendMsg(doSendMsg);
loader.download(linstenter);
}
catch (Exception ex)
{
// 下载出错,发送错误消息
DownMsg msg = new DownMsg();
msg.Id = id;
msg.Length = 0;
msg.Tag = DownStatus.Error;
msg.ErrMessage = ex.Message;
doSendMsg(msg); // 发送错误消息
Console.WriteLine(ex.Message); // 输出异常信息到控制台
}
}
}
四、优化建议
我们的代码仍然由不足之处,为了提高代码的可读性、性能和健壮性,我们可以增加以下的处理:
1. 异常处理
添加异常处理,以防止在文件读取或下载过程中发生错误导致程序崩溃。
private void btnTest_Click(object sender, EventArgs e)
{
try
{
.....
}
catch (Exception ex)
{
MessageBox.Show($"Error: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
使用try,catch当遇到异常情况是可以抛出异常。
2. 文件路径处理
使用相对路径或者用户选择的路径来读取文件,而不是硬编码路径。
using (OpenFileDialog openFileDialog = new OpenFileDialog())
{
openFileDialog.InitialDirectory = "C:\\";
openFileDialog.Filter = "Text files (*.txt)|*.txt|All files (*.*)|*.*";
openFileDialog.FilterIndex = 1;
openFileDialog.RestoreDirectory = true;
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
string filePath = openFileDialog.FileName;
。。。。。
}
}
3. 使用任务并行库
考虑使用任务并行库(TPL)来提高下载的并发性。
string[] lines = File.ReadAllLines("软件下载1.txt");
List<Task> downloadTasks = new List<Task>();
Task.WhenAll(downloadTasks).ContinueWith(_ => dlf.StartDown());
4. 提高UI响应性
确保UI线程在处理下载时保持响应,可以考虑在下载处理过程中使用异步方法。
private async void btnTest_Click(object sender, EventArgs e)
{
try
{
。。。。
}
await Task.WhenAll(downloadTasks);
dlf.StartDown();
}
catch (Exception ex)
{
。。。。
}
}
通过这些优化,可以提高代码的健壮性、性能和用户体验。尤其是增加异常处理、使用并行库和异步编程技术,可以使应用程序更加高效和可靠。