2017-02-23 30 views
12

Poniższy kod WPF zawiesza się na zawsze, gdy połączenie sieciowe zostanie utracone na co najmniej 3 minuty. Po przywróceniu połączenia nie wyrzuca ani nie kontynuuje pobierania ani limitów czasu. Jeśli połączenie sieciowe zostanie utracone na krótszy okres, powiedzmy za pół minuty, wyrzuci po przywróceniu połączenia. Jak mogę uczynić go bardziej wytrzymałym, aby przetrwać awarię sieci?.Net PobierzFileTaskAsync solidny kod 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:\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

Aktualny Rozwiązanie bazuje na ponownym Timer w DownloadProgressChangedEventHandler, więc pożary czasowy tylko jeśli nie DownloadProgressChanged zdarzenia w ramach limitu czasu. Wygląda jak brzydki hack, wciąż szuka lepszego rozwiązania.

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; 
     } 
    } 
} 
+2

Sugeruję utworzenie nowej klasy zawierającej kod pobierania, a następnie z btnDownload_Click nazwij metodę oo tej klasy DownloadFileTA. Następnie możesz wyczyścić kod i łatwiej go debugować. – Tony

Odpowiedz

9

Musisz zaimplementować odpowiedni limit czasu dla tego pobierania. Ale nie trzeba używać samowyzwalacza, wystarczy użyć Task.Delay i Task.WaitAny. Na przykład:

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

Zastosowanie:

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 

aktualizacji w odpowiedzi na komentarz. Jeśli chcesz uzyskać timeout w oparciu o odebrane dane, a nie o cały czas działania, to jest to również możliwe z Task.Delay. Na przykład:

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

Naprawdę nie obchodzi mnie, jak długo będzie działać pobieranie. Jeśli połączenie zostanie utracone/przywrócone, a pobieranie będzie kontynuowane kilka razy, wszystko będzie w porządku. Muszę wykryć przypadek, gdy pobieranie nie trwa w danym momencie, czego "WebClient" mi nie zrobi. Ale dzięki za pomysł, spróbuję dopasować 'await Task.WhenAny (Task.Delay (timeout), download)' umieszczając go w pętli, rodzaj 'do { IsInProgress = false; czekać na Task.WhenAny (Task.Delay (TIMEOUT), pobierz); } podczas (IsInProgress &&! Download.IsCompleted); ' – Serg

+0

@Serg Zaktualizowałem odpowiedź w jeden sposób radzenia sobie z tym przy użyciu tych samych konstrukcji. – Evk

+0

Spróbuję, thx. – Serg

1

Osobiście, gdybym miał zrobić solidne rozwiązanie pobierania, dodałbym monitor połączenia sieciowego, ponieważ to, co mamy rzeczywiście czekać. Dla uproszczenia wystarczy coś takiego.

online = true; 

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

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

Wtedy rzeczywiście można sprawdzić dostępność sieci i poczekać odpowiednio przed przystąpieniem do pobierania lub postęp ... na pewno będę zaakceptować, że to proste rozwiązanie ping wydaje się działać lepiej niż to czasami oparte na doświadczeniu.

W zależności od rozmiaru pobieranego pliku, monitorowanie szybkości sieci może również pomóc w podjęciu decyzji o porwaniu w przypadku niestabilnych połączeń. Sprawdź pomysły na this project.

+0

Czy" NetworkAvailabilityChanged "jest uruchamiany, gdy użytkownik znajduje się za/przechodzi przez portal przechwytujący? – Serg

+1

Aha, nie ... Dlatego powiedziałem, że rozwiązanie ping zazwyczaj działa lepiej w prawdziwym życiu, ponieważ portale przechwytujące, serwery proxy itp. prosta klasa ping, która wyzwala niedostępne po trzech niepowodzeniach, przyniesie trochę dobrego. "NetworkAvailabilityChanged" nadal pomaga określić, czy test ping powinien zostać wykonany, czy nie. –