|
Gli Stream
Le operazioni di input e output, in .NET come in molti altri linguaggi, hanno come target uno stream, ossia un flusso di dati. In .NET,
tale flusso viene rappresentato da una classe astratta, System.IO.Stream, che espone alcuni metodi per accedere e manipolare i dati ivi
contenuti. Dato che si tratta di una classe astratta, non possiamo utilizzarla direttamente, poiché, appunto, rappresenta un concetto
astratto non istanziabile. Come già spiegato nel capitolo relativo, classi del genere rappresentano un archetipo per diverse altre
classi derivate. Infatti, un flusso di dati può essere tante cose, e provenire da molti posti diversi:
- può trattarsi di un file, come vedremo fra poco; allora la classe derivata opportuna sarà FileStream;
- può trattarsi di dati grezzi presenti in memoria, ed avremo, ad esempio, MemoryStream;
- potrebbe trattarsi, invece, di un flusso di dati proveniente dal server a cui siamo collegati, e ci sarà allora, un NetworkStream;
- e così via, per molti diverse casistiche...
Globalmente parlando, quindi, si può associare uno stream al flusso di dati proveniente da un qualsiasi
dispositivo virtuale o fisico o da qualunque entità astratta all'interno della macchina: ad esempio è possibile avere uno stream associato
a una stampante, a uno scanner, allo schermo, ad un file, alla memoria temporanea, a qualsiasi altra cosa. Per ognuno di questi casi,
esisterà un'opportuna classe derivata di Stream studiata per adempiere a quello specifico compito.
In questo capitolo, vedremo cinque classi del genere, ognuna altamente specializzata: FileStream, StreamReader, StreamWriter, BinaryReader
e BinaryWriter.
FileStream
Questa classe offre funzionalità generiche per l'accesso a un file. Il suo costruttore più semplice accetta due parametri:
il primo è il percorso del file a cui accedere ed il secondo indica le modalità di apertura. Quest'ultimo parametro è
di tipo IO.FileMode, un enumeratore che contiene questi campi:
- Append : apre il file e si posiziona alla fine (in questo modo, potremo velocemente aggiungere dati senza sovrascrivere quelli
precedentemente esistenti);
- Create : crea un nuovo file con il percorso dato nel primo parametro; se il file esiste già, sarà sovrascritto;
- CreateNew : crea un nuovo file con il percorso dato nel primo parametro del costruttore; se il file esiste già, verrà
sollevata un'eccezione;
- Open : apre il file e si posiziona all'inizio;
- OpenOrCreate : apre il file, se esiste, e si posiziona all'inizio; se non esiste, crea il file;
- Truncate : apre il file, cancella tutto il suo contenuto, e si posiziona all'inizio.
Un terzo parametro opzionale può specificare i permessi (solo lettura, solo scrittura o entrambe), ma per ora non lo useremo.
Prima di vedere un esempio del suo utilizzo, è necessario dire che questa classe considera i file aperti come file binari. Si parla
di file binario quando esiste una corrispondenza biunivoca tra i bytes esistenti in esso e i dati letti. Questa condizione non si verifica
con i file di testo, in cui, ad esempio, il singolo carattere "a capo" corrisponde a due bytes: in questo caso non si può parlare di
file binari, ma è comunque possibile leggerli come tali, e ciò che si otterrà sarà solo una sequenza di numeri.
Ma vedremo meglio queste differenze nel paragrafo successivo.
Ora, ammettendo di avere aperto il file, sia che si voglia leggere, sia che si voglia scrivere, sarà necessario adottare un buffer,
ossia un array di bytes che conterrà temporaneamente i dati letti o scritti. Tutti i metodi di lettura/scrittura binari del Framework,
infatti, richiedono come minimo tre parametri:
- buffer : un array di bytes in cui porre i dati letti o da cui prelevare i dati da scrivere;
- index : indice del buffer da cui iniziare l'operazione;
- length : numero di bytes da processare.
Seguendo questa logica, avremo la funzione Read:
Read(buffer, index, length)
che legge length bytes dallo stream aperto e li pone in buffer (a partire da index); e, parimenti, la funzione Write:
Write(buffer, index, length)
che scrive sullo stream length bytes prelevati dall'array buffer (a partire da index). Ecco un esempio:
Module Module1
Sub Main()
Dim File As IO.FileStream
Dim FileName As String
Console.WriteLine("Inserire il percorso di un file:")
FileName = Console.ReadLine
'IO.File.Exists(path) restituisce True se il percorso
'path indica un file esistente e False in caso contrario
If Not IO.File.Exists(FileName) Then
Console.WriteLine("Questo file non esiste!")
Console.ReadKey()
Exit Sub
End If
Console.Clear()
'Apre il file specificato, posizionandosi all'inizio
File = New IO.FileStream(FileName, IO.FileMode.Open)
Dim Buffer() As Byte
Dim Number, ReadBytes As Int32
'Chiede all'utente quanti bytes vuole leggere, e
'memorizza tale numero in Number
Console.WriteLine("Quanti bytes leggere?")
Number = CType(Console.ReadLine, Int32)
'Se Number è un numero positivo e non siamo ancora
'arrivati alla fine del file, allora legge quei bytes.
'La proprietà Position restituisce la posizione
'corrente all'interno del file (a iniziare da 0), mentre
'File.Length restituisce la lunghezza del file, in bytes.
Do While (Number > 0) And (File.Position < File.Length - 1)
'Ridimensiona il buffer
ReDim Buffer(Number - 1)
'Legge Number bytes e li mette in Buffer, a partire
'dall'inizio dell'array. Read è una funzione, e
'restituisce come risultato il numero di bytes
'effettivamente letti dallo stream.
ReadBytes = File.Read(Buffer, 0, Number)
Console.WriteLine("Bytes letti:")
For I As Int32 = 0 To ReadBytes - 1
Console.Write("{0:000} ", Buffer(I))
Next
Console.WriteLine()
'Se abbiamo letto tanti bytes quanti ne erano stati
'chiesti, allora non siamo ancora arrivati alla
'fine del file. Richiede all'utente un numero
If ReadBytes = Number Then
Console.WriteLine("Quanti bytes leggere?")
Number = CType(Console.ReadLine, Int32)
End If
Loop
'Controlla se si è raggiunta la fine del file.
'Infatti, il ciclo potrebbe terminare anche se l'utente
'immettesse 0.
If File.Position >= File.Length - 1 Then
Console.WriteLine("Raggiunta fine del file!")
End If
'Chiude il file
File.Close()
Console.ReadKey()
End Sub
End Module
Bisogna sempre ricordarsi di chiudere il flusso di dati quando si è finito di utilizzarlo. FileStream, e in generale anche Stream,
implementa l'interfaccia IDisposable e il metodo Close non è altro che un modo indiretto per richiamare Dispose (a cui, comunque,
possiamo fare ricorso). Allo stesso modo, possiamo usare la funzione Write per scrivere dati, oppure WriteByte per scrivere un byte alla
volta.
Come avrete notato, la classe Stream espone anche delle proprietà in sola lettura come CanRead, CanWrite e CanSeek. Infatti, non
tutti i flussi di dato supportano tutte le operazioni di lettura, scrittura e ricerca: un esempio può essere il NetworkStream (che
analizzeremo nella sezione dedicata al Web) associato alle richieste http, il quale non supporta le operazioni di ricerca e restituisce
un errore se si prova ad utilizzare il metodo Seek. Questo metodo serve per spostarsi velocemente da una parte all'altra del flusso di
dati, e accetta solo due argomenti:
Seek(offset, origin)
offset è un intero che specifica la posizione a cui recarsi, mentre origin è un valore enumerato di tipo IO.SeekOrigin che
può assumere tre valori: Begin (si riferisce all'inizio del file), Current (si riferisce alla posizione corrente) ed End (si
riferisce alla fine del file). Ad esempio:
'Si sposta alla posizione 100
File.Seek(100, IO.SeekOrigin.Begin)
'Si sposta di 250 bytes indietro rispetto alla posizione corrente
File.Seek(-250, IO.SeekOrigin.Current)
'Si sposta a 100 bytes dalla fine del file
File.Seek(-100, IO.SeekOrigin.End)
Certo che leggere e scrivere dati un byte alla volta non è molto comodo. Vediamo, allora, la prima categoria di file: i file testuali.
Lettura/scrittura di file testuali
I file testuali sono così denominati perchè contengono solo testo, ossia bytes codifcabili in una delle codifiche standard dei caratteri
(ASCII, UTF-8, eccetera...). Alcuni particolari bytes vengono intepretati in modi diversi, come ad esempio la tabulazione, che viene rappresentata con
uno spazio più lungo; altri vengono tralasciati nella visualizzazione e sembrano non esistere, ad esempio il NULL terminator, che rappresenta
la fine di una stringa, oppure l'EOF (End Of File); altri ancora vengono presi a gruppi, come il carattere a capo, che in realtà è formato
da una sequenza di due bytes (Carriage Return e Line Feed, rispettivamente 13 e 10). La differenza insita in questi tipi di file rispetto a
quelli binari è il fatto di non poter leggere i singoli bytes perchè non ce n'è necessità: quello che importa è l'informazione che il
testo porta al suo interno. La classe usata per la lettura è StreamReader, mentre quella per la scrittura StreamWriter: il costruttore di
entrambi accetta un unico parametro, ossia il percorso del file in questione; esistono anche altri overloads dei costruttori, ma il più
usato e quindi il più importante di tutti è quello appena citato. Ecco un piccolo esempio di come utilizzare tali classi in una semplice
applicazione console:
Module Module1
Sub Main()
Dim File As String
Dim Mode As Char
Console.WriteLine("Premere R per leggere un file, W per scriverne uno.")
'Console.ReadKey restituisce un oggetto ConsoleKeyInfo,
'al cui interno ci sono tre proprietà: Key,
'enumeratore che definisce il codice del pulsante premuto;
'KeyChar, il carattere corrispondente a quel pulsante;
'Modifier, enumeratore che definisce i modificatori attivi,
'ossia Ctrl, Shift e Alt.
'Quello che serve ora è solo KeyChar
Mode = Console.ReadKey.KeyChar
'Dato che potrebbe essere attivo il Bloc Num, ci si
'assicura che Mode contenga un carattere maiuscolo
'con la funzione statica ToUpper del tipo base Char
Mode = Char.ToUpper(Mode)
'Pulisce lo schermo
Console.Clear()
Select Case Mode
Case "R"
Console.WriteLine("Inserire il percorso del file da leggere:")
File = Console.ReadLine
'Cosntrolla che il file esista
If Not IO.File.Exists(File) Then
'Se non esiste, visualizza un messggio ed esce
Console.WriteLine("Il file specificato non esiste!")
Console.ReadKey()
Exit Sub
End If
Dim Reader As New IO.StreamReader(File)
'Legge ogni singola riga del file, fintanto che non
'si è raggiunta la fine
Do While Not Reader.EndOfStream
'Come Console.Readline, la funzione d'istanza
'ReadLine restituisce una linea di testo
'dal file
Console.WriteLine(Reader.ReadLine)
Loop
'Quindi chiude il file
Reader.Close()
Case "W"
Console.WriteLine("Inserire il percorso del file da creare:")
File = Console.ReadLine
Dim Writer As New IO.StreamWriter(File)
Dim Line As String
Console.WriteLine("Immettere il testo del file, " & _
"premere due volte invio per terminare")
'Fa immettere righe di testo fino a quando
'si termina
Do
Line = Console.ReadLine
'Come Console.WriteLine, la funzione d'istanza
'WriteLine scrive una linea di testo sul file
Writer.WriteLine(Line)
Loop While Line <> ""
'Chiude il file
Writer.Close()
Case Else
Console.WriteLine("Comando non valido!")
End Select
Console.ReadKey()
End Sub
End Module
Ovviamente esistono anche i metodi Read e Write, che scrivono del testo senza mandare a capo: inoltre, Write e WriteLine hanno degli overloads
che accettano anche stringhe di formato come quelle viste nei capitoli precedenti.
Come si è visto, le classi analizzate (e quelle che andremo a vedere tra breve) hanno metodi molti simili a quelli di Console: questo perchè
anche la console è uno stream, capace di input e output allo stesso tempo. Per coloro che provengono dal C non sarà difficile richiamare
questo concetto.
Lettura/scrittura di file binari
Come già accennato nel paragrafo precedente, la distinzione tra file binari e testuali avviene tramite l'interpretazione dei singoli bytes.
Con questo tipo di file, c'è una corrispondenza biunivoca tra i bytes del file e i dati letti dal programma: infatti, non a caso, l'I/O
viene gestito attraverso un array di byte.
BinaryWriter e BinaryReader espongono, oltre alle canoniche Read e Write già analizzate per FileStream, altre procedure di lettura e
scrittura, che, di fatto, scendono a più basso livello. Ad esempio, all'inizio della guida ho illustrato alcuni tipi di dato basilari,
riportando anche la loro grandezza (in bytes). Integer occupa 4 bytes, Int16 ne occupa 2, Single he occupa 4 e così via. Valori di
tipo base vengono quindi salvati in memoria in notazione binaria, rispettando quella specifica dimensione. Ora, esistono modi ben definiti
per convertire un numero in base 10 in una sequenza di bit facilmente manipolabile dall'elaboratore: mi riferisco, ad esempio, alla notazione
in complemento a 2 per gli interi e al formato in virgola mobile per i reali. Potete documentarvi su queste modalità di rappresentazione
dell'informazione altrove: in questo momento ci interessa sapere che i dati sono "pensati" dal calcolatore in maniera diversa da come li
concepiamo noi. BinaryWriter e BinaryReader sono classi appositamente create per far da tramite tra ciò che capiamo noi e ciò
che capisce il computer. Proprio perchè sono dei "mezzi", il loro costruttore deve specificare lo stream (già aperto) su
cui lavorare.
Ecco un esempio:
Module Module1
Sub Main()
'Apre il file "prova.dat", creandolo o sovrascrivendolo
Dim File As New IO.FileStream("prova.dat", IO.FileMode.Create)
'Writer è lo strumento che ci permette di scrivere
'sullo stream File con codifica binaria
Dim Writer As New IO.BinaryWriter(File)
Dim Number As Int32
Console.WriteLine("Inserisci 10 numeri da scrivere sul file:")
For I As Int32 = 1 To 10
Console.Write("{0}: ", I)
Number = CType(Console.ReadLine, Int32)
Writer.Write(Number)
Next
Writer.Close()
Console.ReadKey()
End Sub
End Module
Io ho inserito questi numeri: -10 -5 0 1 20 8000 19001 -345 90 22. Provando ad aprire il file con un editor di testo vedrete solo caratteri
strani, in quanto questo non è un file testuale. Aprendolo, invece, con un editor esadecimale, otterrete questo:
f6 ff ff ff fb ff ff ff 00 00 00 00 01 00 00 00
14 00 00 00 40 lf 00 00 39 4a 00 00 a7 fe ff ff
5a 00 00 00 16 00 00 00
Ogni gruppetto di quattro bytes rappresenta un numero intero codificato in binario. Potremmo fare la stessa cosa con Single, Double, Date,
Boolean, String e altri tipi base per vedere cosa succede.
BinaryWriter e BinaryReader sono molto utili quando bisogna leggere dati in codifica binaria, ad esempio per molti famosi formati di file,
come mp3, wav (vedi sezione FFS), zip, mpg, eccetera...
Esempio: steganografia su immagini
La steganografia è l'arte di nascondere del testo all'interno di un'immagine. Per i più curiosi, mi avventurerò nella scrittura
di un semplicissimo programma di steganografia su immagini, nascondendo del testo al loro interno.
Per prima cosa, si costruisca l'interfaccia grafica, con questi controlli:
- Una Label, Label1, Text = "Introdurre il percorso di un'immagine:"
- Una TextBox, txtPath, cone AutoCompleteMode = Suggest e AutoCompleteSource = FileSystem. In questo modo, la textbox suggerirà
il nome di file e cartelle esistenti mentre state digitando, rendendo più semplice l'indtroduzione del percorso;
- Una TextBox, txtText, ScrollBars = Both, MultiLine = True
- Un Button, btnHide, Text = "Nascondi"
- Un Button, btnRead, Text = "Leggi"

