Utilizzo di un socket server asincrono

I socket server asincroni consentono di utilizzare il modello di programmazione asincrona di .NET Framework per elaborare le richieste di servizi di rete. Per la classe Socket si segue il criterio standard di denominazione asincrona .NET Framework. Il metodo sincrono Accept, ad esempio, corrisponde ai metodi asincroni BeginAccept e EndAccept.

Per il funzionamento di un socket server asincrono è necessario utilizzare un metodo per l'inizio dell'accettazione delle richieste di connessione provenienti dalla rete, un metodo di callback per la gestione delle richieste di connessione e l'inizio della ricezione dei dati dalla rete e un metodo di callback che la terminazione della ricezione dei dati. Questi metodi verranno presi in considerazione più avanti in questa sezione.

Nell'esempio riportato di seguito, per iniziare ad accettare le richieste di connessione provenienti dalla rete, si inizializza la classe Socket tramite il metodo StartListening, quindi si utilizza il metodo BeginAccept per iniziare ad accettare nuove connessioni. Al momento della ricezione sul socket di una nuova richiesta di connessione viene chiamato il metodo di callback per l'accettazione, che è responsabile dell'ottenimento dell'istanza di Socket che dovrà gestire la connessione e del passaggio di tale Socket al thread per l'elaborazione della richiesta. Il metodo di callback per l'accettazione consente di implementare il delegato AsyncCallback, di restituire un valore void e di accettare un unico parametro di tipo IAsyncResult. Nell'esempio riportato di seguito viene illustrata la shell di un metodo di callback per l'accettazione.

Sub acceptCallback(ar As IAsyncResult)
    ' Add the callback code here.
