C# WebClient异步下载取消、暂停恢复实现及取消报错问题咨询
问题解决与实现方案
取消功能报错修复
报错根因
- 重复释放WebClient实例:你在
DownloadFileCompleted事件中手动调用了client.Dispose(),而WebClient实例是包裹在using块中的,using块退出时会自动执行Dispose,重复释放会导致对象访问异常。 - 取消后残留事件回调未处理:调用
CancelAsync()后,尚未执行完的DownloadProgressChanged事件仍会触发,此时访问已释放的client的ResponseHeaders等属性会抛出ObjectDisposedException。 - 取消操作抛出的异常未捕获:
DownloadFileTaskAsync在被取消时会抛出TaskCanceledException,你没有捕获该异常导致程序直接崩溃。 - 全局client变量悬空:using块执行完后全局client变量指向已释放的对象,再次点击停止按钮会访问无效对象。
修复代码要点
首先修改DownloadAsync方法:
private async Task DownloadAsync() { client = new WebClient(); try { client.DownloadFileCompleted += (s, e) => { if (e.Cancelled) { lblStatus.Text = "Download Stopped"; return; } if (e.Error != null) { lblStatus.Text = $"Download Error: {e.Error.Message}"; return; } lblStatus.Text = "Download File Completed."; }; client.DownloadProgressChanged += (s, e) => { // 先判断是否已取消/释放,避免访问无效对象 if (client == null || e.Cancelled) return; tracker.SetProgress(e.BytesReceived, e.TotalBytesToReceive); lblAmount.Text = SizeSuffix(e.BytesReceived) + "/" + SizeSuffix(e.TotalBytesToReceive); lblSpeed.Text = tracker.GetBytesPerSecondString(); if (client.ResponseHeaders != null && client.ResponseHeaders["Content-Length"] != null) { myLong = Convert.ToInt64(client.ResponseHeaders["Content-Length"]); } progressBar1.Value = e.ProgressPercentage; lblDownloadProgress.Text = "%" + e.ProgressPercentage.ToString(); lblDownloadProgress.Left = Math.Min( (int)(progressBar1.Left + e.ProgressPercentage / 100f * progressBar1.Width), progressBar1.Width - lblDownloadProgress.Width ); }; for (int i = 0; i < urls.Count; i++) { tracker.NewFile(); // 捕获取消异常 try { await client.DownloadFileTaskAsync(new Uri(urls[i]), @"d:\satImages\img" + i + ".gif"); } catch (TaskCanceledException) { // 取消操作属于正常逻辑,不抛异常 break; } catch (OperationCanceledException) { break; } } } finally { client.Dispose(); client = null; } }
然后修改停止按钮逻辑:
private void btnStop_Click(object sender, EventArgs e) { if (client != null) { client.CancelAsync(); } }
暂停/恢复功能实现
WebClient本身不支持断点续传,需要改用HttpClient或HttpWebRequest实现,核心逻辑如下:
实现思路
- 新增全局变量记录状态:
bool isPaused、long downloadedBytes、string currentUrl、string currentFilePath - 暂停时:关闭当前请求,将
isPaused置为true,记录当前已下载的字节数、当前下载的url和文件路径 - 恢复时:发送带
Range请求头的Http请求,指定从已下载的字节位置开始获取数据,将返回的流追加到本地已存在的文件末尾,继续更新进度 - 执行续传前需要先判断目标服务器是否支持Range请求(响应头包含
Accept-Ranges: bytes),不支持的服务器无法实现续传
核心代码示例
// 新增全局变量 private bool isPaused = false; private long downloadedBytes = 0; private string currentUrl; private string currentFilePath; private HttpClient httpClient = new HttpClient(); private Stream downloadStream; private FileStream fileStream; // 暂停按钮逻辑 private void btnPause_Click(object sender, EventArgs e) { isPaused = true; downloadStream?.Dispose(); fileStream?.Dispose(); lblStatus.Text = "Download Paused"; } // 恢复下载逻辑 private async void btnResume_Click(object sender, EventArgs e) { if (!isPaused || string.IsNullOrEmpty(currentUrl)) return; isPaused = false; lblStatus.Text = "Download Resuming..."; await ResumeDownloadAsync(); } private async Task ResumeDownloadAsync() { var fileInfo = new FileInfo(currentFilePath); if (fileInfo.Exists) { downloadedBytes = fileInfo.Length; } else { downloadedBytes = 0; } using (var request = new HttpRequestMessage(HttpMethod.Get, currentUrl)) { // 添加上下文范围请求头 request.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(downloadedBytes, null); using (var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) { if (!response.IsSuccessStatusCode) { lblStatus.Text = "Server does not support resume download"; return; } var totalBytes = response.Content.Headers.ContentLength ?? 0 + downloadedBytes; downloadStream = await response.Content.ReadAsStreamAsync(); fileStream = new FileStream(currentFilePath, FileMode.Append, FileAccess.Write, FileShare.None); var buffer = new byte[8192]; int bytesRead; while ((bytesRead = await downloadStream.ReadAsync(buffer, 0, buffer.Length)) > 0) { if (isPaused) break; await fileStream.WriteAsync(buffer, 0, bytesRead); downloadedBytes += bytesRead; // 更新进度UI var progress = (int)(downloadedBytes * 100 / totalBytes); progressBar1.Value = progress; lblDownloadProgress.Text = $"%{progress}"; lblAmount.Text = $"{SizeSuffix(downloadedBytes)}/{SizeSuffix(totalBytes)}"; } downloadStream.Dispose(); fileStream.Dispose(); if (!isPaused) { lblStatus.Text = "Download Completed"; downloadedBytes = 0; currentUrl = null; currentFilePath = null; } } } }
内容的提问来源于stack exchange,提问作者Daniel Lip
相关产品推荐
相关产品推荐

