Minecraft などのゲームサーバーを無料で用意できるサービスを作った

2022/09/18に公開

概要

Minecraft に代表されるように、ゲームの中には複数人で遊ぶためにサーバーを用意しなければならないものがあります。
ゲームサーバーを用意するには Linux やネットワークの知識も必要になりますが、何よりもサーバー代がかかるのが問題です。

そこで、無料でゲームサーバーを用意できるサービスをつくってみました

この記事では作るのにあたった背景や使用した技術について紹介します

背景

ゲームを複数人で遊べるようにするためには、通常はサーバーが必要になります。サーバーはゲームの提供側が用意している場合もありますが、 Minecraft に代表されるように自分でサーバーを用意しなければならない場合もあります。
Minecraft に話を絞るといくつかの選択肢があります

Realms を使う

Minecraft 公式のサーバーとして Realms があります。簡単に使えますが最大 10 人まで、 MOD が使えないといった制約があります。
月額 904 円から利用できます。

サーバーを借りる

クラウドや VPS を借りて自分でサーバーを用意する方法です。サーバーの設定のために Linux やネットワークの知識が要求されるため少し難しいです。
ただし Conoha VPS のように簡単にサーバーを設定できる機能をもつサービスもあります。
人数や MOD の制限は特にありませんが、それ相応のスペックのサーバーを借りる必要があります。 ConoHa VPS の場合、 10 人遊べるスペックのサーバーの場合月額 2772 円かかります

自宅にサーバーを建てる

自宅のゲーミング PC や余っている PC をゲームサーバーとして使う方法です。クラウドや VPS を使ったサーバー構築に必要な知識に加え、自宅サーバーを構築するためのネットワークの知識が必要なため上級者向けです。
ファイアウォールの設定やポートフォワーディングの設定が必要になるかもしれませんし、 ISP から配られる IP アドレスがプライベート IP アドレスの場合はそもそも自宅でサーバーをたてることができません。

作りたいもの

サーバー代をどのように工面するかはサーバー管理者の悩みのタネです。ゲームで遊ぶのが自分だけだったら話は簡単なのですが、複数人でゲームを遊ぶときには誰にどれだけサーバー代を負担してもらうのかが難しかったりします。
そのため、自宅サーバーの設定をすごく簡単にできるようなサービスを作りたいなというのがモチベーションでした。

関連技術

ngrok について

ngrok というサービスがあります。これはローカルの HTTP サーバーをインターネットに公開できるサービスです。 OAuth のリダイレクト先を localhost にしたい、といった用途に便利です。

ngrok http 3000

というコマンドを実行すると、以下のような出力が得られます:

ngrok                                                                                          (Ctrl+C to quit)

Hello World! https://ngrok.com/next-generation

Session Status                online
Session Expires               1 hour, 59 minutes
Terms of Service              https://ngrok.com/tos
Version                       3.0.7
Region                        Japan (jp)
Latency                       -
Web Interface                 http://127.0.0.1:4040
Forwarding                    https://aaaa-bbbb-cccc-ddd-ee-fff.jp.ngrok.io -> http://localhost:3000

Connections                   ttl     opn     rt1     rt5     p50     p90
                              0       0       0.00    0.00    0.00    0.00
Forwarding                    https://aaaa-bbbb-cccc-ddd-ee-fff.jp.ngrok.io -> http://localhost:3000

とあるように、 https://aaaa-bbbb-cccc-ddd-ee-fff.jp.ngrok.io へアクセスすると http://localhost:3000 に HTTP 通信を中継してくれます。

これを実現しているのが ngrok edge です。以下は ngrok の Web サイトにある概念図です:

ngrok overview

ngrok edge の仕事は

  • https://aaaa-bbbb-cccc-ddd-ee-fff.jp.ngrok.io などの公開 URL を発行する
  • localhost:3000 とトンネルを張る
  • 公開 URL とトンネルの間で通信を中継する

ことです。
これらの機能を実装すれば ngrok のようなサービスを作ることができます。

ngrok クローン tunnelto

ngrok の昔のコードは以下のリポジトリで公開されています

https://github.com/inconshreveable/ngrok

ngrok は Go 言語で記述されているのですが、 Rust 言語を使いたかったので別の実装を参考にすることにしました。

tunnelto は Rust で書かれた ngrok ライクなサービスです。ソースコードの一部は以下のリポジトリで公開されています

実は ngrok は HTTP 以外にも TCP をサポートしているのですが、 tunnelto は HTTP のみサポートします。
そこで tunnelto のソースコードを改造して TCP と UDP をサポートするようにしました。

作ったもの

https://ownserver.kumassy.com/ で公開しています

cli とライブラリは Kumassy/ownserver に、 GUI は Kumassy/ownserver-client-gui にあります

cli

スクリーンショットは以下のようになります。 --local-port 3000 で 3000/TCP をインターネットに公開しています。
screenshot of Kumassy/ownserver

ownserver --payload udp --local-port 3000

とすることで 3000/UDP をインターネットに公開することもできます

GUI

このようなウィザードに沿って進めていくことでゲームサーバーを用意できます

screenshot of Kumassy/ownserver-client-gui

screenshot of Kumassy/ownserver-client-gui

screenshot of Kumassy/ownserver-client-gui

システム構成

システムの概要はこのようになっています:
system_architecture.png

