Amazon Linux 2 上に .NET 5 と MagicOnion を使ったゲームサーバー開発環境作ってみた #csharp

先日リリースされた .NET 5 SDKを使い、Amazon Linux 2上にMagicOnionを使ったゲームサーバー開発環境を作ってみました。
2020.12.08

こんにちは、事業開発部の高野です。このエントリは下記のAdvent Calendar 2020への、8日目の参加エントリです。

前日はそれぞれ

でした。

はじめに

クラスメソッドではゲーム領域も強化しようと、先日Cysharpの河合さん(@neuecc)を講師に、ゲームサーバーおよびその開発をC#で行うためのライブラリMagicOnionについて社内勉強会を開催しました(参考:Cysharpの河合様をゲスト講師にお招きしてゲームサーバーに関する社内勉強会を開催しました!)。

そんなおり、ちょうど先日.NET 5リリースされました。

そこで、本エントリではこの2つを使ったゲームサーバー開発環境を、Amazon Linux 2のEC2インスタンス上に構築してみようと思います。

目標

  • OSはAmazon Linux 2
  • .NET 5 SDKのCLIが実行できる
  • デスクトップ上でVisual Studio Codeが動作する
  • MagicOnionを使ったサーバー/クライアントサンプルアプリが動作する

TL;DR

  • Linuxデスクトップ環境を作成する
    • Amazon Linux 2 AMIをつかってEC2インスタンス作成
    • MATE、TigerVNC、xrdpインストール
  • 開発環境を整える
    • .NET 5、VSCodeインストール
  • MagicOnionを使ったサーバー/クライアントアプリのサンプルを作成する
    • MagicOnionのREADMEを参照してコード作成して実行
    • コード生成、ビルドなどはdotnetコマンドで行う

Linuxデスクトップ環境を作成する

Amazon Linux 2上で動くデスクトップ環境を作っていきましょう。動作実績のあるデスクトップ環境について調べたところ、MATEを使った公式AMIがあるようです。

ただ、上記の手順では、下記のエントリで紹介されている「.NET Core 3.1 LTS」を含んだAMIを使用するように記載があります。今回は.NET 5を使いたいため、素のAmazon Linux 2上にMATEを導入していきます。

.NET Core と Mono を備えた Amazon Linux 2 AMI に追加された MATE デスクトップ環境

Amazon Linux 2 EC2インスタンス作成

まず、Amazon Linux 2のEC2インスタンスを、AWSコンソールなどを使って作成します。特に気をつけるべき構成は下記のとおりです。記載がないところは適宜補ってください。

  • AMI: Amazon Linux 2 (AMI ID: ami-00f045aed21a55240)
  • インスタンスタイプ: t2.small
    • 今回はデスクトップ上でVSCodeを使って開発することを目標としているため、多少リソースに余裕がある状態がよい
  • セキュリティグループ (いずれもソースIPを絞ったほうが良いでしょう)
    • RDP (TCP 3389)を許可
      • 作成したデスクトップ環境にRDPプロトコルを使って接続するため
    • TCP 5000を許可
      • MagicOnionのサンプルアプリが5000ポートを使用するため

作成したEC2インスタンスが出来たら、ターミナルから接続しておきましょう。

MATEのインストール

Amazon Linux 2 ECインスタンスの準備ができたので、次にMATEをインストールします。Amazon Linux 2にMATEを導入する手順については、すでに弊社の望月がブログエントリにしていましたので、参考にします。

ターミナルから次のコマンドを実行してインストールします。

sudo amazon-linux-extras install -y mate-desktop1.x

インストールされたら、すべてのユーザーがMATEデスクトップセッションが使えるよう、下記コマンドも実行します1

sudo bash -c 'echo PREFERRED=/usr/bin/mate-session > /etc/sysconfig/desktop'

xrdpのインストール

今度はRDP接続できるようにしていきます。LinuxデスクトップにRDP接続するには、xrdpを使います。

xrdpのインストールはyumコマンドで行なえますが、

On RedHat and CentOS, make sure to enable EPEL packages first.

と記載がある通り、Extra Packages for Enterprise Linux (EPEL) リポジトリを有効にする必要があります。この方法は、弊社irbbbが下記エントリにまとめていましたので、参考にします。

Amazon Linux 2でEPELリポジトリを有効にするには、amazon-linux-extrasコマンドを使用します。

sudo amazon-linux-extras install -y epel

EPELリポジトリが有効になったら、xrdpをインストールします。

sudo yum install -y xrdp

xrdpはVNCの上で動くので、VNCサーバーもインストールが必要です。今回はTigerVNCをインストールします。

sudo yum install -y tigervnc-server

ここまで終わったら、xrdpを起動します。

sudo systemctl start xrdp
sudo systemctl enable xrdp

