在开始今天的作业之前,我们先了解一下到底什么是多线程文件并发下载。
多线程文件并发下载是一种提高文件下载速度的技术,它通过同时启动多个线程来实现文件的分段下载。
- 在传统的单线程下载中,只有一个线程负责从服务器下载文件数据并保存到本地。而在多线程下载中,我们可以同时启动多个线程去请求不同的文件片段。
- 线程可以理解为下载的通道,一个线程就是一个文件的下载通道,多线程也就是同时开启好几个下载通道。这样,可以充分利用网络带宽资源,从而提高整体的下载速度。
为什么我们要学习多线程文件并发下载呢?
当然是因为对我们的程序有帮助了
- 提高下载速度:通过并发地执行下载任务,将文件分割成多个片段进行下载,最后将这些片段合并成完整的文件,从而充分利用带宽资源,提高下载效率。
- 充分利用资源:可以根据系统的CPU核心数量或网络带宽情况,合理设置线程数,以充分利用可用的计算和网络资源。
现在我们大概的了解了多线程文件并发下载,开始我们今天的学习吧!
1、功能描述
本次作业主要是利用C#实现了一个多线程文件的下载器,能够从文本文件中读取下载链接和文件名,然后通过多线程文件并发下载的方式下载文件。
2、功能分析与详解
2.1 Form1.cs
这段代码定义了一个名为
Form1
的Windows窗体(Form
)类,该类属于Demo
命名空间。该窗体包含了一个文件下载的功能,通过读取一个文本文件(软件下载1.txt
)来获取下载链接和相关信息,并使用DownLoadFile
类来执行实际的下载操作。SendMsgHander
方法是一个事件处理器,它接收一个DownMsg
类型的参数msg
,该参数应该包含有关下载任务状态的信息。这个方法根据msg
中的Tag
属性(代表下载的不同状态)来更新一个ListView
控件中的对应项
2.1.1 窗体初始化
public Form1()
{
InitializeComponent();
}
这是窗体的构造函数,调用InitializeComponent
方法来加载窗体上定义的所有控件
2.1.2下载文件相关成员变量
DownLoadFile dlf = new DownLoadFile();
这里创建了一个DownLoadFile
类的实例dlf
,该类应该包含文件下载的逻辑。注意,这个类在提供的代码段中并没有定义,所以我们只能假设它的功能。
2.1.3 btnTest_Click事件处理器:
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\DELL\Desktop\测试";
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();
}
- 当名为btnTest的按钮被点击时,该事件处理器会被触发。
- 它从软件下载1.txt文件中读取所有行,每行应该包含一个软件下载链接和其他相关信息,这些信息由|分隔。
- 对于每一行,它检查是否有两个由|分隔的部分。
- 如果有,它会提取链接和文件名,并创建一个新的ListViewItem来显示在listView1控件中。
- 然后,它将下载任务添加到dlf对象的下载队列中,并传入相关的参数,如目标路径、列表视图的项索引等。
- 在所有行都被处理后,它调用dlf.StartDown()来开始下载。
2.1.4 Form1_Load事件处理器
private void Form1_Load(object sender, EventArgs e)
{
dlf.ThreadNum = 3;//线程数,不设置默认为3
dlf.doSendMsg += SendMsgHander;//下载过程处理事件
}
- 当窗体加载时,这个事件处理器会被触发。
- 它设置
dlf
对象的ThreadNum
属性为3,这意味着它将使用3个线程来同时下载文件(如果不设置,则默认为3个线程)。 - 它还订阅了
dlf
对象的doSendMsg
事件,将处理函数SendMsgHander
作为事件处理器。这个事件可能用于在下载过程中更新UI或执行其他操作.
2.1.5 事件处理器 SendMsgHander
事件处理器 SendMsgHander
,用于响应从某个文件下载任务中传递出来的消息。从代码的内容可以看出,这个消息通常是一个自定义类型 DownMsg
,其中包含了文件下载的状态信息(如开始、获取长度、下载中、完成、错误等)以及其他相关的数据(如下载的文件大小、已下载大小、速度等)。
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;
}
}
2.1.6 整体代码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Text;
using System.Windows.Forms;
using Gac;
namespace Demo
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
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 = @"F:\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();
}
private void Form1_Load(object sender, EventArgs e)
{
dlf.ThreadNum = 3;//线程数,不设置默认为3
dlf.doSendMsg += SendMsgHander;//下载过程处理事件
}
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;
}
}
}
}
2.2 DownLoadFile.cs
定义了一个名为
DownLoadFile
的类,用于管理文件下载任务.download
方法是执行文件下载任务的核心方法。它接收四个参数:path
(下载路径)、dir
(文件保存目录)、filename
(文件名)和id
(任务ID,默认为0)
2.2.1 初始化
- 创建一个
DownMsg
对象msg
,用于传递下载状态、进度等信息。 - 设置
msg
的Id
为传入的id
,Tag
为初始状态(0,可能是尚未开始的状态)。 - 通过
doSendMsg
事件触发一个消息,通知监听者下载任务即将开始
2.2.2 创建 FileDownloader 实例
- 创建一个
FileDownloader
对象loader
,传入下载路径、保存目录、文件名以及可能的线程数量(但此处传入的是ThreadNum
,它在DownloadFile
类中定义,但通常这种下载器类不会需要线程数量作为构造函数的参数,除非它是为了管理多个并发的下载任务)。 - 清空
loader
的data
(虽然这里没有明确说明data
是什么,但很可能是与下载任务相关的内部状态或缓存数据)。 - 设置
msg
的Tag
为DownStatus.Start
,并通过doSendMsg
事件触发一个消息,通知监听者下载已经开始。 - 获取文件的总大小,并设置到
msg
的Length
属性中,再次通过doSendMsg
事件通知监听者。
2.2.3 设置下载进度监听器
- 创建一个
DownloadProgressListener
对象linstenter
(注意这里可能是一个拼写错误,通常我们会用listener
)。 - 为
linstenter
的doSendMsg
事件分配一个新的委托,将DownloadFile
类中的doSendMsg
方法与之关联,这样当下载进度发生变化时,DownloadFile
类可以收到通知。 - 调用
loader
的download
方法,传入linstenter
作为参数,开始实际的下载过程。
2.2.4 异常处理
- 如果在下载过程中发生任何异常,
catch
块将捕获该异常。 - 创建一个新的
DownMsg
对象msg
,设置其Id
、Length
(设置为0表示没有下载任何数据)和Tag
(设置为DownStatus.Error
表示发生错误)。 - 将异常的消息设置到
msg
的ErrMessage
属性中。 - 通过
doSendMsg
事件触发一个消息,通知监听者下载任务出错,并传递错误消息。 - 将异常消息输出到控制台。
2.2.5 整体代码
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace Gac
{
public class DownLoadFile
{
public int ThreadNum = 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;
//public event doSendMsg;
//public dlgSendMsg doSendMsg = null;
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);
}
}
}
}
2.3 DownloadProgressListener
2.3.1 成员变量
presize
:一个long
类型的变量,但在类中没有使用到。downMsg
:一个DownMsg
类型的对象,用于存储和更新下载相关的信息(如速度、剩余时间、总大小等)
2.3.2 构造函数
接收一个 DownMsg
类型的参数 downmsg
,并将其赋值给 downMsg
成员变量
2.3.3 OnDownloadSize 方法
- 当文件下载的大小发生变化时,这个方法会被调用。
- 如果
downMsg
为null
,代码创建了一个新的DownMsg
对象,但并未将其赋值给类的成员变量downMsg
,这是一个逻辑错误,因为后续代码将使用这个成员变量。 - 计算下载速度(
Speed
):- 如果
downMsg.Size
为0(即这是第一次调用),则将当前大小size
设置为速度。 - 否则,将当前大小与上一次大小之差作为速度。
- 如果
- 计算剩余时间(
Surplus
):- 如果速度为0,则剩余时间和剩余时间信息(
SurplusInfo
)被设置为一个默认值。 - 否则,根据剩余大小和当前速度计算剩余时间。
- 如果速度为0,则剩余时间和剩余时间信息(
- 更新下载总量(
Size
)。 - 根据下载是否完成设置不同的状态信息(
Tag
、SpeedInfo
和SurplusInfo
)。 - 如果
doSendMsg
委托不为null
,则调用它并将更新后的downMsg
对象传递出去。
2.3.4 下载速度和剩余时间计算
在 OnDownloadSize 方法中,计算下载速度和剩余时间。如果当前下载大小与上一次的大小相同,则速度为0,剩余时间设置为未知。如果下载速度不为0,则计算剩余时间和转换为友好的格式。
2.3.5 DownMsg
定义了一个名为 DownMsg
的类,用于存储和表示下载过程中的相关信息。该类包含多个私有字段(以 _
开头)和对应的公共属性(没有 _
前缀)。每个公共属性都包含 get
和 set
访问器,允许外部代码读取和修改这些字段的值。
2.3.6 整体代码
using System;
using System.Collections.Generic;
using System.Text;
namespace Gac
{
public class DownloadProgressListener : IDownloadProgressListener
{
private long presize=0;
DownMsg downMsg = null;
public DownloadProgressListener(DownMsg downmsg)
{
this.downMsg = downmsg;
//this.id = id;
//this.Length = Length;
}
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);//通知具体调用者下载进度
}
}
public enum DownStatus
{
Start,
GetLength,
DownLoad,
End,
Error
}
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;
}
}
/// </summary>
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);
}
}
}
2.4 DownloadThread
该类用于管理多线程文件下载,用
ThreadRun
方法和isFinish
方法实现
2.4.1 ThreadRun 方法
2.4.1.1 请求头设置
request.Accept
设置了客户端可以接受的 MIME 类型列表。request.Timeout
设置了请求的超时时间为10秒。request.AllowAutoRedirect
被设置为true
,这允许HTTP请求自动跟随重定向。但考虑到你正在做分块下载,通常你会希望这个设置为false
以避免意外的重定向。request.AddRange(startPos, endPos)
正确地设置了Range
头,用于请求文件的特定部分。但请注意,endPos
应该是一个包含结束字节的索引,而不是一个不包含结束字节的索引。因此,你可能需要调整endPos
的计算方式。
2.4.1.2 获取响应
使用了 HttpWebResponse response = (HttpWebResponse)request.GetResponse();
来获取HTTP响应,但随后又使用 WebResponse wb = request.GetResponse();
再次获取了响应,这是不必要的,并且可能导致资源泄露。你应该只使用 response
变量
2.4.1.3 处理响应流
- 使用
Stream _stream = wb.GetResponseStream()
获取了响应流,但应该使用response.GetResponseStream()
。 - 创建了一个
FileStream
对象threadfile
来写入文件。但是,使用FileMode.Open
可能会导致问题,因为你正在尝试在已经存在的文件上写入数据。为了安全起见,你可能想使用FileMode.OpenOrCreate
并确保文件在写入之前被截断到正确的位置(如果它已存在)。 - 使用
threadfile.Seek(startPos, SeekOrigin.Begin)
将文件指针移动到正确的位置以开始写入。 - 在循环中从网络流读取数据并写入文件是正确的做法。
downloader.append(offset)
和downloader.update(this.threadId, downLength)
似乎是用于更新下载进度的回调方法,但它们不是.NET框架中的标准方法,所以假设这些方法在FileDownloader
类中已正确实现。
2.4.1.4 资源管理
- 使用
using
语句来确保Stream
和FileStream
对象在使用后正确关闭和释放资源是一个好习惯。但是,你仍然手动调用了threadfile.Close()
和_stream.Close()
,这是不必要的,因为using
语句会自动处理这些。 Console.WriteLine("Thread " + this.threadId + ":" + e.Message);
这行代码中的e.Message
似乎是一个错误,因为在这个上下文中没有定义e
(可能是一个Exception
对象)。你应该删除e.Message
或用适当的信息替换它
2.4.1.5 线程启动
td.IsBackground = true;
将线程设置为后台线程,这意味着当主应用程序结束时,后台线程将被自动终止,而不是阻止应用程序关闭。td.Start();
启动线程。
2.4.2 isFinish 方法
这个方法简单地返回了一个布尔值 finish
,但在ThreadRun
方法中,这个值从未被设置或更改,所以它始终返回 false
(除非在其他地方有代码设置了这个值)。你可能需要在下载完成时设置 finish
为 true
。
2.4.3 范围请求
使用 request.AddRange(startPos, endPos) 设置HTTP请求的范围,以实现断点续传和多线程下载。
2.4.4 HTTP请求设置
在 ThreadRun 方法中,设置 HttpWebRequest 的属性,如Referer、UserAgent、ContentType、Accept 和 Timeout,以适应不同的服务器和下载需求。
2.4.5 整体代码
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.IO;
using System.Threading;
namespace Gac
{
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;
}
public void ThreadRun()
{
//task
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);
//Console.WriteLine(request.Headers.ToString()); //输出构建的http 表头
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)
{
//offset 实际下载流大小
downloader.append(offset); //更新已经下载当前总文件大小
threadfile.Write(buffer, 0, (int)offset);
downLength += offset; //设置当前线程已下载位置
downloader.update(this.threadId, downLength);
}
threadfile.Close(); //using 用完后可以自动释放..手动释放一遍.木有问题的(其实是多余的)
_stream.Close();
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();
}
/// <summary>
/// 下载是否完成
/// </summary>
/// <returns></returns>
public bool isFinish()
{
return finish;
}
/// <summary>
/// 已经下载的内容大小
/// </summary>
/// <returns>如果返回值为-1,代表下载失败</returns>
public long getDownLength()
{
return downLength;
}
}
}
2.5 FileDownloader
用于通过多线程方式从指定的URL下载文件
2.5.1 成员变量
downloadSize
:用于跟踪已下载的字节数。fileSize
:用于存储要下载的文件的大小。threads
:一个DownloadThread
类型的数组,用于存储下载线程。saveFile
:要保存的文件路径。data
:一个字典,用于跟踪每个线程已下载的字节位置。block
:每个线程应下载的文件块大小。downloadUrl
:要下载的文件的URL。
2.5.2 构造函数
- 初始化
FileDownloader
对象时,会检查并设置文件名(如果未提供,则从URL中提取),创建下载目录(如果不存在),并初始化下载线程数组。 - 通过HTTP请求获取文件大小,并检查HTTP状态码是否成功。
2.5.3 HTTP请求设置
在构造函数中,设置 HttpWebRequest 的属性,如 Referer、UserAgent、ContentType、Accept等,以适应不同的服务器和下载需求。
2.5.4 下载方法
- 初始化
data
字典以跟踪每个线程的进度。 - 为每个线程分配一个文件块,并启动线程进行下载。
- 使用一个循环来检查所有线程是否都已完成下载。如果某个线程失败(例如,返回-1作为已下载的字节数),则重新创建并启动该线程。
- 如果提供了
IDownloadProgressListener
接口的实现,则在每次循环时更新已下载的字节数。
2.5.5 整体代码
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.IO;
using System.Net;
namespace Gac
{
public class FileDownloader
{
/// <summary>
/// 已下载文件长度
/// </summary>
private long downloadSize = 0;
/// <summary>
/// 原始文件长度
/// </summary>
private long fileSize = 0;
/// <summary>
/// 线程数
/// </summary>
private DownloadThread[] threads;
/// <summary>
/// 本地保存文件
/// </summary>
private string saveFile;
/// <summary>
/// 缓存各线程下载的长度
/// </summary>
public Dictionary<int, long> data = new Dictionary<int, long>();
/// <summary>
/// 每条线程下载的长度
/// </summary>
private long block;
/// <summary>
/// 下载路径
/// </summary>
private String downloadUrl;
/// <summary>
/// 获取线程数
/// </summary>
/// <returns> 获取线程数</returns>
public int getThreadSize()
{
return threads.Length;
}
/// <summary>
/// 获取文件大小
/// </summary>
/// <returns>获取文件大小</returns>
public long getFileSize()
{
return fileSize;
}
/// <summary>
/// 累计已下载大小
/// </summary>
/// <param name="size">累计已下载大小</param>
public void append(long size)
{
lock (this) //锁定同步..........
{
downloadSize += size;
}
}
/// <summary>
/// 更新指定线程最后下载的位置
/// </summary>
/// <param name="threadId">threadId 线程id</param>
/// <param name="pos">最后下载的位置</param>
public void update(int threadId, long pos)
{
if (data.ContainsKey(threadId))
{
this.data[threadId] = pos;
}
else
{
this.data.Add(threadId, pos);
}
}
/// <summary>
/// 构建下载准备,获取文件大小
/// </summary>
/// <param name="downloadUrl">下载路径</param>
/// <param name="fileSaveDir"> 文件保存目录</param>
/// <param name="threadNum">下载线程数</param>
public FileDownloader(string downloadUrl, string fileSaveDir,string filename="", int threadNum=3)
{
try
{
if (string.IsNullOrEmpty(filename))
{
filename = Uri.UnescapeDataString(Path.GetFileName(downloadUrl));//获取文件名称 uri 解码中文字符
}
//构建http 请求
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("无法连接下载地址");
}
}
/// <summary>
/// 开始下载文件
/// </summary>
/// <param name="listener">监听下载数量的变化,如果不需要了解实时下载的数量,可以设置为null</param>
/// <returns>已下载文件大小</returns>
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)
{//判断线程是否已经完成下载,否则继续下载 +
// Console.WriteLine("threads" + i.ToString() + ",下载块" + this.block.ToString() + " " + this.data[i + 1].ToString() + " " + downloadSize.ToString());
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;
}
}
}
2.6 IDownloadProgressListener
定义了一个简单的C#接口,名为
IDownloadProgressListener
,它位于名为Gac
的命名空间中。这个接口定义了一个方法,即OnDownloadSize
,该方法接收一个long
类型的参数size。
2.6.1 整体代码
using System;
using System.Collections.Generic;
using System.Text;
namespace Gac
{
public interface IDownloadProgressListener
{
void OnDownloadSize(long size);
}
}
2.6.2 代码解释
using System;
:这行代码引入了C#的基本命名空间,它包含了.NET Framework的核心类型和基本功能。using System.Collections.Generic;
:这行代码引入了.NET Framework中的通用集合类型,尽管在提供的代码片段中并没有直接使用到这些类型。using System.Text;
:这行代码引入了与文本处理相关的类,如StringBuilder
等,但在这个特定的接口定义中也没有使用到。namespace Gac
:这定义了一个名为Gac
的命名空间。命名空间是一种将相关的代码(类、接口、结构等)组织在一起的方式,有助于避免命名冲突并提供了一种组织代码的方法。public interface IDownloadProgressListener
:这定义了一个公共接口,名为IDownloadProgressListener
。接口是一种定义一组方法、属性、索引器和事件的合同,但不包含它们的实现。任何实现这个接口的类都必须提供这个接口中定义的所有成员的实现。void OnDownloadSize(long size);
:这是IDownloadProgressListener
接口中定义的一个方法。它名为OnDownloadSize
,没有返回值(即返回类型为void
),并接受一个long
类型的参数size
。这个方法可能被用于通知监听器(即实现了这个接口的任何对象)有关文件下载进度的更新。
3、效果预览
4、实验小结
这次的实验是用C#实现多线程文件下载的,展现了一个模块化的设计。通过本次的实验,我终于成功的实现了一个基于C#的多线程文件下载器。该下载器能够读取文本文件中的下载链接和文件名,并利用多线程技术实现文件的并发下载。在测试过程中,我发现多线程下载器能够同时处理多个下载任务,并且每个任务的下载进度都能够实时更新。此外,我们还对下载器进行了性能优化,如使用线程池来管理线程资源,避免了频繁创建和销毁线程的开销。本次实验让我深入了解了C#多线程编程的原理和应用,以及如何在实际项目中利用多线程技术提高程序的性能和效率。通过实现一个多线程文件下载器,我们不仅掌握了多线程编程的基本技能,还学会了如何分析和解决多线程编程中可能遇到的问题。在实验过程中,我们也遇到了一些挑战和困难,如线程同步、数据共享等问题。通过老师讲解、查阅资料和不断尝试,我们最终找到了解决方案并成功实现了实验目标。这次实验让我们深刻体会到了编程的乐趣和挑战性,也为我们今后的学习和工作打下了坚实的基础。