2013-03-11 15 views
9

Sono lo sviluppatore principale di un gioco online. I giocatori utilizzano un software client specifico che si connette al server di gioco con TCP/IP (TCP, non UDP)Dal classico server multithreaded a java.nio asincrono/non bloccante

Al momento, l'architettura del server è un classico server multithread con un thread per connessione. Ma nelle ore di punta, quando ci sono spesso 300 o 400 persone connesse, il server diventa sempre più lento.

Mi stavo chiedendo, passando a un java.nio. * Modello I/O asincrono con pochi thread che gestiscono molte connessioni, se le prestazioni sarebbero state migliori. Trovare codici di esempio sul web che coprano le basi di tale architettura server è molto semplice. Tuttavia, dopo ore di ricerca su google, non ho trovato le risposte ad alcune domande più avanzate:

1 - Il protocollo è basato sul testo, non su base binaria. I client e il server scambiano righe di testo codificate in UTF-8. Una singola riga di testo rappresenta un singolo comando, ogni riga è terminata correttamente da \ n o \ r \ n. Per il server multithread classico, ho quel tipo di codice:

public Connection (Socket sock) { 
this.in = new BufferedReader(new InputStreamReader(sock.getInputStream(), "UTF-8")); 
this.out = new BufferedWriter(new OutputStreamWriter(sock.getOutputStream(), "UTF-8")); 
new Thread(this) .start(); 
} 

E poi la linea in corsa, i dati vengono letti dalla linea con readLine.

Nel documento, ho trovato una classe di utilità che può creare un Reader da un SocketChannel. Ma si dice che il Reader prodotto non funzioni se il Canale è in modalità non bloccante, ciò che contraddice il fatto che la modalità non bloccante è obbligatoria per utilizzare l'API di selezione del canale altamente performante che sono disposto a utilizzare. Quindi, sospetto che non sia la soluzione giusta per quello che mi piacerebbe fare. La prima domanda è quindi la seguente: se non riesco a usarlo, come gestire efficacemente e correttamente le linee di interruzione e convertire le stringhe java native da/verso i dati codificati UTF-8 nell'API nio, con buffer e canali? Devo giocare con get/put o all'interno dell'array di byte avvolto a mano? Come passare da ByteBuffer alle stringhe codificate in UTF-8? Ammetto di non capire molto bene come usare le classi nel pacchetto charset e come funziona per farlo.

2 - Nel mondo di I/O asincrono/non bloccante, che dire della gestione di una lettura/scrittura consecutiva che, per sua natura, deve essere eseguita sequenzialmente una dopo l'altra? Ad esempio, la procedura di login, che è tipicamente basata su challenge-response: il server invia una domanda (un calcolo particolare), il client invia la risposta e quindi il server controlla la risposta fornita dal client. La risposta è, credo, non certo di fare una singola attività da inviare ai thread di lavoro per l'intero processo di login, poiché è piuttosto lunga, con il rischio di congelare i thread di lavoro per troppo tempo (Immagina quello scenario: 10 pool thread, 10 giocatori cercano di connettersi allo stesso tempo, le attività relative ai giocatori già online sono in ritardo fino a quando un thread è di nuovo pronto).

3 - Cosa succede se due thread differenti chiamano simultaneamente Channel.write (ByteBuffer) sullo stesso canale? Il client potrebbe ricevere linee miste? Ad esempio se un thread invia "aaaaa" e un altro invia "bbbbb", potrebbe il client ricevere "aaabbbbbaa", o sono sicuro che tutto viene inviato in un ordine consistente? Sono autorizzato a modificare il buffer utilizzato subito dopo la chiamata? Oppure chiesto in modo diverso, ho bisogno di sincronizzazione aggiuntiva per evitare questo tipo di situazione? Se ho bisogno di ulteriore sincronizzazione, come sapere quando i blocchi di rilascio e così via, al termine della scrittura? Ho paura che la risposta non sia semplice come registrarsi per OP_WRITE nel selettore. Provando questo, ho notato che ottengo sempre l'evento write-ready e sempre per tutti i client, selezionando Selector.selezionare in anticipo principalmente per niente, dal momento che ci sono solo 3 o 4 messaggi per inviare secondi per cliente, mentre il ciclo di selezione viene eseguito centinaia di volte al secondo. Quindi, potenzialmente, l'attesa attiva in prospettiva, ciò che è molto cattivo.

4 - Più thread possono chiamare Selector.select sullo stesso selettore simultaneamente senza problemi di concorrenza come mancare un evento, programmarlo due volte, ecc.?

5 - In effetti, nio è buono come si dice? Sarebbe interessante rimanere al classico modello multithreaded, ma non creare un thread per connessione, usare meno thread e fare un loop sulle connessioni per cercare la disponibilità dei dati usando InputStream.isAvailable? È un'idea stupida e/o inefficiente?

+0

Per alcuni esempi di codice, dare un'occhiata all'origine di Netty: https://github.com/netty/netty è una libreria davvero buona. –

risposta

4

1) Sì. Penso che tu abbia bisogno di scrivere il tuo metodo readLine non bloccante. Si noti inoltre che una lettura non bloccante può essere segnalato quando ci sono diverse linee nel buffer, o quando v'è una linea incompleta:

Esempio: (prima lettura)

USER foo 
PASS 

(seconda lettura)

bar 

È necessario memorizzare (vedere 2) i dati che non sono stati consumati, finché non sono disponibili informazioni sufficienti per elaborarli.

//channel was select for OP_READ 
read data from channel 
prepend data from previous read 
split complete lines 
save incomplete line 
execute commands 

2) Sarà necessario mantenere lo stato di ciascun client.

Map<SocketChannel,State> clients = new HashMap<SocketChannel,State>(); 

Quando un canale viene collegato, put stato fresco nella mappa

clients.put(channel,new State()); 

o conservare lo stato attuale come the attached object del SelectionKey.

Quindi, quando si esegue ciascun comando, aggiornare lo stato. Puoi scriverlo come metodo monolitico o fare qualcosa di più sofisticato come le implementazioni polimorfiche di State, dove ogni stato sa come gestire alcuni comandi (ad esempio LoginState si aspetta USER e PASS, quindi lo stato viene modificato in un nuovo AuthorizedState).

3) Non ricordo di aver utilizzato NIO con molti scrittori asincroni per canale, ma la documentazione dice che è thread-safe (non elaborerò, poiché non ne ho la prova). Riguardo a OP_WRITE, si noti che segnala quando il buffer di scrittura è non pieno. In altre parole, come già detto here: OP_WRITE è quasi sempre pronto, ad eccezione di quando il buffer di invio socket è pieno, quindi il tuo metodo Selector.select() deve girare senza mente.

4) Sì. Selector.select() esegue un blocking selection operation.

5) Penso che la parte più difficile sia passare da un'architettura thread-per-client a una diversa struttura in cui le letture e le scritture vengono disgiunte dall'elaborazione. Una volta fatto ciò, è più facile lavorare con i canali che lavorare a modo tuo con i flussi di blocco.

+0

Potresti aggiungere delle precisioni?Q1: devo giocare manualmente con get/put nel buffer, o cercare manualmente \ n nell'array di byte avvolto, per dividere le righe, o c'è un modo migliore? Q2: potrei usare l'oggetto utente di un SelectionKey per memorizzare quelle informazioni di stato? + Aggiungerò un'ulteriore sottoquestione alla domanda 1 per essere più precisi. Grazie per le tue risposte finora. – QuentinC