DOBON.NET DOBON.NETプログラミング掲示板過去ログ

WebClientで確実に同時ダウンロードするには?

環境/言語:[VB2005 Express Edition]
分類:[.NET]

はじめまして、
DOBON.NETのTipsを参考にしながらVB2005の勉強を始めましたKOJIと申します。
複数のHTMLや画像をまとめてダウンロードしたいと思いますがうまく行きません。
とりあえず下記のようにやってみましたが、乏しい知識で無理やり考えた方法なのでかなり怪しいです。
実際、いくつか0バイトのファイルができてしまったりしますし、そういう場合でもなぜか例外がスローされず、対処できません。

Dim sync_limit = 4
Dim sync_count = 0
-------------------------------------------------
Public Sub DownLoadFiles(Byval.............)
  ・
  ・
  Dim i As Integer = 0
  Do
    If i <= urlList.Count - 1 Then
      '同時ダウンロードカウントがリミットより少ないなら1つDLして
      '同時ダウンロード数sync_countを+1
      'sync_countがリミットの場合は素通りループ
      If sync_count < sync_limit Then
        Download(urlList(i), path_str)
        sync_count +=1
        i += 1
       End If
    Else
      Exit Do
    End If
    Application.DoEvents()
  Loop
  ・
  ・
End Sub
-----------------------------------------------------
'WebClientでダウンロードするメソッド
Public Sub Download(ByVal url As String, ByVal path_str As String,........)
  Dim c As New WebClient
  '完了イベントを関連付ける
  AddHandler c.DownloadFileCompleted, AddressOf dlcompleted
  Dim uni As New Uri(url)
  '非同期でダウンロード
  c.DownloadFileAsync(uni, path)
End Sub
-----------------------------------------------------
'WebClientの完了イベント
Public Sub dlcompleted(ByVal sender As Object, ..........)
  '1つ終了するたびに同時ダウンロードカウントを引く
  sync_count -= 1
  '用済みになったWebClientを破棄
  sender.Dispose()
End Sub
------------------------------------------------------

URLリストが全部ダウンロード完了されるまでDo〜Loopでずっとループし続けます。
sync_countという数値の変数を作り、WebClientが1つDLをはじめるたびに1追加され、完了するごとに1マイナスされます。それを変数sync_limitで制限し、同時ダウンロード数がリミットの場合は素通りさせることで同時ダウンロード数が常に一定になるようにしました。動作は良好に思えたのですが、いくつか0バイトの未完ファイルができたりしてうまくいきません。また、DL速度も遅いようです。

ファイルを1つダウンロードする方法は、DOBIN.NETさんのTipsを参考にしていろいろためしました。My.Computer.Network.DownloadFileメソッドが接続も早く安定している気がしたので、BackGroundWorkerと併用して試そうかとも思ったのですが、他に同時ダウンロードの方法が思いつかないため同じことになってしまいそうです。
とりあえず時々ファイルが0バイトでDL完了してしまうのをなんとかしたいです。
改善点や、同時DLするにあたって良い方法がありましたらご教授ください。今思いつくのは、とりあえず全てループした後に全てのファイルサイズを調べて未完ファイルをリストアップし、再度DLすることぐらいなのですが、もっと適切な方法がある気がしてなりません。

長文になってしまいすみません。
まだ始めたばかりで質問の仕方も不慣れですが、何卒よろしくお願いいたします。
2007/11/16(Fri) 18:45:23 編集(投稿者)

■No20963に返信(KOJIさんの記事)
> とりあえず時々ファイルが0バイトでDL完了してしまうのをなんとかしたいです。

エラーが出れば0になってしまうのは仕方ないでしょう。
エラーかどうかを確認しなければいけないのです。
AsyncCompletedEventArgsのプロパティにはErrorがあります。

> ファイルを1つダウンロードする方法は、DOBIN.NETさんのTipsを参考にしていろいろためしました。My.Computer.Network.DownloadFileメソッドが接続も早く安定している気がしたので、BackGroundWorkerと併用して試そうかとも思ったのですが、他に同時ダウンロードの方法が思いつかないため同じことになってしまいそうです。

DownloadFileで並列ダウンロードもいろいろ手がありますが、
ダウンローダーを作るなら、きちんとエラー処理ができるよう
HttpWebRequestを使うのがいいと思います。


> 'WebClientの完了イベント
> Public Sub dlcompleted(ByVal sender As Object, ..........)
>   '1つ終了するたびに同時ダウンロードカウントを引く
>   sync_count -= 1
>   '用済みになったWebClientを破棄
>   sender.Dispose()
> End Sub

イベントハンドラでsenderをDisposeするのは普通の場合危険です。
やめたほうがいいでしょう。
同時DLしている数をlimitで制限したい風な処理をされていますが、こういうときにはsemaphoreを使用するのが一般的です。
あと、非常に無責任ではありますが私はC#使いなので以下はVBでは実際にはコードを作って確認してないのでそのつもりで。