End Sub 'acceptCallback
[C#]
void acceptCallback( IAsyncResult ar) {
    // Add the callback code here.
}

Nel metodo BeginAccept vengono accettati due parametri: un delegato AsyncCallback che punta a un metodo di callback per l'accettazione e un oggetto utilizzato per passare le informazioni di stato al metodo di callback. Nell'esempio riportato di seguito la classe Socket in attesa viene passata al metodo di callback mediante il parametro state. Viene, inoltre, creato un delegato AsyncCallback e avviata l'accettazione delle connessioni di rete.

listener.BeginAccept( _
    New AsyncCallback(SocketListener.acceptCallback),_
    listener)
[C#]
listener.BeginAccept(
    new AsyncCallback(SocketListener.acceptCallback), 
    listener);

Per elaborare le connessioni in ingresso tramite i socket asincroni si utilizzano thread del pool di thread di sistema. Un thread è responsabile dell'accettazione delle connessioni, uno viene impiegato per gestire ciascuna connessione in ingresso e un altro è responsabile della ricezione di dati dalla connessione. Può anche trattarsi di un unico thread, a seconda di quale thread viene assegnato dal relativo pool. Nell'esempio che segue la classe System.Threading.ManualResetEvent consente di sospendere l'esecuzione del thread principale e di segnalare quando può essere ripresa.

Nell'esempio si illustra l'impiego di un metodo asincrono per la creazione di un socket TCP/IP asincrono sul computer locale e l'avvio dell'accettazione delle connessioni. In questo esempio si presuppone che sia presente un'istanza dell'oggetto globale ManualResetEvent denominata allDone, che il metodo sia membro di una classe denominata SocketListener e che venga definito un metodo di callback denominato acceptCallback.

Public Sub StartListening()
    Dim ipHostInfo As IPHostEntry = Dns.Resolve(Dns.GetHostName())
    Dim localEP = New IPEndPoint(ipHostInfo.AddressList(0), 11000)
    
    Console.WriteLine("Local address and port : {0}", localEP.ToString())
    
    Dim listener As New Socket(localEP.Address.AddressFamily, _
       SocketType.Stream, ProtocolType.Tcp)
    
    Try
        listener.Bind(localEP)
        s.Listen(10)
        
        While True
            allDone.Reset()
            
            Console.WriteLine("Waiting for a connection...")
            listener.BeginAccept(New _
                AsyncCallback(SocketListener.acceptCallback), _
                listener)
            
            allDone.WaitOne()
        End While
    Catch e As Exception
        Console.WriteLine(e.ToString())
    End Try
    Console.WriteLine("Closing the listener...")
End Sub 'StartListening

[C#]
public void StartListening() {
    IPHostEntry ipHostInfo = Dns.Resolve(Dns.GetHostName());
    IPEndPoint localEP = new IPEndPoint(ipHostInfo.AddressList[0],11000);

    Console.WriteLine("Local address and port : {0}",localEP.ToString());

    Socket listener = new Socket( localEP.Address.AddressFamily,
        SocketType.Stream, ProtocolType.Tcp );

    try {
        listener.Bind(localEP);
        s.Listen(10);

        while (true) {
            allDone.Reset();

            Console.WriteLine("Waiting for a connection...");
            listener.BeginAccept(
                new AsyncCallback(SocketListener.acceptCallback), 
                listener );

            allDone.WaitOne();
        }
    } catch (Exception e) {
        Console.WriteLine(e.ToString());
    }

    Console.WriteLine( "Closing the listener...");
}

Tramite il metodo di callback di accettazione (nell'esempio precedente, il metodo acceptCallback) si segnala al thread principale dell'applicazione la continuazione dell'elaborazione, lo stabilimento della connessione con il client e l'avvio della lettura asincrona di dati dal client. L'esempio riportato di seguito rappresenta la parte iniziale di un'implementazione del metodo acceptCallback. In questa parte del metodo viene segnalato al thread principale dell'applicazione di continuare l'elaborazione e viene stabilita la connessione al client e si presuppone l'impiego di un'istanza dell'oggetto globale ManualResetEvent denominata allDone.

Public Sub acceptCallback(ar As IAsyncResult)
    allDone.Set()
    
    Dim listener As Socket = CType(ar.AsyncState, Socket)
    Dim handler As Socket = listener.EndAccept(ar)

    ' Additional code to read data goes here.
End Sub 'acceptCallback
[C#]
public void acceptCallback(IAsyncResult ar) {
    allDone.Set();

    Socket listener = (Socket) ar.AsyncState;
    Socket handler = listener.EndAccept(ar);

    // Additional code to read data goes here.  
}

Per la lettura dei dati da un socket client è necessaria la presenza di un oggetto di stato che consenta di passare i valori tra le chiamate asincrone. Nell'esempio che segue viene implementato un oggetto di stato per la ricezione di una stringa dal client remoto. Esso contiene un campo per il socket client, un buffer di dati per i dati ricevuti e uno StringBuilder per creare la stringa di dati inviata dal client. Grazie all'inserimento di tali campi nell'oggetto di stato, i valori in essi specificati possono essere conservati tra una chiamata e l'altra per la lettura dei dati dal socket client.

Public Class StateObject
    Public workSocket As Socket = Nothing
    Public BufferSize As Integer = 1024
    Public buffer(BufferSize) As Byte
    Public sb As New StringBuilder()
End Class 'StateObject
[C#]
public class StateObject {
    public Socket workSocket = null;
    public const int BufferSize = 1024;
    public byte[] buffer = new byte[BufferSize];
    public StringBuilder sb = new StringBuilder();
}

Nella sezione del metodo acceptCallback, con cui viene avviata la ricezione di dati dal socket client, viene dapprima inizializzata un'istanza della classe StateObject, quindi viene chiamato il metodo BeginReceive per avviare la lettura asincrona di dati dal socket client.

Nell'esempio riportato di seguito viene illustrato il completamento del metodo acceptCallback. Nell'esempio si presuppongono la presenza di un'istanza dell'oggetto globale ManualResetEvent denominata allDone e la definizione della classe StateObject nonché, in una classe denominata SocketListener, del metodo readCallback.

Public Shared Sub acceptCallback(ar As IAsyncResult)
    ' Get the socket that handles the client request.
    Dim listener As Socket = CType(ar.AsyncState, Socket)
    Dim handler As Socket = listener.EndAccept(ar)
    
    ' Signal the main thread to continue.
    allDone.Set()
    
    ' Create the state object.
    Dim state As New StateObject()
    state.workSocket = handler
    handler.BeginReceive(state.buffer, 0, state.BufferSize, 0, _
        AddressOf AsynchronousSocketListener.readCallback, state)
End Sub 'acceptCallback

[C#]
    public static void acceptCallback(IAsyncResult ar) {
        // Get the socket that handles the client request.
        Socket listener = (Socket) ar.AsyncState;
        Socket handler = listener.EndAccept(ar);

        // Signal the main thread to continue.
        allDone.Set();

        // Create the state object.
        StateObject state = new StateObject();
        state.workSocket = handler;
        handler.BeginReceive( state.buffer, 0, StateObject.BufferSize, 0,
            new AsyncCallback(AsynchronousSocketListener.readCallback), state);
    }

Il metodo finale da implementare per il socket server asincrono è il metodo di callback per la lettura che consente di restituire i dati inviati dal client. Come il metodo di callback per l'accettazione, il metodo di callback per la lettura è un delegato AsyncCallback. Tale metodo consente di leggere uno o più byte dal socket client nel buffer di dati e di chiamare il metodo BeginReceive fino a che l'invio di dati dal client non sia stato completato. Una volta che l'intero messaggio proveniente dal client è stato letto, la relativa stringa viene visualizzata nella console e il socket server che gestisce la connessione viene chiuso.

Nell'esempio riportato di seguito viene implementato il metodo readCallback. In questo esempio si presuppone che sia stata definita la classe StateObject.

Public Shared Sub readCallback(ar As IAsyncResult)
    Dim state As StateObject = CType(ar.AsyncState, StateObject)
    Dim handler As Socket = state.workSocket
    
    ' Read data from the client socket. 
    Dim read As Integer = handler.EndReceive(ar)
    
    ' Data was read from the client socket.
    If read > 0 Then
        state.sb.Append(Encoding.ASCII.GetString(state.buffer, 0, read))
        handler.BeginReceive(state.buffer, 0, state.BufferSize, 0, _
            AddressOf readCallback, state)
    Else
        If state.sb.Length > 1 Then
            ' All the data has been read from the client;
            ' display it on the console.
            Dim content As String = state.sb.ToString()
            Console.WriteLine("Read {0} bytes from socket." + _
                ControlChars.Cr + " Data : {1}", content.Length, content)
        End If
    End If
End Sub 'readCallback

[C#]
public void readCallback(IAsyncResult ar) {
    StateObject state = (StateObject) ar.AsyncState;
    Socket handler = state.WorkSocket;

    // Read data from the client socket.
    int read = handler.EndReceive(ar);

    // Data was read from the client socket.
    if (read > 0) {
        state.sb.Append(Encoding.ASCII.GetString(state.buffer,0,read));
        handler.BeginReceive(state.buffer,0,StateObject.BufferSize, 0,
            new AsyncCallback(readCallback), state);
    } else {
        if (state.sb.Length > 1) {
            // All the data has been read from the client;
            // display it on the console.
            string content = state.sb.ToString();
            Console.WriteLine("Read {0} bytes from socket.\n Data : {1}",
               content.Length, content);
        }
        handler.Close();
    }
}

Vedere anche

Utilizzo di un socket server sincrono | Esempio di socket server asincrono | Programmazione asincrona | Threading | Attesa mediante socket