Oppure

Loading
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

E si presenta graficamente così:

SocketFileSender2.jpg
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

L'interfaccia si presenta così:

SocketFileSender1.jpg
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 
A cura di: Il Totem