Thursday, January 5, 2012

Server Multi-client in C# .NET

Un altro progetto a scopo didattico che è sempre carino da affrontare è la comunicazione Client/Server via socket TCP.

E cosa schiarisce meglio le idee di un immagine?

Il progetto in questione si divide quindi in due parti. La prima è semplicemente un server, realizzato in c# .net, che:
-riceve in ingresso delle stringhe formattate in un certo modo,
-effettua un operazione a seconda di che stringa riceve,
-rimanda al client la risposta in formato stringa.

Questo tipo di progetto può avere numerosissimi tipi di applicazione: in teoria tutto quello che può fare un applicativo .Net è "attivabile/reperibile" via socket. Le più stupide modalità di utilizzo che mi vengono in mente ora possono essere: reperire dei dati da un database, lanciare procedure, controllare funzioni di sistema ecc... ecc... 

Vediamo un po' più nel dettaglio come funziona.
Innanzitutto come ogni server che si rispetti bisogna che siano gestiti gli stati come in immagine.
N.B. l'immagine (trovata sul web) descrive il comportamento di un singolo client rispetto ad un server, nel nostro caso avremo la possibilità di collegare più client contemporaneamente al server.


Come (quasi) sempre lavorare col framework .NET è relativamente semplice e chiaro.
Con la prima riga di codice effettuiamo il Bind dell IP, con la seconda cominciamo ad ascoltare (Listening).

TcpListener serverSocket = new TcpListener(IPAddress.Parse("192.168.1.2"), 8888);  
serverSocket.Start();  

Se nessuna SocketException verrà lanciata allora il socket è aperto.
La prossima mossa da fare è accettare (Accept) le connessioni dai Client. Per comodità e design del progetto faccio partire un thread con il metodo che si occupa di accettare le connessioni pendenti. In questo modo il server accetta in background senza essere bloccato. Se si vuole prevedere un pannello di controllo con bottoni, configurazioni ecc.. in questo modo diventa disponibile.

Questo è sostanzialmente quello che fa il thread.
while (true)  
{  
     //Rimane in attesa per una connessione Client  
     clientSocket = serverSocket.AcceptTcpClient();  
  
     Client nClient = new Client(ID); //Classe per manipolare un client  
     //Lancio il metodo startClient che gestisce il dialogo tra questo cilent e il server  
     nClient.start(clientSocket);
  
     //Loggo il tutto eventualmente  
     Logger.Log(this, "Client No:" + Convert.ToString(counter), "Accepted connection.");   
}  

Client è una classe che aiuta a gestire il dialogo (richiesta/risposta) tra client n-esimo e server.
Guardiamo al suo interno com'è fatta.

 //Variabili Globali della classe   
 TcpClient clientSocket;    
 int clNo;    
 Thread ctThread;
  
 public void start(TcpClient inClientSocket, int ID)    
 {    
  this.clientSocket = inClientSocket;    
  this.clNo = ID;    
  //Comincio ad ascoltare il dialogo    
  ctThread = new Thread(Chat);    
  ctThread.Start();    
 }
    