Dim sync_limit = 4
Dim sync_count = 0
Dim sema As New System.Threading.Semaphore(sync_limit, sync_limit)
-------------------------------------------------
Public Sub DownLoadFiles(Byval.............)
  ・
  ・
  Dim i As Integer = 0
  Do
    If i <= urlList.Count - 1 Then
      '同時ダウンロードカウントがリミットより少ないなら1つDLして
      '同時ダウンロード数sync_countを+1
      '(↑これをsemaが勝手にやる。同時DL数がlimit)
      sema.WaitOne()
      '(↑同時DL数がlimitに達していたらここで停止する)
      Download(urlList(i), path_str)
      i += 1
    Else
      Exit Do
    End If
    Application.DoEvents()
  Loop
  ・
  ・
End Sub
-----------------------------------------------------
'WebClientでダウンロードするメソッド
Public Sub Download(ByVal url As String, ByVal path_str As String,........)
  Dim c As New WebClient
  '完了イベントを関連付ける
  AddHandler c.DownloadFileCompleted, AddressOf dlcompleted
  Dim uni As New Uri(url)
  '非同期でダウンロード
  c.DownloadFileAsync(uni, path)
End Sub
-----------------------------------------------------
'WebClientの完了イベント
Public Sub dlcompleted(ByVal sender As Object, ..........)
  '1つ終了するたびに同時ダウンロードカウントを引く
  '(↑これをsemaが勝手にやります)
  sema.Release()
  'sender のdisposeはFrameworkが勝手にやるので、れいさんもおっしゃってますが、ここではやらなくてよいと思います)
End Sub
------------------------------------------------------
れい様
ご返信、ほんとうにありがとうございます。

> エラーが出れば0になってしまうのは仕方ないでしょう。
> エラーかどうかを確認しなければいけないのです。
> AsyncCompletedEventArgsのプロパティにはErrorがあります。

完了通知イベント内部----------------------------------
If Not (e.Error Is Nothing) Then
  debug.writeline("完了")
ElseIf e.Cancelled Then
  debug.writeline("キャンセルされました")
Else
  debug.writeline(file & " ダウンロード失敗")
End If

----------------------------------
すみません、エラー処理を省略して書いてしまいました。
じつはこのような感じでエラーを受け取るようにしており、
エラーの場合はさらに別対処をしようと考えていたのですが、
まだ0バイトで完了していないファイルさえ、"正常にダウンロード"と帰ってきておりました。ファイルが10〜50個あったとして、最後の1〜2個だけが高い確率でタイムアウトになり、0バイトで止まってしまうという感じです。

> DownloadFileで並列ダウンロードもいろいろ手がありますが、
> ダウンローダーを作るなら、きちんとエラー処理ができるよう
> HttpWebRequestを使うのがいいと思います。

おっしゃるようにHttpWebRequestで挑戦してみましたところ、タイムアウトを指定できるようになったので、タイムアウトで例外時にそのファイルだけリトライさせることで解決できました。ただ、なぜ最後の1〜2個だけがタイムアウトになるのかは疑問ですが・・・。
方法は最初に書いた方法と同じで、WebClientの変わりにBackgroundWorkerのインスタンスを同時DLしたい分だけループで作り、DoWorkイベント内でhttpWebRequestでダウンロードしています。この方法自体は問題ないでしょうか?

> イベントハンドラでsenderをDisposeするのは普通の場合危険です。
> やめたほうがいいでしょう。

アドバイスありがとうございます。全て終わったあとに別プロシージャからDisposeするようにします。Disposeについてまだ勉強不足なのですが、BackgroundWorkerに関してもやはり最後にDisposeする必要はあるでしょうか?

BackgroundWorkerは初心者にはやや難易度が高いとのことなので、注意点などありましたら、重ねてアドバイスいただけると助かります。DoWorkイベント内からユーザーコントロールや別なクラスで宣言した変数から値を受け取れないというのはわかりました。

どうもありがとうございました。
がる様

アドバイスありがとうございます!
ここを開いたまま丸一日リロードせずに再コーディングに挑戦してましたので、
書き込んでいただいていたのに気づくのが遅れてしまいました<(_ _)>

> 同時DLしている数をlimitで制限したい風な処理をされていますが、こういうときにはsemaphoreを使用するのが一般的です。

どうりで自分が考えたよりもっとスマートな方法がある気がしておりました。
ご教授頂いたsemaphoreを使用する方法をこれから試してみます。
使用例まで書いていただきましてありがとうございます。

>   'sender のdisposeはFrameworkが勝手にやるので、れいさんもおっしゃってますが、ここではやらなくてよいと思います)

