Vom klassischen Multithread-Server zum java.nio asynchronen / nicht blockierenden Server

9

Ich bin der Hauptentwickler eines Online-Spiels. Die Spieler verwenden eine spezielle Client-Software, die eine Verbindung zum Spieleserver über TCP / IP (TCP, nicht UDP) herstellt.

Im Moment ist die Architektur des Servers ein klassischer Multithread-Server mit einem Thread pro Verbindung. Aber in den Spitzenzeiten, wenn es oft 300 oder 400 verbundene Menschen gibt, wird der Server immer lückiger.

Ich habe mich gefragt, ob ich mit einem java.nio. * asynchronen I / O-Modell mit wenigen Threads viele Verbindungen verwalten würde, wenn die Leistung besser wäre. Das Finden von Beispielcodes im Internet, die die Grundlagen einer solchen Serverarchitektur abdecken, ist sehr einfach. Nach stundenlangem Googlen fand ich jedoch keine Antworten auf einige weiterführende Fragen:

1 - Das Protokoll ist textbasiert, nicht binärbasiert. Die Clients und der Server tauschen Textzeilen aus, die in UTF-8 codiert sind. Eine einzelne Textzeile stellt einen einzelnen Befehl dar, jede Zeile wird ordnungsgemäß mit \ n oder \ r \ n abgeschlossen. Für den klassischen Multithread-Server habe ich diese Art von Code:

%Vor%

Und dann werden die Daten zeilenweise mit readLine gelesen.

Im Dokument habe ich eine Utility-Klasse gefunden, die einen Reader aus einem SocketChannel erstellen kann. Aber es wird gesagt, dass der produzierte Reader nicht funktioniert, wenn der Channel im nicht-blockierenden Modus ist, was der Tatsache widerspricht, dass der nicht-blockierende Modus zwingend erforderlich ist, um die hochperformante Kanalauswahl-API zu verwenden, die ich verwenden möchte. Also, ich vermute, dass es nicht die richtige Lösung für das ist, was ich gerne machen würde. Die erste Frage ist daher die folgende: Wenn ich das nicht verwenden kann, wie kann ich effizient und korrekt die Bruchzeilen und die Umwandlung nativer Java-Strings von / in UTF-8-codierte Daten in der nio-API mit Puffern und Kanälen umgehen? Muss ich mit get / put oder im Wrapped-Byte-Array von Hand spielen? Wie gehe ich von ByteBuffer zu in UTF-8 codierten Strings? Ich gebe zu, nicht sehr gut zu verstehen, wie man Klassen im Zeichensatzpaket verwendet und wie es funktioniert.

2 - In der asynchronen / nicht-blockierenden I / O-Welt, was ist mit der Behandlung von aufeinanderfolgendem Lesen / Schreiben, die von Natur aus sequentiell nacheinander ausgeführt werden müssen? Zum Beispiel die Login-Prozedur, die typischerweise Challenge-Response-basiert ist: der Server sendet eine Frage (eine bestimmte Berechnung), der Client sendet die Antwort, und dann überprüft der Server die Antwort des Clients. Die Antwort ist, denke ich, sicherlich nicht eine einzige Aufgabe an Worker-Threads für den gesamten Login-Prozess zu senden, da es sehr lang ist, mit dem Risiko, Arbeiter Threads für zu lange Zeit einzufrieren (Stellen Sie sich dieses Szenario vor: 10 Pool-Threads) , 10 Spieler versuchen sich gleichzeitig zu verbinden, Aufgaben, die sich auf Spieler beziehen, die bereits online sind, werden verzögert, bis ein Thread wieder bereit ist.)

