一.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、服务器不可用等),在下载线程中捕获异常,并重新启动失败的线程,通过完成这个文件下载器,可以掌握一些多线程和网络编程的基本原理,还可以提升设计和调试复杂系统的能力,这些经验在之后我将遇到的软件开发中可以起到帮助作用