Oppure

Loading

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.

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

Client

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.

Un semplice scambio di messaggi

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:

Socket1.jpg
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à.

A cura di: Il Totem