问题
I'm trying to create a Web API using Asp.NET Core that exposes routes to start and cancel long downloads of large files. The server should be able to handle multiple downloads at the same time.
- The download is performed using
WebClient.DownloadFileAsyncin order to have a short response time and returning adownloadIdfor later use. The instance of the WebClient is stored as value in a static dictionary whose correspondingkeyis of naturally thedownloadId. - The download should be canceled using
WebClient.CancelAsyncon the instance of the client retrieved by accessing the value of the dictionary corresponding to thedownloadIdkey.
The following code works perfectly when the download reaches its completion without being canceled; the AsyncCompletedEventHandler (OnDownloadFileCompleted in that case) is properly invoked.
PROBLEM: When invoking WebClient.CancelAsync, the file keeps on downloading and OnDownloadFileCompleted is not invoked right away. The WebClient seems to wait until the download finishes before invoking the handler. In both case however, the property AsyncCompletedEventArgs.Canceled is properly set (e.g to true if WebClient.CancelAsync was indeed invoked.
- Am I missing something?
- Is there a better/smarter pattern to handle multiple downloads in a WebAPI?
Any help would much appreciated!
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);
}
}
}
回答1:
I was able to replicate what you saw: CancelAsync does not actually cancel the download.
Using HttpClient, you can get the stream and save it to a file using CopyToAsync, accepts a CancellationToken. Cancelling the token stops the download immediately.
Here is the DownloadService class that I modified to use 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();
}
}
This uses a Download class that looks like this:
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();
}
}
You will still have some work to do to remove successfully-completed downloads from your Downloads list, but I'll leave that with you.
来源:https://stackoverflow.com/questions/48187976/webclient-cancelasync-file-still-downloading