ユーザーの認証のための Auth, TCP/UDP の中継をするための Server, Minecraft などのゲームサーバーである Local Game Server と、それを起動するユーザーである Client, Minecraft などのゲーム自体のクライアントである Remote User がいます。

図の矢印の方向は TCP コネクションを張る向きです。 Client -> Server, Remote User -> Server という向きになっているのがポイントです。 Remote User -> Server -> Client と張れれば話が早いのですが、 ClientServer の間には大抵 NAT やファイアウォールがあるので Client -> Server の向きで張る必要があります。

ClientServer の通信は WebSocket を使っています。 1 つの WebSocket の中で複数の Remote User の通信を扱う必要があるので、 Remote User ごとに Stream ID を発行して多重化します。また、 Remote User と WebSocket のソケットを紐付けるために Client にも Client ID を発行しています:
tunnel.png

Server には Client ID <-> WebSocket, Client ID <-> Stream ID, Stream ID <-> Remote User のソケットの変換テーブルがあるわけです。
通常はコネクションを 5 tuple (source IP, source port, dest IP, dest port, protocol) で識別しているのですが、ふたつの TCP コネクションを中継するために Client IDStream ID を使っています。

Auth の認証を通過してトークンをもらったあとの、TCP のトンネルを張るときの動作はこのようになります:
system_architecture_detailed.png

  1. WebSocket の接続を要求します
  2. WebSocket のコネクションが確立されます
  3. Remote User 用に TCP ポートを採番して Listen しておきます
  4. Client には採番した TCP ポートと Server の FQDN を伝えます
    1. Server の FQDN とポート番号は Client のユーザーが任意の手段で Remote User に伝えます
  5. Remote User が TCP 接続を開始します
  6. Stream ID が発行され、 WebSocket 経由で TCP パケットが Client に伝送されます
  7. ClientLocal Game Server に対して TCP 接続を開始します
    1. Client の中にも Local Game Server で使う TCP ソケットと Stream ID の変換テーブルがあります
  8. Local Game Server から TCP ACK が返ります
  9. WebSocket 経由で TCP ACK が Server に渡り、 ServerRemote User に TCP ACK を返します

以降は WebSocket を経由して Local Game ServerRemote User の間で TCP パケットが交換されます。

UDP の場合も通信の流れは同様です。

インフラ

Auth は AWS の Lambda と API Gateway を組み合わせて構築しました。その他のコンポーネントは Oracle Cloud で動作しています

採用技術

Rust

Rust 言語はメモリ安全性とパフォーマンスに力点をおいたプログラミング言語です。強力な型システムや clippy のおかげで厄介なバグに遭遇することはほとんどなく、開発体験は非常によかったです。

ただしデッドロックの発生には悩まれました。dashmap というデータ構造を利用したのですが、 getget_mut が特定の状況でデッドロックしてしまうという問題 (xacrimon/dashmap#79) を引き当てました。
最終的に dashmap を利用せず、 tokio::sync::RwLock<HashMap<K, V>> を使う方向でデッドロックの問題を回避しました。

tokio

デファクトスタンダードな非同期ランタイムです

tokio-tungstenite

snapview/tokio-tungstenite は WebSocket ライブラリの tungstenite-rs を tokio で利用できるようにしたものです。

tokio-tungstenite は Client - Server 間のトンネリングで利用しています。任意の TCP/UDP パケットは WebSocket のペイロードとして伝送されます。

tokio-console

tokio-rs/console は tokio のデバッグのためのツールです。
tokio::spawn して作成したタスクの実行状況を top コマンドのようにインタラクティブに確認することができます。

tokio-console

tokio-console は初めて使ったのですが、手放すことのできないツールになりました。 tokio-console を使うと、どのタスクがどの部分で時間を使っているか、そもそもタスクが実行されているのかどうかが一目瞭然です。
また、パフォーマンスのボトルネックを確認するときにも参考になりました。

tauri

Tauri は Rust で書かれたクロスプラットフォームの GUI フレームワークです。バイナリのサイズが小さく、速いことが特徴です。

Tauri に関しては手前味噌ですが私が書いた解説記事があるのでよければご参照ください:

Rust GUI の決定版! Tauri を使ってクロスプラットフォームなデスクトップアプリを作ろう
https://zenn.dev/kumassy/books/6e518fe09a86b2

Rust で書かれたライブラリと結合しやすいような GUI フレームワークを探していました。 Electron の Rust バインディングもありましたが、結合のしやすさから Tauri を採用することになりました。
Tauri では GUI 側から Rust の関数をそのまま呼び出せるので、 Rust のコードにほとんど手を加えることなく GUI を作ることができました。

少々厄介だったのが TypeScript - Rust 間のデータ変換です。 Rust の関数で Result<(), MyError> を返すこともできるのですが、 TypeScript 側で MyError 型を正しく解釈させるためには工夫が必要でした。具体的には Rust 側では

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum MyError {
    ErrorA {
        message: String
    },
    ErrorB {
        message: String
    },
}

のようなフォーマットでシリアライズするようにすると、 TypeScript 側では

export type MyError =
  | {
    kind: 'ErrorA',
    message: string
  }
  | {
    kind: 'ErrorB',
    message: string
  };

のような型定義が実現でき、 kind の値をもとに型を決定することができるようになります。

展望

OwnServer は本質的には自宅サーバーなので、サーバーをたてている人が PC を閉じてしまうとゲームで遊べなくなってしまいます。そこで、誰でもサーバーを建てられるようにセーブデータの共有機能を作る......かもしれません。

Discussion