2017-02-23 24 views
12

ネットワーク接続が3分以上失われた場合、以下のWPFコードは永久にハングします。接続が復元されると、ダウンロードもタイムアウトも発生しません。半分の時間、ネットワーク接続が失われた場合、接続が復元された後にスローされます。ネットワークの停止から生き残るためには、どうすればより堅牢にすることができますか?.Net DownloadFileTaskAsync堅牢な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

現在のソリューションは何DownloadProgressChangedイベントがタイムアウト時間内に発生しない場合にのみ、DownloadProgressChangedEventHandlerTimerを再起動するに基づいて、そのタイマーが起動されます。醜いハックのように見えますが、より良い解決策を模索しています。

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

新しいクラスを作成してダウンロードコードを格納してから、このクラスのDownloadFileTAメソッドをbtnDownload_Clickから呼び出すことをお勧めします。その後、コードをクリーンアップし、簡単にデバッグすることができます。 – Tony

答えて

9

このダウンロードには、適切なタイムアウトを実装する必要があります。しかし、あなたはちょうど例えばTask.DelayTask.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:\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 

更新コメントに反応して。稼働時間全体ではなく、受信したデータに基づいてタイムアウトしたい場合は、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})"); 
     } 
    } 
} 
+0

本当に私はダウンロードの実行時間を気にしません。接続が失われたり復元されたりしてダウンロードが何度も続行されても問題ありません。私は、WebClientが私にとって何をしないのか、タイムアウトのためにダウンロードが進行中でない場合を検出する必要があります。しかし、このアイデアのおかげで、私は '' Task.WhenAny(Task.Delay(timeout)、download) 'をループに入れて、' do { IsInProgress = false; Task.WhenAny(Task.Delay(TIMEOUT)、ダウンロード)を待ちます。 } while(IsInProgress &&!download.IsCompleted); ' – Serg

+0

@Serg同じ構文を使用して対処する方法を1つ回答しました。 – Evk

+0

私はそれを試してみよう。 – Serg

1

は個人的に、私は、ネットワーク接続のモニターを追加します。簡単にするために、このようなもので十分でしょう。

online = true; 

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

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

次に、あなたが実際にネットワークの可用性をチェックし、あなたがダウンロードまたは進行しようとする前に、必要に応じて待つことができます...私は間違いなく、単純なpingのソリューションは、経験に基づいたタイミングでこれよりも良い仕事に思われること受け入れます。

ダウンロードしているサイズに応じて、ネットワーク速度を監視すると、接続が不安定になった場合にどのようにチャンクを行うかを決めることができます。アイデアはthis projectをご覧ください。

+0

ユーザがキャプティブポータルの後ろにある/通過したときに' NetworkAvailabilityChanged'が起動するのですか? – Serg

+1

Aha、no ...だから私は、キャプチャポータル、プロキシなどのために、 'NetworkAvailabilityChanged'は依然としてpingテストを行うべきかどうかを判断するのに役立ちます。 –