C16. I Socket
La classe System.Net.Sockets offre funzionalità per gestire la comunicazione tra due computer in rete. In questo capitolo mi
sofferò sull'analisi dei metodi di comunicazione tramite TCP (Trasmission Control Protocol).

Ed ecco il codice:

E questo è il codice:
Server
Il server, nel nostro caso, è il computer sul quale risiede l'applicazione principale deputata alla gestione delle connessioni e dei servizi
dei client esterni. Più in generale è una componente informatica che fornisce servizi ad altre componenti attraverso una rete.
Per implementare un'applicazione Server da codice dobbiamo far sì che essa possa accettare connessioni da parte di altri. Per far questo è
necessario usare la classe TcpListener, che si mette in ascolto su di una porta, e riferisce quando ci sono richieste di connessioni in attesa
su di essa.
Questa riga di codice inizializza un TcpListener sulla porta 25 (quella di default per i server di posta :P). I metodi che comunicano ad esso di iniziare o terminare l'ascolto sono Start() e Stop(), mentre la funzione che comunica se ci sono richieste in attesa di essere accettate è Pending() (restituisce un valore Booleano). Per accettare la connessione è necessario richiamare la funzione AcceptTcpClient(), la quale restituisce un oggetto di tipo TcpClient connesso al rispettivo oggetto sul Client. In questo modo, Server e Client risultano legati da una connessione.Private ListenerAs New Sockets.TcpListener(25)
Client
La classe fondamentalmente usata per un client è TcpClient. I suoi membri più significativi sono:
- Avaiable: restituisce il numero di bytes ricevuti e pronti per la lettura
- Close: chiude la connessione
- Connect(IP, P): tenta una connessione verso il server identificato da IP sulla porta P. IP può essere sia un indirizzo IP che DNS
- Connected: restituisce True se è connesso, altrimento False
- GetStream: funzione importantissima che restituisce un oggetto di tipo Sockets.NetworkStream su cui e da cui si scrivono e leggono tutti i dati scambiati tra client e server
- ReceiveBufferSize: imposta la grandezza del buffer di bytes ricevuti
- SendBufferSize: imposta la grandezza del buffer di bytes inviati
Costruire l'applicazione
Le classi TcpListener e TcpClient non possiedono eventi, perciò, per sapere quando ci sono connessioni in attesa o se si sono ricevuti dati, è
necessario controllare con un timer. Sarà tmrControlConnection il timer che controlla se ci sono richieste di connessione sul Server,
mentre tmrGetData quello che controlla la presenza di dati ricevuti. Ci sarà poi un pulsante cmdSend per inviare dati e una textbox txtSend
contenente il messagio da inviare. Di seguito il codice.
Solo per il server:
Per server e client:Imports System.Net.Sockets '...Public ListenerAs New TcpListener(25)Private Sub tmrControlConnection_Tick(ByVal senderAs Object , _ByVal eAs EventArgs)Handles tmrControlConnection.Tick' Se ci sono connessioni in attesa If Listener.PendingThen ' Client = Listener.AcceptTcpClientInizializza il client ' NetStr = Client.GetStreamInizializza lo stream ' tmrControlConnection.Stop()Termina il controllo del timer ' Listener.Stop()Termina l'ascolto del TcpListener ' tmrGetData.Start()Attiva il timer per la ricezione di dati End If End Sub Private Sub Form1_Load(ByVal senderAs Object , _ByVal eAs EventArgs)Handles MyBase .Load' Esiste anche una versione di Start che accetta come parametro ' Listener.Start()il massimo numero di connessioni accettabili End Sub
Solo per il client:Public ClientAs TcpClientPublic NetStrAs NetworkStreamPrivate Sub tmrGetData_Tick(ByVal senderAs Object , _ByVal eAs EventArgs)Handles tmrGetData.Tick' Se il client è connesso If Client.ConnectedThen ' Se ci sono dati da leggere, che possono essere letti If Client.Available > 0And NetStr.CanReadThen ' Legge i dati come array di bytes Dim Bytes(Client.ReceiveBufferSize)As Byte ' Legge Client.ReceiveBufferSize bytes a partire dal primo ' dallo stream e li deposita in Bytes ' se ci sono bytes nulli, non verranno contati ' NetStr.Read(Bytes, 0, Client.ReceiveBufferSize)di default, Client.ReceiveBufferSize = 8129 ' Trasforma i bytes ricevuti in stringa Dim SAs String = System.Text.ASCIIEncoding.ASCII.GetString (Bytes)' MsgBox(S, MsgBoxStyle.Information)Visualizza il messaggio End If End If End Sub Private Sub cmdSend_Click(ByVal senderAs Object , _ByVal eAs EventArgs)Handles cmdSend.Click' Se il client è connesso If Client.ConnectedThen ' Se si può scrivere sullo stream If NetStr.CanWriteThen ' Converte il messaggio in bytes Dim Bytes()As Byte = _ System.Text.ASCIIEncoding.ASCII.GetBytes(txtSend.Text)' NetStr.Write(Bytes, 0, Bytes.Length)E li scrive sullo stream End If End If End Sub
127.0.0.1 è il localhost, ossia la stessa macchina su cui viene eseguito il software.Private Sub Form1_Shown(ByVal senderAs Object , _ByVal eAs EventArgs)Handles MyBase .Shown' Client.Connect("127.0.0.1", 25)Prova a connettersi al server ' Se è avvenuta la connessione If Client.ConnectedThen ' NetStr = Client.GetStreamInizializza lo stream End If End Sub
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
Server
Ed ecco il codice:
Imports System.Net.SocketsImports System.Text.ASCIIEncodingImports System.ComponentModelPublic 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, FileListenerAs 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, FileReceiverAs 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, NetFileAs NetworkStream 'Percorso su cui salvare il file Private FileNameAs String 'Dimensione del file Private FileSizeAs 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 MsgAs String ,ByVal StreamAs NetworkStream) 'Se si può scrivere If Stream.CanWriteThen '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 StreamAs NetworkStream)As String 'Se si può leggere If Stream.CanReadThen Dim Bytes(Client.ReceiveBufferSize)As Byte Dim MsgAs 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.NormalizeElse Return NothingEnd If End Function Private Sub cmdListen_Click(ByVal senderAs Object , _ByVal eAs EventArgs)Handles cmdListen.ClickIf 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 senderAs Object , _ByVal eAs EventArgs)Handles tmrControlConnection.Tick 'Se ci sono connessioni in attesa... If Listener.PendingThen '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.YesThen '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 senderAs Object , _ByVal eAs EventArgs)Handles tmrControlFile.Tick 'Se c'è una richiesta, l'accetta subito If FileListener.PendingThen 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 senderAs Object , _ByVal eAs EventArgs)Handles tmrGetData.TickIf Client.ConnectedAnd Client.AvailableThen 'Ferma il timer mentre si eseguono le operazioni tmrGetData.Stop() 'Legge il messaggio Dim MsgAs 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 FileAs String = Parts(1) 'La terza è la dimensione Dim SizeAs 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 (bin\Debug) 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.YesThen '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 senderAs Object , _ByVal eAs DoWorkEventArgs)Handles bgReceiveFile.DoWork 'Apre un nuovo stream in base al percorso costruito 'nella procedura precedente Dim StreamAs New IO.FileStream(FileName, IO.FileMode.Create) 'Crea un indice che indica il progresso Dim IndexAs Int64 = 0 lblStatus.Text = "In ricezione..."Do If FileReceiver.AvailableThen 'Riceve i bytes necessari Dim Bytes(4096)As Byte Dim MsgAs 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 >= FileSizeThen 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 senderAs Object , _ByVal eAs ProgressChangedEventArgs) _Handles bgReceiveFile.ProgressChanged prgProgress.Value = e.ProgressPercentageEnd 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
Client
E questo è il codice:
Imports System.Net.SocketsImports System.Text.ASCIIEncodingImports System.ComponentModelPublic 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, FileSenderAs TcpClient 'NetStream: lo stream su cui scrivere i dati di comunicazione 'NetFile: lo stream per inviare i dati da scrivere sul file Private NetStream, NetFileAs NetworkStream 'L'IP del server a cui connettersi Private IPAs 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 MsgAs String ,ByVal StreamAs NetworkStream) 'Se si può scrivere If Stream.CanWriteThen '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 StreamAs NetworkStream)As String 'Se si può leggere If Stream.CanReadThen Dim Bytes(Client.ReceiveBufferSize)As Byte Dim MsgAs 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.NormalizeElse Return NothingEnd If End Function Private Sub cmdConnect_Click(ByVal senderAs Object , _ByVal eAs 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 SEAs 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.ConnectedThen '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 senderAs Object , _ByVal eAs EventArgs)Handles cmdBrowse.ClickDim OpenAs New OpenFileDialog Open.Filter = "Tutti i file|*.*"If Open.ShowDialog = Windows.Forms.DialogResult.OKThen txtFile.Text = Open.FileNameEnd If End Sub Private Sub cmdSend_Click(ByVal senderAs Object , _ByVal eAs 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.ConnectedAndAlso NetStream.CanWriteThen 'Manda un messaggio al server, chiedendo 'conferma del trasferimento. Nel messaggio immette anche 'alcune informazioni riguardo il nome e la 'dimensione del file Dim MsgAs 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 senderAs Object , _ByVal eAs EventArgs)Handles tmrGetData.TickIf Client.ConnectedAndAlso Client.AvailableThen 'Ferma il timer mentre si eseguono le operazioni tmrGetData.Stop() 'Legge il messaggio Dim MsgAs 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.ConnectedThen '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 = TrueEnd If 'Riprende il controllo dei dati tmrGetData.Start()End If End Sub Private Sub bgSendFile_DoWork(ByVal senderAs Object , _ByVal eAs DoWorkEventArgs)Handles bgSendFile.DoWork 'Ottiene il nome del file dall'argomento passato al metodo 'RunWorkerAsync nella procedura precedente Dim FileNameAs 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 ReaderAs 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 SizeAs 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 > 4096Then For BlockAs Int64 = 0To SizeStep 4096 'Se i bytes rimanenti sono più di 4096, 'ne legge un blocco intero If Size - Block >= 4096Then 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 = TrueEnd Sub Private Sub bgSendFile_ProgressChanged(ByVal senderAs Object , _ByVal eAs ProgressChangedEventArgs) _Handles bgSendFile.ProgressChanged 'Aggiorna la progressbar prgProgress.Value = e.ProgressPercentageEnd Sub End Class
The Totem's Lair - Copyright (C) 2009
È vietata la riproduzione sia totale che parziale del sito.



