目录
这段代码是一个C#方法,用于处理一个按钮(名为btnTest)的点击事件。以下是代码的详细分析:
3.1.2 这段代码涉及到一个WinForms应用程序中的文件下载功能,特别是与下载状态更新和用户界面(UI)线程之间的交互
这段代码定义了一个名为DownloadThread的类,它代表一个用于文件下载的线程。以下是对这段代码的分析:
3.5 IDownloadProgressListener.cs
一、作业内容
实现文件并发下载,利用C#编写代码,能够从文本文件中读取文件下载链接和文件名,该次作业文件名命名为软件下载1.txt,并将下载内容下载至D:\test,下载界面可以反映出文件名、文件总大小、文件下载速度、下载进度、下载时间、下载状态、下载网址等。可以通过多线程的方式进行文件并发下载,界面可实时观看下载速度、状态和进度。
二、作业设计界面和项目结构
三、实验代码
3.1 Form1.cs
这段代码是一个C#方法,用于处理一个按钮(名为btnTest)的点击事件。以下是代码的详细分析:
文件读取:string[] lines = File.ReadAllLines("软件下载1.txt");
这行代码从名为"软件下载1.txt"的文件中读取所有行,并将它们存储在一个字符串数组中。
代码定义了一个目标目录dir,其值为"D:\test"。
更新ListView:
使用ListViewItem的构造函数创建一个新的列表项,其中包含多个子项(字符串数组)。这些子项包括:当前项在ListView中的索引(加1,以便从1开始计数)文件名(从path中提取)
七个固定的字符串或日期时间值(例如,"0", "0%", DateTime.Now.ToString(), "等待中"等)
原始的文件路径(从line数组的第二个元素获取)
使用listView1.Items.Add方法将新创建的ListViewItem添加到listView1控件中,并获取新项的索引值。将新项的索引值存储在id变量中。
添加下载任务:
dlf.AddDown(path, dir, id, id.ToString());
这行代码调用一个名为dlf的对象的AddDown方法,并传递四个参数:要下载的文件的路径、目标目录、下载任务的ID(整数形式)和下载任务的ID(字符串形式)。注意,这里使用id和id.ToString()作为两个不同的参数,可能是为了方法内部的不同需求(尽管从代码片段中我们不能确定AddDown方法的确切实现)。
开始下载:
dlf.StartDown();
最后,这行代码调用dlf对象的StartDown方法,开始执行之前添加的所有下载任务。
DownLoadFile dlf = new DownLoadFile();
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.1.2 这段代码涉及到一个WinForms应用程序中的文件下载功能,特别是与下载状态更新和用户界面(UI)线程之间的交互
Form1_Load 方法:
在窗体加载时,设置dlf对象的ThreadNum属性为3,表示同时进行的下载线程数。如果不设置,则默认为3。
订阅dlf对象的doSendMsg事件,当下载过程中有消息发送时,会调用SendMsgHander方法。
private void Form1_Load(object sender, EventArgs e)
{
dlf.ThreadNum = 3;//线程数,不设置默认为3
dlf.doSendMsg += SendMsgHander;//下载过程处理事件
}
SendMsgHander 方法:
这是一个事件处理程序,用于处理来自dlf对象的下载消息。
它接受一个DownMsg类型的参数msg,该参数包含了下载相关的状态信息。
使用switch语句根据msg.Tag的值(即下载状态)来更新ListView中的相应项。如:下载、连接成功、下载中、下载完成等。
状态更新:
对于每种状态(如Start, GetLength, DownLoad, End, Error),代码通过调用this.Invoke方法来确保UI更新在UI线程上执行
// 下载消息处理事件处理器
private void SendMsgHander(DownMsg msg) // 处理下载状态更新
{
// 根据下载状态更新ListView控件中的项
switch (msg.Tag)
{
case DownStatus.Start:
// 开始下载
this.Invoke((MethodInvoker)delegate ()
{
listView1.Items[msg.Id].SubItems[8].Text = "开始下载";
// ... 省略其他UI更新代码
});
break;
// ... 省略其他case处理代码
}
}
3.2 DownLoadFile.cs
成员变量:
ThreadNum:定义默认下载线程数为3。
list:一个线程列表,用于存储创建的下载线程。
构造函数:
在构造函数中,将Change方法绑定到doSendMsg事件。这意味着每当doSendMsg事件被触发时,Change方法会被调用。
Change方法:
当下载任务遇到错误或结束时(DownStatus.Error或DownStatus.End),该方法会尝试启动一个新的下载任务(通过调用StartDown(1))。
AddDown方法:
该方法用于向list中添加一个新的下载线程。线程执行的是download方法,并传入相关参数。
StartDown方法:
该方法尝试启动最多StartNum个未启动或挂起的线程。
使用lock关键字确保在遍历和修改list时线程安全。
事件和委托:
定义了dlgSendMsg委托和doSendMsg事件,用于在下载过程中发送消息。
注释掉的代码行(//public event doSendMsg;和//public dlgSendMsg doSendMsg = null;)
download方法:
这是实际执行下载操作的私有方法。
创建了一个DownMsg对象,并通过doSendMsg事件发送了初始消息
创建了FileDownloader对象来执行实际的下载操作,并传递了相关参数。
创建一个DownloadProgressListener对象来监听下载进度,并将doSendMsg事件绑定到该监听器的相应事件上。
public class DownLoadFile
{
// 默认线程数量
public int ThreadNum = 3;
// 用于存储所有下载线程的列表
List<Thread> list = new List<Thread>();
// 构造函数,订阅消息发送事件
public DownLoadFile()
{
doSendMsg += Change; // 注册事件处理
}
// 下载任务添加方法
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 == ThreadState.Unstarted || list[i].ThreadState == ThreadState.Suspended)
{
list[i].Start(); // 启动线程
break;
}
}
}
}
}
// 下载状态更新事件委托
public delegate void dlgSendMsg(DownMsg msg);
// 下载状态更新事件
public event dlgSendMsg doSendMsg;
// 下载状态变化处理方法
private void Change(DownMsg msg)
{
if (msg.Tag == DownStatus.Error || msg.Tag == DownStatus.End)
{
StartDown(1); // 可能的错误处理或重新下载逻辑
}
}
// 私有下载方法
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); // 打印异常信息到控制台
}
}
}
3.3 DownloadThread.cs
这段代码定义了一个名为DownloadThread的类,它代表一个用于文件下载的线程。以下是对这段代码的分析:
成员变量:
saveFilePath:保存文件的路径。
downUrl:下载文件的URL。
block:每个线程应该下载的文件块大小
threadId:线程的ID,用于标识不同的下载线程。
downLength:已经下载的长度(可能表示从文件开始到当前线程应该开始下载的位置)。
finish:一个标志,表示线程是否已完成下载。
downloader:FileDownloader对象的引用,但从给定的代码片段中不清楚其具体用途,因为该对象在DownloadThread类中没有被进一步使用。
构造函数:
构造函数接收必要的参数来初始化下载线程的状态,包括下载URL、保存文件的路径、每个线程应下载的块大小、已下载的长度以及线程的ID。
ThreadRun方法:
这个方法定义了一个内部线程来执行实际的下载任务。
在内部线程中,它首先检查是否还有未下载的数据(即downLength < block)。
然后,它计算当前线程应该开始和结束下载的位置。
接下来,它创建一个HttpWebRequest对象来发送GET请求到指定的URL,并设置了一些HTTP头信息。
// 定义一个下载线程类,用于多线程下载中的单个线程
public class DownloadThread
{
// 下载文件的保存路径
private string saveFilePath;
// 文件的下载URL
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()
{
// 创建一个新的线程来执行下载任务
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 end with {endPos}");
// 创建HTTP请求
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 = "*/*";
request.Timeout = 10 * 1000;
request.AllowAutoRedirect = true;
// 设置请求的范围,实现断点续传
request.AddRange(startPos, endPos);
// 获取HTTP响应
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
// 使用响应流
using (Stream _stream = response.GetResponseStream())
{
// 定义缓冲区
byte[] buffer = new byte[1024 * 50];
long offset;
// 打开文件流,准备写入数据
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);
}
// 关闭流资源
threadfile.Close();
_stream.Close();
}
// 下载完成,输出日志信息
Console.WriteLine($"Thread {this.threadId} download finish");
this.finish = true;
}
}
catch (Exception e)
{
// 异常处理,记录错误信息,并标记下载长度为-1表示失败
this.downLength = -1;
Console.WriteLine($"Thread {this.threadId}:{e.Message}");
}
}
}));
td.IsBackground = true; // 设置为后台线程,主程序结束时此线程也会结束
td.Start(); // 启动线程
}
// 判断下载是否完成
public bool isFinish()
{
return finish;
}
// 获取当前线程已下载的数据长度,失败时返回-1
public long getDownLength()
{
return downLength;
}
}
3.4 FileDownloader.cs
// FileDownloader类负责管理文件下载的核心逻辑
public class FileDownloader
{
// 已下载的文件长度
private long downloadSize = 0;
// 原始文件长度
private long fileSize = 0;
// 下载线程数组
private DownloadThread[] threads;
// 本地保存文件路径
private string saveFile;
// 缓存各线程下载的长度,键为线程ID
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))
{
data[threadId] = pos;
}
else
{
data.Add(threadId, pos);
}
}
// 构造函数,初始化下载器
public FileDownloader(string downloadUrl, string fileSaveDir, string filename = "", int threadNum = 3)
{
try
{
// 省略了URI解码和文件目录创建的代码...
// 创建HTTP请求以获取文件大小
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(downloadUrl);
// 省略了请求头设置的代码...
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
if (response.StatusCode == HttpStatusCode.OK)
{
// 获取文件大小
this.fileSize = response.ContentLength;
// 省略了保存文件路径设置的代码...
// 计算每条线程下载的数据长度
this.block = (this.fileSize % threadNum == 0) ?
this.fileSize / threadNum :
this.fileSize / threadNum + 1;
}
else
{
throw new Exception("服务器返回状态失败");
}
}
}
catch (Exception e)
{
// 异常处理逻辑
}
}
// 开始下载文件
public long download(IDownloadProgressListener listener)
{
try
{
// 省略了文件流设置的代码...
// 初始化线程下载数据长度字典
if (data.Count != threads.Length)
{
data.Clear();
for (int i = 0; i < threads.Length; i++)
{
data.Add(i + 1, 0);
}
}
// 开启线程进行下载
for (int i = 0; i < threads.Length; i++)
{
// 省略了线程创建和启动的代码...
}
// 循环判断所有线程是否完成下载
bool notFinish = true;
while (notFinish)
{
// 省略了线程状态检查和进度更新的代码...
}
}
catch (Exception e)
{
// 异常处理逻辑
}
return this.downloadSize;
}
}
3.5 IDownloadProgressListener.cs
public interface IDownloadProgressListener
{
/// <summary>
/// 当下载的字节大小发生变化时调用此方法。
/// 这个方法允许实现类根据下载的进度执行相应的操作。
/// </summary>
/// <param name="size">当前已下载的字节数。</param>
void OnDownloadSize(long size);
}
四、作业结果
五、作业小结
本次作业实现文件并发下载,利用C#编写代码,下载界面可以反映出文件名、文件总大小、文件下载速度、下载进度、下载时间、下载状态、下载网址等。可以通过多线程的方式进行文件并发下载,界面可实时观看下载速度、状态和进度。
作业目的为验证多线程下载是否能够有效提高文件下载的速度。理解和实现多线程下载的基本机制。作业过程为编写文件读取、更新ListView、添加下载任务、开始下载和状态更新代码等。
项目提供了操作界面,允许用户监控管理下载任务。该项目核心逻辑部分封装了多线程下载的具体实现。总体来说需要花更多时间来学习研究。