| < Precedente | I Socket - Parte II | Successivo > |
Esempio: File Sender
Fino ad ora si è parlato di inviare semplici messaggi sotto forma di stringhe, ma come ci si dovrebbe comportare nel caso il contenuto
da inviare sia un file intero o, perchè no?, molti files? Il procedimento è lo stesso e con questo esempio fornirò una
prova di come sia altrettanto semplice questo compito. L'applicazione File Sender si basa su un semplice scambio di interrogazioni tra i due
computer, al termine delle quali si inizia l'invio effettivo del file. Per prima cosa il client comunica al server che sta per cominciare
il flusso di dati; il server deve perciò rispondere in caso affermativo se l'utente è disposto al trasferimento: in questo caso,
rimanda indietro un messaggio di conferma, e apre una nuova porta per i dati in arrivo; parallelamente, il client si connette alla porta aperta
e inizia il trasferimento.File Sender: server
Ho strutturato l'interfaccia del server in questo modo:
- Label1 : una label esplicativo con il testo "Progresso:"
- prgProgress : la barra del progresso
- cmdListen : il pulsante "Ascolta"
- strStatus : la status strip sul lato basso del form
- lblStatus : la label contenuta in strStatus, con il compito di informare l'utente sullo stato dell'applicazione
- tmrControlConnection : timer con Interval = 100 che ha il compito di controllare se ci sono richieste in attesa
- tmrControlFile : timer con Interval = 100 con il compito di controllare se ci sono richieste in attesa sulla porta 1001, deputata in questo caso alla ricezione del file dal client
- tmrGetData : timer con Interval = 100 con il compito di ottenere i messaggi inviati dal client e di rispondervi
- bgReceiveFile : BackgroundWroker con WrokerReportProgress = True che ha il compito di ricevere il file dal client
Ed ecco il codice:
Imports System.Net.Sockets
Imports System.Text.ASCIIEncoding
Imports System.ComponentModel
Public Class Form1
'Listener: attende una connessione sulla porta 25
'FileListener: attende una connessione sulla porta 1001. Questa
' ha il compito di trasferire i bytes del file
Private Listener, FileListener As TcpListener
'Client: l'oggetto che ha il compito di dialogare con
' il client e confermarne le operazioni
'FileReceiver: l'oggetto che ha il compito di ricevere le
' informazioni contenute nel file e scriverle sulla macchina
' in forma di file concreto
Private Client, FileReceiver As TcpClient
'NetStream: lo stream su cui si scrivono i dati di comunicazione
'NetFile: lo stream da cui si leggono i dati del file
Private NetStream, NetFile As NetworkStream
'Percorso su cui salvare il file
Private FileName As String
'Dimensione del file
Private FileSize As Int64
'I seguenti metodi semplificano le operazioni di invio e
'ricezione di stringhe
'Invia un messaggio su uno stream di rete
Private Sub Send(ByVal Msg As String, ByVal Stream As NetworkStream)
'Se si può scrivere
If Stream.CanWrite Then
'Converte il messaggio in binario
Dim Bytes() As Byte = ASCII.GetBytes(Msg)
'E lo scrive sul network stream
Stream.Write(Bytes, 0, Bytes.Length)
End If
End Sub
'Ottiene un messaggio dallo stream di rete
Private Function GetMessage(ByVal Stream As NetworkStream) As String
'Se si può leggere
If Stream.CanRead Then
Dim Bytes(Client.ReceiveBufferSize) As Byte
Dim Msg As String
'Legge i bytes arrivati
Stream.Read(Bytes, 0, Bytes.Length)
'Li converte in una stringa leggibile
Msg = ASCII.GetString(Bytes)
'E restituisce la stringa
Return Msg.Normalize
Else
Return Nothing
End If
End Function
Private Sub cmdListen_Click(ByVal sender As Object, _
ByVal e As EventArgs) Handles cmdListen.Click
If cmdListen.Text = "Ascolta" Then
'Inizia ad ascoltare sulla porta 25
Listener = New TcpListener(25)
Listener.Start()
'Attiva il timer per controllare le richieste di connesione
tmrControlConnection.Start()
'Cambia il testo e la funzione del pulsante
cmdListen.Text = "Stop"
Else
'Ferma l'operazione di ascolto
Listener.Stop()
'Ripristina il testo
cmdListen.Text = "Ascolta"
End If
End Sub
Private Sub tmrControlConnection_Tick(ByVal sender As Object, _
ByVal e As EventArgs) Handles tmrControlConnection.Tick
'Se ci sono connessioni in attesa...
If Listener.Pending Then
'Ferma il timer per eseguire le operazioni
tmrControlConnection.Stop()
lblStatus.Text = "È stata ricevuta una richiesta"
'Richiede all'utente se accettare la connessione
If MessageBox.Show("È stata ricevuta una richiesta di connessione. Accettare?", _
Me.Text, MessageBoxButtons.YesNo, MessageBoxIcon.Question) = _
Windows.Forms.DialogResult.Yes Then
'Acceta la connessione
Client = Listener.AcceptTcpClient
'Apre lo stream di rete condiviso
NetStream = Client.GetStream
'Termina l'ascolto
Listener.Stop()
'Rende il pulsante cmdListen inutilizzabile, poiché
'una connessione è già stata aperta
cmdListen.Enabled = False
'Inizia la ricezione di messaggi
tmrGetData.Start()
lblStatus.Text = "Connessione riuscita!"
Else
'Altrimenti si rimette in attesa per altre connessioni
tmrControlConnection.Start()
lblStatus.Text = "In attesa di connessioni..."
End If
End If
End Sub
Private Sub tmrControlFile_Tick(ByVal sender As Object, _
ByVal e As EventArgs) Handles tmrControlFile.Tick
'Se c'è una richiesta, l'accetta subito
If FileListener.Pending Then
tmrControlFile.Stop()
FileReceiver = FileListener.AcceptTcpClient
NetFile = FileReceiver.GetStream
'Ferma il listener
FileListener.Stop()
lblStatus.Text = "Flusso di informazioni aperto"
'Attiva la ricezione di dati attraverso un background worker
bgReceiveFile.RunWorkerAsync()
End If
End Sub
Private Sub tmrGetData_Tick(ByVal sender As Object, _
ByVal e As EventArgs) Handles tmrGetData.Tick
If Client.Connected And Client.Available Then
'Ferma il timer mentre si eseguono le operazioni
tmrGetData.Stop()
'Legge il messaggio
Dim Msg As String = GetMessage(NetStream)
If Msg.StartsWith("ConfirmTransfer") Then
'Divide il messagio in parti in base al carattere pipe
Dim Parts() As String = Msg.Split("|")
'La prima parte è "ConfirmTransfer"
'La seconda è il percorso del file sull'altro computer
Dim File As String = Parts(1)
'La terza è la dimensione
Dim Size As Int64 = CType(Parts(2), Int64)
'Ottiene solo il nome del file, senza percorso
File = IO.Path.GetFileName(File)
'Costruisce il percorso del file su questo computer,
'salvandolo nella cartella del progetto (binDebug)
FileName = Application.StartupPath & "" & File
'Imposta Size come variabile globale
FileSize = Size
'Richiede se accettare il trasferimento
If MessageBox.Show(String.Format( _
"È stata ricevuta una richiesta di trasferimento di {0} ({1} bytes). Acettare?", _
File, Size), Me.Text, MessageBoxButtons.YesNo, _
MessageBoxIcon.Question) = Windows.Forms.DialogResult.Yes Then
'Manda OK al client
Send("OK", NetStream)
'Intanto si mette in attesa sulla porta 1001 per
'l'invio dei bytes del file
FileListener = New TcpListener(1001)
FileListener.Start()
'E attiva il timer di controllo
tmrControlFile.Start()
Else
'Altrimenti, risponde di no
Send("NO", NetStream)
End If
End If
'Riprende il controllo
tmrGetData.Start()
End If
End Sub
Private Sub bgReceiveFile_DoWork(ByVal sender As Object, _
ByVal e As DoWorkEventArgs) Handles bgReceiveFile.DoWork
'Apre un nuovo stream in base al percorso costruito
'nella procedura precedente
Dim Stream As New IO.FileStream(FileName, IO.FileMode.Create)
'Crea un indice che indica il progresso
Dim Index As Int64 = 0
lblStatus.Text = "In ricezione..."
Do
If FileReceiver.Available Then
'Riceve i bytes necessari
Dim Bytes(4096) As Byte
Dim Msg As String = ASCII.GetString(Bytes)
'Se i bytes sono un messaggio stringa e contengono
'"END", oppure la dimensione giusta è già stata
'raggiunta, allora si ferma
If Msg.Contains("END") Or Index >= FileSize Then
Exit Do
End If
'Preleva i bytes dallo stream di rete
NetFile.Read(Bytes, 0, 4096)
'E li scrive sul file fisico
Stream.Write(Bytes, 0, 4096)
'Incrementa l'indice di 4096
Index += 4096
'E notifica il progresso
bgReceiveFile.ReportProgress(Index * 100 / FileSize)
End If
Loop
lblStatus.Text = "File ricevuto!"
Stream.Close()
MessageBox.Show("File ricevuto con successo!", Me.Text, _
MessageBoxButtons.OK, MessageBoxIcon.Information)
End Sub
Private Sub bgReceiveFile_ProgressChanged(ByVal sender As Object, _
ByVal e As ProgressChangedEventArgs) _
Handles bgReceiveFile.ProgressChanged
prgProgress.Value = e.ProgressPercentage
End Sub
End Class
File Sender: client
Ho struttura l'interfaccia del client in questo modo:
- grpTrasnfer : un GroupBox con Text = "Trasferimento" che contiene tutti i controlli sul trasferimento del file
- txtFile : una TextBox che contiene il percorso del file da inviare
- cmdBrowse : un pulsante con Text = "Sfoglia" per permettere all'utente di selezionare un file in maniera semplice
- cmdSend : un pulsante con Text = "Invia" che ha il compito di inoltrare la richiesta al server
- prgProgress : una barra di progresso
- cmdConnect : un pulsante con Text = "Connetti" con il compito di connettersi al server
- strStatus : una StatusStrip nel lato inferiore del form
- lblStatus : la label con il compito di tenere l'utente al corrente dello stato dell'applicazione
- tmrGetData : un timer con Interval = 100 per ricevere e inviare messaggi al server
- bgSendFile : un BackgroundWroker con WrokerReportProgress = True che ha il compito di inviare il file
E questo è il codice:
Imports System.Net.Sockets
Imports System.Text.ASCIIEncoding
Imports System.ComponentModel
Public Class Form1
'Client: il client che si dovrà connettere al server
'FileSender: il client che ha il compito di trasferire i
' pacchetti di informazioni al server
Private Client, FileSender As TcpClient
'NetStream: lo stream su cui scrivere i dati di comunicazione
'NetFile: lo stream per inviare i dati da scrivere sul file
Private NetStream, NetFile As NetworkStream
'L'IP del server a cui connettersi
Private IP As String
'I seguenti metodi semplificano le operazioni di invio e
'ricezione di stringhe
'Invia un messaggio su uno stream di rete
Private Sub Send(ByVal Msg As String, ByVal Stream As NetworkStream)
'Se si può scrivere
If Stream.CanWrite Then
'Converte il messaggio in binario
Dim Bytes() As Byte = ASCII.GetBytes(Msg)
'E lo scrive sul network stream
Stream.Write(Bytes, 0, Bytes.Length)
End If
End Sub
'Ottiene un messaggio dallo stream di rete
Private Function GetMessage(ByVal Stream As NetworkStream) As String
'Se si può leggere
If Stream.CanRead Then
Dim Bytes(Client.ReceiveBufferSize) As Byte
Dim Msg As String
'Legge i bytes arrivati
Stream.Read(Bytes, 0, Bytes.Length)
'Li converte in una stringa leggibile
Msg = ASCII.GetString(Bytes)
'E restituisce la stringa
Return Msg.Normalize
Else
Return Nothing
End If
End Function
Private Sub cmdConnect_Click(ByVal sender As Object, _
ByVal e As EventArgs) Handles cmdConnect.Click
'Ottiene l'IP del server
IP = InputBox("Inserire l'IP del server:", Me.Text)
'Controlla che l'IP non sia nullo o vuoto
If String.IsNullOrEmpty(IP) Then
MessageBox.Show("Connessiona annullata!", Me.Text, _
MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
Exit Sub
End If
'Inizializza un nuovo client
Client = New TcpClient
'E tenta la connessione all'IP dato, sulla porta 25
lblStatus.Text = "Connessione in corso..."
Try
Client.Connect(IP, 25)
Catch SE As SocketException
MessageBox.Show("Impossibile stabilire una connessione!", _
Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
Exit Sub
End Try
'Se la connessione è riuscita, ottiene lo
'stream condiviso di rete direttamente collegato con
'il networkstream del server
If Client.Connected Then
'Ora si è sicuri di essere connessi:
'sblocca i comandi per il trasferimento
NetStream = Client.GetStream
grpTransfer.Enabled = True
lblStatus.Text = "Connessione riuscita!"
End If
End Sub
Private Sub cmdBrowse_Click(ByVal sender As Object, _
ByVal e As EventArgs) Handles cmdBrowse.Click
Dim Open As New OpenFileDialog
Open.Filter = "Tutti i file|*.*"
If Open.ShowDialog = Windows.Forms.DialogResult.OK Then
txtFile.Text = Open.FileName
End If
End Sub
Private Sub cmdSend_Click(ByVal sender As Object, _
ByVal e As EventArgs) Handles cmdSend.Click
'Controlla che il file esista
If Not IO.File.Exists(txtFile.Text) Then
MessageBox.Show("Il file non esiste!", Me.Text, _
MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
Exit Sub
End If
'Se si è connessi e si può scrivere
'sullo stream di rete...
If Client.Connected AndAlso NetStream.CanWrite Then
'Manda un messaggio al server, chiedendo
'conferma del trasferimento. Nel messaggio immette anche
'alcune informazioni riguardo il nome e la
'dimensione del file
Dim Msg As String = _
String.Format("ConfirmTransfer|{0}|{1}", txtFile.Text, _
FileLen(txtFile.Text))
'Invia il messaggio con la procedura scritta sopra
Send(Msg, NetStream)
'Attiva il timer per controllare i dati arrivati
tmrGetData.Start()
'Disattiva il pulsante per evitare più azioni
'contemporanee indesiderate
cmdSend.Enabled = False
lblStatus.Text = "In attesa di conferma dal server..."
End If
End Sub
Private Sub tmrGetData_Tick(ByVal sender As Object, _
ByVal e As EventArgs) Handles tmrGetData.Tick
If Client.Connected AndAlso Client.Available Then
'Ferma il timer mentre si eseguono le operazioni
tmrGetData.Stop()
'Legge il messaggio
Dim Msg As String = GetMessage(NetStream)
'Uso Contains per un semplice motivo. Quando si converte
'un array di bytes in una stringa, ci possono essere
'caratteri speciali successivi a questa, come ad esempio
'il NULL terminator (carattere 00), che ne compromettono
'la struttura.
If Msg.Contains("OK") Then
'Termina questa connessione e si connette
'alla porta deputata alla ricezione dei file
FileSender = New TcpClient
FileSender.Connect(IP, 1001)
If FileSender.Connected Then
'Ottiene lo stream associato a questa operaizone
NetFile = FileSender.GetStream
'E inizia la trasmissione dei dati
bgSendFile.RunWorkerAsync(txtFile.Text)
End If
ElseIf Msg.Contains("NO") Then
MessageBox.Show("Il server ha rifiutato il trasferimento!", _
Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
cmdSend.Enabled = True
End If
'Riprende il controllo dei dati
tmrGetData.Start()
End If
End Sub
Private Sub bgSendFile_DoWork(ByVal sender As Object, _
ByVal e As DoWorkEventArgs) Handles bgSendFile.DoWork
'Ottiene il nome del file dall'argomento passato al metodo
'RunWorkerAsync nella procedura precedente
Dim FileName As String = e.Argument
'Crea un nuovo lettore del file a basso livello, così
'da poter ottenere bytes di informazione anziché caratteri
'come nello StreamReader
Dim Reader As New IO.FileStream(FileName, IO.FileMode.Open)
'Calcola la grandezza del file, per poter poi tenere
'l'utente al corrente della percentuale di completamento
Dim Size As Int64 = FileLen(FileName)
'Un blocco di bytes da 4096 posti. Il file viene spedito in
'"pacchettini" per evitare di sovraccaricare la connessione
Dim Bytes(4095) As Byte
'Se il file è più grande di 4KiB, lo divide
'in blocchi di dati da 4096 bytes
If Size > 4096 Then
For Block As Int64 = 0 To Size Step 4096
'Se i bytes rimanenti sono più di 4096,
'ne legge un blocco intero
If Size - Block >= 4096 Then
Reader.Read(Bytes, 0, 4096)
Else
'Altrimenti un blocco più piccolo
Reader.Read(Bytes, 0, Size - Block)
End If
'Scrive i dati prelevati sullo stream di rete,
'inviandoli così al server
NetFile.Write(Bytes, 0, 4096)
'Riporta la percentuale all'utente
bgSendFile.ReportProgress(Block * 100 / Size)
'Smette per 30ms, così da dare tempo dal
'server di poter processare i pacchetti uno per
'uno, evitando confusione
Threading.Thread.Sleep(30)
Next
Else
'Se il file è minore di 4KiB, lo invia tutto
'direttamente dal server
Reader.Read(Bytes, 0, Size)
NetFile.Write(Bytes, 0, Size)
End If
Reader.Close()
'Percentuale massima: lavoro terminato
bgSendFile.ReportProgress(100)
Threading.Thread.Sleep(100)
'Comunica la fine delle operazioni
NetFile.Write(ASCII.GetBytes("END"), 0, 3)
MessageBox.Show("File inviato con successo!", Me.Text, _
MessageBoxButtons.OK, MessageBoxIcon.Information)
cmdSend.Enabled = True
End Sub
Private Sub bgSendFile_ProgressChanged(ByVal sender As Object, _
ByVal e As ProgressChangedEventArgs) _
Handles bgSendFile.ProgressChanged
'Aggiorna la progressbar
prgProgress.Value = e.ProgressPercentage
End Sub
End Class

