WebClient.CancelAsync — 文件仍在下载

WebClient.CancelAsync — File still downloading

我正在尝试使用 Asp.NET 核心创建一个 Web API 公开路由以开始和取消大文件的长时间下载。服务器应该能够同时处理多个下载。

当下载完成且未被取消时,以下代码可以完美运行; AsyncCompletedEventHandler(OnDownloadFileCompleted 在这种情况下)被正确调用。

问题:当调用 WebClient.CancelAsync 时,文件继续下载并且 OnDownloadFileCompleted 没有立即被调用。 WebClient 在调用处理程序之前似乎要等到下载完成。然而,在这两种情况下, 属性 AsyncCompletedEventArgs.Canceled 都已正确设置(例如 true 如果确实调用了 WebClient.CancelAsync

如有任何帮助,我们将不胜感激!

DownloadController.cs

[Route ("api/download")]
public class DownloadController {

    private readonly DownloadService service;

    public DownloadController (DownloadService service) {
        this.service = service;
    }

    [Route ("start")]
    [HttpPost]
    public string Start ([FromForm] string fileUrl) => this.service.StartDownload (fileUrl);

    [Route ("cancel")]
    [HttpPost]
    public void Cancel ([FromForm] string downloadId) => this.service.CancelDownload (downloadId);
}

DownloadService.cs

public class DownloadService {
    public string DOWNLOAD_FOLDER { get => "C:\tmp"; }
    public static Dictionary<string, WebClient> DownloadClients = new Dictionary<string, WebClient> ();

    public string StartDownload (string fileUrl) {
        var downloadId = Guid.NewGuid ().ToString ("N");
        DownloadClients[downloadId] = new WebClient ();
        DownloadClients[downloadId].DownloadFileCompleted += OnDownloadFileCompleted;
        DownloadClients[downloadId].DownloadFileAsync (new Uri (fileUrl), Path.Combine (DOWNLOAD_FOLDER, downloadId), downloadId);
        return downloadId;
    }

    public void CancelDownload (string downloadId) {
        if (DownloadClients.TryGetValue (downloadId, out WebClient client)) {
            client.CancelAsync ();
        }
    }

    private void OnDownloadFileCompleted (object sender, AsyncCompletedEventArgs e) {
        var downloadId = e.UserState.ToString ();
        if (!e.Cancelled) {
            Debug.WriteLine ("Completed");
        } else {
            Debug.WriteLine ("Cancelled"); //will only be reached when the file finishes downloading
        }

        if (DownloadClients.ContainsKey (downloadId)) {
            DownloadClients[downloadId].Dispose ();
            DownloadClients.Remove (downloadId);
        }
    }
}

我能够复制你所看到的:CancelAsync实际上并没有取消下载。

使用 HttpClient,您可以使用 CopyToAsync 获取流并将其保存到文件中,接受 CancellationToken。取消令牌会立即停止下载。

这里是DownloadServiceclass我修改后使用HttpClient.

public class DownloadService {
    public string DOWNLOAD_FOLDER {
        get => "C:\tmp";
    }

    public static readonly ConcurrentDictionary<string, Download> Downloads = new ConcurrentDictionary<string, Download>();

    public async Task<string> StartDownload(string fileUrl) {
        var downloadId = Guid.NewGuid().ToString("N");
        Downloads[downloadId] = new Download(fileUrl);
        await Downloads[downloadId].Start(Path.Combine(DOWNLOAD_FOLDER, downloadId));

        return downloadId;
    }

    public void CancelDownload(string downloadId) {
        if (Downloads.TryRemove(downloadId, out var download)) {
            download.Cancel();
        }
    }

这使用 Download class 看起来像这样:

public class Download {
    private static readonly HttpClient Client = new HttpClient();
    private readonly string _fileUrl;

    private readonly CancellationTokenSource _tokenSource = new CancellationTokenSource();
    private Task _copyTask;
    private Stream _responseStream;
    private Stream _fileStream;

    public Download(string fileUrl) {
        _fileUrl = fileUrl;
    }

    public async Task Start(string saveTo) {
        var response = await Client.GetAsync(_fileUrl, HttpCompletionOption.ResponseHeadersRead);
        _responseStream = await response.Content.ReadAsStreamAsync();
        _fileStream = File.Create(saveTo);
        _copyTask = _responseStream.CopyToAsync(_fileStream, 81920, _tokenSource.Token).ContinueWith(task => {
            if (task.IsCanceled) return;
            _responseStream.Dispose();
            _fileStream.Dispose();
        });
    }

    public void Cancel() {
        _tokenSource.Cancel();
        _responseStream.Dispose();
        _fileStream.Dispose();
    }
}

要从 Downloads 列表中删除成功完成的下载,您还有一些工作要做,但我会把它留给您。