Ed ecco il codice ampiamente commentato:
Public Class Form1
Private Sub btnHide_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnHide.Click
If Not IO.File.Exists(txtPath.Text) Then
MessageBox.Show("File inesistente!", Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Error)
Exit Sub
End If
If IO.Path.GetExtension(txtPath.Text) <> ".jpg" Then
MessageBox.Show("Il file deve essere in formato JPEG!", Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
Exit Sub
End If
Dim File As New IO.FileStream(txtPath.Text, IO.FileMode.Open)
'Converte il testo digitato in una sequenza di bytes,
'secondo gli standard della codifica UTF8
Dim TextBytes() As Byte = _
System.Text.Encoding.UTF8.GetBytes(txtText.Text)
'Va alla fine del file
File.Seek(0, IO.SeekOrigin.End)
'Scrive i bytes
File.Write(TextBytes, 0, TextBytes.Length)
File.Close()
MessageBox.Show("Testo nascosto con successo!", Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Information)
End Sub
Private Sub btnRead_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnRead.Click
If Not IO.File.Exists(txtPath.Text) Then
MessageBox.Show("File inesistente!", Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Error)
Exit Sub
End If
If IO.Path.GetExtension(txtPath.Text) <> ".jpg" Then
MessageBox.Show("Il file deve essere in formato JPEG!", Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
Exit Sub
End If
Dim File As New IO.FileStream(txtPath.Text, IO.FileMode.Open)
Dim TextBytes() As Byte
Dim B1, B2 As Byte
'Legge un byte
B1 = File.ReadByte()
Do
'Legge un altro byte
B2 = File.ReadByte()
'Se i bytes formano la sequenza FF D9, si ferma.
'In Visual Basic, in numeri esadecimali si scrivono
'facendoli precedere da "&H"
If B1 = &HFF And B2 = &HD9 Then
Exit Do
End If
'Passa il valore di B2 in B1
B1 = B2
Loop While (File.Position < File.Length - 1)
ReDim TextBytes(File.Length - File.Position - 1)
'Legge ciò che rimane dopo FF D9
File.Read(TextBytes, 0, TextBytes.Length)
File.Close()
txtText.Text = System.Text.Encoding.UTF8.GetString(TextBytes)
End Sub
End Class
Il testo accodato può essere rilevato facilmente con un Hex Editor, per questo lo si dovrebbe criptare con una password: per ulteriori
informazioni sulla criptazione in .NET, vedere capitolo rekativo.
|
|