Finde Fehler bei Download in Segmenten Code nicht

  • VB.NET

Es gibt 11 Antworten in diesem Thema. Der letzte Beitrag () ist von Kangaroo.

    Finde Fehler bei Download in Segmenten Code nicht

    Hi Community,

    Ich hab hier einen Code geschrieben, der mir das Downloaden von Dateien in Segmenten ermöglicht.
    Das Problem: beim erstellen des 2. Segments passiert gar nichts.
    Ich keinen Plan woran das liegen kann. Das Programm läuft ja. Es wartet nur auf eine Antwort vom Server, die es anscheinend nicht bekommt.
    Hoffe ihr könnt mir helfen und danke schon mal im vorraus.

    P.S: Wenn der Code dann endlich läuft verpack ich den Code in eine Klasse, die den Code asynchron aufruft und stelle es hier ins Forum.

    Hier mein Code:

    VB.NET-Quellcode

    1. Imports System.Net
    2. Imports System.IO
    3. Module SegmentDownlaod
    4. Const BufferSize As Integer = 65536
    5. Sub Main(ByVal args() As String)
    6. If args.Length <> 3 Then
    7. Console.WriteLine("Ungültige Anzahl der Argumente!")
    8. Console.ReadLine()
    9. Exit Sub
    10. End If
    11. 'Einstellungen speichern
    12. Dim DownloadPath As String = args(0)
    13. Dim SavePath As String = args(1)
    14. Dim SegmentCount As Integer = CInt(args(2))
    15. 'Einstellungen anzeigen
    16. Console.WriteLine("Speichere " & DownloadPath & " unter " & SavePath & " in " & SegmentCount & " Segmenten.")
    17. Console.WriteLine()
    18. 'Downloadgröße abrufen und anzeigen
    19. Dim DownloadLength As Long = GetDownloadLength(DownloadPath)
    20. Console.WriteLine("Downloadgröße: " & DownloadLength & "Bytes")
    21. Console.WriteLine()
    22. 'Liste zum speichern der Segmente
    23. Dim Segment(SegmentCount - 1) As BinaryReader
    24. Dim SegmentLength(SegmentCount - 1) As Long
    25. Dim SegmenthRecieve(SegmentCount - 1) As Long
    26. Dim SegmentCompleted(SegmentCount - 1) As Boolean
    27. Dim CompletedSegments As Integer = 0
    28. 'Segmente erstellen
    29. For i% = 1 To SegmentCount
    30. 'Segmentschnitt bestimmen
    31. Dim StartS As Integer = CInt((i - 1) * (DownloadLength / SegmentCount))
    32. Dim EndS As Integer = CInt(i * (DownloadLength / SegmentCount))
    33. If i < SegmentCount Then EndS -= 1
    34. Console.WriteLine("Segment " & i & " von " & StartS & " bis " & EndS)
    35. 'Segment erstellen
    36. 'Download anfordern
    37. Dim Request As HttpWebRequest = CType(HttpWebRequest.Create(DownloadPath), HttpWebRequest)
    38. Request.AddRange(StartS, EndS)
    39. Dim Response As HttpWebResponse = CType(Request.GetResponse(), HttpWebResponse)
    40. 'Download in Liste ablegen
    41. Segment(i - 1) = New BinaryReader(Response.GetResponseStream())
    42. Console.WriteLine("Segment " & i & " ertsellt!")
    43. Console.WriteLine()
    44. Next
    45. 'Datei erstellen
    46. Dim FileStream As New FileStream(SavePath, FileMode.Create)
    47. FileStream.SetLength(DownloadLength)
    48. Dim Writer As New BinaryWriter(FileStream)
    49. 'Segmente zusammenführen und speichern
    50. Do
    51. 'Jedes Segment einzeln bearbeiten
    52. For i% = 0 To SegmentCount - 1
    53. 'Überprüfen, ob segment fertig
    54. If Not SegmentCompleted(i) Then
    55. 'Position festlegen
    56. Dim Length As Long
    57. For n% = 0 To i - 1
    58. Length += SegmentLength(n)
    59. Next
    60. Length += SegmenthRecieve(i)
    61. 'positionieren
    62. Writer.Seek(CInt(Length), SeekOrigin.Begin)
    63. 'Segment in Datei schreiben
    64. Dim Count As Integer = 0
    65. Dim Buffer(BufferSize - 1) As Byte
    66. Count = Segment(i).Read(Buffer, 0, BufferSize)
    67. Writer.Write(Buffer, 0, Count)
    68. 'Informationen aktualisieren und ausgeben
    69. SegmenthRecieve(i) += Count
    70. Console.WriteLine("Segment {0}: {1]Bytes von {2}Bytes : {3}%", i, SegmenthRecieve(i), SegmentLength(i), SegmenthRecieve(i) / SegmentLength(i))
    71. 'Überprüfen, ob Segment fertig ist
    72. If SegmenthRecieve(i) = SegmentLength(i) Then
    73. SegmentCompleted(i) = True
    74. CompletedSegments+= 1
    75. Console.WriteLine("Segment " & i & " fertig!")
    76. End If
    77. End If
    78. Next
    79. Loop Until CompletedSegments = SegmentsCount
    80. Console.WriteLine("Download fertig!")
    81. Console.ReadLine()
    82. End Sub
    83. Private Function GetDownloadLength(ByVal uri As String) As Long
    84. Dim Request As HttpWebRequest = CType(HttpWebRequest.Create(uri), HttpWebRequest)
    85. Request.Method = "HEAD"
    86. Return Request.GetResponse.ContentLength
    87. End Function
    88. End Module
    Auf die Schnelle konnte ich dein Problem jetzt nicht feststellen. Bist du schon einmal mit dem Debugger Schritt für Schritt durchgegangen?
    Aber mir sind ein paar andere Dinge aufgefallen: Die Streams sollten immer irgendwo wieder geschlossen werden (Close-Methode der WebResponse aufrufen). Beim Downloaden eines zweiten Segments muss irgendwo im HttpHeader angegeben werden, dass du die Daten nicht von Beginn an, sondern ab der Position des Segments anfordern willst.

    Und außerdem, normalerweise führt man Downloads an einem Stück durch - was bezweckst du mit den Segmenten, die ja alles nur verkomplizieren? Mehr Geschwindigkeit wirst du damit wohl eher nicht herausbekommen.
    Ja, ich den Code Schritt für Schritt durchgegangen und konnte den Fehler nicht finden.
    Das mit den Streams stimmt, das sollte aber nur ein Test sein. Werds noch ausbessern.
    Der Start- und Endpunkt des Segments werden ja immer durch Request.Addange() festgelegt.

    Und außerdem, das zerlegen des Downloads in Segmente beschert mir einen unglaublichen Geschwindigkeitsschub.
    Manchmal sogar bis zu 100%!
    Falls du es mir nicht galubst: en.wikipedia.org/wiki/Download_manager
    The reason for doing so is to circumvent server side limitations of bandwidth per connection.

    Wenn dieser Fall eintritt hast du recht, dann können mehrere Verbindungen tatsächlich einen Geschwindigkeitsvorteil bringen. Da ich nur DSL 6000 habe und die wenigsten Server (außer Billig-On-Click-Hoster) eine Begrenzung einbauen, die darunter fällt, hab ich garnicht daran gedacht. Natürlich begrenzen auch einige Server die Anzahl der Verbindungen pro IP-Adresse.

    Der Start- und Endpunkt des Segments werden ja immer durch Request.Addange() festgelegt.

    Habe ich übersehen. Und ich habe auch nicht gewusst, dass das so einfach geht mit dem .Net-Framework, dachte man muss selber den Header entsprechend anpassen. Das macht das Framework für dich dann intern, wenn du diese Methode aufrufst. Allerdings kann es sein, dass nicht alle Server diesen Header auch beachten/unterstützen. Vielleicht liegt auch hier der Fehler.

    Das Programm bleibt also beim Aufruf der Methode getResponse() "hängen"?

    Infinity schrieb:

    Wenn dieser Fall eintritt hast du recht, dann können mehrere Verbindungen tatsächlich einen Geschwindigkeitsvorteil bringen.
    Das können sie auf jeden Fall, die erwähnten Downloadmanger arbeiten nach dem gleichen Prinzip. Allerdings nur wenn die Segmente in parallelen Verbindungen heruntergeladen werden.

    Me.Net schrieb:

    Und außerdem, das zerlegen des Downloads in Segmente beschert mir einen unglaublichen Geschwindigkeitsschub
    Das glaube ich so nicht. Du müsstest dazu die Async-Methoden des HttpWebRequest verwenden um die Segmente parallel zu laden. Momentan wartest Du in der Schleife bis der Response des einen Segmentes zurück kommt (=blocking).

    Infinity schrieb:

    Natürlich begrenzen auch einige Server die Anzahl der Verbindungen pro IP-Adresse
    Das tun die meisten, gerade um die Downloadmanager auszubremsen. Bei Apache-Servern ist die Regel nur 2-3 Verbindungen von der gleichen IP-Adresse zuzulassen.

    Infinity schrieb:

    Die Streams sollten immer irgendwo wieder geschlossen werden (Close-Methode der WebResponse aufrufen). Beim Downloaden eines zweiten Segments muss irgendwo im HttpHeader angegeben werden, dass du die Daten nicht von Beginn an, sondern ab der Position des Segments anfordern willst
    Beides richtig. Für nicht verwaltete Ressourcen wie Streams verwendet man das Using-Statement. Wie in dem Beispielcode in diesem Beitrag Post #4

    Es ist einer der ersten Codes wo ich sehe dass jemand den 'HEAD' Request benutzt, das finde ich sehr gut. Allerdings geben nicht alle/nur einge Server die Contentlength einier Datei zurück, oft ist sie -1. Genauso steht es mit dem Ansprechen von bestimmten Segmenten: auch das wird definitv nicht von allen Servern unterstützt.

    Gruss
    @Infinty: Ja, es hängt bei GetResponse(), aber erst, wenn die Schleife den Befehl zum zweiten mal erreicht.

    @Kangoroo: Das mit den Geschwindigkeitsschub war falsch formuliert von mir. Ich meinte die Downloadgeschwindigkeit, nicht die Geschwindigkeit des Programms.
    Das einige Server die Verbindungen pro IP-Adresse begrenzen wusste ich, ich habe aber die Datei, die ich zum testen downloaden wollte schon vorher mit DownThemAll heruntergeladen. Und das Programm baute 10 Verbindungen auf.
    Wie bereits erwähnt ist der Code nur zum Testen des Grundsystems gedacht. Das mit dem Streams habe ich einfach übersehen. Der Code beinhalet auch keine Fehlerbehandlung.

    Langsam vermute ich, dass ich einen Bug im .Net-Framework aufgedeckt habe....

    Me.Net schrieb:

    Langsam vermute ich, dass ich einen Bug im .Net-Framework aufgedeckt habe....
    Das vermuten wir am Anfang alle, bis wir dann merken dass der Fehler direkt vor dem Bildschirm sitzt ;)

    Ich habe Deinen Code mal überflogen, ohne ihn testen zu wollen: sei mir nicht böse, aber der ist schon ziemlich grausam :huh:

    Aber um auf Dein aktuelles Problem zu kommen: ich vermute mal er hängt am ServicePoint Limit. Gib mal versuchsweise gib mal nach Zeile 48 folgendes ein:

    VB.NET-Quellcode

    1. request.servicePoint.connectionLimit = 10
    aber der ist schon ziemlich grausam

    Um den Code zu verbessern, könntest du z. B. damit anfangen, nur einen Ausgabestream zu verwenden, anstatt mehrere, die dann hinterher wieder zusammengefügt werden. Du kannst ja mit der Seek-Methode des Ausgabestreams steuern, an welcher Stelle du die Daten schreiben möchtet. Wenn du später dann die Verbindungen parallel aufbaust, kannst du z. B. die Mutex-Klasse verwenden, um zu verhindern, dass mehrere Threads gleichzeitig auf den Ausgabestream zugreifen wollen. Außerdem kannst du dir die BinaryReader/-Writer sparen, wenn du sowieso nur direkt einzelne Bytes schreibst.
    Ich weiß zwar immer ncht wo das Problem lag, habe aber den Code gerade umgeschrieben, um zu hören was ihr davon haltet.
    Ich wollte ihn Debuggen und siehe da: er funktioniert?!?
    Das einzige, was ich geändert habe, ist, dass der Code nun ein Multithreadanwendung ist.

    Werde ihn morgen noch fertigstellen und dann in den Sourcecode-Austausch stellen.
    Danke für eure Hilfe & Kritik! Ohne euch hätte ich es nicht so ehrgeizig weiter versucht!

    Mein bisheriger Code:

    VB.NET-Quellcode

    1. Imports System.Net
    2. Imports System.IO
    3. Imports System.Threading
    4. Public Class Download
    5. 'Einstellungen
    6. Public Property DownloadPath As String
    7. Public Property SavePath As String
    8. Public Property SegmentCount As Integer
    9. Public Property BufferSize As Integer
    10. 'Rescourcen
    11. Private Writer As BinaryWriter
    12. Private DoneEvents() As ManualResetEvent
    13. Private Sub SegmentDownload(ByVal sets As Object)
    14. Dim Settings As SegmentSettings = CType(sets, SegmentSettings)
    15. 'Downlaod anfordern
    16. Dim Request As HttpWebRequest = CType(HttpWebRequest.Create(Settings.DownloadPath), HttpWebRequest)
    17. Request.AddRange(Settings.StartPoint, Settings.EndPoint)
    18. Dim Response As HttpWebResponse = CType(Request.GetResponse(), HttpWebResponse)
    19. 'Rescourcen
    20. Dim Buffer(Settings.Buffersize - 1) As Byte
    21. Dim n As Integer
    22. Dim Reader As New BinaryReader(Response.GetResponseStream())
    23. Dim Recieved As Long = 0
    24. 'Downloadschleife
    25. Do
    26. SyncLock Writer
    27. 'Writer positionieren
    28. Writer.Seek(CInt(Settings.StartPoint + Recieved), SeekOrigin.Begin)
    29. 'Daten lesen und speichern
    30. n = Reader.Read(Buffer, 0, Settings.Buffersize)
    31. Writer.Write(Buffer, 0, n)
    32. 'Informationen aktualisieren
    33. Recieved += n
    34. End SyncLock
    35. Loop Until n = 0 'Solange Daten gelesen
    36. 'Stream freigeben
    37. Reader.Close()
    38. 'Segment als abgeschlossen melden
    39. DoneEvents(Settings.Index).Set()
    40. End Sub
    41. 'Zum Übertragen der Downloadeinstellungen
    42. Private Structure SegmentSettings
    43. Public DownloadPath As String
    44. Public Buffersize As Integer
    45. Public StartPoint As Long
    46. Public EndPoint As Long
    47. Public Index As Integer
    48. Public Sub New(ByVal path As String, ByVal buffer As Integer, ByVal start As Long, ByVal fin As Long, ByVal id As Integer)
    49. DownloadPath = path
    50. Buffersize = buffer
    51. StartPoint = start
    52. EndPoint = fin
    53. Index = id
    54. End Sub
    55. End Structure
    56. Private Function GetDownloadLength(ByVal uri As String) As Long
    57. Dim Request As HttpWebRequest = CType(HttpWebRequest.Create(uri), HttpWebRequest)
    58. Request.Method = "HEAD"
    59. Return Request.GetResponse.ContentLength
    60. End Function
    61. Public Sub SyncDownload()
    62. 'Downloadgröße empfangen
    63. Dim DownloadLength As Long = GetDownloadLength(DownloadPath)
    64. Debug.Print("Downloadgröße: " & DownloadLength)
    65. 'Stream erstellen
    66. Dim FileStream As New FileStream(SavePath, FileMode.Create)
    67. FileStream.SetLength(DownloadLength)
    68. Writer = New BinaryWriter(FileStream)
    69. 'Download splitten
    70. For i% = 0 To SegmentCount - 1
    71. 'Start- und Endpunkt berechnen
    72. Dim StartS As Integer = CInt(i * (DownloadLength / SegmentCount))
    73. Dim EndS As Integer = CInt((i + 1) * (DownloadLength / SegmentCount))
    74. If i < SegmentCount - 1 Then EndS -= 1
    75. 'DoneEvent erstellen
    76. DoneEvents(i) = New ManualResetEvent(False)
    77. Debug.Print("Segment {0} von {1} bis {2} wird erstellt...", i, StartS, EndS)
    78. 'Segmentdownload in Threadpool einsetzen
    79. ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf SegmentDownload), New SegmentSettings(DownloadPath, BufferSize, StartS, EndS, i))
    80. Next
    81. 'Auf Abschluss des Downloads warten
    82. WaitHandle.WaitAll(DoneEvents)
    83. 'Datei freigeben
    84. Writer.Close()
    85. End Sub
    86. Private DownloadThread As New Thread(AddressOf SyncDownload)
    87. Public Sub Download()
    88. DownloadThread.SetApartmentState(ApartmentState.MTA)
    89. DownloadThread.Start()
    90. End Sub
    91. Public Sub WaitForDownload()
    92. If DownloadThread.IsAlive Then
    93. DownloadThread.Join()
    94. End If
    95. End Sub
    96. Public Sub New(ByVal uri As String, ByVal path As String, ByVal segments As Integer, Optional ByVal buffer As Integer = 65536)
    97. DownloadPath = uri
    98. SavePath = path
    99. SegmentCount = segments
    100. BufferSize = buffer
    101. ReDim DoneEvents(segments - 1)
    102. End Sub
    103. End Class

    Me.Net schrieb:

    Werde ihn morgen noch fertigstellen und dann in den Sourcecode-Austausch stellen.
    Das würde ich bei dem derzeitigen Stand des Codes mal lieber sein lassen, da wirst Du (zu Recht) zerrissen.

    Ein paar Anmerkungen:
    - wenn der 'Head' Request -1 zurückgibt oder der Server den Range Header nicht unterstützt gibt es Müll
    - Streams sollten nicht dazu dienen 'aufbewahrt' zu werden, sondern sie werden (mit Using) geöffnet, verarbeitet und sofort wieder geschlossen
    - die ganze Array-Speicherung ist umständlich und überflüssig
    - genauso die Settings-Stucture
    - ob man für den Thread einen aus dem Threadpool oder einen Threading.Thread nimmt ist hier Stilfrage, ich würde den Threading.Thread bevorzugen
    - mit dem Writer.Seek verlierst Du wieder viel Zeit , die Du mit dem segmentierten Download eigentlich gewinnen wolltest
    - die Idee einen Segementierten Download zur Verfügun zu stellen taugt nur wenn sie in einer Klasse gekapselt ist


    Vorschlag:
    - mach eine eingene Klasse FileDownload mit der Methode Download(url as string, path as String)
    - die Methode muss erst einmal ermitteln ob der Server ein Resume unterstützt: ein Head Request gibt laut Http Specification in diesem Fall den StatusCode 206(=Partial Content) zurück
    - wenn der Server das unterstützt , so wird die Datei segementiert geladen, ansonsten konventionell
    - optimal wären die Async Methoden des httpWebRequests, wenn das Dir zu kompliziert ist so nimm den Threading Weg
    - im Thread lade die Bytes und zwar richtig (abhängig von der Buffersize Property/Constant), momentan ist das falsch
    - gib pro thread das Segment mit SequenceNummer und Buffer per Event zurück
    - die Hauptlogik scheibt dann mit dem Writer das Segment sofort raus wenn es das Nächste in der Folge ist, ansonsten wird es in einer temporären Liste zwischengespeichert
    Ich versuch mal deine Vorschläge ab zu arbeiten:
    - Headrequest und Range Konpatiplität zu prüfen hatte ich bereits vor
    - Ich versteh nicht ganz was du mit Arrayspeicherung meinst
    - Die Structure wird durch Threadsichere Propertys ersetzt
    - Werd es in eine Klasse kapseln
    - Was meinst du mit Bytes richtig laden? Ich kopiere sie doch nur Päckchenweise von Stream des Response in den Filestream.
    - Wenn ich das Schreiben in die Hauptlogik nehme und nach deinem Vorschlag bearbeite, speichere ich doch die einzelnen Segmente wieder zwischen, was du oben als schlecht bezeichnet hast

    Hoffe du hilfst mir weiter den Code zu verbessern.
    Halbzeit in Istanbul.

    Me.Net schrieb:

    Ich versteh nicht ganz was du mit Arrayspeicherung meinst
    Ich bezog mich da eher auf Deinen Anfangscode: dort hast Du statt Segmenten (=Bytes) offene Streams (BinaryReadser) gespeichert.

    Me.Net schrieb:

    speichere ich doch die einzelnen Segmente wieder zwischen, was du oben als schlecht bezeichnet hast
    Wie gesagt hast Du Streams statt Byte zwischengespeichert. Beim Threading hast Du das Problem, daß ein Byte-Segment n eher fertig ist als das eigentlich vorausgehend, insofern brauchst Du eine Art Sequence-Nummer. Speicherst Du erst alles zwischen und schreibst dann, so sprengt es Dir bei grossen Dateien den Arbeitsspeicher. Daher immer nur die Bytes speichern, die Du nicht sofort wegschreiben kannst. Danach werden sie aus der List rausgenommen.