【问题标题】:WebClient asynchronous downloader not working properlyWebClient 异步下载器无法正常工作
【发布时间】:2017-02-01 06:08:41
【问题描述】:

所以,我一直在努力解决这个问题已经有一段时间了。我是一个业余程序员,所以我并不总是知道自己做错了什么。

总之,我最近项目的前提:

我和我的朋友们经常玩《我的世界》,但他们并不是很聪明,而且我们并不总是在附近获取模组并向他们发送链接等等。所以我想我应该编写一些程序来自动下拉模组,以便它们与服务器同步并同时获取服务器数据。

我正在使用免费的 FTP 主机,但我认为这不是问题所在,原因将变得很清楚。

基本上,我想使用进度条和理想的标签来指示整个数据块的进度(所有模块一起......不超过 1GB - 小得多)。但是,我似乎遇到了一些关于 Async 选项的问题:

  • 它会随机选择不下载它应该下载的文件

  • 在声称完整之前,它可能不会完整下载文件

  • 当 msgbox 触发说它已完成下载所有项目时,进度条可能已满 50%。

但是,由于在同步使用 Webclient 时不存在进度报告事件,进度条不起作用,但当我在 BGworker 中运行同步时,它每次都能正确下载。但是,我失去了进度报告,这很重要....

所以,基本上:

  • 有没有更好的方法来实现这一点?

这是我在准备好之前需要开始工作的最后一块,所以我真的很想尝试这样做。感谢您的帮助!

编辑:用代码更新:

Public Function GetDownloadSize(ByVal URL As String) As Long
    Dim request As Net.FtpWebRequest = DirectCast(Net.WebRequest.Create(URL), Net.FtpWebRequest)
    request.Method = Net.WebRequestMethods.Ftp.GetFileSize
    request.Credentials = New Net.NetworkCredential(dl_user, dl_pass)
    Dim response As Net.FtpWebResponse = DirectCast(request.GetResponse(), Net.FtpWebResponse)
    Dim fileSize As Long = response.ContentLength
    Return fileSize
End Function

