Amazon GameLift In C# 11: より良いチャットクライアントを作ります(Part2)

Amazon GameLift Walkthrough in C# (with Unity) Tutorial 11

概要

前回の記事の続編です。
前回はGameLiftの接続担当クラスGameLiftClient.csのコードを分析しましたので、これから残りのChatClient.csManagedConnection.csを解説させていただきます。

マシン環境

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)

ファイルの構造とクラスの運用

このプロジェクトはGitHubにアップロードしています: AGLW-CSharp-BetterChatClientSample

 

ソースコードファイル :

  • Program.cs (プログラムの入口)
  • GameLiftClient.cs (GameLiftとの接続を管理するクラス)
  • ChatClient.cs (接続できたものと受送信クラスの管理クラス)
  • ManagedConnection.cs (受送信クラス)

ChatClient.cs ソースコード

ChatClient.csソースコードを貼り付けます。

using System;

using System.Threading;
using System.Threading.Tasks;
using System.Net.Sockets;

namespace AGLW_CSharp_BetterChatClientSample
{

    class ChatClient
    {
        public TcpClient ManagedConnection { get; private set; } = null;
        public Messenger Messenger { get; private set; } = null;
        
        public ChatClient(TcpClient client)
        {
            ManagedConnection = client;
            Messenger = new Messenger(ManagedConnection.GetStream());
        }

        public void StartClient()
        {
            if (ManagedConnection != null &&
                Messenger != null)
            {
                Messenger.StartMessenger();
            }
        }
    }
}

ChatClient.cs メンバー変数の解説

public TcpClient ManagedConnection { get; private set; } = null;
public Messenger Messenger { get; private set; } = null;
  • TcpClient : ManagedConnection
    GameLiftClient.csからもらったTcpClientです。TCP通信全体を管理するメンバーです。
  • Messenger : Messenger
    入力の監視、受送信の管理を担当するメンバーです。

ChatClient.cs 関数の解説

public ChatClient(TcpClient client)
{
    ManagedConnection = client;
    Messenger = new Messenger(ManagedConnection.GetStream());
}

構造体は簡単です。クラスのインスタンスが作られた際、GameLiftClientからTcpClientを渡してくれます。ManagedConnection.GetStream()NetworkStreamを受け取って、Messengerのインスタンスを作ります。

 

public void StartClient()
{
    if (ManagedConnection != null &&
        Messenger != null)
    {
        Messenger.StartMessenger();
    }
}

インスタンスの所有者がStartClient()をコールします。ManagedConnectionMessenger両方とも存在する場合、Messenger.StartMessenger()Messengerを起動します。

 

Messenger.cs ソースコード

Messenger.csソースコードを貼り付けます。

using System;

using System.Threading;
using System.Net.Sockets;


namespace AGLW_CSharp_BetterChatClientSample
{
    class Messenger
    {
        public readonly System.Text.Encoding Encoder = System.Text.Encoding.UTF8;
        public static int SleepDuration = 100;  // 100ms
        public static int MessageLength = 256;

        public NetworkStream Stream { get; private set; } = null;

        public Thread SenderThread { get; private set; } = null;
        public Thread ReceiverThread { get; private set; } = null;

        public string Username { get; private set; } = string.Empty;

        public Messenger(NetworkStream stream)
        {
            Stream = stream;
        }

        public void StartMessenger()
        {
            Console.WriteLine("Enter your username: ");
            Username = Console.ReadLine();

            Console.Clear();

            SenderThread = new Thread(() => SendMessage());
            SenderThread.Start();

            ReceiverThread = new Thread(() => ReceiveMessage());
            ReceiverThread.Start();
        }

        void SendMessage()
        {
            while (true)
            {
                SleepForAWhile();
                MonitorInput();
            }
        }

        void MonitorInput()
        {
            string text = Console.ReadLine();
            string message = new string($"{Username}: {text}");
            WriteMessage(Encoder.GetBytes(message));
        }

        void WriteMessage(byte[] bytes)
        {
            ClearCurrentConsoleLine();
            Stream.Write(bytes);
            // Console.WriteLine($"Sent : {Encoder.GetString(bytes)}");
        }

        void ReceiveMessage()
        {
            byte[] bytes = new byte[MessageLength];
            while (Stream.Read(bytes) > 0)
            {
                string text = Encoder.GetString(bytes);
                // Console.WriteLine($"Received : {text}");
                Console.WriteLine($"{text}");
            }
        }

