Amazon GameLift In C# 05: 簡単なチャットソフトサーバーを作ります

Amazon GameLift Walkthrough in C# (with Unity) Tutorial 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の転換することです。 TcpListenerTCP通信を監視するクラスです。 そして、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.Successtrueだったら、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を監視します。 要注意なのは、このThreadlistener.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 9080GameLift Localを起動します。 GameLiftが初期化されたら、もう一つコマンドツールと開いて、ころChatServerプロジェクトのパスに入ってdotnet runを実行します。

p1

p2

GameLift Localp1のログが出て、ChatServerのプロジェクトはp2のようにログが出るなら、ChatServerの初期化処理は一応問題ないとなります。

おしまい

今回はChatServerのメッセージ受送信のソースを実装しましたが、ChatClientを作らないとメッセージの受送信はテストできないので、次回はChatClientのプロジェクトを作ります。