Private Sub btn_sync_Click(sender As Object, e As EventArgs) Handles btn_sync.Click
    Dim cont As DialogResult = MsgBox("Continue? " + (total_dl_size / 1000).ToString("N0") + " KB remain to be downloaded.", MsgBoxStyle.YesNo, "CAUTION!")
    If cont = DialogResult.No Then
        tb_warnings.AppendText("-ERR: User declined to synchronize files. Restart the application to sync.")
        tb_warnings.AppendText(ControlChars.NewLine)
        Label3.BackColor = Color.Firebrick
        Return
    End If
    btn_sync.Enabled = False
    btn_scan.Enabled = false
    tb_warnings.AppendText("-Deleting outmoded/unused mods. Protected mods will be kept.")
    For Each i As fdata_obj In deleted_files
        My.Computer.FileSystem.DeleteFile(mc_dir + "\mods\" + i.name)
    Next
    tb_warnings.AppendText(ControlChars.NewLine)
    tb_warnings.AppendText("-Deleting mod subdirectories to ensure no conflicts.")
    tb_warnings.AppendText(ControlChars.NewLine)

    For Each d In My.Computer.FileSystem.GetDirectories(mc_dir + "\mods")
        My.Computer.FileSystem.DeleteDirectory(d, FileIO.DeleteDirectoryOption.DeleteAllContents)
    Next

    initialize_download()


End Sub

Private Sub initialize_download()

           Dim wc As New System.Net.WebClient() ' SORRY, ASSUME THIS IS A PUBLIC VAR SO IT CAN BE REFERENCED ACROSS ITS OTHER METHODS
    AddHandler wc.DownloadProgressChanged, AddressOf OnDownloadProgressChanged
    AddHandler wc, AddressOf OnFileDownloadCompleted

    Dim usr As String = "randouser"
    Dim pass As String = "randopass"
    For Each s In (From dl As fdata_obj In new_files Select dl_server + "/mods/" + mods_dir + "/" + dl.name).ToList
        downloads.Enqueue(s)
    Next
    wc.Credentials = New Net.NetworkCredential(usr, pass)

        Dim urix As String = downloads.Dequeue
        Try
            wc.DownloadFileasync(New Uri(urix), mc_dir + "\mods\" + IO.Path.GetFileName(urix))
        Catch ex As Exception
            MsgBox(ex.Message)
            If tb_warnings.InvokeRequired = True Then
                tb_warnings.Invoke(New tb_updater(AddressOf tb_update), "-ERR: Could not download file: " + urix, urix)
            Else
                tb_warnings.AppendText("-ERR: Could not download file: " + IO.Path.GetFileName(urix))
                tb_warnings.AppendText(ControlChars.NewLine)

            End If
    end try
End Sub
Private Sub OnDownloadProgressChanged(ByVal sender As Object, ByVal e As System.Net.DownloadProgressChangedEventArgs)
    MsgBox("This is happening!")
    total_dl = total_dl + e.BytesReceived
    Dim percentage As Integer = (CType((total_dl / total_dl_size), Integer) * 100)
    if percentage > 100 then
        percentage = 100
    endif
    prog_update(percentage)

End Sub

delegate sub progress_update(byval prog as integer)
' POTENTIAL ISSUES HERE???????
private sub prog_update(byval prog as integer)
    if progressbar1.invokerequired then
        progressbar1.invoke(new prog_update(addressof progress),prog)
    else
        progressbar1.value = prog


Private Sub OnFileDownloadCompleted(ByVal sender As Net.WebClient, ByVal e As System.ComponentModel.AsyncCompletedEventArgs)

    If e.Cancelled Then
        MsgBox(e.Cancelled)
    ElseIf Not e.Error Is Nothing Then
        MsgBox(e.Error.Message)
    Else
    if downloads.count > 0 then
                Dim urix As String = downloads.Dequeue
        Try
            wc.DownloadFileasync(New Uri(urix), mc_dir + "\mods\" + IO.Path.GetFileName(urix))
        Catch ex As Exception
            MsgBox(ex.Message)
            If tb_warnings.InvokeRequired = True Then
                tb_warnings.Invoke(New tb_updater(AddressOf tb_update), "-ERR: Could not download file: " + urix, urix)
            Else
                tb_warnings.AppendText("-ERR: Could not download file: " + IO.Path.GetFileName(urix))
                tb_warnings.AppendText(ControlChars.NewLine)

            End If
        End Try
    End If

End Sub

【问题讨论】:

  • 我上次检查时WebClient 运行良好。请向我们展示您的代码。
  • @VisualVincent #1:不错的用户名,#2:编辑帖子以显示代码。该委托函数调用调用可能略有偏差,因为我正在重写这段代码而没有 IDE 的好处并且不太记得格式。请放心,我的原始代码中的调用模板是正确的。
  • 1: 谢谢 ;) 2: 好的,我先启动我的电脑,然后我会尝试代码。
  • 你真的不应该这样分享你的密码,即使没有任何价值。请删除评论,我有自己的 FTP 服务器,我可以试试。
  • 尝试并找出问题可能需要我大约一两个小时,所以请耐心等待。 :)

标签: vb.net multithreading winforms asynchronous webclient


【解决方案1】:

首先,您的进度条不起作用的主要原因是:

Dim percentage As Integer = (CType((total_dl / total_dl_size), Integer) * 100)

代码将首先计算total_dl / total_dl_size,说它的结果是 0.34,然后将其转换为整数,结果为 0(0.34 向下舍入为零,因为整数没有小数),最后相乘0 和 100(仍然是 0)。
你要做的就是先将被除数乘以 100,这样结果就会从 0-100 变为 0-1:(total_dl * 100) / total_dl_size

至于线程安全(调用)我总是使用我创建的这个extension method

Imports System.Runtime.CompilerServices

Public Module Extensions
    <Extension()> _
    Public Sub InvokeIfRequired(ByVal Control As Control, ByVal Method As [Delegate], ByVal ParamArray Parameters As Object())
        If Parameters Is Nothing OrElse _
            Parameters.Length = 0 Then Parameters = Nothing 'If Parameters is null or has a length of zero then no parameters should be passed.
        If Control.InvokeRequired = True Then
            Control.Invoke(Method, Parameters)
        Else
            Method.DynamicInvoke(Parameters)
        End If
    End Sub
End Module

(最好放在另一个文件中)

Lambda expressions(在 Visual Studio 2010 中引入)一起,将极大地为您简化调用。这是因为不是将If InvokeRequired 模式放在任何地方:

If Me.InvokeRequired Then
    Me.Invoke(New Action(AddressOf SomeMethod), params)
Else
    SomeMethod()
End If

您只需输入:

Me.InvokeIfRequired(AddressOf SomeMethod, params)

扩展方法会为您完成剩下的工作。

如果您使用 lambda 表达式,您可以动态创建方法:

Me.InvokeIfRequired(Sub()
                        Label1.Text = "Hello world!"
                        ProgressBar1.Value += 1
                    End Sub)

现在,到你的代码。

我已经将您的代码分开了一些,以便更容易处理。对于初学者,我没有将下载代码复制粘贴到DownloadFileCompleted 事件处理程序,而是创建了一个更通用的方法,称为DownloadFile()

''' <summary>
''' Downloads a file from the specified URL with the specified credentials.
''' </summary>
''' <param name="URL">The URL of the file.</param>
''' <param name="Username">The username which to login with.</param>
''' <param name="Password">The password which to login with.</param>
''' <remarks></remarks>
Private Sub DownloadFile(ByVal URL As String, ByVal Username As String, ByVal Password As String)
    If wc.IsBusy = True Then Throw New Exception("A download is already ongoing!")

    wc.Credentials = New NetworkCredential(dl_user, dl_pass)
    total_dl_size = GetDownloadSize(URL, Username, Password)

    Try
        Dim FileName As String = Path.GetFileName(URL)
        AppendWarning("Downloading " & FileName & "...")
        wc.DownloadFileAsync(New Uri(URL), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), FileName))
    Catch ex As Exception
        AppendWarning("-ERR: Could not download file: " & Path.GetFileName(URL))
    End Try
End Sub

如你所见,我还制作了一个通用方法来输出警告和错误消息:

''' <summary>
''' (Thread-safe) Appends a warning or status message to the "tb_warnings" text box.
''' </summary>
''' <param name="Text">The text to append.</param>
''' <remarks></remarks>
Private Sub AppendWarning(ByVal Text As String)
    Me.InvokeIfRequired(Sub() tb_warnings.AppendText(Text & Environment.NewLine))
End Sub

这是完整的代码,它对我来说可以正常工作:

Private dl_user As String = "someusername"
Private dl_pass As String = "somepassword"

Private dl_urls As String() = {"URL1", "URL2"} 'Temporary. Use your own code.
Private total_dl_size As Long = 0
Private total_dl As Long = 0

Dim WithEvents wc As New System.Net.WebClient()
Dim downloads As New Queue(Of String)

Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
    'Populate the download queue.
    downloads.Enqueue(dl_urls(0)) 'Temporary. Use your own code here.
    downloads.Enqueue(dl_urls(1))
End Sub

'The download button.
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
    'Do your pre-download stuff here.

    DownloadFile(downloads.Dequeue(), dl_user, dl_pass) 'Download the first file.
End Sub

''' <summary>
''' Downloads a file from the specified URL with the specified credentials.
''' </summary>
''' <param name="URL">The URL of the file.</param>
''' <param name="Username">The username which to login with.</param>
''' <param name="Password">The password which to login with.</param>
''' <remarks></remarks>
Private Sub DownloadFile(ByVal URL As String, ByVal Username As String, ByVal Password As String)
    If wc.IsBusy = True Then Throw New Exception("A download is already ongoing!")

    wc.Credentials = New NetworkCredential(dl_user, dl_pass) 'Set the credentials.
    total_dl_size = GetDownloadSize(URL, Username, Password) 'Get the size of the current file.

    Try
        Dim FileName As String = Path.GetFileName(URL) 'Get the current file's name.
        AppendWarning("Downloading " & FileName & "...") 'Download notice.
        wc.DownloadFileAsync(New Uri(URL), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), FileName)) 'Download the file to the desktop (use your own path here).
    Catch ex As Exception
        AppendWarning("-ERR: Could not download file: " & Path.GetFileName(URL))
    End Try
End Sub

''' <summary>
''' (Thread-safe) Appends a warning or status message to the "tb_warnings" text box.
''' </summary>
''' <param name="Text">The text to append.</param>
''' <remarks></remarks>
Private Sub AppendWarning(ByVal Text As String)
    Me.InvokeIfRequired(Sub() tb_warnings.AppendText(Text & Environment.NewLine))
End Sub

Private Sub wc_DownloadProgressChanged(sender As Object, e As System.Net.DownloadProgressChangedEventArgs) Handles wc.DownloadProgressChanged
    Me.InvokeIfRequired(Sub()
                            Dim Progress As Integer = CType(Math.Round((e.BytesReceived * 100) / total_dl_size), Integer)
                            If Progress > 100 Then Progress = 100
                            If Progress < 0 Then Progress = 0
                            ProgressBar1.Value = Progress
                        End Sub)
End Sub

Private Sub wc_DownloadFileCompleted(sender As Object, e As System.ComponentModel.AsyncCompletedEventArgs) Handles wc.DownloadFileCompleted
    If e.Cancelled Then
        MessageBox.Show(e.Cancelled)

    ElseIf Not e.Error Is Nothing Then
        MessageBox.Show(e.Error.Message)

    Else
        If downloads.Count > 0 Then
            DownloadFile(downloads.Dequeue(), dl_user, dl_pass) 'Download the next file.
        Else
            AppendWarning("Download complete!")
        End If

    End If
End Sub

需要记住的其他一些事项:

  • MsgBox() function 的存在纯粹是为了向后兼容。您应该改用 .NET 的标准 MessageBox.Show() method

  • 字符串连接应使用与号 (&amp;) 而不是加号 (+)。 See why

  • 应始终使用Path.Combine() 连接路径,因为这将确保创建正确的路径。如果您输入任何无效的内容,它将引发异常。

    用法:

    Path.Combine(Path1, Path2, Path3, ...)
    Path.Combine("C:\", "Foo") 'Results in: C:\Foo
    Path.Combine("C:\", "Foo", "Bar", "Hello World.txt") 'Results in: C:\Foo\Bar\Hello World.txt
    

希望这会有所帮助!

【讨论】:

  • 太棒了!当我回到家时,我会尝试实现这一点。谢谢您的帮助!使下载部分正常工作是最后一块拼图。好吧,我仍然需要编写程序来生成配置文件,但这很简单。谢谢!
  • @smitty_werbermanjensen:没问题。希望它有效,如果不告诉我!
  • @smitty_werbermanjensen :请记住,我没有包含您的所有代码。您必须自己再次添加一些部分。主要是队列,填充在Form_Load 事件中,以及预下载的东西,应该在Button_Click 事件中完成。
  • 不确定,但似乎 .invokeifrequired 不是组成方法,或者至少,它会抛出错误。
  • 附录:否则,代码运行得非常好。至于为什么 progbar 很愚蠢:每次报告 prog 时,我都使用 e.bytesreceived 来更新数量,因此它的总和越来越多。我设置了一个新的 var 基线来保存先前的下载量,然后您可以在运行时将它们相加以获得整体 dl。谢谢你!有用!万岁!
猜你喜欢
  • 2013-08-26
  • 1970-01-01
  • 2011-06-26
  • 1970-01-01
  • 1970-01-01
  • 2014-07-12
相关资源
最近更新 更多