ゲームマルチプレイヤーサーバーのプロトタイプの説明

こんにちは、Habr。 私の最初の記事、つまりゲームマルチプレイヤーサーバーのプロトタイプの説明を紹介します。



ソースコード(Apache 2.0でライセンスされています)



内容:





ユーザー受信処理アーキテクチャ



入力ポイントは、ゲームのログインやアプリケーションからゲームの動き、チャットへのメッセージの書き込みまで、ユーザーからのあらゆる種類のリクエストを受け入れるWebSocketコントローラーです。 このコントローラーは、スレッドプール(約20スレッド)を提供します。



ゲームで最も重要なことの1つは、ゲームアクションの迅速な処理です(優先ビジネスケース)。 つまり、理想的には、ゲームは「フリーズ」ではなく、ユーザーのアクションに即座に応答する必要があります。 認証、チャットへのメッセージの書き込み、またはプレイヤーの試合(一緒にプレイするため)など、ゲーム以外の多くのアクションでは、ユーザーは遅延の大小を許容できます。



したがって、私もこの要件に従ってアーキテクチャを設計しようとしました(図を参照)。



画像



認証リクエスト(写真の#1)



着信認証要求は、特別なスレッドプールでキューに入れられます。 その後、スレッドはすぐに解放され、再びユーザーアクションを受け入れる準備が整います。



認証処理自体は非同期に行われます。 以下が含まれます。



-Facebookでの認証。

-データベース内のユーザーデータの作成または更新。

-ログインに成功した場合にクライアントに情報を送信します。



このスレッドプールのサイズは、サーバーの負荷に応じて調整できます。



ゲームのリクエスト(写真の2番目)



プレーヤーのゲームアプリケーションの作成要求-額の解決策は、プレーヤーに一致する機能(関数/メソッド)をすぐに呼び出すことです。 スレッドセーフのために、共有リソースをブロックするオプションを追加します(この場合、これはゲームのアプリケーションのリストです)。 つまり、ロックを取得し、プレーヤーを一致させ、それらのプレーヤーでゲームを作成し、クライアントに通知を送信します。 これらの操作に比較的長い時間がかかる場合、ロックの削除を待機している他のすべてのスレッドは何もせずにアイドル状態になります。 その結果、サーバーは着信ゲームアクションを受け入れない可能性があり、すぐにGame Loopに到達する必要があります。



また、このオプションは拡張性に乏しい可能性があります。スレッドの数が増えると、この共有リソースにアクセスするときにすべてのスレッドが起動(互いにブロック)する可能性があります。 ここからは、垂直スケーリングとスレッド数の増加によるメリットはほとんどありません。 したがって、私は別のオプションを考え出しました(ところで、このアプローチに名前がある場合は、コメントに書いてください):



新しいバージョンでは、ゲームのリクエストが同時マップに追加されます。ここで、キーはユーザーであり、値はゲームのアプリケーションです。

すべて-その後、着信ストリームはすぐに解放されます。

ConcurrentMapはマップ全体ではなくセグメントをブロックするため、ストリームは常に互いにブロックするわけではありません(書き込み時に)。



n秒ごとに、着信ゲームリクエストを処理するために正確に1つのスレッドが呼び出されます(プレイヤーのマッチング)。 彼は落ち着いていて、この地図を処理します。 これにより、ブロックせずにスレッドの安全性を確保し、着信フローをすばやく解放します。



このソリューションにはもう1つの利点があります。アプリケーションを「埋めて」、より適切な方法でそれらを一致させることができます。 ロジックと実装が簡単になりました。



ゲームアクションの処理(写真の3番目)



もう少し複雑です:



1)着信ゲームアクション(移動)は、単純に特別なキューに追加されます(各ゲームには独自のキューがあります)。 このキューは、プレイヤーによってコミットされたアクションを単に保存します。 この後、フローは伝統的にリリースされます。



2)いつものように、GameLoop(ゲームループ)があります。 彼はすべてのゲームのリストを回っています。 さらに、ゲームごとに、彼はそれに関連付けられたキューを取得します。 そしてすでにこのキューから彼はプレイヤーによって行われた動きを取得します。 次に、各アクションを順番に処理します。 すべてがシンプルです。



原則として、スレッドのプール全体で異なるゲームの処理を並列化することもできます。 ゲームは相互に接続されていないため、これは可能です。 この機能は非ブロッキングにすることもできます。たとえば、java.util.concurrentライブラリの非ブロッキングロックを使用するだけで十分です。 または、分散ソリューションがある場合は、共有マップのキーをロックする機能を備えた分散キャッシュとしてHazelcastを使用します...ただし、ゲームアクションの処理が速すぎるため、この機能は必要ありません。 したがって、GameLoopは単一のスレッドで実行されます。



3)別のポイントがあります-ゲームが変更された場合、クライアントに通知を送信する必要があり、必要に応じてデータベースのデータを更新する必要があります。 GameLoopの速度が低下しないように、これは非同期モードでも実行されます(図の4)。



要約する



アーキテクチャは次のように設計されています。



-ゲームアクションを含むリクエストは、他の種類のリクエストよりも優先度が高くなりました(たとえば、ログイン前、ゲームの申し込みなど)。