private void Chat()
{
    int requestCount = 0;
    string serverResponse, rCount, errLog;

    Request req = null;
    InvokedResponse returnvalue = null;
    Action a = null;

    networkStream = clientSocket.GetStream();
    streamIn = new StreamReader(networkStream);
    streamOut = new StreamWriter(networkStream);

    String bufferInput = string.Empty;
    while (isconnected)
    {
        try
        {
            serverResponse = String.Intern(string.Empty);
            errLog = String.Intern(string.Empty);
            requestCount = requestCount + 1;
            a = null;
            returnvalue = null;

            //Leggo il buffer in input
            bufferInput = streamIn.ReadLine();

            //Request
            returnvalue = DoRequest(ref req, ref bufferInput, ref name, ref a, ref errLog);
            rCount = Convert.ToString(requestCount);

            //Response
            serverResponse = DoResponse(ref req, ref errLog, ref returnvalue);
            if (!String.IsNullOrEmpty(serverResponse))
            {
                streamOut.WriteLine(serverResponse);
                streamOut.Flush();
                Logger.Log(this, name, "Sent to client:" + TruncateMsg(serverResponse));
            }
        }
        catch
        {
            stopClient();
        }
        finally
        {
            req = null;
            a = null;
            returnvalue = null;
        }
}
private void stopClient()
{
        isconnected = false;
        dCallBack(this);
        Dispose();
        Logger.Log(this, name, "Connection closed");
}

Entrano in gioco ora altre due classi: Request e Action. Request è una classe che elabora il buffer ricevuto dal client e ne estrapola il messaggio. In questo progetto è stato deciso che il messaggio di input dev'essere così formato:

"NOMEDEVICE|METODO|PAR1,PAR2,PARN$"


Il messaggio in output decidiamo che sarà così:

"NOMESERVER|FLAGERRORE|METODOINVOCATO|VALORERITORNO|MESSAGGIO$"


Ecco la classe in questione:

 class Request  
 {  
   public string Name;  
   public string RequestedMethodName;  
   public bool IsGood;  
   public object[] Parameters;
      
   public Request(String ReceivedData)  
   {  
     IsGood = true;  
     try  
     {  
       ReceivedData = ReceivedData.Substring(0, ReceivedData.IndexOf('$'));  
       
       string[] m = ReceivedData.Split('|');  
       Name = m[0];  
       RequestedMethodName = m[1];
  
       if (m.Length > 2)  //Parametri se presenti
       {  
         string[] p = m[2].Split(',');  
         Parameters = new object[p.Length];  
         for (int i = 0; i < p.Length; i++)  
         {  
           Parameters[i] = p[i];  
         }  
       }  
     }  
     catch  
     {  
       IsGood = false;  
     }  
   }        
 }  

Action è la classe che identifica l'azione, l'operazione, che il server può effettuare e che può essere richiamata da client. (per operazione sto intendendo un elaborazione generica).
Il nostro server avrà allora un set di azioni disponibili che dovranno essere "censite" e rese disponibili.
Per questo progetto (in via molto semplificata) esiste una classe contenitore che fornisce N metodi i quali hanno tutti un attributo stringa che sarà la "parola d'ordine" per far eseguire la determinata azione.
I metodi hanno tutti come valore di ritorno la classe InvokedResponse che incapsula valore di ritorno (Object), messaggio (String) e tipo (Type). Non è sempre la scelta giusta usare dei wrapper, ma in quest'ottica può andare bene.

 
static class ControlMethods  
 {  
   [InvokeCommand("INV_METODO")]  
   public static InvokedResponse Metodo(int value)  
   {  
           //... 
           return new InvokedResponse(value,message,typeof(String)); 
   }
   
   [InvokeCommand("INV_GETPROVA")]  
   public static InvokedResponse GetProva(String s)  
   {  
           return Prova(s);  
   }

   //prova non potrà essere invocato da client
   public static InvokedResponse Prova(s)  
   {  
           return new InvokedResponse("Prova"+s);  
   }  
 }  

Per esempio il mio client potrà evocare il metodo Metodo inviando un messaggio del tipo:

"DEVMCH01|INV_METODO|20$"


Non potrà mai essere invocato da un Client direttamente, il metodo Prova, poichè non ha l'attributo.

Attenzione, poichè essendo il server multithread, è possibile che due client accedano allo stesso metodo e quindi ad un eventuale stessa risorsa contemporaneamente. In questo post non si parla di accesso esclusivo ad una risorsa pertanto è eventualmente da gestire con lock, semafori, code ecc...

Lo schema sotto riassume i vari passaggi di una richiesta client, risposta server (per questo progetto).



Nel metodo doChat della classe Client (qualche snippet fa) possiamo vedere come, lato codice viene gestito il tutto. Ma come si possono censire e ottenere le azioni?
 Action a = MyServer.GetAction(req.RequestedMethodName);   
 ...  
 a.Invoke(req.Parameters);   

Vediamo come caricare e ottenere un azione. Siamo sulla classe principale del nostro server.

 public static List<Action> actions = new List<Action>();  
 
 //...
  
 private void LoadActions()  
 {  
   List<Type> parList = null; //Oggetto di appoggio  
   //Recupero tutti i metodi della classe 'ControlMethods'  
   foreach (MethodInfo M in typeof(ControlMethods).GetMethods())  
   {  
     //Pesco solo quelli che sono compatibili
     if (M.ReturnType==typeof(InvokedResponse) && (M.GetCustomAttributes(typeof(InvokeCommand), false)).Length > 0)  
     {  
       parList = null;  
       //recupero command per invocare  
       string Command = (M.GetCustomAttributes(typeof(InvokeCommand), false)[0] as InvokeCommand).Command;  
       //recupero nome del metodo  
       string Name = M.Name;
  
       //se ci sono parametri  
       if (M.GetParameters().Length > 0)  
       {  
         parList = new List<Type>();  
         foreach (ParameterInfo par in M.GetParameters())  
         {  
           //inserisco tipo parametro  
           parList.Add(par.ParameterType);  
         }  
       }  
       //aggiungo azione nell'array delle azioni  
       actions.Add(new Action(Command, Name, parList));  
     }  
   }  
 }
  
 public static Action GetAction(string CommandName)  
 {  
   foreach (Action act in actions)  
   {  
     if (act.INVOKE_COMMANDNAME.ToLower() == CommandName.ToLower().Trim())  
       return act;  
   }  
   return null;  
 }  

Un altro possibile modo di operare era avere un file o comunque una sorgente dati con specificati i luoghi (classi) e le interfacce dei metodi da richiamare. In questo modo le azioni potevano essere situate in più classi, librerie, o essere remote (WS?). Libera immaginazione al programmatore.

Ecco una variante dello schema di prima in un ottica un po' più Enterprise:


Ora bisogna solo istruire il nostro client a comunicare col server.
Ma questo lo faremo alla prossima puntata.

N.B. in questo server non si menziona o non vengono gestiti: né la grandezza del buffer in ricezione e in invio, nè le varie eccezioni lanciabili.

Se qualcuno si è posto il dubbio "ma non potevo usare un WebService così facevo prima?" ha sicuramente ragione! Effettivamente un WebService offre praticamente le stesse cose ed è anche più facile da programmare.
Ricordiamoci però che WebService vuol dire che tutto passa da http, vuol dire che ci serve un WebServer, vuol dire modeste performance e in termini di programmazione... poco divertimento :)

No comments:

Post a Comment