I socket sono uno strumento che permette di inviare e ricevere dati tra due applicazioni che corrono su macchine collegate da una rete, la quale,
nel caso più frequente, coincide con Internet. Le classi che espongono i metodi necessari sono contenute nel namespace System.Net.Sockets,
di cui la classe Socket costituisce il membro più eminente. In questo capitolo e nel successivo, tuttavia, non useremo direttamente
tale classe, poiché è poco pratica da gestire e fa massiccio uso di tecniche di programmazione un po' complesse, quali il
multithreading, che non ho ancora spiegato. Introdurrò, invece, al suo posto, due classi più semplici che fanno da wrapper
ad alcune funzioni basilari del socket: TcpListener e TcpClient.
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. I suoi membri di spicco sono:
- AcceptTcpClient() : accetta una connessione in attesa e restituisce un oggetto TcpClient collegato al client che ha inviato la richiesta. Usando tale oggetto sarà possibile inviare o ricevere dati, poiché il Listener, di per sé, non fa altro che attendere e accettare connessioni, ma l'azione vera viene intrapresa da oggetti TcpClient;
- Pending() : restituisce True se si cono connessioni in attesa;
- Server : restituisce l'oggetto socket che TcpListener sfrutta. Come avevo detto nel paragrafo introduttivo, queste due classi fanno uso internamente di Socket, ma espongono metodi di più semplice gestione;
- Start() : inizia l'operazione di listening su una porta data;
- Stop() : interrompe il listening;
Il costruttore di TcpListener che useremo richiede come unico parametro la porta su cui mettersi in ascolto.
La classe fondamentalmente usata per un client è TcpClient. I suoi membri più significativi sono:
- Available: 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
Il client tenta la connessione al server e, se accettato, può dialogare con esso scambiando messaggi. I dati vengono inviati e
ricevuti attraverso uno stream di rete bidirezionale, che è possibile ottenere richiamando GetStream(). Quando il client scrive su
questo stream, il server riceve i dati e li può leggere, e viceversa.
Per iniziare scriveremo un semplice programma per scambiare messaggi (chiamarlo "chat" sarebbe a dir poco inopportuno). Per semplicità
d'uso, la stessa applicazione potrà fare sia da server che da client, così un utente potrà sia collegarsi ad un
altro che attendere connessioni (ma non fare le due cose contemporaneamente). L'interfaccia che ho preparato è questa:
Ci sono anche due timer, tmrConnections e tmrData. Ecco il codice:
Imports System.Net.Sockets Imports System.Text.UTF8Encoding Public Class Form1 Private Listener As TcpListener Private Client As TcpClient Private NetStream As NetworkStream 'Questa procedura serve per attivare o disattivare i 'controlli a seconda che si sia connessi oppure no. Serve 'per impedire che si tenti di inviare un messaggio quando 'non si è connessi, ad esempio Private Sub EnableControls(ByVal Connected As Boolean) btnConnect.Enabled = Not Connected btnListen.Enabled = Not Connected txtIP.Enabled = Not Connected btnSend.Enabled = Connected txtMessage.Enabled = Connected btnDisconnect.Enabled = Connected If Connected Then tmrData.Start() Else tmrData.Stop() End If End Sub Private Sub btnListen_Click(ByVal sender As Object, ByVal e As EventArgs) Handles btnListen.Click 'Inizializza il listener e inizia l'ascolto sulla porta '5000. Inoltre, attiva il timer per controllare se ci 'sono connessioni in arrivo. Il timer scatta ogni 100ms Listener = New TcpListener(5000) Listener.Start() tmrConnections.Start() btnListen.Enabled = False btnConnect.Enabled = False txtLog.AppendText("Server - in ascolto..." & Environment.NewLine) End Sub Private Sub tmrConnections_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles tmrConnections.Tick 'Se ci sono connessioni... If Listener.Pending() Then 'Ferma un attimo il timer tmrConnections.Stop() 'Chiede all'utente se confermare la connessione If MessageBox.Show("Rilevato un tentativo di connessione. Accettare?", Me.Text, MessageBoxButtons.YesNo, MessageBoxIcon.Question) = Windows.Forms.DialogResult.Yes Then 'Ottiene l'oggetto TcpClient collegato al client Client = Listener.AcceptTcpClient() 'Ferma il listener Listener.Stop() 'Ottiene il network stream NetStream = Client.GetStream() 'E attiva/disattiva i controlli per quando si è connessi EnableControls(True) Else Listener.Stop() Listener.Start() tmrConnections.Start() End If End If End Sub Private Sub btnConnect_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnConnect.Click Dim IP As Net.IPAddress 'Prima esegue un controllo sull'indirizzo IP per 'controllare che sia valido If Not Net.IPAddress.TryParse(txtIP.Text, IP) Then MessageBox.Show("IP non valido!", Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Exclamation) Exit Sub End If 'Quindi inizializza un client e tenta la connessione 'al dato IP sulla porta 5000 Client = New TcpClient() txtLog.AppendText("Client - tentativo di connessione..." & vbCrLf) Try Application.DoEvents() Client.Connect(IP, 5000) Catch Ex As Exception End Try 'Se la connessione ha avuto successo, ottiene il network 'stream e agisce sui controlli come nel codice precedente If Client.Connected Then txtLog.AppendText("Tentativo di connessione riuscito!" & vbCrLf) NetStream = Client.GetStream() EnableControls(True) Else txtLog.AppendText("Tentativo di connessione fallito..." & vbCrLf) End If End Sub Private Sub tmrData_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles tmrData.Tick 'Se ci sono dati disponibili If Client.Available > 0 Then 'Li legge dallo stream Dim Buffer(Client.Available - 1) As Byte NetStream.Read(Buffer, 0, Buffer.Length) 'Li trasforma in una stringa Dim Msg As String = UTF8.GetString(Buffer) 'Se il messaggio inizia con questa stringa 'particolare signifia che l'altro utente ha chiuso 'la connessione, quindi disconnette anche questo If Msg.StartsWith("\close\") Then btnDisconnect_Click(Me, EventArgs.Empty) Exit Sub End If 'Altrimenti lo accoda alla textbox grande txtLog.AppendText("Ricevuto: ") txtLog.AppendText(Msg) txtLog.AppendText(Environment.NewLine) End If End Sub Private Sub btnSend_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnSend.Click If Not String.IsNullOrEmpty(txtMessage.Text) Then Dim Buffer() As Byte = UTF8.GetBytes(txtMessage.Text) txtLog.AppendText("Inviato: " & txtMessage.Text & Environment.NewLine) NetStream.Write(Buffer, 0, Buffer.Length) txtMessage.Text = "" End If End Sub Private Sub btnDisconnect_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnDisconnect.Click tmrData.Stop() Dim Buffer() As Byte = UTF8.GetBytes("\close\") NetStream.Write(Buffer, 0, Buffer.Length) Client.Client.Close() Client.Close() Client = Nothing If Listener IsNot Nothing Then Listener.Server.Close() Listener = Nothing End If EnableControls(False) txtLog.AppendText("Disconnesso" & Environment.NewLine) End Sub End Class
Come avete visto dal codice non c'è nulla di particolarmente complicato da capire. Tuttavia, questo programma è molto semplice e permette di gestire solo una connessione (in arrivo o in uscita). Il Listener, anche se riavviabile, continuerà a dare Pending = True almeno fino a che tutti i client relativi alla connessione siano stati correttamente chiusi, e questo non si verifica se non alla fine del programma, ossia quando uno dei due si disconnette. Per farla breve, è impossibile creare un'applicazione di scambio messaggi multiutente con questi oggetti e queste modalità.