どれをDisposeしてどれをしなくていいのかがいまひとつ勉強不足でした。
参考に致します。どうもありがとうございました。
■No21012に返信(KOJIさんの記事)
> まだ0バイトで完了していないファイルさえ、"正常にダウンロード"と帰ってきておりました。ファイルが10〜50個あったとして、最後の1〜2個だけが高い確率でタイムアウトになり、0バイトで止まってしまうという感じです。

おや。
失敗してるはずなのに正常終了では困りますよね。
詳しい状況を知りたいです。

> おっしゃるようにHttpWebRequestで挑戦してみましたところ、タイムアウトを指定できるようになったので、タイムアウトで例外時にそのファイルだけリトライさせることで解決できました。ただ、なぜ最後の1〜2個だけがタイムアウトになるのかは疑問ですが・・・。

サーバーへの接続数の問題ではないですか?
同一サイトへの接続は1アプリケーションで2つまで、
と標準ではなっています。
サーバーがHTTP/1.0だったり持続的接続をサポートしてなかったり、
途中にプロキシがあったりするといろいろ複雑です。
(説明はめんどくさいので省略。)

> 方法は最初に書いた方法と同じで、WebClientの変わりにBackgroundWorkerのインスタンスを同時DLしたい分だけループで作り、DoWorkイベント内でhttpWebRequestでダウンロードしています。この方法自体は問題ないでしょうか?

この手のは何通りも方法があります。それぞれ特色があったりなかったり。

BackgroundWorkerを複数作っても十分に可能ですが、
あまりリソース効率よくないですし
プログラミングの手間も多いように感じます。

私でしたらBackgroundWorkerを使わず、
TimerとHttpWebRequest.BeginGetResponseを使います。

> BackgroundWorkerに関してもやはり最後にDisposeする必要はあるでしょうか?

がるさんは
>   'sender のdisposeはFrameworkが勝手にやるので、れいさんもおっしゃってますが、ここではやらなくてよいと思います)
と言ってますが、私はそうは思いません。

Frameworkは場合によってDisposeしたりしてくれる場合もありますが、
今回は確実に廃棄処理をすべきです。

BackgroundWorkerもWebClientもHttpWebResponseも
アンマネージリソースを使います。
BackgroundWorkerがcomponentに含まれるのであれば
component廃棄時にdisposeしてくれますが、
毎回インスタンス作るなら、毎回確実に廃棄されるよう、
コードを組むべきです。
それはWebClientもWebResponseも同様です。
これらはきちんと廃棄しないと接続できなくなったりします。
がる様

書いていただいたコードを参考に、semaphoreを使用してみたのですが、
または私のプログラムでは残念ながらうまく行きませんでした。
同時DL数の申告数(?)が減らず、一つDLした時点で次が始まらずに
無限ループに入ってしまいます。
semaphoreについていろいろ調べたのですが、
なかなか初心者にわかる方法がなくて今回は諦めようと思います。
どうもありがとうございました。
れい様

> 失敗してるはずなのに正常終了では困りますよね。
> 詳しい状況を知りたいです。

WebClientのcompletedインベトで、
エラーを受け取る際のケアレスミスかもしれません^^;
BackgroundWorkerをつかって再度組みなおしたときに、
httpWebClientでタイムアウトを設定できるようになったので改善できました。
やはり最後にタイムアウトがでるのですが、
タイムアウトの場合はそのファイルだけリトライさせることで凌いでおります。


> サーバーへの接続数の問題ではないですか?
> 同一サイトへの接続は1アプリケーションで2つまで、
> と標準ではなっています。
> サーバーがHTTP/1.0だったり持続的接続をサポートしてなかったり、
> 途中にプロキシがあったりするといろいろ複雑です。
> (説明はめんどくさいので省略。)

速度が遅いのはやはりそれが原因みたいです!
(最後のタイムアウトも関係しているのでしょうかね)
接続数の制限は知っていたのですが、
素人考えで別スレッドから別のリクエストが行けば、
3つ以上接続できるのではないかと安易に考えておりました。

エクスプローラを開いて、作成されるファイルを監視していたのですが、
非同期で接続のリミットを増やしても2つずつしかDLされていませんでした。

同じプログラムから複数接続させるのは、初心者には難しいでしょうか?
引き続きhttpWebRequestで接続するのですが、
何卒、ヒントを頂きたく思います。(>_<)


> BackgroundWorkerを複数作っても十分に可能ですが、
> あまりリソース効率よくないですし
> プログラミングの手間も多いように感じます。
>
> 私でしたらBackgroundWorkerを使わず、
> TimerとHttpWebRequest.BeginGetResponseを使います。

