この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
概要
以前の記事に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
のプロジェクトを作ります。