使用C#实现文件下载器的功能

一.C#文件下载器实现功能

  编写的c#文件下载器可以实现提供下载地址便可以完成整个下载任务的功能,在下载过程中,可以显示正在下载的文件名称,文件的总大小,下载完成的进度和数据量,网络带宽的大小以及剩余的时间,末尾还能显示下载文件的地址信息以供纠错。

二.代码分析

设计区域控件

使用工具箱中的listview控件功能构建下载文件信息显示区,button控件来控制整个界面的下载开始 

program.cs代码分析

namespace Demo
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private DownLoadFile dlf = new DownLoadFile();
        private readonly string downloadDir = @"D:\test";

首先是构造了 Form1 初始化窗体组件,使用download file dlf来处理文件的下载功能 downloadDir显示了下载完成的内容存放的文件夹

private void btnTest_Click(object sender, EventArgs e)
{
    try
    {
        string[] lines = File.ReadAllLines("软件下载1.txt");
        foreach (var line in lines)
        {
            string[] parts = line.Split(new string[] { "|" }, StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length == 2)
            {
                string path = Uri.EscapeUriString(parts[1]);
                string filename = parts[0];
                var item = new ListViewItem(new string[]
                {
                    (listView1.Items.Count + 1).ToString(),
                    filename,
                    "0",
                    "0",
                    "0%",
                    "0",
                    "0",
                    DateTime.Now.ToString(),
                    "等待中",
                    parts[1]
                });
                listView1.Items.Add(item);
                int id = item.Index;
                dlf.AddDown(path, downloadDir, id, filename);
            }
        }
        dlf.StartDown();
    }
    catch (Exception ex)
    {
        MessageBox.Show($"Error reading file: {ex.Message}");
    }
}

读取 软件下载1.txt 中的每一行,解析后添加到 ListView 控件中,在软件下载txt中存放的是所有软件的下载地址以及文件名,格式为文件名|下载链接,使用 dlf.AddDown 方法添加下载任务,并调用 dlf.StartDown 开始下载。

private void SendMsgHander(DownMsg msg)
{
    Action updateUI = null;
    switch (msg.Tag)
    {
        case DownStatus.Start:
            updateUI = () =>
            {
                listView1.Items[msg.Id].SubItems[8].Text = "开始下载";
                listView1.Items[msg.Id].SubItems[7].Text = DateTime.Now.ToString();
            };
            break;
        case DownStatus.GetLength:
            updateUI = () =>
            {
                listView1.Items[msg.Id].SubItems[3].Text = msg.LengthInfo;
                listView1.Items[msg.Id].SubItems[8].Text = "连接成功";
            };
            break;
        case DownStatus.End:
        case DownStatus.DownLoad:
            updateUI = () =>
            {
                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;
                listView1.Items[msg.Id].SubItems[8].Text = msg.Tag == DownStatus.DownLoad ? "下载中" : "下载完成";
            };
            break;
        case DownStatus.Error:
            updateUI = () =>
            {
                listView1.Items[msg.Id].SubItems[6].Text = "失败";
                listView1.Items[msg.Id].SubItems[8].Text = msg.ErrMessage;
            };
            break;
    }

    if (updateUI != null)
    {
        this.Invoke(updateUI);
    }
}

根据 msg.Tag 处理不同的下载状态(开始、获取长度、下载中、下载完成、错误),并更新 ListView 控件的相应项。

DownloadFile代码分析

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace Gac
{
    public class DownLoadFile
    {
        ...
    }
}

使用namespace Gac包含DownloadFile类

public int ThreadNum = 1;
List<Thread> list = new List<Thread>();

public DownLoadFile()
{
    doSendMsg += Change;
}

将下载的线程数ThreadNum为1,List<Thread>,用于存储下载线程。 构造函数 DownLoadFile 初始化类,并将 Change 方法绑定到 doSendMsg 事件。

private void Change(DownMsg msg)
{
    if (msg.Tag == DownStatus.Error || msg.Tag == DownStatus.End)
    {
        StartDown(1);
    }
}

Change 方法在下载出现错误或结束时触发,调用 StartDown 方法重新开始下载。

public void AddDown(string DownUrl, string Dir, int Id = 0, string FileName = "")
{
    Thread tsk = new Thread(() =>
    {
        download(DownUrl, Dir, FileName, Id);
    });
    list.Add(tsk);
}

AddDown 方法添加一个新的下载任务,并将其作为线程添加到 list 中,下载任务的URL、目录、文件名和ID通过参数传递

public void StartDown(int StartNum = 1)
{
    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;
                }
            }
        }
    }
}

开始数StartNum设置为1,并且使用 lock 关键字确保线程安全,检查线程状态并启动未开始或暂停的线程。

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 listener = new DownloadProgressListener(msg);
        listener.doSendMsg = new DownloadProgressListener.dlgSendMsg(doSendMsg);
        loader.download(listener);
    }
    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);
    }
}