とりあえずタイムアウトを設定したかったので、
BeginGetResponseをつかわずBackgroundWorkerを使ってみました。
BeginGetResponseはTimeoutプロパティをそのまま受け取らないと聞きまして、
自分にとって簡単そうなほうで試してみました。
同時接続数などの問題が解決しましたら、
れいさんのおっしゃるようにTimerを使い、組みなおしたいと思います。


> BackgroundWorkerもWebClientもHttpWebResponseも
> アンマネージリソースを使います。
> BackgroundWorkerがcomponentに含まれるのであれば
> component廃棄時にdisposeしてくれますが、
> 毎回インスタンス作るなら、毎回確実に廃棄されるよう、
> コードを組むべきです。
> それはWebClientもWebResponseも同様です。
> これらはきちんと廃棄しないと接続できなくなったりします。

了解しました。
たくさんアドバイスいただきまして、どうもありがとうございます。

HTTP1.1の同時接続数に関して、
恐れ入りますが引き続きアドバイス頂ければと思います。
IPで接続数を制限しているわけではないようなので、
こちらで工夫すればなんとかなりそうなのですが・・・。

重ね重ねすみませんが、何卒よろしくお願いいたします。
■No21026に返信(KOJIさんの記事)
> BeginGetResponseはTimeoutプロパティをそのまま受け取らないと聞きまして、

?
そんな話は聞いたことが無いですが。

> HTTP1.1の同時接続数に関して、
> 恐れ入りますが引き続きアドバイス頂ければと思います。

System.Net.ServicePointManager.DefaultConnectionLimit
れい様

ありがとうございます!!
System.Net.ServicePointManager.DefaultConnectionLimitを増やすことで、
タイムアウトもなくなりまして快適にDLできるようになりました。

やはり最終的には、れいさんのおっしゃるとおり、
httpWebRequestのBeginGetResponseで非同期DLするのが理想的なのですね。
BackgroundWorker内のDoWork内での接続では、BgWorkerインスタンスをDisposeしないと、
うまく同時接続数がうまく伝わらずに失敗するなど、
不要なエラーが増えてスマートではない気がいたしました。

とりあえずBackgroundworkerを使うのをやめ、
WebClientのDownloadFileAsync()を使う方法に戻して正常に動作確認できました。
結局最初の投稿時との違いは、
System.Net.ServicePointManager.DefaultConnectionLimitを増やしたのみです。



>>BeginGetResponseはTimeoutプロパティをそのまま受け取らないと聞きまして、
>
> ?
> そんな話は聞いたことが無いですが。

http://dobon.net/vb/dotnet/internet/webrequest.html
こちらに「非同期要求では自分でタイムアウト処理を行う必要がありますが、・・・」
とありましたので、難しそうだなと思い敬遠してしまいました。

WebClientでもうまく行くようになりましたが、
勉強してhttpWebRequestのBeginGetResponseで作り直してみたいと思います。
本当にどうもありがとうございました<(_ _)>
解決済み!
■No21030に返信(KOJIさんの記事)
> やはり最終的には、れいさんのおっしゃるとおり、
> httpWebRequestのBeginGetResponseで非同期DLするのが理想的なのですね。

本当に理想かどうかは理想の定義によるんですが、
スレッドを生成したりするより、非同期IOを用いるのが、
Windowsでは最も効率が良くなります。
リソース使用量も速度も。

プログラミングしやすいかどうかは個々の経験によると思います。

慣れるとスレッドより楽につくれると私は思いますので、
下手なマルチスレッドよりおすすめです。

> >>BeginGetResponseはTimeoutプロパティをそのまま受け取らないと聞きまして、
>>?
>>そんな話は聞いたことが無いですが。
> http://dobon.net/vb/dotnet/internet/webrequest.html
> こちらに「非同期要求では自分でタイムアウト処理を行う必要がありますが、・・・」
> とありましたので、難しそうだなと思い敬遠してしまいました。

!!
私の間違いです。
確かにTimeoutプロパティは使われません。
だって、BeginGetResponseは非同期要求だから、必要ないのです。

Timeoutが必要ならIAsyncResult.AsyncWaitHandle.WaitOneの引数でどうぞ。
解決済み!
れい様

ご返信遅れましてすみません。
どうもありがとうございました。

やはりWebClientでもなかなか繋がらずに、早くタイムアウトを受け取りたいという
状況もありましたので、
WebRequestを使った非同期ダウンロードに挑戦したいと思います。

> 確かにTimeoutプロパティは使われません。
> だって、BeginGetResponseは非同期要求だから、必要ないのです。
>
> Timeoutが必要ならIAsyncResult.AsyncWaitHandle.WaitOneの引数でどうぞ。

こちら、勉強してみたいと思います。
これからはじめるのでまた躓くことがあるかもしれませんが、
そのときにはまたご指導頂けるとたすかります。
どうもありがとうございました<(_ _)>

DOBON.NET | プログラミング道 | プログラミング掲示板