- Come creare un semplice server TCP senza utilizzare i socket -
 
COSA SERVE PER QUESTO TUTORIAL
Download | Chiedi sul FORUM | Glossario cognizioni basiche di un qualsiasi linguaggio .Net
Le classi per semplificare i socket: TcpClient, TcpListener e NetworkStream

UN SERVER CON THREAD O CON I/O ASINCRONO
Differenze tra l'uso di thread e chiamate asincrone.

In questo tutorial ci occuperemo di come realizzare un semplice server echo, ovvero che reinvia al client ogni riga ricevuta dallo stesso. Il server dovrà essere in grado di gestire più client in parallelo e per fare ciò dimostreremo due diverse tecniche supportate dal framework .Net, la prima semplicemente consiste nel creare un thread per ogni richiesta, mentre la seconda si serve delle chiamate asincrone e quindi del pool di thread di sistema.

Per prima cosa dovremo metterci in ascolto su una porta TCP, ci serviremo dunque della classe TcpListener (in System.Net.Sockets), che offre alcune semplificazioni rispetto all'uso della classe Socket grezza.


 'Crea il socket per il server
Dim listener As New TcpListener(IPAddress.Loopback, echoPort)
'Inizia ad ascoltare sul socket creato
listener.Start()

I parametri del costruttore di TcpListener indicano l'indirizzo IP su cui porsi in ascolto (in questo caso è stato scelto quello di loopback, ovvero 127.0.0.1) e la porta, che può essere scelta a piacere tra quelle non già occupate. Per mettersi in ascolto non si deve quindi far altro che invocare il metodo Start.
Vediamo prima la versione che fa uso esplicito dei thread:


Do
     'Si blocca in attesa di un client
    Dim clientSocket As TcpClient = listener.AcceptTcpClient()

    'Avvia un nuovo thread per rispondere al client
    Dim clientManager As New Thread(AddressOf EchoServer)
    clientManager.Start(clientSocket)
Loop

Si tratta sostanzialmente di chiamare sul TcpListener il metodo AcceptTcpClient, che, essendo bloccante, interromperà il flusso del programma fino alla ricezione di una connessione. Quando la connessione è stabilita il flusso riprende creando un nuovo thread a cui viene passato un riferimento al socket (oggetto TcpClient, altra astrazione di un Socket) tramite cui comunicare con il client. In questo modo il ciclo può ricominciare bloccandosi nuovamente su AcceptTcpClient.
La versione che utilizza i metodi asincroni è invece leggermente differente:


Do
     'Effettua una chiamata asincrona per accettare un client:
    'all'arrivo di un client la funzione SocketAccepted verrà
    'chiamata
    listener.BeginAcceptTcpClient(AddressOf SocketAccepted, listener)

    'Si pone in attesa su waitObject, che verrà sbloccato all'inizio
    'di SocketAccepted
    SyncLock waitObject
        Monitor.Wait(waitObject)
    End SyncLock
Loop

In questo caso utilizziamo il metodo BeginAcceptClient che non è bloccante, ma quando una connessione in ingresso è pronta invoca il metodo passato come primo parametro, in questo caso SocketAccepted (che avrà accesso al secondo parametro, listener). Non essendo bloccante l'esecuzione continuerà ma si fermerà su Monitor.Wait su un oggetto globale, che verrà sbloccato all'interno di SocketAccepted facendo ricominciare il ciclo:


Public Sub SocketAccepted(ByVal result As IAsyncResult)
    'Sblocca la funzione in attesa così che possa mettersi in ascolto nuovamente
    'per gestire un nuovo client
    SyncLock waitObject
        Monitor.Pulse(waitObject)
    End SyncLock

    'Accetta la connessione in ingressa a la gestisce
    Dim listener As TcpListener = DirectCast(result.AsyncState, TcpListener)
    EchoServer(listener.EndAcceptTcpClient(result))
End Sub

Come detto il metodo SocketAcceped sblocca l'oggetto waitObject e quindi il ciclo, quindi accetta la connessione in ingresso tramite EndAcceptTcpClient.
È importante comprendere che sono stati usati i metodi di sincronizzazione tipici di quando si ha che fare con più thread anche se in questo caso non vi è riferimento esplicito a nuovi thread: questo perché in realtà i metodi asincroni (Begin...) si servono del thread pool di sistema, ovvero un bacino di thread che vengono creati e distrutti a seconda delle necessità, ma che sono pur sempre thread e richiedono perciò tutte le accortezze del caso.
EchoServer è una funzione molto semplice:


Sub EchoServer(ByVal connection As TcpClient)
    'Creiamo stream testuali per comunicare con il client
    Dim reader As New StreamReader(connection.GetStream(), noBomUTF8)
    Dim writer As New StreamWriter(connection.GetStream(), noBomUTF8)

    'Ad ogni chiamata a WriteLine il messaggio viene inviato al client
    writer.AutoFlush = True

    'Saluto iniziale con l'indirizzo IP del client
    writer.WriteLine("Welcome {0}", connection.Client.RemoteEndPoint.ToString)

    Dim inputLine As String = reader.ReadLine()

    'Gestiamo l'input dell'utente e rispondiamo
    Do While inputLine <> "quit"
        writer.WriteLine(inputLine)
        inputLine = reader.ReadLine()
    Loop
    writer.WriteLine("Bye bye!")

    connection.Close()
End Sub

Non fa altro che creare due oggetti, uno per leggere e uno per scrivere su uno stream trattandoli come testuali. Per avere lo stream di comunicazione con il client basta invocare il metodo TcpClient.GetStream(), dopodiché si può agire come su un qualsiasi StreamReader/StreamWriter. Infine chiude la connessione.

UN SEMPLICE CLIENT
Come connettersi al server echo.

Vediamo ora come si può creare un piccolo client per il server echo che abbiamo creato: 


Sub Test(ByVal output As Stream)
    Dim client As New TcpClient()

    client.Connect("localhost", echoPort)
    Dim writer As New StreamWriter(client.GetStream(), noBomUTF8)
    writer.WriteLine("time")
    writer.WriteLine("hello")
    writer.WriteLine("don't repeat!")
    writer.WriteLine("quit")
    writer.Flush()

    client.GetStream.CopyTo(output)

    client.Close()
End Sub

Non dobbiamo far altro che utilizzare nuovamente la classe TcpClient, questa volta però istanziandola direttamente e comunicando al metodo Connect indirizzo e porta del server. Quindi otteniamo lo stream per comunicare come visto poco fa e inviamo alcuni messaggi. Infine copiamo l'output su un altro stream (ad esempio potrebbe essere Console.Out) per visualizzare la risposta.

 

<< INDIETRO by VeNoM00