首先在实现文件的并行下载之前我们要明白为什么要实现文件的并行下载,他有什么样的意义?
当我们需要从网上下载多个文件时,如果一个接一个地下载,速度可能会很慢,用户也需要等一个文件下载完才能开始下一个。这时候,使用文件的并行下载就非常有用了。这意味着我们可以同时下载多个文件,就像同时用多个手势抓取东西一样,速度更快,效率更高。
举个例子,想象一下你需要下载一本电子书里的所有章节,如果一个章节一个章节地下载,可能需要很长时间。但如果你能同时下载所有章节,那速度会明显提升。
并行下载不仅提高了下载速度,还能让服务器和你的电脑资源得到更好的利用,避免资源浪费。此外,对于用户来说,不用等待一个文件下载完才能下载下一个,这样的体验也更好,特别是在下载大量文件或大文件时尤为明显。
总结一下,文件的并行下载在实际应用中有几个重要的意义和优势:
1. 提高下载速度和效率:通过同时下载多个文件,可以利用并行处理的优势,显著缩短整体下载时间。特别是在需要下载大量文件或大文件时,效果尤为明显。
2. 节省客户端和服务器资源:并行下载允许客户端同时向服务器发起多个请求,这样可以更好地利用网络带宽和服务器资源,避免过度负载或等待。
3. 增强用户体验:用户不需要等待一个文件下载完毕才能开始另一个下载,这提升了用户体验,尤其是在需要下载多个文件的应用场景(例如批量下载、多媒体文件等)。
4. 容错和恢复:通过适当的实现,可以在某些下载失败或中断后自动进行重试,以确保所有文件能够成功下载完成,提高应用的健壮性和可靠性。
话不多说,我们接下来来看看代码的实现:
1.用户界面(UI)设计
在设计器中添加以下组件:
- DataGridView 控件,用于显示下载列表。
- Buttons 用于开始、暂停和取消下载。
将DataGridView控件命名为dataGridViewDownloads
,并添加以下列:
- 序号:
Index
- 文件名:
FileName
- 总大小:
TotalSize
- 已完成:
CompletedSize
- 进度:
Progress
- 速度:
Speed
- 剩余:
Remaining
- 时间:
StartTime
- 状态:
Status
- 网址:
URL
在Form的代码文件中实现逻辑:
表格内部的代码:
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using System.Windows.Forms;
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
InitializeDownloadList();
}
private void InitializeDownloadList()
{
dataGridViewDownloads.Columns.Add("Index", "序号");
dataGridViewDownloads.Columns.Add("FileName", "文件名");
dataGridViewDownloads.Columns.Add("TotalSize", "总大小");
dataGridViewDownloads.Columns.Add("CompletedSize", "已完成");
dataGridViewDownloads.Columns.Add("Progress", "进度");
dataGridViewDownloads.Columns.Add("Speed", "速度");
dataGridViewDownloads.Columns.Add("Remaining", "剩余");
dataGridViewDownloads.Columns.Add("StartTime", "时间");
dataGridViewDownloads.Columns.Add("Status", "状态");
dataGridViewDownloads.Columns.Add("URL", "网址");
// Example data
dataGridViewDownloads.Rows.Add(1, "file1.txt", "10MB", "2MB", "20%", "500KB/s", "8MB", DateTime.Now.ToString("HH:mm:ss"), "正在下载", "http://example.com/file1.txt");
}
private async void StartDownload(string url, int rowIndex)
{
WebClient webClient = new WebClient();
DateTime startTime = DateTime.Now;
dataGridViewDownloads.Rows[rowIndex].Cells["StartTime"].Value = startTime.ToString("HH:mm:ss");
dataGridViewDownloads.Rows[rowIndex].Cells["Status"].Value = "正在下载";
webClient.DownloadProgressChanged += (s, e) =>
{
dataGridViewDownloads.Rows[rowIndex].Cells["CompletedSize"].Value = (e.BytesReceived / 1024.0 / 1024.0).ToString("F2") + "MB";
dataGridViewDownloads.Rows[rowIndex].Cells["Progress"].Value = e.ProgressPercentage + "%";
dataGridViewDownloads.Rows[rowIndex].Cells["Speed"].Value = (e.BytesReceived / 1024.0 / (DateTime.Now - startTime).TotalSeconds).ToString("F2") + "KB/s";
dataGridViewDownloads.Rows[rowIndex].Cells["Remaining"].Value = ((e.TotalBytesToReceive - e.BytesReceived) / 1024.0 / 1024.0).ToString("F2") + "MB";
};
webClient.DownloadFileCompleted += (s, e) =>
{
dataGridViewDownloads.Rows[rowIndex].Cells["Status"].Value = "已完成";
};
await webClient.DownloadFileTaskAsync(new Uri(url), "downloaded_file_" + rowIndex);
}
private void btnStartDownload_Click(object sender, EventArgs e)
{
foreach (DataGridViewRow row in dataGridViewDownloads.Rows)
{
if (row.Cells["Status"].Value.ToString() == "等待中")
{
string url = row.Cells["URL"].Value.ToString();
int rowIndex = row.Index;
StartDownload(url, rowIndex);
}
}
}
}
在设计器中添加一个按钮,用于开始下载。将其命名为btnStartDownload
,并在点击事件中调用StartDownload
方法。
private void btnStartDownload_Click(object sender, EventArgs e)
{
foreach (DataGridViewRow row in dataGridViewDownloads.Rows)
{
if (row.Cells["Status"].Value.ToString() == "等待中")
{
string url = row.Cells["URL"].Value.ToString();
int rowIndex = row.Index;
StartDownload(url, rowIndex);
}
}
}
2.下载流程设计
当用户点击下载按钮时,开始执行以下流程:
验证输入的URL是否有效。
发送HTTP GET请求到指定的URL,获取文件的元数据信息(包括文件总大小)。
根据获取到的文件总大小,初始化进度条和进度信息面板。
创建一个下载任务,开始接收服务器的响应数据,并将数据写入到本地的文件中。
在接收数据的过程中,实时更新进度条、已下载大小、下载速度、剩余大小等信息。
估算剩余时间(可以使用已下载数据量和平均下载速度来计算)。
当所有数据接收完毕后,标记下载状态为“已完成”,并关闭相关的下载资源。相关代码的实现如下(为了增强代码的健壮性,我们可以添加错误提示):
private async void btnStartDownload_Click(object sender, EventArgs e)
{
string url = txtURL.Text;
if (Uri.IsWellFormedUriString(url, UriKind.Absolute))
{
int rowIndex = dataGridViewDownloads.Rows.Add();
dataGridViewDownloads.Rows[rowIndex].Cells["Index"].Value = rowIndex + 1;
dataGridViewDownloads.Rows[rowIndex].Cells["URL"].Value = url;
dataGridViewDownloads.Rows[rowIndex].Cells["Status"].Value = "正在准备";
await StartDownload(url, rowIndex);
}
else
{
MessageBox.Show("无效的URL");
}
}
private async Task StartDownload(string url, int rowIndex)
{
try
{
WebClient webClient = new WebClient();
DateTime startTime = DateTime.Now;
dataGridViewDownloads.Rows[rowIndex].Cells["StartTime"].Value = startTime.ToString("HH:mm:ss");
dataGridViewDownloads.Rows[rowIndex].Cells["Status"].Value = "正在下载";
webClient.DownloadProgressChanged += (s, e) =>
{
dataGridViewDownloads.Invoke((MethodInvoker)delegate
{
dataGridViewDownloads.Rows[rowIndex].Cells["CompletedSize"].Value = (e.BytesReceived / 1024.0 / 1024.0).ToString("F2") + "MB";
dataGridViewDownloads.Rows[rowIndex].Cells["Progress"].Value = e.ProgressPercentage + "%";
dataGridViewDownloads.Rows[rowIndex].Cells["Speed"].Value = (e.BytesReceived / 1024.0 / (DateTime.Now - startTime).TotalSeconds).ToString("F2") + "KB/s";
dataGridViewDownloads.Rows[rowIndex].Cells["Remaining"].Value = ((e.TotalBytesToReceive - e.BytesReceived) / 1024.0 / 1024.0).ToString("F2") + "MB";
dataGridViewDownloads.Rows[rowIndex].Cells["TotalSize"].Value = (e.TotalBytesToReceive / 1024.0 / 1024.0).ToString("F2") + "MB";
});
};
webClient.DownloadFileCompleted += (s, e) =>
{
if (e.Error != null)
{
dataGridViewDownloads.Rows[rowIndex].Cells["Status"].Value = "下载错误";
MessageBox.Show("下载过程中发生错误:" + e.Error.Message);
}
else if (e.Cancelled)
{
dataGridViewDownloads.Rows[rowIndex].Cells["Status"].Value = "下载取消";
}
else
{
dataGridViewDownloads.Rows[rowIndex].Cells["Status"].Value = "已完成";
}
};
await webClient.DownloadFileTaskAsync(new Uri(url), "downloaded_file_" + rowIndex);
}
catch (Exception ex)
{
dataGridViewDownloads.Rows[rowIndex].Cells["Status"].Value = "下载错误";
MessageBox.Show("下载过程中发生错误:" + ex.Message);
}
}
private void btnPauseDownload_Click(object sender, EventArgs e)
{
// 暂停下载的实现
}
private void btnCancelDownload_Click(object sender, EventArgs e)
{
// 取消下载的实现
}
}
接下来我们来看看大概的一个面板:
文件多线程的处理逻辑的实现:
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();
}
public bool isFinish()
{
return finish;
}
public long getDownLength()
{
return downLength;
}
}
上述代码中:
1.构造函数 `DownloadThread`:
- 接收一个 `FileDownloader` 对象 `downloader`,用于更新下载进度。
- `downUrl` 表示要下载的文件的URL。
- `saveFilePath` 是文件保存的路径。
- `block` 表示每个线程下载的数据块大小。
- `downLength` 是当前线程已下载的字节数。
- `threadId` 是当前线程的ID。
2. 方法 `ThreadRun`:
创建了一个新的线程 `td`,在这个线程中进行实际的下载操作。
3. 下载逻辑:
- 在线程的主体中,首先检查 `downLength` 是否小于 `block`,以确定是否需要继续下载。
- 计算本线程的起始位置 `startPos` 和结束位置 `endPos`,通过 `HttpWebRequest` 构建HTTP请求。
- 设置请求的头部信息,包括 `UserAgent` 和 `Accept`。
- 使用 `request.AddRange(startPos, endPos)` 设置请求的范围,实现断点续传。
- 获取响应 `HttpWebResponse`,并从中获取响应流 `_stream`。
- 使用缓冲区 `buffer` 逐段读取响应流,并将数据写入本地文件,同时更新下载进度。
- 最后关闭文件流和响应流,并标记该线程下载完成。
4. 异常处理:
在下载过程中捕获可能发生的异常,例如网络连接问题或者文件操作问题,并记录错误信息。
3.对用户的操作进行一个监听
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);//通知具体调用者下载进度
}
}
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);
}
}
通过以上代码,我们处理数据以能够在下载过程中显示信息(如总大小、已下载大小、进度、下载速度、剩余大小、剩余时间、状态、下载的网址等)。
4.成果验收
我们实现先把文件添加到我们的UI界面,然后点击开始下载按钮。
和我们预期的结果是一样的。