Amazon GameLift In C# 06: 簡単なチャットソフトクライアントを作ります
概要
前回の記事で簡単なチャットソフトのサーバー部分を作りました。チャットソフトはサーバーだけでは成立できないので、今回の記事はチャットソフトのクライアントを作ります。
既に作成したプロジェクトはGitHubに上がっています:
AGLW-CSharp-BasicChatClientSample
マシン環境
CPU : Intel i7-6920HQ
GPU : AMD Radeon Pro 460
Memory : 16GB
System : Windows 10 (with .NET 5 installed)
IDE : Visual Studio 2019 Community
Editor : Visual Studio Code
Terminal : Windows Terminal (or Command Prompt)
プロジェクト作成
Sampleを作成した際のプロジェクト作成部分読んでない方はそちに参考してください。
こちの場合は既にAGLW-CSharp-BasicChatClientSample
どいうフォルダを用意して、それに入ってからコマンドツールでdotnet new console
を実行してプロジェクトを作りました。
(スキップOK)自分で必要な.dll
ファイルのダウンロードしたい場合は Obtaining assemblies for the AWS SDK for .NET にあるaws-sdk-netstandard2.0.zip
をダウンロードして、下記の.dll
ファイルを取り出します。
- AWSSDK.SecurityToken.dll
- AWSSDK.GameLift.dll
- AWSSDK.Core.dll
- AWSSDK.CognitoSync.dll
- AWSSDK.CognitoIdentity.dll
GameLift Client用のライブラリを探すのは大変なので、事前に必要なライブラリを揃ってNuGetにアップロードしました: Amazon GameLift Client Lib Collection
コマンドツールでプロジェクトパスに入ってる状態で
dotnet add package devio.amazon.gamelift.client.lib.collection --version 0.0.1
というコマンドを実行すれば、ライブラリの追加は完了となります。
Chat Clientの作成
Amazon GameLiftにの接続はAmazon Cognitoというユーザー権限検証が必要なので、今回はまずGameLift Localでクライアントを作ります。Amazon Cognitoの獲得手順次回のブログで紹介します。
ソースコード
ChatClient.cs
というファイルをプロジェクト内作成します。
ChatClient
というクラスのソースコードを貼って置きます。
using System; using System.Threading; using System.Threading.Tasks; using System.Net.Sockets; using Amazon; using Amazon.GameLift; using Amazon.GameLift.Model; using Amazon.CognitoIdentity; namespace AGLW_CSharp_BasicChatClientSample { class ChatClient { // A UTF-8 encoder to process byte[] <-> string conversion public readonly System.Text.Encoding Encoder = System.Text.Encoding.UTF8; // .NET Socket TCP client private TcpClient client = null; // NetworkStream(derives from Stream class) of a TCP client private NetworkStream stream = null; // A unique player id will be generated later private string playerId = string.Empty; // All the GameLift related things will be processed by this AmazonGameLiftClient private AmazonGameLiftClient gameLiftClient = null; // Sessions contain general information private GameSession gameSession = null; private PlayerSession playerSession = null; // A instance's status flag of this class public bool IsAlive { get; private set; } = false; public ChatClient() { IsAlive = true; playerId = Guid.NewGuid().ToString(); Console.WriteLine($"Client : playerId {playerId}"); // Create GameLift Local client gameLiftClient = new AmazonGameLiftClient("fakeAccessKeyId", "fakeSecretAccessKey", new AmazonGameLiftConfig() { ServiceURL = "http://localhost:9080" }); } public void Start() { // Create GameSession(async) -> Create PlayerSession(async) -> Connect () CreateSessionsAndConnect(); } async private Task CreateSessionsAndConnect() { await CreateGameSessionAsync(); await CreatePlayerSessionAsync(); // Connect to the IP provided by PlayerSession Connect(); } async private Task CreateGameSessionAsync() { Console.WriteLine($"Client : CreateGameSessionAsync() start"); // GameSession gameSession = await CreateGameSessionAsync(); var request = new CreateGameSessionRequest(); request.FleetId = "fleet-fakeId"; request.CreatorId = playerId; request.MaximumPlayerSessionCount = 1; Console.WriteLine($"Client : Sending request and await"); var response = await gameLiftClient.CreateGameSessionAsync(request); Console.WriteLine($"Client : request sent"); if (response.GameSession != null) { Console.WriteLine($"Client : GameSession Created!"); Console.WriteLine($"Client : GameSession ID {response.GameSession.GameSessionId}!"); gameSession = response.GameSession; } else { Console.Error.WriteLine($"Client : Failed creating GameSession!"); IsAlive = false; } } async private Task CreatePlayerSessionAsync() { Console.WriteLine($"Client : CreatePlayerSessionAsync() start"); if (gameSession == null) return; var request = new CreatePlayerSessionRequest(); request.GameSessionId = gameSession.GameSessionId; request.PlayerId = playerId; Console.WriteLine($"Client : Sleep for a while"); Thread.Sleep(10000); Console.WriteLine($"Client : Sending request and await"); var response = await gameLiftClient.CreatePlayerSessionAsync(request); Console.WriteLine($"Client : request sent"); if (response.PlayerSession != null) { Console.WriteLine($"Client : PlayerSession Created!"); Console.WriteLine($"Client : PlayerSession ID {response.PlayerSession.PlayerSessionId}!"); playerSession = response.PlayerSession; } else { Console.Error.WriteLine($"Client : Failed creating PlayerSession!"); IsAlive = false; } } // Connect to the IP which PlayerSession provides // When client connects : // 1) Receive the msg sent by server // 2) Send another msg back // 3) Close the connection private void Connect() { Console.WriteLine($"Client : Connect() start"); if (playerSession == null) return; Console.WriteLine($"Client : Try to connect"); client = new TcpClient(playerSession.IpAddress, playerSession.Port); if (client.Connected) { Console.WriteLine($"Client : Connected"); stream = client.GetStream(); // A binary buffer for message byte[] msg = new byte[256]; // Read the message (Stream.Read() blocks) Console.WriteLine($"Client : Wait to read from stream"); while (stream.Read(msg) > 0) { // Decode the binary msg to string string str = Encoder.GetString(msg); Console.WriteLine($"Received : {str}"); // Encode the string to binary message and send it msg = Encoder.GetBytes("Hello Server, this is Client."); stream.Write(msg); Console.WriteLine($"Client : Message sent"); break; } } // After a successfule connection, close it client.Close(); IsAlive = false; } } }
using宣言
using System; using System.Threading; using System.Threading.Tasks; using System.Net.Sockets; using Amazon; using Amazon.GameLift; using Amazon.GameLift.Model; using Amazon.CognitoIdentity;
main threadはクライアント全体プログラムを実行するため、通信の部分は新しいThread
に動かすので(今回は一つメッセージだけを送信するので、しなくても大丈夫)、一部非同期処理を含めて、System.Threading
とSystem.Threading.Tasks
を追加しました。
.NET Socket 通信クラスを使うためSystem.Net.Sockets
を追加しました。
Amazon.GameLift
とAmazon.GameLift.Model
にはGameLiftのクラスが含まれます。
Amazon
とAmazon.CognitoIdentity
はさっき言ったユーザー権限検証の部分に関わるライブラリです。今回使わないですが、次回使うので、一応宣言しておきます。
メンバー定義
// A UTF-8 encoder to process byte[] <-> string conversion public readonly System.Text.Encoding Encoder = System.Text.Encoding.UTF8; // .NET Socket TCP client private TcpClient client = null; // NetworkStream(derives from Stream class) of a TCP client private NetworkStream stream = null; // A unique player id will be generated later private string playerId = string.Empty;
UTF8のデータを送信したいですが、毎回System.Text.Encoding.UTF8
のように関数を使うのは長くて読みにくいので、先にEncoder
に定義します。
そして .NET Socket のTCP通信用のクライアントクラスTcpClient
を定義し、それと同時に通信データ受送信作業を担当するNetworkStream
を定義します。
playerId
という文字列は唯一のPlayer IDとし、後で生成します。
// All the GameLift related things will be processed by this AmazonGameLiftClient private AmazonGameLiftClient gameLiftClient = null; // Sessions contain general information private GameSession gameSession = null; private PlayerSession playerSession = null;
AmazonGameLiftClient
はほぼすべてGameLiftと関わる作業の担当です。
GameSession
はサーバー情報周りを管理するクラスです。
PlayerSession
はプレイヤーの接続情報を管理しています。
構造体
public ChatClient() { IsAlive = true; playerId = Guid.NewGuid().ToString(); Console.WriteLine($"Client : playerId {playerId}"); // Create GameLift Local client gameLiftClient = new AmazonGameLiftClient("fakeAccessKeyId", "fakeSecretAccessKey", new AmazonGameLiftConfig() { ServiceURL = "http://localhost:9080" }); }
Guid.NewGuid()
という関数でPlayer IDを生成します。Documentationに書いてる通り、この関数が保証できるのは、Guid.Empty
にならない番号だけです。「唯一」という保証はされてないですが、長いHex番号が生成され、一つゲームセッションに同じ番号が存在する可能性ほぼないので、効果的には「唯一」と思っても大丈夫と思います。
new AmazonGameLiftClient
になんでもよく仮情報を渡して、そしてnew AmazonGameLiftConfig()
の部分はlocalhost
で初期化します。
GameSessionの獲得
async private Task CreateGameSessionAsync() { Console.WriteLine($"Client : CreateGameSessionAsync() start"); // GameSession gameSession = await CreateGameSessionAsync(); var request = new CreateGameSessionRequest(); request.FleetId = "fleet-fakeId"; request.CreatorId = playerId; request.MaximumPlayerSessionCount = 1; Console.WriteLine($"Client : Sending request and await"); var response = await gameLiftClient.CreateGameSessionAsync(request); Console.WriteLine($"Client : request sent"); if (response.GameSession != null) { Console.WriteLine($"Client : GameSession Created!"); Console.WriteLine($"Client : GameSession ID {response.GameSession.GameSessionId}!"); gameSession = response.GameSession; } else { Console.Error.WriteLine($"Client : Failed creating GameSession!"); IsAlive = false; } }
GameSession
の獲得には非同期処理の関数で待つので、CreateGameSessionAsync()
をasync Task
にしました。
var request = new CreateGameSessionRequest(); request.FleetId = "fleet-fakeId"; request.CreatorId = playerId; request.MaximumPlayerSessionCount = 1;
テストのため、初期状態はGameSession
がないので、Fleetに確認する必要なく直接新しいGameSession
を作る請求を出します。GameLift Fleetに提出するリクエストにはFleetId
(GameLift Localのため、なんの文字列でもOK)、CreatorId
(Player ID)、MaximumPlayerSessionCount(GameSession
に接続する最大Player数)が必要です。今回は一人クライアントなので、1
にしました。
var response = await gameLiftClient.CreateGameSessionAsync(request); /// ...省略 gameSession = response.GameSession;
非同期関数CreateGameSessionAsync()
にリクエストを渡して、Fleetに承認されるまで待ちます。
問題なくresponse
を返してくれたら、メンバーに保存します。
PlayerSessionの獲得
async private Task CreatePlayerSessionAsync() { Console.WriteLine($"Client : CreatePlayerSessionAsync() start"); if (gameSession == null) return; var request = new CreatePlayerSessionRequest(); request.GameSessionId = gameSession.GameSessionId; request.PlayerId = playerId; Console.WriteLine($"Client : Sleep for a while"); Thread.Sleep(10000); Console.WriteLine($"Client : Sending request and await"); var response = await gameLiftClient.CreatePlayerSessionAsync(request); Console.WriteLine($"Client : request sent"); if (response.PlayerSession != null) { Console.WriteLine($"Client : PlayerSession Created!"); Console.WriteLine($"Client : PlayerSession ID {response.PlayerSession.PlayerSessionId}!"); playerSession = response.PlayerSession; } else { Console.Error.WriteLine($"Client : Failed creating PlayerSession!"); IsAlive = false; } }
これも非同期関数です。
var request = new CreatePlayerSessionRequest(); request.GameSessionId = gameSession.GameSessionId; request.PlayerId = playerId;
PlayerSession
の獲得にもリクエストが必要です。
さっき貰ったGameSession
のGameSessionId
と生成されたplayerId
をリクエストに渡します。
var response = await gameLiftClient.CreatePlayerSessionAsync(request); /// ...省略 playerSession = response.PlayerSession;
CreatePlayerSessionAsync()
という非同期関数にリクエストを渡して、Fleetからの返事を待ちます。
無事PlayerSession
をもらったら、メンバーに保存します。
サーバーに接続
// Connect to the IP which PlayerSession provides // When client connects : // 1) Receive the msg sent by server // 2) Send another msg back // 3) Close the connection private void Connect() { Console.WriteLine($"Client : Connect() start"); if (playerSession == null) return; Console.WriteLine($"Client : Try to connect"); client = new TcpClient(playerSession.IpAddress, playerSession.Port); if (client.Connected) { Console.WriteLine($"Client : Connected"); stream = client.GetStream(); // A binary buffer for message byte[] msg = new byte[256]; // Read the message (Stream.Read() blocks) Console.WriteLine($"Client : Wait to read from stream"); while (stream.Read(msg) > 0) { // Decode the binary msg to string string str = Encoder.GetString(msg); Console.WriteLine($"Received : {str}"); // Encode the string to binary message and send it msg = Encoder.GetBytes("Hello Server, this is Client."); stream.Write(msg); Console.WriteLine($"Client : Message sent"); break; } } // After a successfule connection, close it client.Close(); IsAlive = false; }
GameSession
とPlayerSession
両方とも問題なくもらったら、Connect()
関数を実行します。
client = new TcpClient(playerSession.IpAddress, playerSession.Port);
PlayerSession
にあるIpAddress
とPort
情報でTcpClient
を作ります。
stream = client.GetStream(); // A binary buffer for message byte[] msg = new byte[256];
client.Connected
はtrue
の場合はif
処理の中に入ります。
まずclient.GetStream()
で受送信担当のStream
をメンバーに保存し、byte[]
タイプのメッセージバッファを用意ます。
while (stream.Read(msg) > 0) { // Decode the binary msg to string string str = Encoder.GetString(msg); Console.WriteLine($"Received : {str}"); // Encode the string to binary message and send it msg = Encoder.GetBytes("Hello Server, this is Client."); stream.Write(msg); Console.WriteLine($"Client : Message sent"); break; } // ...省略 client.Close();
stream.Read()
関数はメッセージが来ないとそのまま止まります。
メッセージがきたら、while
の中に入って、Encoder.GetString(msg)
でメッセージをbyte[]
からstring
に解析します。
そしてEncoder.GetBytes()
で"Hello Server, this is Client."
という文字列をbyte[]
に転換し、stream.Write()
でサーバーに送信します。
送信が終わったらbreak;
で出て、client.Close()
で接続を切断します。
Program.csでChatClientを起動する
class Program { static private ChatClient client = new ChatClient(); static void Main(string[] args) { client.Start(); while(client.IsAlive) { } Console.WriteLine("Program ends."); } }
その前に作ったGameServer
やChatServer
のように、普通の構造体で生成し、Start()
関数で起動します。今は何もやってないですがwhile(client.IsAlive)
でクライアントの状態を判断し、将来いろいろ処理できると思います。
テスト
前回の記事のようにGameLift LocalとChatServer
を起動します。
コマンドツールで今回のプロジェクトに入ってdotnet run
で起動します。
GameLift LocalとChatClientのPortを必ず一致してください。例のportは9080です
GameLift Local
Chat Server
Chat Client
上記のようなGameLift Local、Chat Server、Chat Clientのメッセージが出ればテスト完了となります。
おしまい
今回はGameLift Localだけでチャットソフトのサーバーとクライアントのテストができました。クライアントがネット上本物のAmazon GameLiftに接続するには、Amazon Cognitoというユーザー権限検証が必要なので、それを次回のブログでやります。