そのため、100件の認証リクエストが送信され、スレッドプールを「ハンマー」でつなぐ(ユーザーリクエストを処理する)ことはありません。 同時に、入ってくるゲームアクションが並んで、すべてのゲームが数秒間「スローダウン」します。



-ノンブロッキングマルチスレッドがいたるところにありました。



このアプローチは、垂直スケーリングとスレッド数の増加をサポートしています。 トピックの記事、「スケーラビリティの問題」セクションを参照してください



他のポイントの簡単な説明



優先度の低いタスクの1つは、 サーバーモジュールとゲームモジュールの間に弱いバインディングを作成することでした。 つまり、ゲーム自体を簡単に「引き出し」て、デスクトップUIに接続できるようにします。 または、別の卓上ゲームをマルチユーザーサーバーに配置します。



画像



これは、責任範囲を区切ることによって達成されます。 このプロジェクトは、サーバー、ゲームAPI、ゲーム実装の3つのモジュールに分かれています。



すべてのゲームロジックは、ゲームモジュールで「配線」されています。 サーバーは、受け取ったゲームアクションをコードAPI(Javaコード)を介してゲームコントローラーにリダイレクトするだけです。 ゲームからの応答は、すぐにまたは延期されます-サブスクリプション(サブスクライバーテンプレート)を介して。



ゲームモジュールは、誰がjava APIを介してそれを呼び出し、イベントをサブスクライブするかについて何も知りません。 サーバーとゲームの間の相互作用をよりよく理解するために、別個のモジュールが割り当てられています-ゲームAPIは一連のコントラクト(インターフェース)です。 サーバーはそれらのみを呼び出します。 そして、ゲームモジュールは実装を提供します。



単体テストと統合テストの両方があります。



一般的に、テストは、難しい/長く/退屈なテストケースがある場合に使用されます。



たとえば、切断のさまざまなオプションがあります。たとえば、ユーザーが新しいデバイスから接続する場合、古い接続を閉じる必要があります。 または、たとえば、これはユーザーが15分間非アクティブだった場合の切断チェックです(それほど長く待たないように-多くのパラメーターが環境変数に取り込まれ、クイックテスト実行のために数ミリ秒間「ロック」されました)。



チャットチェックもあります。異なるユーザーが互いのメッセージを見ることができます。

ゲームリクエストチェックとゲーム作成があります。



上記のケースでは、サーバーとIoCコンテキスト(ロックされた外部システム)を上げる統合テストが適していました。



単体テストも使用されます。 たとえば、コンテキストを上げる必要がない場合。 または、入力パラメーターのさまざまなバリエーションを確認する必要がある場合。



たとえば、ゲームのルールをカバーするために単体テストが使用されます。 各ゲームホイールは、1つのパブリックメソッドを持つ個別のクラスに実装されます。 それは純粋な機能であり、生地で簡単に覆われています。

さらに、別のクラスのビジネスロジックは、すでにこれらの関数ホイールで構成されています。 これにより、コードを読みやすく、理解しやすくなります。



一般に、テストを書くタイプと方法を選択するとき、私はこのレポートが好きでした: 「Hexlet-Testing and TDD」



ゲームリクエストの迅速な処理と同様に、 データベース呼び出しキャッシュすることでも計算されます 。 認証中に、ユーザーデータはキャッシュに読み込まれます(まだキャッシュにない場合)。 その後、このデータに対するまれなリクエストはすべてキャッシュから取得されます。 ゲームの最後に(これは頻繁には発生しません)、キャッシュ内の情報の更新とともにデータベースにエントリが作成されます。



クライアントのコードと機能を確認しない方が良いです。プロトタイプには、必要な機能がほとんどなかったため、迅速に作成されました。 機能拡張のすべてのポイント、バックエンドに置かれた一般化されたコード。



すべてのポイントがこの記事で開示されているわけではありません。 特に、接続と切断の管理について(たとえば、新しいデバイスのセッションを開く場合)。 このゲームには、レーティングシステムとメインテーブルのトップ100プレイヤーもいます。 マルチプレイヤーだけでなく、ボットを使用したゲームもあります。 機能拡張のポイントは、サーバーとゲームの両方のさまざまな側面に基づいています。



ゲームはJavaで書かれています。 すぐに使用できるWebフレームワーク(Spring WebSocket)、統合テスト(Spring Boot Test)、およびその他の機能(DIなど)を提供するSpring Frameworkを積極的に使用します。



Webソケットの水平スケーリングはそれほど簡単ではありません。 したがって、プロトタイプの速度を上げるために、それを行わないことが決定されました。



面白い瞬間のカップル



サーバーは無料のHerokuアカウントでホストされています。 この無料料金表によると、30分以内にサーバーへの要求がなかった場合、サーバーは削減されます。 エレガントなソリューションが見つかりました-定期的にサーバーにpingを送信する監視サイトに登録しました。 ボーナスとして-監視に関する追加情報を受け取ります。



1万行の制限がある無料のPostgreもあります。 このため、無関係なアカウントの削除を定期的に実行する必要があります。



All Articles