Amazon Linux 2 上に .NET 5 と MagicOnion を使ったゲームサーバー開発環境作ってみた #csharp
こんにちは、事業開発部の高野です。このエントリは下記のAdvent Calendar 2020への、8日目の参加エントリです。
前日はそれぞれ
でした。
はじめに
クラスメソッドではゲーム領域も強化しようと、先日Cysharpの河合さん(@neuecc)を講師に、ゲームサーバーおよびその開発をC#で行うためのライブラリMagicOnionについて社内勉強会を開催しました(参考:Cysharpの河合様をゲスト講師にお招きしてゲームサーバーに関する社内勉強会を開催しました!)。
そこで、本エントリではこの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ポートを使用するため
- RDP (TCP 3389)を許可
作成した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
および先程リセットしたパスワードを指定します。
接続すると次のようにデスクトップが表示されます。
以降はこのデスクトップのターミナルから操作してみましょう。左上のメニューバーから、[Applications]ー[System Tools]ー[MATE Terminal]を選び、起動しましょう。
開発環境を整える
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]が追加されます。
メニューから起動したら、C#を書くために必須のC#拡張をインストールしておきましょう。
これで、開発準備が整いました。
MagicOnionを使ったサンプルアプリ作成
開発準備ができたので、いよいよMagicOnionを使ったサンプルアプリ作成に入っていきましょう。
概ねMagicOnionのREADMEに則って次の順に作業していきますが、実際に進める中で気づいた辺を適宜補足しながら進めます。
- 作業フォルダ作成
- サーバーアプリセットアップ
- サービス定義作成
- サービス実装
- クライアントアプリ作成
作成したサンプルアプリは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プロジェクトを作成すると、次のようなフォルダ、ファイルが生成されます。
MagicOnionのREADMEの手順に従い、MagicOnion.Server
パッケージをインストールします。
dotnet add package MagicOnion.Server
続いて、不要なProtos
、Services
フォルダを削除します。
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
プロジェクト作成後のフォルダ構成は次のようになります。
共有ライブラリプロジェクトに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
作成直後のフォルダ構成は次のとおりです。
クライアントアプリには、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生活を!
次の日は
それぞれ下記のエントリです。どうぞお楽しみください!
- AWS & Game
- C# その2
参考
- AmazonLinux2 に RemoteDesktop でログインする - Qiita
- Amazon Linux 2 を実行している Amazon EC2 インスタンスへのグラフィカルユーザーインターフェイス (GUI) のインストール
- AmazonLinux2 に リモートデスクトップ接続 ( MATE ) - Qiita
- .NET Core での gRPC のトラブルシューティング | Microsoft Docs
- c# - How do I get the kestrel web server to listen to non-localhost requests? - Stack Overflow
- Amazon Linux Extras - よくある質問 - Amazon Linux 2 | AWS
- Amazon Linux 2 EC2 インスタンスに Extras Library からソフトウェアをインストールする
- CentOS、RHEL、または Amazon Linux を実行している EC2 インスタンスの EPEL リポジトリを有効にする
- この設定について一次情報らしきものを見つけられなかったので、見つけ次第追記します。 ↩