最後に、RDP接続するec2-userにはパスワードが必要なので、パスワードをリセットします。この手順は、下記のページを参考にします。

sudo passwd ec2-user

以上で、Linuxデスクトップ環境にRDP接続する準備が整いました。

RDP接続

それでは、完成したLinuxデスクトップ環境にRDP接続してみましょう。EC2インスタンスのパブリックIPを指定し、Macのリモートデスクトップアプリや、Parallels Clientなどで接続します。

接続時のユーザー名およびパスワードは、ec2-userおよび先程リセットしたパスワードを指定します。

01

接続すると次のようにデスクトップが表示されます。

02

以降はこのデスクトップのターミナルから操作してみましょう。左上のメニューバーから、[Applications]ー[System Tools]ー[MATE Terminal]を選び、起動しましょう。

03

04

開発環境を整える

Linuxデスクトップ環境が出来たので、今度はここに.NET 5の開発環境を作っていきましょう。.NET 5 SDKとVSCodeをインストールします。

.NET 5 SDKインストール

.NET 5を用いた開発を行うため、.NET 5 SDKをインストールします。公式サイトより、CentOS 7の手順を参考に行います。

sudo rpm -Uvh https://packages.microsoft.com/config/centos/7/packages-microsoft-prod.rpm
sudo yum install -y dotnet-sdk-5.0

VSCodeインストール

.NETおよびC#を用いて開発するなら、IDEが欠かせません。WindowsであればVisual Studio、MacであればVisual Studio for Macを使えばよいですが、これらはLinuxでは使えません。そこで、今回はVSCodeをその代わりに使うことにします。

VSCodeもyumでインストールできます。公式サイトの情報を元にインストールしましょう。

sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc
sudo sh -c 'echo -e "\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com/yumrepos/vscode\nenabled=1\ngpgcheck=1\ngpgkey=https://packages.microsoft.com/keys/microsoft.asc" > /etc/yum.repos.d/vscode.repo'
yum check-update
sudo yum install -y code

インストールすると、左上のメニューバーの[Applications]ー[Programming]ー[Visual Studio Code]が追加されます。

05

メニューから起動したら、C#を書くために必須のC#拡張をインストールしておきましょう。

06

これで、開発準備が整いました。

MagicOnionを使ったサンプルアプリ作成

開発準備ができたので、いよいよMagicOnionを使ったサンプルアプリ作成に入っていきましょう。

概ねMagicOnionのREADMEに則って次の順に作業していきますが、実際に進める中で気づいた辺を適宜補足しながら進めます。

  1. 作業フォルダ作成
  2. サーバーアプリセットアップ
  3. サービス定義作成
  4. サービス実装
  5. クライアントアプリ作成

作成したサンプルアプリはGitHubリポジトリにアップしてありますので、参考にしてください。

なお、以降のサンプルアプリのコードはMagicOnionのREADMEのものを使っているため、MagicOnionのライセンス(MIT)に準拠します。

作業フォルダ作成

実際にはどこでも良いのですが、下記のフォルダーをサンプルアプリの作業フォルダとして作成します。

mkdir -p ~/projects/magiconion-myapp
cd ~/projects/magiconion-myapp

VSCodeを起動し、作業フォルダを開いておくと、この後の作業がしやすくなります。

code .

サーバーアプリセットアップ

最初はMagicOnionのREADMEのSetup a project for MagicOnionを参考にサーバーアプリを作成します。

サーバーアプリ用フォルダを作成した後、dotnetコマンドを使ってgRPCプロジェクトを作成します。

mkdir MyApp.Server
cd MyApp.Server
dotnet new grpc

gRPCプロジェクトを作成すると、次のようなフォルダ、ファイルが生成されます。

07

MagicOnionのREADMEの手順に従い、MagicOnion.Serverパッケージをインストールします。

dotnet add package MagicOnion.Server

続いて、不要なProtosServicesフォルダを削除します。

rm -rf Protos/
rm -rf Services/

そして、MyApp.Server.csprojファイルを開き、Protobug要素を含むItemGroup要素を削除します(下記コードのハイライト部分)。

サンプルコード

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.32.0" />
    <PackageReference Include="MagicOnion.Server" Version="4.0.2" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\MyApp.Shared\MyApp.Shared.csproj" />
  </ItemGroup>

</Project>

そして、Startup.csファイルを編集します。

サンプルコード

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace MyApp.Server
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddGrpc();
            services.AddMagicOnion();   // add
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                // endpoints.MapGrpcService<GreeterService>();  // delete
                endpoints.MapMagicOnionService();

                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
                });
            });
        }
    }
}

ここまでやったところで、dotnet runコマンドを実行してサーバーアプリがコンパイルエラーとならず、起動できることを確認しておきましょう。

