Un Server TCP con .NET Gadgeteer (Italiano)

.NET Gadgeteer offre un metodo molto semplice per creare un web server in grado di rispondere alle richieste GET, attraverso la funzione SetupWebEvent fornita dall’API del modulo Ethernet di GHI Electronics. Esempi di tale sistema sono disponibili su questo stesso blog, ad esempio nel post .NET Gadgeteer Web Services; Picture, Audio, Application.

Se, invece, lavoriamo con scenari più complessi, oppure abbiamo bisogno di maggiore versatilità, possiamo creare in maniera esplicita un server TCP multithread, in grado di accettare richieste, elaborarle e restituire risposte al client. Il suo funzionamento è molto simile a quello di un classico server TCP realizzato con la versione full del Framework .NET.

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using Microsoft.SPOT;
using Socket = System.Net.Sockets.Socket;

namespace ServerExample
{
    public delegate void DataReceivedEventHandler(object sender, DataReceivedEventArgs e);

    public class SocketServer
    {
        public const int DEFAULT_SERVER_PORT = 8080;
        private Socket socket;
        private int port;

        public event DataReceivedEventHandler DataReceived;

        public SocketServer()
            : this(DEFAULT_SERVER_PORT)
        { }

        public SocketServer(int port)
        {
            this.port = port;
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        }

        public void Start()
        {
            IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, port);
            socket.Bind(localEndPoint);
            socket.Listen(Int32.MaxValue);

            new Thread(StartServerInternal).Start();
        }

        private void StartServerInternal()
        {
            while (true)
            {
                // Wait for a request from a client.
                Socket clientSocket = socket.Accept();

                // Process the client request.
                var request = new ProcessClientRequest(this, clientSocket);
                request.Process();
            }
        }

        private void OnDataReceived(DataReceivedEventArgs e)
        {
            if (DataReceived != null)
                DataReceived(this, e);
        }

        private class ProcessClientRequest
        {
            private Socket clientSocket;
            private SocketServer socket;

            public ProcessClientRequest(SocketServer socket, Socket clientSocket)
            {
                this.socket = socket;
                this.clientSocket = clientSocket;
            }

            public void Process()
            {
                // Handle the request in a new thread.
                new Thread(ProcessRequest).Start();
            }

            private void ProcessRequest()
            {
                const int c_microsecondsPerSecond = 1000000;

                using (clientSocket)
                {
                    while (true)
                    {
                        try
                        {
                            if (clientSocket.Poll(5 * c_microsecondsPerSecond, SelectMode.SelectRead))
                            {
                                // If the buffer is zero-length, the connection has been closed
                                // or terminated.
                                if (clientSocket.Available == 0)
                                    break;

                                byte[] buffer = new byte[clientSocket.Available];
                                int bytesRead = clientSocket.Receive(buffer, clientSocket.Available,
                                                                             SocketFlags.None);

                                byte[] data = new byte[bytesRead];
                                buffer.CopyTo(data, 0);

                                DataReceivedEventArgs args = new DataReceivedEventArgs(
                                                                  clientSocket.LocalEndPoint,
                                                                  clientSocket.RemoteEndPoint, data);
                                socket.OnDataReceived(args);

                                if (args.ResponseData != null)
                                    clientSocket.Send(args.ResponseData);

                                if (args.Close)
                                    break;
                            }
                        }
                        catch (Exception)
                        {
                            break;
                        }
                    }
                }
            }
        }
    }
}

La classe ServerSocket permette di specificare su quale porta mettersi in ascolto. Se non specificata, viene utilizzata quella di default, ovvero 8080. Il metodo ServerSocket.Start crea un nuovo thread che si mette in attesa di connessione da parte di client (il thread separato è necessario per non bloccare l’applicazione). Quando un client si connette al server (ovvero il metodo socket.Accept ritorna un valore), creiamo un oggetto che si occupa di gestire la richiesta, anch’esso in un thread a parte, dopodiché ci rimettiamo in attesa di nuove connessioni.

