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?
Per alcuni esempi di codice, dare un'occhiata all'origine di Netty: https://github.com/netty/netty è una libreria davvero buona. –