简体   繁体   English

.Net DownloadFileTaskAsync强大的WPF代码

[英].Net DownloadFileTaskAsync robust WPF code

The WPF code below hangs forever when network connection is lost for 3 or more minutes. 当网络连接丢失3分钟或更长时间时,下面的WPF代码将永久挂起。 When connection is restored it neither throws nor continues downloading nor timeouts. 当连接恢复时,它既不会抛出也不会继续下载或超时。 If network connection is lost for a shorter period say half a minute, it throws after connection is restored. 如果网络连接在较短的时间内丢失(例如半分钟),则在连接恢复后会丢失。 How can i make it more robust to survive network outage? 如何让网络中断更加强大?

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:\stuff\10Mio.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;
        }
    }
}

UPDATE UPDATE

Current solution is based on restarting Timer in DownloadProgressChangedEventHandler , so the timer fires only if no DownloadProgressChanged events occur within the timeout. 当前的解决方案基于在DownloadProgressChangedEventHandler重新启动Timer ,因此只有在超时内没有发生DownloadProgressChanged事件时才会触发计时器。 Looks like an ugly hack, still looking for a better solution. 看起来像一个丑陋的黑客,仍在寻找更好的解决方案。

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:\stuff\10Mio.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;
        }
    }
}

You need to implement proper timeout for that download. 您需要为该下载实现适当的超时。 But you don't need to use timer, just use Task.Delay and Task.WaitAny. 但是你不需要使用计时器,只需使用Task.DelayTask.WaitAny. For example: 例如:

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})");
        }
    }
}

Usage: 用法:

const string SRC = "http://ovh.net/files/10Mio.dat";
const string TARGET = @"d:\stuff\10Mio.dat";
// Time needed to restore network connection
TimeSpam TIMEOUT = TimeSpan.FromSeconds(30);
DownloadFile(SRC,TARGET, TIMEOUT); // might want to await this to handle exceptions

Update in response to comment. 更新以回应评论。 If you want timeout based on received data, not on whole operation time, it's also possible with Task.Delay . 如果您希望基于接收的数据而不是整个操作时间超时,则还可以使用Task.Delay For example: 例如:

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})");
        }
    }
}

Personally, if I were to make a robust download solution, I would add a Network connection monitor because that's what we are actually waiting for. 就个人而言,如果我要制作一个强大的下载解决方案,我会添加一个网络连接监视器,因为这是我们实际等待的。 For simplicity, something like this will be enough. 为简单起见,这样的事情就足够了。

online = true;

NetworkChange.NetworkAvailabilityChanged += NetworkChange_NetworkAvailabilityChanged;
_isNetworkOnline = NetworkInterface.GetIsNetworkAvailable();

void NetworkChange_NetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e)
{
    online  = e.IsAvailable;
}

Then you can actually check for network availability and wait as appropriate before you attempt to download or progress... I will definitely accept that a simple ping solution seems to work better than this at times based on experience. 然后,您可以实际检查网络可用性并在尝试下载或进度之前等待...我绝对会接受一个简单的ping解决方案似乎比根据经验更好地工作。

Depending on the size of what you're downloading, monitoring the network speed may also help so you can decide how to chunk in case of choppy connections. 根据您下载的内容的大小,监控网络速度也可能有所帮助,因此您可以决定如何连接断开连接时的块。 Take a look at this project for ideas. 看看这个项目的想法。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM