.Net DownloadFileTaskAsync 健壮的 WPF 代码
.Net DownloadFileTaskAsync robust WPF code
当网络连接丢失 3 分钟或更长时间时,下面的 WPF 代码将永远挂起。恢复连接后,它既不会抛出也不会继续下载,也不会超时。如果网络连接丢失的时间较短,比如半分钟,它会在连接恢复后抛出。我怎样才能让它更健壮以应对网络中断?
using System;
using System.Net;
using System.Net.NetworkInformation;
using System.Windows;
namespace WebClientAsync
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
NetworkChange.NetworkAvailabilityChanged +=
(sender, e) => Dispatcher.Invoke(delegate()
{
this.Title = "Network is " + (e.IsAvailable ? " available" : "down");
});
}
const string SRC = "http://ovh.net/files/10Mio.dat";
const string TARGET = @"d:\stuffMio.dat";
private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
btnDownload.IsEnabled = false;
btnDownload.Content = "Downloading " + SRC;
try {
using (var wcl = new WebClient())
{
wcl.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
await wcl.DownloadFileTaskAsync(new Uri(SRC), TARGET);
btnDownload.Content = "Downloaded";
}
}
catch (Exception ex)
{
btnDownload.Content = ex.Message + Environment.NewLine
+ ((ex.InnerException != null) ? ex.InnerException.Message : String.Empty);
}
btnDownload.IsEnabled = true;
}
}
}
更新
当前解决方案基于在 DownloadProgressChangedEventHandler
中重新启动 Timer
,因此只有在超时内没有发生 DownloadProgressChanged 事件时才会触发计时器。看起来像一个丑陋的 hack,仍在寻找更好的解决方案。
using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
namespace WebClientAsync
{
public partial class MainWindow : Window
{
const string SRC = "http://ovh.net/files/10Mio.dat";
const string TARGET = @"d:\stuffMio.dat";
// Time needed to restore network connection
const int TIMEOUT = 30 * 1000;
public MainWindow()
{
InitializeComponent();
}
private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
btnDownload.IsEnabled = false;
btnDownload.Content = "Downloading " + SRC;
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
Timer timer = new Timer((o) =>
{
// Force async cancellation
cts.Cancel();
}
, null //state
, TIMEOUT
, Timeout.Infinite // once
);
DownloadProgressChangedEventHandler handler = (sa, ea) =>
{
// Restart timer
if (ea.BytesReceived < ea.TotalBytesToReceive && timer != null)
{
timer.Change(TIMEOUT, Timeout.Infinite);
}
};
btnDownload.Content = await DownloadFileTA(token, handler);
// Note ProgressCallback will fire once again after awaited.
timer.Dispose();
btnDownload.IsEnabled = true;
}
private async Task<string> DownloadFileTA(CancellationToken token, DownloadProgressChangedEventHandler handler)
{
string res = null;
WebClient wcl = new WebClient();
wcl.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
wcl.DownloadProgressChanged += handler;
try
{
using (token.Register(() => wcl.CancelAsync()))
{
await wcl.DownloadFileTaskAsync(new Uri(SRC), TARGET);
}
res = "Downloaded";
}
catch (Exception ex)
{
res = ex.Message + Environment.NewLine
+ ((ex.InnerException != null) ? ex.InnerException.Message : String.Empty);
}
wcl.Dispose();
return res;
}
}
}
您需要为该下载实施适当的超时。但是你不需要使用定时器,只需要使用 Task.Delay
和 Task.WaitAny.
例如:
static async Task DownloadFile(string url, string output, TimeSpan timeout) {
using (var wcl = new WebClient())
{
wcl.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
var download = wcl.DownloadFileTaskAsync(url, output);
// await two tasks - download and delay, whichever completes first
await Task.WhenAny(Task.Delay(timeout), download);
var exception = download.Exception; // need to observe exception, if any
bool cancelled = !download.IsCompleted && exception == null;
// download is not completed yet, nor it is failed - cancel
if (cancelled) {
wcl.CancelAsync();
}
if (cancelled || exception != null) {
// delete partially downloaded file if any (note - need to do with retry, might not work with a first try, because CancelAsync is not immediate)
int fails = 0;
while (true) {
try {
File.Delete(output);
break;
}
catch {
fails++;
if (fails >= 10)
break;
await Task.Delay(1000);
}
}
}
if (exception != null) {
throw new Exception("Failed to download file", exception);
}
if (cancelled) {
throw new Exception($"Failed to download file (timeout reached: {timeout})");
}
}
}
用法:
const string SRC = "http://ovh.net/files/10Mio.dat";
const string TARGET = @"d:\stuffMio.dat";
// Time needed to restore network connection
TimeSpam TIMEOUT = TimeSpan.FromSeconds(30);
DownloadFile(SRC,TARGET, TIMEOUT); // might want to await this to handle exceptions
更新以回应评论。如果您希望超时基于接收到的数据,而不是整个操作时间,也可以使用 Task.Delay
。例如:
static async Task DownloadFile(string url, string output, TimeSpan timeout)
{
using (var wcl = new WebClient())
{
wcl.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
DateTime? lastReceived = null;
wcl.DownloadProgressChanged += (o, e) =>
{
lastReceived = DateTime.Now;
};
var download = wcl.DownloadFileTaskAsync(url, output);
// await two tasks - download and delay, whichever completes first
// do that until download fails, completes, or timeout expires
while (lastReceived == null || DateTime.Now - lastReceived < timeout) {
await Task.WhenAny(Task.Delay(1000), download); // you can replace 1 second with more reasonable value
if (download.IsCompleted || download.IsCanceled || download.Exception != null)
break;
}
var exception = download.Exception; // need to observe exception, if any
bool cancelled = !download.IsCompleted && exception == null;
// download is not completed yet, nor it is failed - cancel
if (cancelled)
{
wcl.CancelAsync();
}
if (cancelled || exception != null)
{
// delete partially downloaded file if any (note - need to do with retry, might not work with a first try, because CancelAsync is not immediate)
int fails = 0;
while (true)
{
try
{
File.Delete(output);
break;
}
catch
{
fails++;
if (fails >= 10)
break;
await Task.Delay(1000);
}
}
}
if (exception != null)
{
throw new Exception("Failed to download file", exception);
}
if (cancelled)
{
throw new Exception($"Failed to download file (timeout reached: {timeout})");
}
}
}
就我个人而言,如果我要制作一个强大的下载解决方案,我会添加一个网络连接监视器,因为这正是我们真正需要的。为简单起见,像这样的东西就足够了。
online = true;
NetworkChange.NetworkAvailabilityChanged += NetworkChange_NetworkAvailabilityChanged;
_isNetworkOnline = NetworkInterface.GetIsNetworkAvailable();
void NetworkChange_NetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e)
{
online = e.IsAvailable;
}
然后您可以实际检查网络可用性并在尝试下载或继续之前适当等待...我肯定会接受一个简单的 ping 解决方案有时似乎比根据经验更有效。
根据您正在下载的内容的大小,监控网络速度也可能有所帮助,这样您就可以决定在连接不稳定的情况下如何分块。查看 this project 以获取想法。
当网络连接丢失 3 分钟或更长时间时,下面的 WPF 代码将永远挂起。恢复连接后,它既不会抛出也不会继续下载,也不会超时。如果网络连接丢失的时间较短,比如半分钟,它会在连接恢复后抛出。我怎样才能让它更健壮以应对网络中断?
using System;
using System.Net;
using System.Net.NetworkInformation;
using System.Windows;
namespace WebClientAsync
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
NetworkChange.NetworkAvailabilityChanged +=
(sender, e) => Dispatcher.Invoke(delegate()
{
this.Title = "Network is " + (e.IsAvailable ? " available" : "down");
});
}
const string SRC = "http://ovh.net/files/10Mio.dat";
const string TARGET = @"d:\stuffMio.dat";
private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
btnDownload.IsEnabled = false;
btnDownload.Content = "Downloading " + SRC;
try {
using (var wcl = new WebClient())
{
wcl.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
await wcl.DownloadFileTaskAsync(new Uri(SRC), TARGET);
btnDownload.Content = "Downloaded";
}
}
catch (Exception ex)
{
btnDownload.Content = ex.Message + Environment.NewLine
+ ((ex.InnerException != null) ? ex.InnerException.Message : String.Empty);
}
btnDownload.IsEnabled = true;
}
}
}
更新
当前解决方案基于在 DownloadProgressChangedEventHandler
中重新启动 Timer
,因此只有在超时内没有发生 DownloadProgressChanged 事件时才会触发计时器。看起来像一个丑陋的 hack,仍在寻找更好的解决方案。
using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
namespace WebClientAsync
{
public partial class MainWindow : Window
{
const string SRC = "http://ovh.net/files/10Mio.dat";
const string TARGET = @"d:\stuffMio.dat";
// Time needed to restore network connection
const int TIMEOUT = 30 * 1000;
public MainWindow()
{
InitializeComponent();
}
private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
btnDownload.IsEnabled = false;
btnDownload.Content = "Downloading " + SRC;
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
Timer timer = new Timer((o) =>
{
// Force async cancellation
cts.Cancel();
}
, null //state
, TIMEOUT
, Timeout.Infinite // once
);
DownloadProgressChangedEventHandler handler = (sa, ea) =>
{
// Restart timer
if (ea.BytesReceived < ea.TotalBytesToReceive && timer != null)
{
timer.Change(TIMEOUT, Timeout.Infinite);
}
};
btnDownload.Content = await DownloadFileTA(token, handler);
// Note ProgressCallback will fire once again after awaited.
timer.Dispose();
btnDownload.IsEnabled = true;
}
private async Task<string> DownloadFileTA(CancellationToken token, DownloadProgressChangedEventHandler handler)
{
string res = null;
WebClient wcl = new WebClient();
wcl.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
wcl.DownloadProgressChanged += handler;
try
{
using (token.Register(() => wcl.CancelAsync()))
{
await wcl.DownloadFileTaskAsync(new Uri(SRC), TARGET);
}
res = "Downloaded";
}
catch (Exception ex)
{
res = ex.Message + Environment.NewLine
+ ((ex.InnerException != null) ? ex.InnerException.Message : String.Empty);
}
wcl.Dispose();
return res;
}
}
}
您需要为该下载实施适当的超时。但是你不需要使用定时器,只需要使用 Task.Delay
和 Task.WaitAny.
例如:
static async Task DownloadFile(string url, string output, TimeSpan timeout) {
using (var wcl = new WebClient())
{
wcl.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
var download = wcl.DownloadFileTaskAsync(url, output);
// await two tasks - download and delay, whichever completes first
await Task.WhenAny(Task.Delay(timeout), download);
var exception = download.Exception; // need to observe exception, if any
bool cancelled = !download.IsCompleted && exception == null;
// download is not completed yet, nor it is failed - cancel
if (cancelled) {
wcl.CancelAsync();
}
if (cancelled || exception != null) {
// delete partially downloaded file if any (note - need to do with retry, might not work with a first try, because CancelAsync is not immediate)
int fails = 0;
while (true) {
try {
File.Delete(output);
break;
}
catch {
fails++;
if (fails >= 10)
break;
await Task.Delay(1000);
}
}
}
if (exception != null) {
throw new Exception("Failed to download file", exception);
}
if (cancelled) {
throw new Exception($"Failed to download file (timeout reached: {timeout})");
}
}
}
用法:
const string SRC = "http://ovh.net/files/10Mio.dat";
const string TARGET = @"d:\stuffMio.dat";
// Time needed to restore network connection
TimeSpam TIMEOUT = TimeSpan.FromSeconds(30);
DownloadFile(SRC,TARGET, TIMEOUT); // might want to await this to handle exceptions
更新以回应评论。如果您希望超时基于接收到的数据,而不是整个操作时间,也可以使用 Task.Delay
。例如:
static async Task DownloadFile(string url, string output, TimeSpan timeout)
{
using (var wcl = new WebClient())
{
wcl.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
DateTime? lastReceived = null;
wcl.DownloadProgressChanged += (o, e) =>
{
lastReceived = DateTime.Now;
};
var download = wcl.DownloadFileTaskAsync(url, output);
// await two tasks - download and delay, whichever completes first
// do that until download fails, completes, or timeout expires
while (lastReceived == null || DateTime.Now - lastReceived < timeout) {
await Task.WhenAny(Task.Delay(1000), download); // you can replace 1 second with more reasonable value
if (download.IsCompleted || download.IsCanceled || download.Exception != null)
break;
}
var exception = download.Exception; // need to observe exception, if any
bool cancelled = !download.IsCompleted && exception == null;
// download is not completed yet, nor it is failed - cancel
if (cancelled)
{
wcl.CancelAsync();
}
if (cancelled || exception != null)
{
// delete partially downloaded file if any (note - need to do with retry, might not work with a first try, because CancelAsync is not immediate)
int fails = 0;
while (true)
{
try
{
File.Delete(output);
break;
}
catch
{
fails++;
if (fails >= 10)
break;
await Task.Delay(1000);
}
}
}
if (exception != null)
{
throw new Exception("Failed to download file", exception);
}
if (cancelled)
{
throw new Exception($"Failed to download file (timeout reached: {timeout})");
}
}
}
就我个人而言,如果我要制作一个强大的下载解决方案,我会添加一个网络连接监视器,因为这正是我们真正需要的。为简单起见,像这样的东西就足够了。
online = true;
NetworkChange.NetworkAvailabilityChanged += NetworkChange_NetworkAvailabilityChanged;
_isNetworkOnline = NetworkInterface.GetIsNetworkAvailable();
void NetworkChange_NetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e)
{
online = e.IsAvailable;
}
然后您可以实际检查网络可用性并在尝试下载或继续之前适当等待...我肯定会接受一个简单的 ping 解决方案有时似乎比根据经验更有效。
根据您正在下载的内容的大小,监控网络速度也可能有所帮助,这样您就可以决定在连接不稳定的情况下如何分块。查看 this project 以获取想法。