windows程序设计--------文件并发下载

一、项目说明

        还在担心qq嘤乐下载速度慢?

        还在烦恼xx网盘限速?

        还在疑惑为什么自己的程序不能实现并发下载?

        文件并发下载,程序猿们绕不过去的坎,就今天,来不及了,就这篇吧!

        圆规正传,本项目旨在通过多线程并发下载文件,提高下载速度和效率。that‘s all。

二、实现思路

界面设计:我使用了 Windows 窗体框架来设计界面,这使得用户可以直观地与应用程序进行交互。界面包含一个按钮,用户点击该按钮即可触发下载操作,并有一个列表视图控件用于显示下载任务信息。

文件读取:我利用了 C# 提供的文件操作库,特别是 File.ReadAllLines() 方法,来读取指定文本文件中的所有行。这样我就能够轻松地获取到包含下载链接的文本文件内容。

下载任务添加:在读取到文本文件的内容后,我对每一行进行处理,提取出文件名和下载链接。
使用 Split() 方法按照 | 符号分割文件名和下载链接,并对下载链接进行 URL 编码,以确保特殊字符被正确处理。然后,我将每一个有效的文件名和下载链接添加到下载任务列表中,以便后续启动下载操作。

下载任务启动:当用户点击按钮时,我调用 StartDown() 方法来启动下载任务。这样就能够保证用户在点击按钮后,下载操作会立即开始。

下载状态更新:在下载过程中,我根据下载状态更新界面显示,例如显示下载进度、下载速度等信息。为了确保界面的响应性和线程安全性,我使用了委托和异步操作来更新界面,这样用户就能够实时地了解到下载任务的执行情况。


        综上所述,这段代码是一个简单而功能强大的 Windows 窗体应用程序,用于从文本文件中读取下载链接并启动下载任务。它充分利用了 .NET 平台提供的基本库以及我自己编写的下载任务管理库,为用户提供了便捷的下载服务。

三、代码分析

1.windows窗体页面展示:

         我们从测试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 方法的作用:

  1. 将操作委托给 UI 线程执行

    • Invoke方法可以将操作(例如更新界面控件)委托给创建控件的 UI 线程执行。这样做可以避免在非 UI 线程上直接操作界面控件,从而保证了线程安全性。
  2. 阻塞调用

    • 调用 Invoke方法时,当前线程会被阻塞,直到操作完成并返回结果。这确保了在操作执行完成之前,不会执行任何其他的操作,避免了竞态条件和数据不一致问题。

5.如何使用 Invoke 方法:

  1. 使用匿名方法或委托

    • Invoke方法接受一个委托作为参数,通常使用匿名方法、Lambda 表达式或者命名委托来定义需要执行的操作。
  2. 确保在 UI 线程上执行

    • 在委托中执行的操作应该是对界面控件的访问和操作,确保在 UI 线程上执行,以避免线程安全性问题。
  3. 处理异常

    • 在委托中执行的操作可能会抛出异常,确保在 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)
    {
        。。。。
    }
}

         通过这些优化,可以提高代码的健壮性、性能和用户体验。尤其是增加异常处理、使用并行库和异步编程技术,可以使应用程序更加高效和可靠。

五、代码仓库

仓库地址:Xccccc/文件并发下载代码

  • 19
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值