        void SleepForAWhile()
        {
            Thread.Sleep(SleepDuration);
        }

        void ClearCurrentConsoleLine()
        {
            int currentLineCursor = Console.CursorTop;
            Console.SetCursorPosition(0, Console.CursorTop - 1);
            Console.Write(new string(' ', Console.WindowWidth));
            Console.SetCursorPosition(0, currentLineCursor - 1);
        }
    }
}

Messenger.cs メンバー変数の解説

public readonly System.Text.Encoding Encoder = System.Text.Encoding.UTF8;
public static int SleepDuration = 100;  // 100ms
public static int MessageLength = 256;

public NetworkStream Stream { get; private set; } = null;

public Thread SenderThread { get; private set; } = null;
public Thread ReceiverThread { get; private set; } = null;

public string Username { get; private set; } = string.Empty;
  • System.Text.Encoding : Encoder
    byte[](送信)とstring(解析)を転換する担当です。
  • int : SleepDurationMessageLength
    常に瞬時処理することは負荷が高いので、処理頻度をSleepDuration100msにします。
    サンプルとして長文の処理は対応していませんので、一つメッセージの長さはMessageLength256bytesにします。
  • NetworkStream : Stream
    このクラスのインスタンスが作られた時点に貰った通信の通路(stream)です。
  • Thread : SenderThreadReceiverThread
    受信(SenderThread)と送信(`ReceiverThread)処理は二つThread``に分かれています。
  • string : Username
    接続できてから受送信処理を起動する前に、Usernameをユーザーに入力してもらいます。Usernameは送信者としてメッセージの先頭に表示されます。

Messenger.cs 関数の解説

構造体は非常に簡単なので、省略いたします。

 

public void StartMessenger()
{
    Console.WriteLine("Enter your username: ");
    Username = Console.ReadLine();

    Console.Clear();

    SenderThread = new Thread(() => SendMessage());
    SenderThread.Start();

    ReceiverThread = new Thread(() => ReceiveMessage());
    ReceiverThread.Start();
}

StartMessenger()でこのクラスを起動します。
Username = Console.ReadLine();でユーザーネームを入力して貰ってから、送信作業SendMessage()SenderThreadに、受信作業ReceiveMessage()ReceiverThreadに管理してもらいます。

 

void SendMessage()
{
    while (true)
    {
        SleepForAWhile();
        MonitorInput();
    }
}

void MonitorInput()
{
    string text = Console.ReadLine();
    string message = new string($"{Username}: {text}");
    WriteMessage(Encoder.GetBytes(message));
}

void WriteMessage(byte[] bytes)
{
    ClearCurrentConsoleLine();
    Stream.Write(bytes);
    // Console.WriteLine($"Sent : {Encoder.GetString(bytes)}");
}

void SleepForAWhile()
{
    Thread.Sleep(SleepDuration);
}

void ClearCurrentConsoleLine()
{
    int currentLineCursor = Console.CursorTop;
    Console.SetCursorPosition(0, Console.CursorTop - 1);
    Console.Write(new string(' ', Console.WindowWidth));
    Console.SetCursorPosition(0, currentLineCursor - 1);
}

SleepForAWhile()MonitorInput()100ms毎ユーザーの入力を確認しています。
入力された情報はstring message = new string($"{Username}: {text}")という形式、Encoder.GetBytes(message)byte化してからWriteMessage(byte[] bytes)で送信されます。
ClearCurrentConsoleLine()は送信する直前、画面に入力したものを消して、送信したものはサーバーから返してくれます。
byte化されるメッセージはStream.Write()を使ってTcpClientを経由して、サーバーに送ります。

 

void ReceiveMessage()
{
    byte[] bytes = new byte[MessageLength];
    while (Stream.Read(bytes) > 0)
    {
        string text = Encoder.GetString(bytes);
        // Console.WriteLine($"Received : {text}");
        Console.WriteLine($"{text}");
    }
}

長さはMessageLength(256bytes)のbyte[]バファを用意し、while (Stream.Read(bytes) > 0)で常に受信確認を行っています。
受信できたものをbyte[]からstringに転換し、Console.WriteLine($"{text}")で画面にプリントします。

 

最後

Cognito記事のようにAmazon Cognitoを用意してから、アプリのビルドとAmazon GameLiftにアップロード手順前に作ったBetterChatServerをアップロードします。
サーバー部分が準備できてからこのクライアントプログラムを起動すると、普通にチャットできる状態になります。