2009-07-22 17 views
29

Ho visto molte risorse qui su SO su Sockets. Credo che nessuno di loro riguardasse i dettagli che volevo sapere. Nella mia applicazione, il server esegue tutta l'elaborazione e invia aggiornamenti periodici ai client.Introduzione alla programmazione dei socket in C# - Best practice

Intenzione di questo post è di coprire tutte le idee di base richieste durante lo sviluppo di un'applicazione socket e discutere le migliori pratiche. Ecco le cose di base che vedrai con quasi tutte le applicazioni basate su socket.

1 - rilegatura e l'ascolto su un socket

Sto usando il seguente codice. Funziona bene sulla mia macchina. Devo occuparmi di qualcos'altro quando lo distribuisco su un server reale?

IPHostEntry localHost = Dns.GetHostEntry(Dns.GetHostName()); 
IPEndPoint endPoint = new IPEndPoint(localHost.AddressList[0], 4444); 

serverSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, 
        ProtocolType.Tcp); 
serverSocket.Bind(endPoint); 
serverSocket.Listen(10); 

2 - Ricezione dati

ho utilizzato un array di dimensioni 255 byte. Quindi, quando sto ricevendo dati che sono più di 255 byte, ho bisogno di chiamare il metodo di ricezione fino a quando non ottengo i dati completi, giusto? Una volta ottenuti i dati completi, devo aggiungere tutti i byte ricevuti finora per ottenere il messaggio completo. È corretto? O c'è un approccio migliore?

3 - invio di dati e specificando la lunghezza dei dati

Poiché non v'è alcun modo in TCP per trovare la lunghezza del messaggio per ricevere, sto progettando di aggiungere la lunghezza al messaggio. Questo sarà il primo byte del pacchetto. Quindi i sistemi client sanno quanti dati sono disponibili per la lettura.

Qualsiasi altro approccio migliore?

4 - Chiusura del cliente

Quando cliente è chiuso, invierà un messaggio al server che indica la chiusura. Il server rimuoverà i dettagli del client dal suo elenco di client. Di seguito è riportato il codice utilizzato sul lato client per disconnettere il socket (parte di messaggistica non mostrata).

client.Shutdown(SocketShutdown.Both); 
client.Close(); 

Eventuali suggerimenti o problemi?

5 - Chiusura server

Server invia un messaggio a tutti i clienti che indicano l'arresto. Ogni client disconnetterà il socket quando riceve questo messaggio. I client invieranno il messaggio di chiusura al server e chiuderanno. Una volta che il server riceve il messaggio di chiusura da tutti i client, disconnette il socket e interrompe l'ascolto. Chiama Disponi su ogni socket client per rilasciare le risorse. È l'approccio corretto?

6 - sconosciuto disconnessioni client

A volte, un client può staccare senza informare il server. Il mio piano per gestire questo è: quando il server invia messaggi a tutti i client, controlla lo stato del socket. Se non è connesso, rimuovi il client dall'elenco dei client e chiudi il socket per quel client.

Qualsiasi aiuto sarebbe fantastico!

+0

Ho avuto problemi con IPAddress ascolto su qualche versione precedente di Windows. Uso di IPAddress ipAddress = new IPAddress (new byte [] {0, 0, 0, 0}); sembra essere il migliore per me! – clamchoda

risposta

24

Poiché si tratta di "iniziare", la mia risposta si baserà su una semplice implementazione piuttosto che su una molto scalabile. È meglio prima sentirsi a proprio agio con l'approccio semplice prima di rendere le cose più complicate.

1 - rilegatura e l'ascolto
Il tuo codice sembra che vada bene a me, personalmente utilizzo:

serverSocket.Bind(new IPEndPoint(IPAddress.Any, 4444)); 

Invece di andare via DNS, ma non credo ci sia un problema reale sia modo.

1,5 - Accettare le connessioni client
Basta ricordare questo per completezza ... Io parto dal presupposto che si sta facendo questo altrimenti non si otterrebbe al punto 2.