La parte più importante del lavoro è svolta dal metodo ProcessRequest della classe ProcessClientRequest. Al suo interno, si verifica la disponibilità di dati e, in caso positivo, si effettua una receive sul socket, la quale restituisce l’array di byte che è stato trasmesso dal client. Dopodiché, creiamo un oggetto di tipo DataReceivedEventArgs (che vedremo tra poco) contenente i dati ricevuti e gli indirizzi IP di mittente e destinatario). Richiamiamo poi il metodo ServerSocket.OnDataReceived, con cui chi utilizza il ServerSocket può essere notificato dell’arrivo di nuovi dati.

Se la classe che si è registrata sull’evento DataReceived (che illustreremo nel seguito), assegna un valore alla proprietà ResponseData dell’oggetto DataReceivedEventArgs, tale array di byte sarà spedito in risposta al client. Analogamente, se la proprietà Close viene impostata su true, si causerà la chiusura della connessione.

Ecco dunque la classe DataReceivedEventArgs:

public class DataReceivedEventArgs : EventArgs
{
    public EndPoint LocalEndPoint { get; private set; }
    public EndPoint RemoteEndPoint { get; private set; }
    public byte[] Data { get; private set; }
    public bool Close { get; set; }
    public byte[] ResponseData { get; set; }

    public DataReceivedEventArgs(EndPoint localEndPoint, EndPoint remoteEndPoint, byte[] data)
    {
        LocalEndPoint = localEndPoint;
        RemoteEndPoint = remoteEndPoint;
        if (data != null)
        {
            Data = new byte[data.Length];
            data.CopyTo(Data, 0);
        }
    }
}

Utilizziamo la classe ServerSocket

Passiamo ora ad un semplice esempio di utilizzo della classe ServerSocket. Colleghiamo alla scheda FEZ Spider i moduli Display T35 e Ethernet J11D. Come prima cosa, inseriamo il codice per inizializzare l’interfaccia di rete:

void ProgramStarted()
{
    ethernet.NetworkUp += new GTM.Module.NetworkModule.NetworkEventHandler(ethernet_NetworkUp);
    ethernet.NetworkDown += new GTM.Module.NetworkModule.NetworkEventHandler(ethernet_NetworkDown);
    ethernet.UseDHCP();

    Debug.Print("Program Started");
}

private void ethernet_NetworkDown(GTM.Module.NetworkModule sender,
                                             GTM.Module.NetworkModule.NetworkState state)
{
    Debug.Print("Network Down!");
}

private void ethernet_NetworkUp(GTM.Module.NetworkModule sender,
                                           GTM.Module.NetworkModule.NetworkState state)
{
    Debug.Print("Network Up!");
}

Vogliamo anche configurare il display  inserendovi due caselle di testo: una per mostrare l’indirizzo IP assegnato dal DHCP e l’altra riservata ai messaggi che sono ricevuti dal server TCP. Definiamo quindi una routine SetupWindow, che richiameremo all’interno di ProgramStarted:

private Text txtAddress;
private Text txtReceivedMessage;

private void SetupWindow()
{
    var window = display.WPFWindow;
    var baseFont = Resources.GetFont(Resources.FontResources.NinaB);

    Canvas canvas = new Canvas();
    window.Child = canvas;

    txtAddress = new Text(baseFont, "Loading, please wait...");
    canvas.Children.Add(txtAddress);
    Canvas.SetTop(txtAddress, 50);
    Canvas.SetLeft(txtAddress, 30);

    txtReceivedMessage = new Text(baseFont, string.Empty);
    txtReceivedMessage.Width = 300;
    txtReceivedMessage.TextWrap = true;
    canvas.Children.Add(txtReceivedMessage);
    Canvas.SetTop(txtReceivedMessage, 100);
    Canvas.SetLeft(txtReceivedMessage, 10);
}

In fase di caricamento dell’applicazione, mostriamo un messaggio di attesa, che sarà sostituito non appena l’interfaccia di rete è pronta:

