Lavorando con i Socket

Lavorando con i Socket

Nov 10, 2014. | Da: Kevin Guglielmetti

Concetti Generali

Un altro metodo per il trasferimento di dati attraverso una rete è utilizzare i socket: questi permettono a due applicazioni(anche di macchine separate) di scambiarsi dei dati.

Un socket rappresenta un punto di connessione per una comunicazione ed è identificato da: famiglia del protocollo, indirizzo IP e numero di porta. Una porta è un valore numerico specificato su 2 byte (da 0 a 65535) che identifica un particolare canale utilizzabile per la comunicazione.

Le porte da 0 a 1023 sono le cosiddette "well-knownports" e sono riservate per l' offerta di servizi(http, dns, ftp, ecc.).

L' indirizzo IP definisce il calcolatore su cui risiede il processo, mentre il numero di porta identifica il processo destinatario.

Per la comunicazione dovranno essere creati due socket, uno locale e uno di destinazione così strutturati:

  1. Indirizzo IP locale: Numero porta locale
  2. Indirizzo IP destinatario: Numero porta destinatario

    In c# per poter utilizzare i socket occorre aggiungere i namespace System.Net.Sockets e System.Net.

    Per creare un socket si deve specificare:

    • La famiglia di indirizzi(ES. ipV4)
    • Il tipo di socket(ad esempio se effettua il controllo di flusso)
    • Il protocollo di rete da utilizzare(TCP o UDP)

      ES: Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);


      Occorre poi definire il computer della rete con cui si vuole comunicare:
      ES: IPEndPointremoteEP = new IPEndPoint (IP address,port number);


      La comunicazione client/server può avvenire utilizzando due tipi di socket:

      • Socket sincrono, cioè il server sospende l' esecuzione in attesa di una richiesta di connessione. (Un client alla volta)
      • Socket asincrono, cioè il server non sospende l' esecuzione mentre attende richieste di connessione dai client. (Più client contemporaneamente)

        Il processo di comunicazione tramite socket, dettagliato maggiormente nella nostra implementazione C#, è riassumibile in breve da questa immagine:


        Esempio C#

        In Crypthat si è scelto di utilizzare i socket asincroni in quanto, essendo una chat, saranno presenti più client.
        Questo implica l'utilizzo dei metodi "BeginConnect", "BeginReceive", "BeginEnd" e "BeginAccept" della classe System.Net.Sockets.Socket.
        Nel progetto questi metodi sono implementati in un unica classe che viene utilizzata sia dal Server che dal Client chiamata SocketManager.


        Premessa



        Prima di iniziare occore specificare che StateObject è una classe che, come suggerito dalla documentazione ufficiale Microsoft, verrà utilizzata per memorizzare i dati in arrivo da un Socket temporaneamente.
        Questo perchè, a differenza dell'Rs232 che presenta dei buffer dati permanenti in ricezione ed invio, i Socket una volta ricevuti i dati in memoria, se non salvati in un opportuno buffer, vengono cancellati.

        Ascolto ed Accettazione delle Connessioni (Server)

        Sebbene in Crypthat la classe di gestione dei socket sia unica, alcuni metodi differiscono tra client e server.
        Secondo la logica client/server, infatti, il server deve rimanere in ascolto ad un determinato EndPoint per iniziare la comunicazione con un qualsiasi client che vi si connette.
        Il codice utilizzato dal server per mettersi in ascolto (in maniera Asincrona, quindi senza bloccare il processo principale) è il seguente:


        Il metodo Ascolta crea un EndPoint (IPAddress.Any:11000) su cui il server si mette in ascolto tramite il metodo BeginAccept.
        Il valore IPAddress.Any indica l'indirizzo IP 0.0.0.0 che permette di ascoltare su tutti gli indirizzi IP di tutte le interfacce della macchina su cui è in esecuzione il codice.
        Le istrunzioni Socket.Bind e Socket.Listen sono utilizzate per legare l'oggetto Socket ad un determinato EndPoint (nel nostro caso a localEP) e iniziare l'ascolto di connessioni in arrivo.
        All'istrunzione Listen viene inoltre specificata la grandezza massima della coda delle connessioni in ingresso in attesa di accettazione.
        Al metodo BeginAccept, che inizia l'accettazione di una connessione in coda, è passato un CallBack ovvero un metodo che verrà chiamato in caso di avvenuta accettazione ed a cui verranno passati i parametri specificati, nel nostro caso il Socket "listener" da cui proviene il collegamento.

        Il CallBack ConnessioneAccettata è un metodo chiamato all'accettazione di una connessione a cui vengono passati i parametri relativi al risultato dell'operazione tramite un Interfaccia chiamata IAsyncResult.
        Da questo parametro si può ricavare il socket di provenienza della richiesta (inviato come parametro del metodo BeginAccept). Il metodo EndAccept, che può essere chiamato soltato all'interno del CallBack, viene poi utilizzato per ricevere il Socket con cui si potrà comunicare con il client richiedente.
        Per ogni client connesso al server, infatti, vi saranno N Sockets aperti su N porte diverse, oltre a quello utilizzato dal server (0.0.0.0:11000) per l'accettazione delle connessioni.
        Come si può notare dal codice, ad ogni nuova connessione viene assegnato uno StateObject (descritto in precedenza) utilizzato per memorizzare i dati in arrivo da un determinato client.
        Tramite il metodo BeginReceive si inizia ad attendere il flusso di dati (Stream) proveniente dal client. Come per BeginAcept, BeginReceive ha un suo CallBack chiamato alla ricezione di dati.


        Nel codice è inoltre utilizzato un Semaforo (tuttoPronto) per la gestione delle accettazioni di connessioni.



        Connessione al Server (Client)


        Analogamente al Server, il client utilizza il metodo asincrono BeginConnect ed il corrispondente CallBack Connesso per l'instaurazione di una connessione con il server.


        L'unica cosa che è necessario precisare è che, una volta connesso, il client non inizia subito ad ascoltare per l'eventuale ricezione di dati.

        Per permettere la ricezione dei dati è stato creato un metodo che è in comune con quello del server chiamato RiceviMessaggio.


        Invio/Ricezione di Dati


        L'invio e la ricezione dei dati è effetuata sempre a connessione stabilita ed è similare come funzionamento alle parti descritte prima.
        I metodi BeginSend e BeginReceive si sono rivelati semplici da implementare perchè utilizzano la stessa tecnica a CallBack. Nel progetto quindi sono stati gestiti come gli eventi di Rs232.
        Durante l'invio viene apposto alla fine del messaggio un tag personalizzato <eof> che indica la fine di ogni messaggio e per proteggere questo tag di escape ogni < del messaggio originale viene convertito in </ evitando così combinazioni che permetterebbero un exploit della comunicazione.
        Analogamente in fase di ricezione si continuano a riceveri i dati fino al tag <eof>(End of File). Una volta ricevuto un messaggio intero, si risostituiscono tutti i </ con <.



        Annotazioni

        Durante lo scambio delle chiavi di crittografia, il gruppo ha scoperto un grave problema nell'Encoding delle stringe. Trasferendo un Array di bytes, infatti, effettuando una veloce conversione di questo a stringa, interviene la classe Encoding (di default ASCII) che però non utilizza 8bit alla volta ma bensì 7bit. Così facendo viene alterato il valore della chiave generando problemi.
        Analogamente Unicode ha generato problemi simili utilizzando 2Byte per carattere.
        La soluzione trovata per mantenere invariato il valore dell'array di byte è stato quello di adoperare una stringa Base64 che, evitando l'Encoding, è in grado di mantenere invariato l'array di byte.
        Base64 infatti divide l'array di partenza in gruppi da 6bit (da cui derivano i 64 possibili caratteri) che poi ricompone in gruppi da 8bit identici a quelli di partenza.
        Ed esempio un array di byte che rappresenta la parola "Test" in ASCII, risulterà "VGVzdA==" in Base64.

        Un altro problema riscontrato in fase di test è stato l'alto numero di eccezioni possibili generate dall'utilizzo di un componente come i Socket.
        Per questo per ogni metodo sono state previste situazioni di errore critici (disconnessione forzate dal server, etc.) oppure semplici avvisi (Errori nel parsing di messaggi, etc.) accerchiando tutti i metodi da try e catch.
        Questo approccio si è rivelato efficacie per la gestione degli errori di connettività ma ha complicato il debug dei livelli più alti del progetto (classi logiche).

Sottoscrizione

Sottoscriviti a questo Blog via RSS.

Categorie

Programmazione 2

Organizzazione 1

Connessioni 2

Interfaccia 1

Crittografia 3

Sito 1

Posts Recenti

Tags Popolari

Programmazione (2) Organizzazione (1) Connessioni (2) Interfaccia (1) Crittografia (3) Sito (1)

Chi Siamo

Il gruppo è stato formato per una serie di progetti scolastici di cui Crypthat è il primo. Sebbene le disuguaglianze interne cerca costantemente di rendere al meglio rispettando tempi e consegne previste.

Dove Siamo

ISII Marconi, via IV Novembre,
29122, Piacenza,
Italy.