[ec2-user@ip-172-31-35-240 MyApp.Server]$ dotnet run
Building...
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /home/ec2-user/projects/magiconion-myapp/MyApp.Server

起動を確認したら、コマンド実行結果にも書いてあるとおり、Ctrl+Cでサーバーアプリを終了させてください。

サービス定義作成

次にImplements a service on MagicOnionを参考に、共有ライブラリプロジェクトを作成し、サービス定義を追加します。

まずは共有ライブラリプロジェクト用フォルダを作成後、dotnetコマンドを使いclasslibプロジェクトを作成します。

cd ~/projects/magiconion-myapp
mkdir MyApp.Shared
cd MyApp.Shared
dotnet new classlib

プロジェクト作成後のフォルダ構成は次のようになります。

08

共有ライブラリプロジェクトにMagicOnion.Abstractionsパッケージをインストールします。

dotnet add package MagicOnion.Abstractions

Class1.csファイルをIMyFirstService.csにリネームします。

mv Class1.cs IMyFirstService.cs

コードをMagicOnionのREADMEのものに書き換えます。

サンプルコード

using System;
using MagicOnion;

namespace MyApp.Shared
{
    public interface IMyFirstService : IService<IMyFirstService>
    {
        UnaryResult<int> SumAsync(int x, int y);
    }
}

dotnet buildコマンドでコンパイルエラーにならないことを確認します。

[ec2-user@ip-172-31-35-240 MyApp.Shared]$ dotnet build
Microsoft (R) Build Engine version 16.8.0+126527ff1 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  All projects are up-to-date for restore.
  MyApp.Shared -> /home/ec2-user/projects/magiconion-myapp/MyApp.Shared/bin/Debug/net5.0/MyApp.Shared.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:03.05

サービス実装

今度はサーバーアプリにサービス定義を使った実装を追加します。

まず、サービス定義を使えるよう、サーバーアプリプロジェクトに共用プロジェクトへの参照を追加します。

cd ~/projects/magiconion-myapp/MyApp.Server
dotnet add reference ../MyApp.Shared/MyApp.Shared.csproj

そして、サービス実装のため、MyFirstService.csファイルを作成します。

touch MyFirstService.cs

サンプルコード

using System;
using MagicOnion;
using MagicOnion.Server;
using MyApp.Shared;

namespace MyaApp.Server
{
    public class MyFirstService : ServiceBase<IMyFirstService>, IMyFirstService
    {
        public async UnaryResult<int> SumAsync(int x, int y)
        {
            Console.WriteLine($"Received:{x}, {y}");
            return x + y;
        }
    }
}

これでサービス実装も終わりです。再びdotnet runコマンドでサーバーアプリを実行しておきましょう。

クライアントアプリ作成

最後にClient-side: Call the service on MagicOnionを参考にクライアントアプリを作成し、実行中のサーバーアプリとの疎通を確認しましょう。

まず、クライアントアプリ用フォルダを作成し、dotnetコマンドでconsoleプロジェクトを作成します。

mkdir -p ~/projects/magiconion-myapp/MyApp.Client
cd ~/projects/magiconion-myapp/MyApp.Client
dotnet new console

作成直後のフォルダ構成は次のとおりです。

09

クライアントアプリには、dotnetコマンドを使いGrpc.Net.ClientパッケージとMagicOnion.Clientパッケージをインストールします。

dotnet add package Grpc.Net.Client
dotnet add package MagicOnion.Client

また、サーバーアプリと同様に共用ライブラリプロジェクトへの参照も追加します。

cd ~/projects/magiconion-myapp/MyApp.Client
dotnet add reference ../MyApp.Shared/MyApp.Shared.csproj

最後に、サーバーアプリを呼び出すコードを、Program.csファイルに書きます。こういう動作確認用の簡単なコードでは、C# 9で導入されたトップレベルステートメントが活きます。

なお、本来gRPCはHTTPSでの通信が基本ですが、エントリを書くための検証の中で、下記のような証明書関連のエラーが発生しサーバーアプリとの通信を確立することが出来ませんでした。