获取文件大小、下载进度和下载完成时,通过 doSendMsg 事件发送消息,并使用 FileDownloader 类和 DownloadProgressListener 类处理下载逻辑和进度监听 

DownloadProgressListener代码分析

public class DownloadProgressListener : IDownloadProgressListener
{
    private long presize = 0;
    DownMsg downMsg = null;

    public DownloadProgressListener(DownMsg downmsg)
    {
        this.downMsg = downmsg;
    }

    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); // 通知具体调用者下载进度
    }
}

DownloadProgressListener 类实现了 IDownloadProgressListener 接口,用于监听下载进度并显示,OnDownloadSize 方法根据下载的大小更新 downMsg 对象的属性,并通过 doSendMsg 委托通知调用者下载进度。

public enum DownStatus
{
    Start,
    GetLength,
    DownLoad,
    End,
    Error
}

 DownStatus 枚举定义了下载过程中的各种状态。

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; }
    }

    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
    {
        get { return _Speed; }
        set
        {
            _Speed = value;
            SpeedInfo = GetFileSize(value);
        }
    }

    public string SpeedInfo
    {
        get { return _SpeedInfo; }
        set { _SpeedInfo = value; }
    }

    public float Surplus
    {
        get { return _Surplus; }
        set
        {
            _Surplus = value;
            if (value > 0)
            {
                SurplusInfo = GetDateName((int)Math.Round(value, 0));
            }
        }
    }

    public string ErrMessage
    {
        get { return _ErrMessage; }
        set { _ErrMessage = value; }
    }

    public string SizeInfo
    {
        get { return _SizeInfo; }
        set { _SizeInfo = value; }
    }

    public string LengthInfo
    {
        get { return _LengthInfo; }
        set { _LengthInfo = value; }
    }

    public double Progress
    {
        get { return _Progress; }
        set { _Progress = value; }
    }

    public string SurplusInfo
    {
        get { return _SurplusInfo; }
        set { _SurplusInfo = value; }
    }

    private string GetFileSize(float Len)
    {
        float temp = Len;
        string[] sizes = { "B", "KB", "MB", "GB" };
        int order = 0;
        while (temp >= 1024 && order + 1 < sizes.Length)
        {
            order++;
            temp = temp / 1024;
        }
        return String.Format("{0:0.##} {1}", temp, sizes[order]);
    }

    private string GetDateName(int Second)
    {
        float temp = Second;
        string suf = "秒";
        if (Second > 60)
        {
            suf = "分钟";
            temp = temp / 60;
            if (Second > 60)
            {
                suf = "小时";
                temp = temp / 60;
                if (Second > 24)
                {
                    suf = "天";
                    temp = temp / 24;
                    if (Second > 30)
                    {
                        suf = "月";
                        temp = temp / 30;
                        if (Second > 12)
                        {
                            suf = "年";
                            temp = temp / 12;
                        }
                    }
                }
            }
        }
        return String.Format("{0:0} {1}", temp, suf);
    }
}

 DownMsg 类包含了一系列属性,用于存储和计算下载过程中的各种状态和信息,通过 Length, Size, Speed, Surplus 等属性的 setter 方法计算并更新相应的描述信息。

DownloadThread代码分析

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;
    }
}

私有成员变量用于存储下载的相关信息,包括保存文件路径 (saveFilePath)、下载URL (downUrl)、下载块大小 (block)、线程ID (threadId)、已下载长度 (downLength)、是否完成 (finish) 和下载器对象 (downloader)。

public void ThreadRun()
{
    Thread td = new Thread(new ThreadStart(() =>
    {
        if (downLength < block) // 未下载完成
        {
            try
            {
                int startPos = (int)(block * (threadId - 1) + downLength); // 开始位置
                int endPos = (int)(block * threadId - 1); // 结束位置
                Console.WriteLine("Thread " + this.threadId + " start download from position " + startPos + "  and endwith " + endPos);
                
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(downUrl);
                request.Referer = downUrl.ToString();
                request.Method = "GET";
                request.UserAgent = "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; SV1; .NET CLR 2.0.1124)";
                request.AllowAutoRedirect = false;
                request.ContentType = "application/octet-stream";
                request.Accept = "image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*";
                request.Timeout = 10 * 1000;
                request.AllowAutoRedirect = true;
                request.AddRange(startPos, endPos);
                
                HttpWebResponse response = (HttpWebResponse)request.GetResponse();
                WebResponse wb = request.GetResponse();
                
                using (Stream _stream = wb.GetResponseStream())
                {
                    byte[] buffer = new byte[1024 * 50]; // 缓冲区大小
                    long offset = -1;
                    using (Stream threadfile = new FileStream(this.saveFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite))
                    {
                        threadfile.Seek(startPos, SeekOrigin.Begin); // 移动文件位置
                        while ((offset = _stream.Read(buffer, 0, buffer.Length)) != 0)
                        {
                            downloader.append(offset); // 更新已下载总文件大小
                            threadfile.Write(buffer, 0, (int)offset);
                            downLength += offset; // 更新当前线程已下载位置
                            downloader.update(this.threadId, downLength);
                        }
                    }
                }
                Console.WriteLine("Thread " + this.threadId + " download finish");
                this.finish = true;
            }
            catch (Exception e)
            {
                this.downLength = -1;
                Console.WriteLine("Thread " + this.threadId + ":" + e.Message);
            }
        }
    }));
    
    td.IsBackground = true;
    td.Start();
}

