9

私はオンラインゲームの主な開発者です。プレーヤーは、TCP / IP(UDPではなくTCP)でゲームサーバーに接続する特定のクライアントソフトウェアを使用します

現時点では、サーバーのアーキテクチャは、接続ごとに1つのスレッドを持つ従来のマルチスレッドサーバーです。しかし、接続されている人が300人または400人いることが多いピーク時には、サーバーの遅延がますます大きくなっています。

多くの接続を管理するスレッドが少ないjava.nio。*非同期I/Oモデルに切り替えることで、パフォーマンスが向上するかどうか疑問に思いました。このようなサーバーアーキテクチャの基本をカバーするサンプルコードをWeb上で見つけるのは非常に簡単です。しかし、何時間もグーグルした後、私はいくつかのより高度な質問に対する答えを見つけられませんでした:

1-プロトコルはテキストベースであり、バイナリベースではありません。クライアントとサーバーは、UTF-8でエンコードされたテキスト行を交換します。1行のテキストは1つのコマンドを表し、各行は\nまたは\r\nで適切に終了します。従来のマルチスレッドサーバーの場合、私はそのようなコードを持っています:

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();
}

そして実行時に、データはreadLineを使用して1行ずつ読み取られます。

ドキュメントで、SocketChannelからリーダーを作成できるユーティリティクラスChannelsを見つけました。ただし、チャネルが非ブロッキングモードの場合、生成されたリーダーは機能しないと言われています。これは、私が使用する高性能のチャネル選択APIを使用するために非ブロッキングモードが必須であるという事実と矛盾します。ですから、それは私がやりたいことに対する正しい解決策ではないのではないかと思います。したがって、最初の質問は次のとおりです。それを使用できない場合、バッファとチャネルを使用して、nio APIで改行を効率的かつ適切に処理し、ネイティブJava文字列をUTF-8でエンコードされたデータに変換する方法を教えてください。get / putを操作する必要がありますか、それともラップされたバイト配列内を手動で操作する必要がありますか?ByteBufferからUTF-8でエンコードされた文字列に移行する方法は?私はしないことを認めます」

2-非同期/非ブロッキングI/Oの世界では、本質的に次々に実行される連続した読み取り/書き込みの処理についてはどうでしょうか。たとえば、通常はチャレンジレスポンスベースのログイン手順です。サーバーは質問(特定の計算)を送信し、クライアントは応答を送信してから、サーバーはクライアントからの応答を確認します。答えは、ログインプロセス全体でワーカースレッドに送信する単一のタスクを作成しないことです。これは非常に長いため、ワーカースレッドを長時間フリーズするリスクがあります(そのシナリオを想像してみてください:10プールスレッド、10人のプレーヤーが同時に接続を試みます。すでにオンラインになっているプレーヤーに関連するタスクは、1つのスレッドが再び準備できるまで遅延します)。

3- 2つの異なるスレッドが同じチャネルでChannel.write(ByteBuffer)を同時に呼び出すとどうなりますか?クライアントは混同された行を受け取る可能性がありますか?たとえば、スレッドが「aaaaa」を送信し、別のスレッドが「bbbbb」を送信する場合、クライアントは「aaabbbbbaa」を受信できますか、それともすべてが一貫した順序で送信されるようにしていますか?呼び出しが返された直後に使用されるバッファーを変更することはできますか?または、別の質問をすると、この種の状況を回避するために追加の同期が必要ですか?追加の同期が必要な場合、書き込みが終了したときにロックを解除するタイミングなどを知る方法は?答えは、セレクターでOP_WRITEに登録するほど簡単ではないのではないかと思います。それを試してみると、常にすべてのクライアントに対して書き込み準備完了イベントが発生し、Selector.selectをほとんど無料で早期に終了することに気付きました。選択ループは1秒間に数百回実行されますが、クライアントごとに1秒間に送信するメッセージは3つまたは4つしかないためです。したがって、潜在的に、アクティブな待機は、非常に悪いことです。

4-複数のスレッドが同じセレクターでSelector.selectを同時に呼び出すことができますか?イベントの欠落、2回のスケジュール設定などの同時実行の問題はありませんか?

5-実際、nioは言われているほど良いですか?従来のマルチスレッドモデルにとどまるのは興味深いことですが、接続ごとにスレッドを作成する代わりに、使用するスレッドを減らし、接続をループして、InputStream.isAvailableを使用してデータの可用性を探しますか?そのアイデアは愚かで非効率的ですか?

4

1 に答える 1

4

1)はい。独自のノンブロッキングreadLineメソッドを作成する必要があると思います。バッファに複数の行がある場合、または不完全な行がある場合、非ブロッキング読み取りが通知される場合があることにも注意してください。

例:(最初に読んだ)

 USER foo
 PASS

(2回目の読み取り)

 bar

十分な情報が処理できるようになるまで、消費されなかったデータを保存する必要があります(2を参照)。

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

2)各クライアントの状態を維持する必要があります。

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

チャネルが接続さputれると、マップに新しい状態が追加されます

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

または、現在の状態をの添付オブジェクトとして保存しますSelectionKey

次に、各コマンドを実行するときに、状態を更新します。モノリシックメソッドとして記述したり、のポリモーフィック実装などのより凝った操作を実行したりできますState。各状態は、いくつかのコマンドの処理方法を知っています(たとえばLoginState、USERとPASSを期待し、状態をnewに変更しますAuthorizedState)。

3)チャネルごとに多くの非同期ライターでNIOを使用したことを覚えていませんが、ドキュメントにはスレッドセーフであると記載されています(これについての証拠がないため、詳しく説明しません)。OP_WRITEについては、書き込みバッファがいっぱいでないときに通知することに注意してください。言い換えると、ここで述べたように、 OP_WRITEはほとんどの場合準備ができています。つまり、ソケット送信バッファーがいっぱいの場合を除いて、Selector.select()メソッドを無意識にスピンさせるだけです。

4)はい。ブロッキング選択操作Selector.select()を実行します。

5)最も難しいのは、クライアントごとのスレッドアーキテクチャから、読み取りと書き込みが処理から切り離された別の設計に切り替えることだと思います。一度それを行うと、ストリームをブロックする独自の方法で作業するよりも、チャネルで作業する方が簡単です。

于 2013-03-11T20:56:17.467 に答える