Streams

    • Allgemein

    Es gibt 3 Antworten in diesem Thema. Der letzte Beitrag () ist von Niko Ortner.

      Die Stream-Klasse ist eine abstrakte Basisklasse. D.h. - Stream an sich hat also gar keinen Konstruktor (kann man nicht mit New erstellen), aber es gibt viele konkrete Klassen, Stream-Erben wie FileStream, MemoryStream, NetworkStream, GZipStream - halt für unterschiedliche Aufgaben.

      Stream-Aufgaben / Konzepte
      • Lesen, Schreiben
        Transportiert die Bytes woanders hin: zB FileStream, NetworkStream etc.
      • Pufferung
        Streams sind puffer-optimiert, lesen also immer einen internen Puffer voll, aus dem sie DatenAbrufe dann befriedigen. Bzw. schreibend: Erst wenn der Puffer voll ist, wird er in einem Aufwasch in die Ausgabe geschrieben
      • Umarbeiten/Modifizieren
        Verändert die Byte-Folge: zB ZipStream, CryptoStream.
      • modulare Rekombination ("Verarbeitungsstrecken zusammenstöpseln")
        Im Stream-Konstruktor kann meist ein anderer Stream übergeben werden, der als Ein- oder Aus-gabe fungiert. Dieses kann auch mehrstufig erfolgen, etwa für den a) verschlüsselten b) komprimierten c) Download einer d) Datei könnte man a) CryptoStream b) GZipStream c) Networkstream d) FileStream verstöpseln und fertig.
      • Kodieren/Dekodieren mit Readern/Writern/Serialisierern
        Umwandlung der Bytes zu anderen Objekten: zB StreamReader/Writer (=> Text), XmlReader/Writer (=> Xml), BinaryReader/Writer (=> diverse Basis-Datentypen), Serialisierer (=> komplexe, benutzerdefinierte Klassen)
      Letztere - Reader & Writer sind natürlich keine Streams, sind aber imo Bestandteil des Stream-Gesamt-Konzepts

      Zunächst mal ein Blick in den ObjectBrowser ( <- wers nicht kennt, der folge dem Link):


      Wie so oft: im Grunde erklärt sich das meiste einfach durch Hingucken, und auch die Definition, was ein Stream ist: "Provides a generic view of a sequence of bytes." - "Stellt eine allgemeine Sicht auf eine Byte-Folge bereit" - ist ziemlich perfekt.
      Also wenn man hinguckt erkennt man, dass es beim Stream um Lesen (Read) und Schreiben (Write) geht, und dass es eine Position und eine Länge gibt - also eine Byte-Folge ist damit ziemllich gut beschrieben, oder?
      Ausserdem gibts noch Member, die mit Threading und Threadsicherheit zu tun haben (BeginRead, EndRead, BeginWrite, EndWrite, Synchronisized), und auch zur Thematik TimeOut scheints was zu geben.
      Aber ich beschränke mich hier auf die Hauptsache: Lesen und Schreiben.

      Lesen und Schreiben
      Einfachstes Beispiel wäre eine Datei zu kopieren - dazu muss man ja grad die eine Datei lesen, und die andere schreiben. Und man nimmt für Dateien natürlich FileStreams:

      VB.NET-Quellcode

      1. Private Sub CopyFile(srcName As String, destName As String)
      2. Using src = New FileStream(srcName, FileMode.Open), dest = New FileStream(destName, FileMode.Create)
      3. Dim buf(1023) As Byte
      4. Dim read = buf.Length
      5. While read = buf.Length
      6. read = src.Read(buf, 0, read)
      7. dest.Write(buf, 0, read)
      8. End While
      9. End Using
      10. End Sub
      Jo, diese 10 Zeilen beinhalten schon so einiges:
      Zunächstmal der Using-Block: Anders als in c# kann ein vb.net-Using-Block mehrere Objekte in einer Zeile zur Bereinigung vorbereitet instanzieren - hier nämlich gleich beide FileStreams. (Hat aber mit Stream-Konzepten nix zu tun)
      Ein stream-spezifisches Konzept aber ist die Pufferung: Es wird immer ein kleiner Teil vom einen Stream in ein Byte-Array gelesen, und dann in den anderen hineingeschrieben.
      Nur Pufferung ermöglicht es, auch gigabyte-große Dateien zu bewegen, ohne sie komplett in den Speicher laden zu müssen.
      Insbesondere der Rückgabewert der Read()-Methode ist hier essentiell (Zeile #6): Wird nämlich weniger gelesen, als zu lesen angefordert wurde, ist damit (bei den meisten Streams) signalisiert, dass der Lese-Stream am Ende angekommen ist.
      Nächste Zeile schreibt das dann auch noch weg (und nicht mehr als was gelesen wurde!), und die While-Bedingung beendet dann die Schleife.

      Also obige Schleife bitte merken, denn (mit Abwandlungen) tritt sie sehr oft auf, wenn man mit Streams hantiert.

      Vereinfachungen
      Weil Datei kopieren so häufig ist, gibts dafür auch Abkürzungen, die stärkste Abkürzung macht die komplette obige Methode überflüssig:

      VB.NET-Quellcode

      1. File.Copy(srcName, destName)


      Dennoch ist obige Schleife wichtig zu kennen, zB zeigt sie, dass FileStreams in verschiedenen Modi erstellt werden können: src zum Lesen und dest zum schreiben (Filemode.Open, Filemode.Create) - und es gibt noch weitere FileModi - bitte im ObjectBrowser selbst danach gucken).
      Und - nochmal OB-Bildle geguckt: Die Stream-Klasse stellt auch die Properties CanRead, CanWrite bereit, denn wie fast alle Stream-Klassen kann eine FileStream-Instanz nur entweder lesen oder schreiben (es gibt aber auch welche, wie NetworkStream, MemoryStream, die können beides).

      Bereinigung ist wichtig
      Wurde schon (durch den Using-Block) angedeutet: Stream implementiert IDisposable, und das bedeutet, ein Stream ist unbedingt aufzuräumen, wenn er nicht mehr gebraucht wird: entweder Stream.Dispose() aufrufen, oder ihn gleich in einem Using-Block erstellen. (Wer Using nicht kennt - bitte danach googeln - ist wichtig.)
      Etwa ein unaufgeräumter FileStream sperrt die Datei für weitere Zugriffe - nicht nur der eigenen Anwendung - auch andere Anwendungen sind betroffen.
      Aber auch für die Pufferung ist die Stream-Bereinigung wichtig, denn .Dispose() entleert auch letztmalig den internen Puffer in die Ausgabe.
      Man kann diese Entleerung auch selbst erzwingen, mit Stream.Flush() (s. OB-Bildchen), aber disposen muss man immer noch.
      Das mit dem Daten-Rest im Puffer, wenn eiglich alles schon geschrieben wurde, sorgt oft für Ratlosigkeit, denn es erweckt den Eindruck, dass nur ein Teil der Daten "ankommt".

      Stream-VerarbeitungsStrecke: modifizieren (GZip-(De-)Komprimierung) und (String-)(De-)Kodierung
      Folgender Code bildet aus einem Base64String ein Byte-Array, daraus einen MemoryStream, setzt einen entpackenden GZipStream auf, und ein StreamReader liest letzteren als Klartext aus:

      VB.NET-Quellcode

      1. Private Function GZip64ToString(cypherText As String) As String
      2. Using ms = New MemoryStream(Convert.FromBase64String(cypherText)), unZipper = New GZipStream(ms, CompressionMode.Decompress), rd = New StreamReader(unZipper)
      3. Return rd.ReadToEnd
      4. End Using
      5. End Function

      Kein Problem, und man sieht wieder, wie fein ein Using-Block alle 3 Resourcen aufräumt, sogar noch nachdem die Methode returnt ist. Wie gesagt (googeln!): Using-Block ist zwar ein anderes Thema, aber unbedingt wichtig zu kennen und zu verstehen.

      Nun die Gegenrichtung, die einen Klartext einzugeben versucht und zu komprimieren:

      VB.NET-Quellcode

      1. Private Function StringToGZip64Fail(clearText As String) As String
      2. Using ms = New MemoryStream, zipper = New GZipStream(ms, CompressionLevel.Optimal), wr = New StreamWriter(zipper)
      3. wr.Write(clearText)
      4. Return Convert.ToBase64String(ms.ToArray())
      5. End Using
      6. End Function
      7. ' ... Aufruf:
      8. Dim zippedFail = StringToGZip64Fail("Hallo Welt!")

      zippedFail bleibt leider leer - warum? Weil die Methode returnt, bevor die (Rest-)Puffer von StreamWriter und GZipStream sich in den MemoryStream ergossen haben.

      Logische Folgerung ist, diesen Erguss mit .Flush() zu erzwingen:

      VB.NET-Quellcode

      1. Private Function StringToGZip64Success1(clearText As String) As String
      2. Using ms = New MemoryStream, zipper = New GZipStream(ms, CompressionLevel.Optimal), wr = New StreamWriter(zipper)
      3. wr.Write(clearText)
      4. wr.Flush()
      5. zipper.Flush()
      6. Return Convert.ToBase64String(ms.ToArray())
      7. End Using
      8. End Function


      Aber es geht noch einfacher, denn MemoryStream hat eine Besonderheit: Man kann von ihm das Byte-Array auch dann noch abrufen, wenn er bereits disposed ist. Das ist ungewöhnlich, denn normalerweise macht ein Objekt sich beim Disposen komplett unbrauchbar, aber hier wurde wohl absichtsvoll davon eine Ausnahme gemacht, und ermöglicht eine solche Lösung:

      VB.NET-Quellcode

      1. Private Function StringToGZip64(clearText As String) As String
      2. Dim ms = New MemoryStream
      3. Using zipper = New GZipStream(ms, CompressionLevel.Optimal), wr = New StreamWriter(zipper)
      4. wr.Write(clearText)
      5. End Using ' Disposing von Writern und Streams disposed die BaseStreams gleich mit (kann auch geändert werden).
      6. Return Convert.ToBase64String(ms.ToArray())
      7. End Function
      Das Base64String-Format ( <- folget dem Link!) ist hier übrigens nur als "Krücke" eingesetzt, damit ich eine Messagebox anzeigen kann. Denn die eigentliche Kompression generiert eine Byte-Folge, keinen String.
      Für reale Verwendung wäre dieser Krücken-Schritt sinnlos und sogar kontraproduktiv, denn er bläht die Daten nur wieder auf - lesbar ist das Komprimat eh nicht.

      Zusammenfassung - angesprochen wurden folgende Konzepte:
      • Die Klasse Stream als Basisklasse - für Stream-Erben unterschiedlichster Ausprägung
      • Streams als Instrument, um Daten zu lesen oder zu schreiben
      • Die Kern-Kopier-Methode (häppchenweise)
      • Pufferung - der Vorteil, aber auch die Tücke
      • Verarbeitungs-Streams mehrstufig zusammenstöpseln
      • (De-)Kodierung mittels Reader und Writer
      • eine Besonderheit des MemoryStreams
      Hier nicht angesprochen wurde:
      • Serialisierung: Konvertierung komplexer Objekte von und zu Streams (Bei Interesse: Singelton, Databinding, Serialisierung)
      • Streams im Multi-Thread-Betrieb: Wenn Lese/Schreib-Vorgänge länger dauern verlagert man sie in einen NebenThread
      • Streams zur Kommunikation: Networkstream, named Pipes, Process.StandardInput/Output (Bei Interesse: VersuchsChat & named pipes, cmd-Remote)
      (die Links fokussieren nicht das Thema Stream, aber es werden dort welche entsprechend verwendet)

      Sample-Code
      Das BeispielProjekt hat noch paar andere Spielereien mit Streams eingebaut, ein Word-Dokument wird verzippt, oder unterschiedliche Eingaben gespeichert/geladen (BinaryReader/Writer).
      Dateien
      • AboutStreaming.zip

        (27,52 kB, 212 mal heruntergeladen, zuletzt: )

      Dieser Beitrag wurde bereits 10 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Hi,

      interessanter Beitrag zum Thema Streams.
      Folgende Stelle ist mir noch nicht ganz verständlich:

      ErfinderDesRades schrieb:

      zippedFail bleibt leider leer - warum?
      Weil die Methode returnt, bevor die (Rest-)Puffer von StreamWriter und
      GZipStream sich in den MemoryStream ergossen haben.


      Warum genau ist der Memorystream noch leer, wenn die StreamWriter.Write-Methode aufgerufen wird?
      Ich habe beim bisherigen Arbeiten mit StreamWritern zB. nie Flush aufrufen müssen, also scheint die Methode ja nur unter bestimmten Bedingungen erforderlich zu sein, warum also hier?

      Grüße
      Aber das steht da doch:

      ErfinderDesRades schrieb:

      VB.NET-Quellcode

      1. Private Function StringToGZip64Fail(clearText As String) As String
      2. Using ms = New MemoryStream, zipper = New GZipStream(ms, CompressionLevel.Optimal), wr = New StreamWriter(zipper)
      3. wr.Write(clearText)
      4. Return Convert.ToBase64String(ms.ToArray())
      5. End Using
      6. End Function
      7. ' ... Aufruf:
      8. Dim zippedFail = StringToGZip64Fail("Hallo Welt!")
      zippedFail bleibt leider leer, weil die StringToGZip64Fail()-Methode returnt, bevor die (Rest-)Puffer von StreamWriter und GZipStream sich in den MemoryStream ergossen haben.
      Das wr.Write() writet erstmal in den Writer-Puffer. Erst wenn der Puffer voll ist writet es weiter, und zwar in den zipper-Puffer. Und erst wenn der auch voll ist, writet der zipper weiter in den MemoryStream.
      Es sei denn, man erzwingt das "weiter-writen" durch .Flush() oder durch . Dispose().
      Aber wie's im Fail gemacht ist, returnt die Methode bereits vorm Dispose den MemoryStream-Inhalt ( also vor End Using ) - und an der Stelle ist der ms noch leer, weil die Bytes noch in Puffern rumhängen.

      Wenns bei dir bisher geklappt hat, dann, weil diese Konstellation bislang bei dir nicht aufgetreten ist - ist ja auch relativ ungewöhnlich.

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Es wurde zwar ganz nebenbei in einem Kommentar erwähnt, aber ich finde, man sollte da nochmal ausdrücklich drauf hinweisen:
      Wenn man einen draufgesetzten Stream (z.B. CryptoStream) schließt, dann wird meistens auch der zugrundeliegende Stream (z.B. FileStream) geschlossen.
      Das kann so gewollt sein (dazu fällt mir kein Beispiel ein).
      Oder auch egal sein:

      VB.NET-Quellcode

      1. Using FileStream = File.OpenWrite("...")
      2. Using CStream As New CryptoStream(FileStream, ...)
      3. CStream.Write(...)
      4. End Using 'CStream.Dispose ruft auch vom übergebenen FileStream Dispose auf.
      5. End Using 'Hier wird nochmal FileStream.Dispose aufgerufen. Das ist egal. Mehrmals Dispose aufzurufen ist kein Problem.

      Aber es kann auch ungewollt sein:

      VB.NET-Quellcode

      1. Dim SourceFiles As New List(Of FileInfo)
      2. '...
      3. Using FileStream = File.OpenWrite("Pfad")
      4. For Each i In SourceFiles
      5. Using BWriter As New BinaryWriter(FileStream)
      6. BWriter.WriteInt32(i.Length)
      7. End Using
      8. Using CStream As New CryptoStream(FileStream, CryptoTransformUndSoWeiter, ...)
      9. Using SourceStream = File.OpenRead(i.FullName)
      10. SourceStream.CopyTo(CStream)
      11. End Using
      12. End Using
      13. Next
      14. End Using

      Hier möchte man nicht, dass beim End Using des ersten BinaryWriters der FileStream auch geschlossen wird, denn man möchte nachher ja noch weiter reinschreiben.
      Das sorgt unter Umständen für Verwirrung, wie man hier gut sehen kann.
      Seit - ich glaube - .NET Framework 4.5 gibt es für viele Streams und Writer einen Konstruktorparameter, mit dem man bestimmen kann, ob der zugrundeliegende Stream auch geschlossen wird. Wenn man aber weiterhin 4.0 verwenden möchte (oder man z.B. auf eine externe Bibliothek angewiesen ist), dann muss man da eine Lösung finden (z.B. das hier).

      Ich persönlich empfehle da, (wenn nicht z.B. durch so einen Parameter im Konstruktor angegeben,) sich an die Konvention zu halten, nur Dinge zu disposen, die man auch selbst erstellt hat.
      Der StreamWriter macht das z.B. so, dass er im Konstruktor, der einen String (Pfad) entgegen nimmt, selbst einen FileStream öffnet. Dieser wird beim Disposen auch automatisch verworfen. Das ist richtig so.
      Wenn man einem Stream/Writer einen anderen Stream mitgibt, ist es fast immer der Fall, dass man den sowieso auch in einem Using-Block verwendet, oder dass man den weiterverwenden möchte. Dass man einen Stream erstellt und sich darauf verlässt, dass ein anderes Objekt den Dispose-Aufruf weiterleitet, dürfte wohl sehr selten passieren:

      VB.NET-Quellcode

      1. Dim FileStream = File.OpenWrite("Pfad")
      2. Using CStream As New CryptoStream(FileStream, ...)
      3. CStream.Write(...)
      4. End Using
      5. 'Hier kein FileStream.Dispose, weil man sich drauf verlässt, dass der CryptoStream den FileStream gleich mit verwirft.

      Sieht komisch aus und sollte höchstwahrscheinlich so abgeändert werden, dass man besser erkennen kann, dass der FileStream auch geschlossen wird (z.B. den auch in ein Using packen).
      "Luckily luh... luckily it wasn't poi-"
      -- Brady in Wonderland, 23. Februar 2015, 1:56
      Desktop Pinner | ApplicationSettings | OnUtils