Guida al Visual Basic .NET
Capitolo 83° - I Socket Parte II
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:
E si presenta graficamente così:
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:
L'interfaccia si presenta così:
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
C#, TypeScript, java, php, EcmaScript (JavaScript), Spring, Hibernate, React, SASS/LESS, jade, python, scikit, node.js, redux, postgres, keras, kubernetes, docker, hexo, etc...
|