2 - Ricezione dati
Vorrei rendere il buffer un po 'più lungo di 255 byte, a meno che non ci si possa aspettare che tutti i messaggi del server siano al massimo 255 byte. Penso che vorreste un buffer che sia probabilmente più grande delle dimensioni del pacchetto TCP, così potrete evitare di fare più letture per ricevere un singolo blocco di dati.

Direi che selezionare 1500 byte dovrebbe andare bene, o forse anche 2048 per un bel numero di round.

In alternativa, forse si può evitare di utilizzare un byte[] per memorizzare i frammenti di dati, e invece avvolgere il vostro socket client server-side in un NetworkStream, avvolto in un BinaryReader, in modo da poter leggere i componenti del vostro messaggio direttamene dalla presa senza preoccuparsi delle dimensioni del buffer.

3 - Invio di dati e specificando la lunghezza dei dati
Il tuo approccio funziona bene, ma non, ovviamente, richiede che è facile calcolare la lunghezza del pacchetto, prima di avviare l'invio di esso.

In alternativa, se il formato del messaggio (l'ordine dei suoi componenti) è progettato in modo tale che in qualsiasi momento il client sarà in grado di determinare se ci dovrebbero essere più dati successivi (ad esempio, il codice 0x01 significa prossimo sarà un int e una stringa, il codice 0x02 significa che il prossimo sarà 16 byte, ecc. ecc.). Combinato con l'approccio NetworkStream sul lato client, questo può essere un approccio molto efficace.

Per motivi di sicurezza, è possibile aggiungere la convalida dei componenti ricevuti per assicurarsi di elaborare solo i valori sensati. Ad esempio, se si riceve un'indicazione per una stringa di lunghezza 1 TB, è possibile che si sia verificata una corruzione del pacchetto da qualche parte e potrebbe essere più sicuro chiudere la connessione e forzare la riconnessione del client e il "riavvio". Questo approccio offre un ottimo comportamento generale in caso di guasti imprevisti.

4/5 - Chiusura del client e il server
Personalmente opterei per appena Close senza ulteriori messaggi; quando una connessione viene chiusa si otterrà un'eccezione su qualsiasi blocco di lettura/scrittura all'altra estremità della connessione che si dovrà provvedere.

Dal momento che è necessario gestire "disconnessioni sconosciute" comunque per ottenere una soluzione robusta, la disconnessione più complicata è generalmente inutile.

6 - Sconosciuto disconnessioni
io non mi fiderei nemmeno lo stato presa ... è possibile che una connessione a morire da qualche parte lungo il percorso tra client/server senza client o se ne accorga server.

L'unico modo garantito per segnalare una connessione che è morto in modo imprevisto è quando si tenta di inviare qualcosa lungo la connessione. A quel punto riceverai sempre un'eccezione che indica un errore se qualcosa è andato storto con la connessione.

Come risultato, l'unico modo per rilevare tutte le connessioni impreviste è implementare un meccanismo di "ping", dove idealmente il client e il server invieranno periodicamente un messaggio all'altro capo che produce solo una risposta messaggio che indica che il "ping" è stato ricevuto.

Per ottimizzare i ping inutili, è consigliabile disporre di un meccanismo di "time-out" che invia un ping solo quando nessun altro traffico è stato ricevuto dall'altra estremità per un determinato periodo di tempo (ad esempio, se il l'ultimo messaggio dal server è più vecchio di x secondi, il client invia un ping per assicurarsi che la connessione non sia morta senza notifica).

più avanzata
Se volete elevata scalabilità si dovrà esaminare metodi asincroni per tutte le operazioni di socket (Accetta/Invia/Ricevi). Queste sono le varianti 'Begin/End', ma sono molto più complicate da usare.

Mi raccomando di provare questo fino ad avere la versione semplice attiva e funzionante.

Si noti inoltre che se non si prevede di scalare più di qualche dozzina di client questo non sarà in realtà un problema a prescindere. Le tecniche asincrone sono davvero necessarie solo se intendi scalare le migliaia o centinaia di migliaia di client connessi senza che il tuo server muoia completamente.

