Amazon GameLift In C# 09: より良いチャットサーバーを作ります(Part2)

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

概要

前回の記事ChatServer.csのソースコードを解説しました。今回はConnectedClient.csを中心に より良いチャットサーバーののPart2 の記事です。

マシン環境

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-BetterChatServerSample

ソースコードファイル : - Program.cs (プログラムの入口)
- GameLiftServer.cs (Amazon GameLiftとの接続、そしてChatServerを管理するクラス)
- ChatServer.cs (クライアントConnectedClientとの接続、通信管理クラス)
- ConnectedClient.cs (クライアントの接続情報とNetworkStreamでの受送信管理クラス)

ChatServerは接続している複数のConnectedClientを管理しています。ChatServerは全体クライアントとの通信を管理していますが、細かいメッセージ処理などはConnectedClientでやっています(NetworkStream周り)。

ConnectedClient.cs ソースコード

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

class ConnectedClient
{
    public readonly System.Text.Encoding Encoder = System.Text.Encoding.UTF8;

    public TcpClient TargetClient { get; private set; } = null;
    public NetworkStream TargetStream { get; private set; } = null;

    public ConcurrentQueue<byte[]> SendingQueue { get; private set; } = null;
    public ConcurrentQueue<byte[]> ReceivingQueue { get; private set; } = null;

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

    public ConnectedClient(TcpClient client)
    {
        TargetClient = client;
        TargetStream = client.GetStream();

        SendingQueue = new ConcurrentQueue<byte[]>();
        ReceivingQueue = new ConcurrentQueue<byte[]>();
    }

    public void StartClient()
    {
        SenderThread = new Thread(() => Send());
        SenderThread.Start();

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

    // Should be called by server, add a message to queue, will be sent to client later
    public void SendMessage(byte[] bytes)
    {
        SendingQueue.Enqueue(bytes);
    }

    // Should be called by server, retrieve message
    public bool RetrieveMessage(out byte[] bytes)
    {
        bool retrieved = ReceivingQueue.TryDequeue(out bytes);

        return retrieved;
    }

    // Looping in a sender thread
    private void Send()
    {
        byte[] bytes;

        while (TargetStream != null)
        {
            if (SendingQueue.TryDequeue(out bytes))
            {
                TargetStream.Write(bytes);
            }
        }
    }

    // Looping in a receiver thread
    private void Receive()
    {
        byte[] bytes = new byte[ChatServer.MessageLength];

        if (TargetStream != null)
        {
            try
            {
                while (TargetStream.Read(bytes) > 0)
                {
                    Console.WriteLine($"Message Received: {Encoder.GetString(bytes)}");
                    ReceivingQueue.Enqueue(bytes);
                    bytes = new byte[ChatServer.MessageLength];
                }
            }
            catch(SocketException e)
            {
                Console.WriteLine($"Excpetion catched : {e}");
            }
            catch(IOException e)
            {
                Console.WriteLine($"Excpetion catched : {e}");
            }
        }
    }
}

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

public readonly System.Text.Encoding Encoder = System.Text.Encoding.UTF8;

    public TcpClient TargetClient { get; private set; } = null;
    public NetworkStream TargetStream { get; private set; } = null;

    public ConcurrentQueue<byte[]> SendingQueue { get; private set; } = null;
    public ConcurrentQueue<byte[]> ReceivingQueue { get; private set; } = null;

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

- TcpClient : TargetClient
ChatServerから渡してきたクライアント接続情報を持つTcpClientです。 - NetworkStream : TargetStream
TargetClientNetworkStreamです。メッセージ受送信を担当しています。

  • ConcurrentQueue<byte[]> : SendingQueue / ReceivingQueue
    メッセージの受送信のは一瞬でやるではなく、これから送信するメッセージはSendingQueueに保存して送信を待ち、受信したメッセージはReceivingQueueに保存してChatServerは後ほど集めます。
  • Thread : SenderThread / ReceiverThread 送信することはSenderThreadで行い、受送することはReceiverThreadで行います。

ConnectedClient.cs 関数の解説

// Should be called by server, add a message to queue, will be sent to client later
public void SendMessage(byte[] bytes)
{
    SendingQueue.Enqueue(bytes);
}

// Should be called by server, retrieve message
public bool RetrieveMessage(out byte[] bytes)
{
    bool retrieved = ReceivingQueue.TryDequeue(out bytes);

    return retrieved;
}

SendMessage()RetrieveMessage()ChatServerで実行される関数です。

SendMessage()は最初からDelegateに登録されます。一斉送信される際は、すぐ送信ではなく、SendMessageSendingQueueにメッセージを保存し、順序的に送信することを待ちます。

RetrieveMessage()は一旦受信してReceivingQueueに保存されるメッセージを取り出す関数です。ChatServerで呼ばれて、ReceivingQueueのメッセージを順序の並びで送信隊列に保存します。

 
 

// Looping in a sender thread
private void Send()
{
    byte[] bytes;

    while (TargetStream != null)
    {
        if (SendingQueue.TryDequeue(out bytes))
        {
            TargetStream.Write(bytes);
        }
    }
}

Send()関数はSenderThreadで実行され、SendingQueueに残っているメッセージを常に検出し、送信します。

 
 

// Looping in a receiver thread
private void Receive()
{
    byte[] bytes = new byte[ChatServer.MessageLength];

    if (TargetStream != null)
    {
        try
        {
            while (TargetStream.Read(bytes) > 0)
            {
                Console.WriteLine($"Message Received: {Encoder.GetString(bytes)}");
                ReceivingQueue.Enqueue(bytes);
                bytes = new byte[ChatServer.MessageLength];
            }
        }
        catch(SocketException e)
        {
            Console.WriteLine($"Excpetion catched : {e}");
        }
        catch(IOException e)
        {
            Console.WriteLine($"Excpetion catched : {e}");
        }
    }
}

Receive()ReceiverThreadで実行されます。

 

while (TargetStream.Read(bytes) > 0)
{
    Console.WriteLine($"Message Received: {Encoder.GetString(bytes)}");
    ReceivingQueue.Enqueue(bytes);
    bytes = new byte[ChatServer.MessageLength];
}

TargetStream(NetworkStream)に送信してきたメッセージを取り出して、ReceivingQueueに保存します。

 

catch(SocketException e)
{
    Console.WriteLine($"Excpetion catched : {e}");
}
catch(IOException e)
{
    Console.WriteLine($"Excpetion catched : {e}");
}

クライアントが接続した際、一瞬Exceptionが出ます。catchしないとプログラムが落ちるので、ここで予想したExceptioncatchして、落ちないようにします。

これでConnectedClients.csの説明が完了いたします。

最後

より良いチャットサーバーその前簡単なチャットサーバー記事GameLift接続部分が基本一緒ですが、それ以外の違う部分はこれで説明完了になります。

次回の記事はクライアントのソースコードを解説します。