3 - Was passiert, wenn zwei verschiedene Threads gleichzeitig Channel.write (ByteBuffer) auf demselben Kanal aufrufen? Könnte der Client durcheinander geratene Zeilen erhalten? Zum Beispiel, wenn ein Thread "aaaaa" sendet und ein anderer "bbbbb" sendet, könnte der Client "aaabbbbbaa" erhalten, oder bin ich sicher, dass everyting in einer bestellten Reihenfolge gesendet wird? Darf ich den verwendeten Puffer direkt nach dem Aufruf ändern? Oder anders gefragt, brauche ich zusätzliche Synchronisation, um solche Situationen zu vermeiden? Wenn ich eine zusätzliche Synchronisation benötige, wie weiß ich, wann die Freigabe gesperrt wird usw., wenn der Schreibvorgang abgeschlossen ist? Ich befürchte, dass die Antwort nicht so einfach ist wie die Registrierung für OP_WRITE im Selektor. Indem ich das versuchte, bemerkte ich, dass ich das write-ready-Ereignis immer und immer für alle Clients erhalte, Selector.select früh meist für nichts auswählend, da es nur 3 oder 4 Nachrichten gibt, die Sekunde pro Client senden, während die Auswahl Die Schleife wird hunderte Male pro Sekunde ausgeführt. Also, möglicherweise aktiv in der Perspektive warten, was sehr schlecht ist.

4 - Können mehrere Threads Selector.select für denselben Selektor gleichzeitig aufrufen, ohne dass Probleme mit der Nebenläufigkeit auftreten, z. B. ein Ereignis zu verpassen, es zweimal zu planen usw.?

5 - Ist das tatsächlich so gut wie es heißt? Wäre es interessant, beim klassischen Multithread-Modell zu bleiben, aber anstatt einen Thread pro Verbindung zu erstellen, verwenden Sie weniger Threads und eine Schleife über die Verbindungen, um mit InputStream.isAvailable nach Datenverfügbarkeit zu suchen. Ist diese Idee dumm und / oder ineffizient?

    
QuentinC 11.03.2013, 20:31
quelle

1 Antwort

4

1) Ja. Ich denke, dass Sie Ihre eigene nonblocking readLine Methode schreiben müssen. Beachten Sie auch, dass ein nicht blockierender Lesevorgang signalisiert werden kann, wenn mehrere Zeilen im Puffer vorhanden sind oder wenn eine unvollständige Zeile vorhanden ist:

Beispiel: (zuerst lesen)

%Vor%

(zweiter Lesevorgang)

%Vor%

Sie müssen die Daten, die nicht verbraucht wurden, speichern (siehe 2), bis genügend Informationen zur Verarbeitung bereitstehen.

%Vor%

2) Sie müssen den Status jedes Clients beibehalten.

%Vor%

Wenn ein Kanal verbunden ist, put ein neuer Zustand in die Karte

%Vor%

Oder speichern Sie den aktuellen Status als das angehängte Objekt von SelectionKey .

Aktualisieren Sie dann bei der Ausführung jedes Befehls den Status. Sie können es als eine monolithische Methode schreiben, oder machen Sie etwas mehr Phantasie wie polymorphe Implementierungen von State , wo jeder Staat weiß, wie man mit einigen Befehlen umgeht (zB LoginState erwartet USER und PASS, dann ändert man den Zustand in a new AuthorizedState ).

3) Ich kann mich nicht erinnern, NIO mit vielen asynchronen Writern pro Kanal zu verwenden, aber die Dokumentation sagt, dass es Thread-sicher ist (ich werde das nicht näher ausführen, da ich keinen Beweis dafür habe). Über OP_WRITE, beachten Sie, dass es signalisiert, wenn der Schreibpuffer nicht voll ist. Mit anderen Worten, wie gesagt hier : OP_WRITE ist fast immer bereit, dh außer wenn der Socket-Sendepuffer ist voll, so wirst du deine Selector.select() -Methode einfach dazu bringen, sich gedankenlos zu drehen.

4) Ja. Selector.select() führt eine blockierende Auswahloperation aus .

5) Ich denke, der schwierigste Teil ist der Wechsel von einer Thread-pro-Client-Architektur zu einem anderen Design, bei dem Lese- und Schreibvorgänge von der Verarbeitung abgekoppelt sind. Sobald Sie dies getan haben, ist es einfacher mit Kanälen zu arbeiten als mit blockierenden Streams zu arbeiten.

    
Javier 11.03.2013, 20:56
quelle

Tags und Links