ThreadRun 方法启动一个新的线程来执行下载任务,在新线程中,如果未完成下载,计算开始和结束位置,发送 HTTP 请求以下载指定范围的数据,并将数据写入文件,在下载过程中,更新下载进度和已下载的长度,如果下载完成,将 finish 设置为 true

public bool isFinish()
{
    return finish;
}

public long getDownLength()
{
    return downLength;
}

isFinish 方法返回下载是否完成的状态,getDownLength 方法返回已下载的内容大小,如果返回值为 -1,代表下载失败。

FIleloader代码分析

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 FileDownloader(string downloadUrl, string fileSaveDir, string filename="", int threadNum=3)
{
    try
    {
        if (string.IsNullOrEmpty(filename))
        {
            filename = Uri.UnescapeDataString(Path.GetFileName(downloadUrl)); // 获取文件名称并进行URI解码
        }
        
        this.downloadUrl = downloadUrl;
        if (!Directory.Exists(fileSaveDir)) Directory.CreateDirectory(fileSaveDir);
        this.threads = new DownloadThread[threadNum];
        
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(downloadUrl);
        request.Referer = downloadUrl.ToString();
        request.Method = "GET";
        request.UserAgent = "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; SV1; .NET CLR 2.0.1124)";
        request.ContentType = "application/octet-stream";
        request.Accept = "image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*";
        request.Timeout = 20 * 1000;
        request.AllowAutoRedirect = true;

        using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
        {
            if (response.StatusCode == HttpStatusCode.OK)
            {
                this.fileSize = response.ContentLength; // 根据响应获取文件大小
                if (this.fileSize <= 0) throw new Exception("获取文件大小失败");
                
                if (filename.Length == 0) throw new Exception("获取文件名失败");
                this.saveFile = Path.Combine(fileSaveDir, filename); // 构建保存文件路径
                
                // 计算每条线程下载的数据长度
                this.block = (this.fileSize % this.threads.Length) == 0 ? this.fileSize / this.threads.Length : this.fileSize / this.threads.Length + 1;
            }
            else
            {
                throw new Exception("服务器返回状态失败, StatusCode:" + response.StatusCode);
            }
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
        throw new Exception("无法连接下载地址");
    }
}

构造函数初始化下载参数,包括下载URL、本地保存路径、线程数量等,使用 HttpWebRequest 获取文件大小并计算每个线程需要下载的数据块大小

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)
            {
                this.threads[i] = new DownloadThread(this, downloadUrl, this.saveFile, this.block, this.data[i + 1], i + 1);
                this.threads[i].ThreadRun();
            }
            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;
}

download 方法启动所有下载线程并监控下载进度,创建一个文件流,根据文件大小设置文件长度,初始化每条线程已经下载的数据长度,循环启动每个线程并监控它们的下载进度,如果某个线程下载失败,则重新启动该线程,使用监听器通知下载进度。

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);
    }
}

getThreadSize 返回线程数量,getFileSize 返回文件大小,append 累计已下载大小,使用锁确保线程安全,update 更新指定线程最后下载的位置。

IDownloadProgressListener代码分析

using System;
using System.Collections.Generic;
using System.Text;

namespace Gac
{
   public interface IDownloadProgressListener
   {
       void OnDownloadSize(long size);
   }
}

使用IDownloadProgressListener 该接口来用于定义一个方法 OnDownloadSize,其作用是当下载的文件大小发生变化时调用此方法,参数 size 表示当前已下载的字节数。

三.代码功能实现

软件下载1.txt中的内容是

运行代码后,由于设置的是单线程startnum为1的下载,所以会从第一个应用或文件一直到最后一个文件依次下载,其余文件进入等待阶段,下载完成一个文件后继续。

下载的文件的文件夹保存地址D:/test

 可以看到,所有文件都已经完成下载且可以正常使用。

四.代码设计总结

  由于多个线程同时访问和修改下载进度,需要在更新进度时使用锁机制确保线程安全。在非UI线程中更新UI控件会引发异常。使用Invoke方法确保在UI线程上执行更新操作。网络请求可能失败(如无效URL、服务器不可用等),在下载线程中捕获异常,并重新启动失败的线程,通过完成这个文件下载器,可以掌握一些多线程和网络编程的基本原理,还可以提升设计和调试复杂系统的能力,这些经验在之后我将遇到的软件开发中可以起到帮助作用

  • 32
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值