[ec2-user@ip-172-31-41-233 MyApp.Client]$ dotnet run
Unhandled exception. Grpc.Core.RpcException: Status(StatusCode="Internal", Detail="Error starting gRPC call. HttpRequestException: The SSL connection could not be established, see inner exception. AuthenticationException: The remote certificate is invalid because of errors in the certificate chain: PartialChain", DebugException="System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.
 ---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid because of errors in the certificate chain: PartialChain
   at System.Net.Security.SslStream.SendAuthResetSignal(ProtocolToken message, ExceptionDispatchInfo exception)
   at System.Net.Security.SslStream.ForceAuthenticationAsync[TIOAdapter](TIOAdapter adapter, Boolean receiveFirst, Byte[] reAuthenticationData, Boolean isApm)
   at System.Net.Http.ConnectHelper.EstablishSslConnectionAsyncCore(Boolean async, Stream stream, SslClientAuthenticationOptions sslOptions, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   at System.Net.Http.ConnectHelper.EstablishSslConnectionAsyncCore(Boolean async, Stream stream, SslClientAuthenticationOptions sslOptions, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.GetHttp2ConnectionAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at Grpc.Net.Client.Internal.GrpcCall`2.RunCall(HttpRequestMessage request, Nullable`1 timeout)")
   at MagicOnion.Client.ResponseContext`1.Deserialize()
   at MagicOnion.UnaryResult`1.UnwrapResponse()
   at <Program>$.<<Main>$>d__0.MoveNext() in /home/ec2-user/projects/MyApp/MyApp.Client/Program.cs:line 17
--- End of stack trace from previous location ---
   at <Program>$.<Main>(String[] args)

そのため、今回はHTTPでの通信でお茶を濁させていただきます。

サンプルコード

using System;

using Grpc.Net.Client;
using MagicOnion.Client;
using MyApp.Shared;

// Connect to the server using gRPC channel.
var channel = GrpcChannel.ForAddress("http://localhost:5000");

// Create a proxy to call the server transparently.
var client = MagicOnionClient.Create<IMyFirstService>(channel);

// Call the server-side method using the proxy.
var result = await client.SumAsync(123, 456);
Console.WriteLine($"Result: {result}");

準備が整いましたので、dotnet runコマンドでクライアントアプリを実行し、次のように結果が表示されれば成功です。

[ec2-user@ip-172-31-35-240 MyApp.Client]$ dotnet run
Result: 579

このとき、サーバーアプリ側のコンソールにも、クライアントアプリからの呼び出しについてログが出力されます。

info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 POST http://localhost:5000/IMyFirstService/SumAsync application/grpc -
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'gRPC - /IMyFirstService/SumAsync'
Received:123, 456
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'gRPC - /IMyFirstService/SumAsync'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished HTTP/2 POST http://localhost:5000/IMyFirstService/SumAsync application/grpc - - 200 - application/grpc 92.4599ms

PCからサーバーアプリを呼び出す

最後に、おまけとしてPCからサーバーアプリを呼び出すための方法についても触れておきます。

サーバーアプリの実行ログをもう一度見てみましょう。

info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001

書いてあるとおり、gRPCプロジェクト生成したデフォルトの状態では、localhostでしかリスニングしていないのです。したがって、PCから今回作成したクライアントアプリの宛先アドレスを変更して実行しても、サーバーアプリには接続できません。

本来gRPCはHTTPS上で通信をするのが基本です。しかし、今回はあくまでサンプルだということで、HTTPの5000番ポートでリッスンするため、サーバーアプリのProgram.csに手を入れましょう。下記のコードの24行目を追加してください。

サンプルコード

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

namespace MyApp.Server
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        // Additional configuration is required to successfully run gRPC on macOS.
        // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682
        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseUrls("http://*:5000");

                    webBuilder.UseStartup<Startup>();
                });
    }
}

変更したら、サーバーアプリをdotnet runコマンドで起動すると、次のようにHTTPの5000番ポートでリスニングしていることがわかります。

info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://[::]:5000

あとは、PC上のクライアントアプリプロジェクトのProgram.csにて、通信先IPアドレスをEC2インスタンスの「パブリック IPv4 アドレス」に変更して実行すれば良いです。

using System;
using Grpc.Net.Client;
using MagicOnion.Client;
using MyApp.Shared;

// Connect to the server using gRPC channel.
//var channel = GrpcChannel.ForAddress("http://localhost:5000");
var channel = GrpcChannel.ForAddress("http://52.194.245.209:5000"); // public IPv4 address

// Create a proxy to call the server transparently.
var client = MagicOnionClient.Create<IMyFirstService>(channel);

// Call the server-side method using the proxy.
var result = await client.SumAsync(123, 456);
Console.WriteLine($"Result: {result}");
takano.sho@xxxxx:~/dotnetproj/MyApp/MyApp.Client
$ dotnet run
Result: 579

まとめ

少し手間はかかりますが、案外簡単にAmazon Linux 上に.NET 5を用いたMagicOnion開発環境を作成できることがわかっていただけたのではないかと思います。

いずれもすでに用意されたリポジトリからパッケージをインストールできるようになっているため、少し工夫すれば簡単に開発環境を作って壊してができそうですね。

これであなたも楽しい.NET 5 + MagicOnion生活を!

次の日は

それぞれ下記のエントリです。どうぞお楽しみください!

参考


  1. この設定について一次情報らしきものを見つけられなかったので、見つけ次第追記します。