Amazon GameLift In C# 05: 簡単なチャットソフトサーバーを作ります
概要
以前の記事にGameLift CustomServerのサンプルコードを実行できました。最終的にはオンラインゲームを作りたいですが、裏側のネット通信はどう動いているのかを理解したいので、これからC#(.NET) の基礎ネット通信について説明し、チャットソフトを作ります。
チャットソフトは二つ部分に分かれてます : - ChatServer - ChatClient
本物のチャットソフトは、一対一だけじゃなくて、複数の方が同じチャットルームで話し合うことができます。一遍にそこまで作るのは分かりづらいと思いますので、まず一対一で簡単なメッセージを送れるプログラムを作ります。
これから作るChatServer
は下記の機能があります :
- GameLiftに繋がる
- ChatClient(TcpClient)の接続を待つ
- 接続できたChatClient(TcpClient)にメッセージを送る
- ChatClient(TcpClient)のメッセージを待つ
- ChatClient(TcpClient)のメッセージを受け取りましたら接続を切断して、プログラム終了する。
この記事のプロジェクトはGithubに上がっています : AGLW-CSharp-BasicChatServerSample
マシン環境
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)
Chat Serverの作成
ChatServer
はその前の
Amazon GameLift In C# 03: Sample Codeをビルドし、実行ファイルをGameLift上動かすという記事で作ったサンプルサーバーを土台にしました。クラス名とファイル名はGameServer
からChatServer
に変更しました。
ソースコード
とりあえずChatServer.cs
のソースコードを貼っておきます。
using System; using System.Collections.Generic; using System.Threading; using System.Net; using System.Net.Sockets; using Aws.GameLift.Server; using Aws.GameLift.Server.Model; class ChatServer { public readonly System.Text.Encoding Encoder = System.Text.Encoding.UTF8; // TCP lisenter has it's own thread private TcpListener listener = null; private Thread listenerThread = null; // A instance's status flag of this class public bool IsAlive { get; private set; } = false; //This is an example of a simple integration with GameLift server SDK that will make game server processes go active on GameLift! public void Start() { //Identify port number (hard coded here for simplicity) the game server is listening on for player connections var listeningPort = 7777; //InitSDK will establish a local connection with GameLift's agent to enable further communication. Console.WriteLine(GameLiftServerAPI.GetSdkVersion().Result); var initSDKOutcome = GameLiftServerAPI.InitSDK(); if (initSDKOutcome.Success) { ProcessParameters processParameters = new ProcessParameters( this.OnStartGameSession, this.OnUpdateGameSession, this.OnProcessTerminate, this.OnHealthCheck, listeningPort, //This game server tells GameLift that it will listen on port 7777 for incoming player connections. new LogParameters(new List<string>() { //Here, the game server tells GameLift what set of files to upload when the game session ends. //GameLift will upload everything specified here for the developers to fetch later. "/local/game/logs/myserver.log" })); //Calling ProcessReady tells GameLift this game server is ready to receive incoming game sessions! var processReadyOutcome = GameLiftServerAPI.ProcessReady(processParameters); if (processReadyOutcome.Success) { // Set Server to alive when ProcessReady() returns success IsAlive = true; // Create a TCP listener(in a listener thread) from port when when ProcessReady() returns success LaunchListenerThread(listeningPort); Console.WriteLine("ProcessReady success."); } else { IsAlive = false; Console.WriteLine("ProcessReady failure : " + processReadyOutcome.Error.ToString()); } } else { IsAlive = true; Console.WriteLine("InitSDK failure : " + initSDKOutcome.Error.ToString()); } } void OnStartGameSession(GameSession gameSession) { //When a game session is created, GameLift sends an activation request to the game server and passes along the game session object containing game properties and other settings. //Here is where a game server should take action based on the game session object. //Once the game server is ready to receive incoming player connections, it should invoke GameLiftServerAPI.ActivateGameSession() Console.WriteLine($"Server : OnStartGameSession() called"); GameLiftServerAPI.ActivateGameSession(); } void OnUpdateGameSession(UpdateGameSession updateGameSession) { //When a game session is updated (e.g. by FlexMatch backfill), GameLiftsends a request to the game //server containing the updated game session object. The game server can then examine the provided //matchmakerData and handle new incoming players appropriately. //updateReason is the reason this update is being supplied. Console.WriteLine($"Server : OnUpdateGameSession() called"); } void OnProcessTerminate() { //OnProcessTerminate callback. GameLift will invoke this callback before shutting down an instance hosting this game server. //It gives this game server a chance to save its state, communicate with services, etc., before being shut down. //In this case, we simply tell GameLift we are indeed going to shutdown. Console.WriteLine($"Server : OnProcessTerminate() called"); GameLiftServerAPI.ProcessEnding(); } bool OnHealthCheck() { //This is the HealthCheck callback. //GameLift will invoke this callback every 60 seconds or so. //Here, a game server might want to check the health of dependencies and such. //Simply return true if healthy, false otherwise. //The game server has 60 seconds to respond with its health status. GameLift will default to 'false' if the game server doesn't respond in time. //In this case, we're always healthy! Console.WriteLine($"Server : OnHealthCheck() called"); return true; } // A method creates thread for listener void LaunchListenerThread(int port) { listenerThread = new Thread(() => { Listen(port); }); listenerThread.Start(); Console.WriteLine($"Server : Listener thread is created and started"); } // A method listens the port. // When client connects : // 1) Send msg to client -> 2) Wait msg from client -> 3) Close then connection and break void Listen(int port) { listener = TcpListener.Create(port); listener.Start(); Console.WriteLine($"Server : Start listening port {port}"); while (true) { // TcpClient.AccecptTcpClient() blocks TcpClient client = listener.AcceptTcpClient(); // Print client's IP address IPEndPoint endPoint = client.Client.RemoteEndPoint as IPEndPoint; Console.WriteLine($"Server : Accepted client with IP address {endPoint.Address}"); NetworkStream stream = client.GetStream(); // Get the binary form of a string message and send it byte[] msg = Encoder.GetBytes("Hello Client, this is Server."); stream.Write(msg); // Create a binary buffer to receive message from client msg = new byte[256]; while (stream.Read(msg) > 0) { string str = Encoder.GetString(msg); Console.WriteLine($"From Client : {str}"); // Close the connection when message is received client.Close(); break; } } } void OnApplicationQuit() { //Make sure to call GameLiftServerAPI.Destroy() when the application quits. This resets the local connection with GameLift's agent. GameLiftServerAPI.Destroy(); IsAlive = false; } }
using宣言
GameLiftサーバーサンプルより追加した部分using
宣言は下記です(あった部分はコメントアウトしました)。
using System; // using System.Collections.Generic; using System.Threading; using System.Net; using System.Net.Sockets; // using Aws.GameLift.Server; using Aws.GameLift.Server.Model;
軽く説明します。
using System;
Console.WriteLine()
を使って情報を出力する。-
using System.Threading;
このプログラムはmain thread
で実行していますが、他のタスクは同時に処理できないので、別の通信処理は新しく作ったthread
で動かす。 -
using System.Net; using System.Net.Sockets;
.NET Socket通信APIを使います。 -
using Aws.GameLift.Server.Model;
一部GameLift専用のクラスを使います。
メンバー変数定義
public readonly System.Text.Encoding Encoder = System.Text.Encoding.UTF8; // TCP lisenter has it's own thread private TcpListener listener = null; private Thread listenerThread = null;
Encoder
の役割は読めるstring
と送受信データタイプbyte
の転換することです。
TcpListener
はTCP通信を監視するクラスです。
そして、Thread
というクラスを定義し、通信の作業はほぼlistenerThread
でやります。
関数の説明
var processReadyOutcome = GameLiftServerAPI.ProcessReady(processParameters); if (processReadyOutcome.Success) { // Set Server to alive when ProcessReady() returns success IsAlive = true; // Create a TCP listener(in a listener thread) from port when when ProcessReady() returns success LaunchListenerThread(listeningPort); Console.WriteLine("ProcessReady success."); }
processReadyOutcome.Success
がtrue
だったら、GameLiftServerの初期化が完了となり、
LaunchListenerThread(listeningPort);
でTcpListener
用のThread
を作ります。
// A method creates thread for listener void LaunchListenerThread(int port) { listenerThread = new Thread(() => { Listen(port); }); listenerThread.Start(); Console.WriteLine($"Server : Listener thread is created and started"); }
定義されたport
番号を渡して、Listen(port)
を実行するThread
が作られます。
listenerThread.Start()
でそのThread
を開始させます。
Listen(port)
の中身を見てみましょう:
// A method listens the port. // When client connects : // 1) Send msg to client -> 2) Wait msg from client -> 3) Close then connection and break void Listen(int port) { listener = TcpListener.Create(port); listener.Start(); Console.WriteLine($"Server : Start listening port {port}"); while (true) { // TcpClient.AccecptTcpClient() blocks TcpClient client = listener.AcceptTcpClient(); // Print client's IP address IPEndPoint endPoint = client.Client.RemoteEndPoint as IPEndPoint; Console.WriteLine($"Server : Accepted client with IP address {endPoint.Address}"); NetworkStream stream = client.GetStream(); // Get the binary form of a string message and send it byte[] msg = Encoder.GetBytes("Hello Client, this is Server."); stream.Write(msg); // Create a binary buffer to receive message from client msg = new byte[256]; while (stream.Read(msg) > 0) { string str = Encoder.GetString(msg); Console.WriteLine($"From Client : {str}"); // Close the connection when message is received client.Close(); break; } } }
listener = TcpListener.Create(port); listener.Start();
最初はport
を渡してTcpListener
を作り、初期化し、Start()
で実行します。
// TcpClient.AccecptTcpClient() blocks TcpClient client = listener.AcceptTcpClient();
listener.AcceptTcpClient()
で接続しにくるTcpClient
を監視します。
要注意なのは、このThread
はlistener.AcceptTcpClient()
で止まるので、TcpClient
の接続請求が来るまではそれ以降のソースに進みません。
NetworkStream stream = client.GetStream();
TcpClient
の接続がきたら、まずclient.GetStream()
でその中のStream
を貰います。
NetworkStream
の親クラスはStream
です。これからのメッセージ受送信は貰ったNetworkStream
で処理します。
// Get the binary form of a string message and send it byte[] msg = Encoder.GetBytes("Hello Client, this is Server."); stream.Write(msg);
Hello Client, this is Server.
という文字列をEncoder.GetBytes()
という関数でbyte[]
に転換してから、stream.Write()
に渡して、リモートのTcpClient
に送信します。
// Create a binary buffer to receive message from client msg = new byte[256]; while (stream.Read(msg) > 0)
さっき送信用のmsg
を初期化し、バッファとして再利用します。
Network.Read()
という関数に止まります。メッセージが来るまでWhile
ループの中に入りません。
string str = Encoder.GetString(msg); Console.WriteLine($"From Client : {str}"); // Close the connection when message is received client.Close();
メッセージが来て、Encoder.GetString()
でbyte[]
からstring
に解析し、Console.WriteLine()
でプリントします。
受送信はここで完了なので、メッセージ通信のテストは成功となります。TcpClient
との接続を切断します。
GameLift Localでテストする
前にも紹介したローカル環境のGameLiftでさっき作ったChatServer
の起動テストをします。
コマンドツールでGameLiftLocal.jar
を置いてあるパスに入ってjava -jar GameLiftLocal.jar -p 9080
でGameLift Localを起動します。
GameLiftが初期化されたら、もう一つコマンドツールと開いて、ころChatServer
プロジェクトのパスに入ってdotnet run
を実行します。
p1
p2
GameLift Localがp1
のログが出て、ChatServer
のプロジェクトはp2のようにログが出るなら、ChatServer
の初期化処理は一応問題ないとなります。
おしまい
今回はChatServer
のメッセージ受送信のソースを実装しましたが、ChatClient
を作らないとメッセージの受送信はテストできないので、次回はChatClient
のプロジェクトを作ります。