probabilmente ho dimenticato un sacco di altri suggerimenti importanti, ma questo dovrebbe essere sufficiente per ottenere un'implementazione abbastanza robusta e affidabile per iniziare con

+1

Questa è stata una grande spiegazione. Grazie –

+0

Tieni presente che una volta che hai fatto tutto questo ci sono probabilmente più cose da considerare che ho dimenticato di menzionare ... – jerryjvl

13

1 - rilegatura e l'ascolto su un socket

Mi sembra soddisfacente. Il tuo codice legherà il socket solo a un indirizzo IP. Se si vuole semplicemente ascolto su qualsiasi interfaccia IP/di rete, utilizzare IPAddress.Any:

serverSocket.Bind(new IPEndPoint(IPAddress.Any, 4444)); 

Per essere a prova di futuro, si consiglia di supportare IPv6. Per ascoltare su qualsiasi indirizzo IPv6, utilizzare IPAddress.IPv6Any al posto di IPAddress.Any.

Nota che non è possibile ascoltare contemporaneamente alcun indirizzo IPv4 e IPv6, tranne se si utilizza un Dual-Stack Socket.Questo richiederà di disinserire l'opzione IPV6_V6ONLY presa:

serverSocket.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName)27, 0); 

Per abilitare Teredo con il vostro zoccolo, è necessario impostare l'opzione PROTECTION_LEVEL_UNRESTRICTED presa:

serverSocket.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName)23, 10); 

2 - dati Ricezione

Si consiglia di utilizzare uno NetworkStream che avvolge il socket in un Stream invece di leggere manualmente i blocchi.

La lettura di un numero fisso di byte è un po 'scomodo però:

using (var stream = new NetworkStream(serverSocket)) { 
    var buffer = new byte[MaxMessageLength]; 
    while (true) { 
     int type = stream.ReadByte(); 
     if (type == BYE) break; 
     int length = stream.ReadByte(); 
     int offset = 0; 
     do 
     offset += stream.Read(buffer, offset, length - offset); 
     while (offset < length); 
     ProcessMessage(type, buffer, 0, length); 
    } 
} 

Dove NetworkStream brilla davvero è che si può utilizzare come qualsiasi altro Stream. Se la sicurezza è importante, è sufficiente racchiudere lo NetworkStream in un SslStream per autenticare il server e (facoltativamente) i client con certificati X.509. La compressione funziona allo stesso modo.

var sslStream = new SslStream(stream, false); 
sslStream.AuthenticateAsServer(serverCertificate, false, SslProtocols.Tls, true); 
// receive/send data SSL secured 

3 - invio di dati e specificando la lunghezza dei dati

Il tuo approccio dovrebbe funzionare, anche se probabilmente non può decidere di andare in fondo alla strada per reinventare la ruota e la progettazione di un nuovo protocollo per questo. Dai un'occhiata a BEEP o forse anche qualcosa di semplice come protobuf.

A seconda dei tuoi obiettivi, potrebbe valere la pena di pensare a scegliere un'astrazione sopra socket come WCF o qualche altro meccanismo RPC.

4/5/6 - chiusura & disconnessioni sconosciuta

Cosa jerryjvl dette :-) L'unico meccanismo di rilevamento affidabile sono i rumori metallici o l'invio di keep-alive quando la connessione è inattivo.

Mentre si ha a che fare con disconnessioni sconosciute in ogni caso, personalmente tengo qualche elemento del protocollo per chiudere una connessione in comune accordo invece di chiuderlo senza preavviso.

+0

Anche se non può far male, ho trovato che in pratica si concentra sul recupero robusto dall'imprevisto funziona meglio dei messaggi di "disconnessione graduale" ... YMMV :) – jerryjvl

+0

Great post. Grazie. Avete ulteriori informazioni sull'uso di NetworkStream? Qualsiasi link dovrebbe fare. –

+0

Link ed esempio aggiunti per rispondere. – dtb