private void ethernet_NetworkUp(GTM.Module.NetworkModule sender,
                                           GTM.Module.NetworkModule.NetworkState state)
{
    Debug.Print("Network Up!");

    txtAddress.TextContent = "IP Address: " + ethernet.NetworkSettings.IPAddress + ", port 8080";

    SocketServer server = new SocketServer(8080);
    server.DataReceived += new DataReceivedEventHandler(server_DataReceived);
    server.Start();
}

private void server_DataReceived(object sender, DataReceivedEventArgs e)
{
    string receivedMessage = BytesToString(e.Data);
    txtReceivedMessage.Dispatcher.BeginInvoke(
        delegate(object arg) {
            txtReceivedMessage.TextContent = "Received message: " + arg.ToString();
            return null;
        },
        receivedMessage);

    string response = "Response from server for the request '" + receivedMessage + "'";
    e.ResponseData = System.Text.Encoding.UTF8.GetBytes(response);

    if (receivedMessage == "close")
        e.Close = true;
}

private string BytesToString(byte[] bytes)
{
    string str = string.Empty;
    for (int i = 0; i < bytes.Length; ++i)
        str += (char)bytes[i];

    return str;
}

In questo esempio, creiamo il server sulla porta 8080, ci registriamo sull’evento generato quando viene ricevuto un messaggio e, infine, avviamo il server. Quando arrivano dati, viene invocato il metodo server_DataReceived. Utilizziamo il metodo di utilità BytesToString, riportato subito dopo, per recuperare la stringa corrispondente ai byte ricevuti, quindi stampiamo il messaggio a video. Notiamo l’utilizzo del metodo Dispatcher.BeginInvoke sulla casella di testo: questo accorgimento è necessario perché l’evento è lanciato da un thread diverso da quello dell’interfaccia, quindi dobbiamo ricorrere ad esso per eseguire l’aggiornamento sul thread che possiede la UI, altrimenti otterremo una eccezione.

Successivamente, creiamo un messaggio e lo assegniamo alla proprietà ResponseData di DataReceivedEventArgs: ricordando il codice del server visto in precedenza, questo è il modo per specificare una risposta da inviare al client. In questo supponiamo, supponiamo anche di chiudere la connessione nel caso in cui il client invii il messaggio “close”.

Un client per il nostro server

Per testare il nostro server, abbiamo bisogno di un’applicazione client con cui inviare dati ad esso. Creiamo dunque una Console Application:

class Program
{
    const string SERVER_IP = "192.168.1.102";
    const int SERVER_PORT = 8080;

    static void Main(string[] args)
    {
        TcpClient client = new TcpClient();
        Console.Write("Connecting... ");

        client.Connect(SERVER_IP, SERVER_PORT);
        Console.WriteLine("Connected\n");

        using (Stream stream = client.GetStream())
        {
            while (true)
            {
                Console.Write("Enter a string and press ENTER (empty string to exit): ");

                string message = Console.ReadLine();
                if (string.IsNullOrEmpty(message))
                    break;

                byte[] data = Encoding.Default.GetBytes(message);
                Console.WriteLine("Sending... ");

                stream.Write(data, 0, data.Length);

                byte[] response = new byte[4096];
                int bytesRead = stream.Read(response, 0, response.Length);
                Console.WriteLine("Response: " + Encoding.Default.GetString(response, 0, bytesRead));

                Console.WriteLine();
            }
        }

        client.Close();
    }
}

L’unica modifica necessaria per utilizzare questa applicazione è cambiare la costante SERVER_IP in base all’indirizzo IP che viene assegnato all’applicazione .NET Gadgeteer. Un volta avviato, possiamo provare ad inviare tutte le stringhe che vogliamo verso il server: quest’ultimo stamperà a video il messaggio ricevuto e restituirà una risposta al client.

L’applicazione di esempio è disponibile per il download.

ServerExample.zip

Advertisements

,

  1. Leave a comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: