Minecraft などのゲームサーバーを無料で用意できるサービスを作った
概要
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 edge の仕事は
-
https://aaaa-bbbb-cccc-ddd-ee-fff.jp.ngrok.io
などの公開 URL を発行する -
localhost:3000
とトンネルを張る - 公開 URL とトンネルの間で通信を中継する
ことです。
これらの機能を実装すれば ngrok のようなサービスを作ることができます。
ngrok クローン tunnelto
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 をインターネットに公開しています。
ownserver --payload udp --local-port 3000
とすることで 3000/UDP をインターネットに公開することもできます
GUI
このようなウィザードに沿って進めていくことでゲームサーバーを用意できます
システム構成
システムの概要はこのようになっています:
ユーザーの認証のための Auth
, TCP/UDP の中継をするための Server
, Minecraft などのゲームサーバーである Local Game Server
と、それを起動するユーザーである Client
, Minecraft などのゲーム自体のクライアントである Remote User
がいます。
図の矢印の方向は TCP コネクションを張る向きです。 Client -> Server
, Remote User -> Server
という向きになっているのがポイントです。 Remote User -> Server -> Client
と張れれば話が早いのですが、 Client
と Server
の間には大抵 NAT やファイアウォールがあるので Client -> Server
の向きで張る必要があります。
Client
と Server
の通信は WebSocket を使っています。 1 つの WebSocket の中で複数の Remote User
の通信を扱う必要があるので、 Remote User
ごとに Stream ID
を発行して多重化します。また、 Remote User
と WebSocket のソケットを紐付けるために Client
にも Client ID
を発行しています:
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 ID
や Stream ID
を使っています。
Auth
の認証を通過してトークンをもらったあとの、TCP のトンネルを張るときの動作はこのようになります:
- WebSocket の接続を要求します
- WebSocket のコネクションが確立されます
-
Remote User
用に TCP ポートを採番して Listen しておきます -
Client
には採番した TCP ポートとServer
の FQDN を伝えます-
Server
の FQDN とポート番号はClient
のユーザーが任意の手段でRemote User
に伝えます
-
-
Remote User
が TCP 接続を開始します -
Stream ID
が発行され、 WebSocket 経由で TCP パケットがClient
に伝送されます -
Client
はLocal Game Server
に対して TCP 接続を開始します-
Client
の中にもLocal Game Server
で使う TCP ソケットとStream ID
の変換テーブルがあります
-
-
Local Game Server
から TCP ACK が返ります - WebSocket 経由で TCP ACK が
Server
に渡り、Server
はRemote User
に TCP ACK を返します
以降は WebSocket を経由して Local Game Server
と Remote User
の間で TCP パケットが交換されます。
UDP の場合も通信の流れは同様です。
インフラ
Auth
は AWS の Lambda と API Gateway を組み合わせて構築しました。その他のコンポーネントは Oracle Cloud で動作しています
採用技術
Rust
Rust 言語はメモリ安全性とパフォーマンスに力点をおいたプログラミング言語です。強力な型システムや clippy のおかげで厄介なバグに遭遇することはほとんどなく、開発体験は非常によかったです。
ただしデッドロックの発生には悩まれました。dashmap というデータ構造を利用したのですが、 get
や get_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 を使うと、どのタスクがどの部分で時間を使っているか、そもそもタスクが実行されているのかどうかが一目瞭然です。
また、パフォーマンスのボトルネックを確認するときにも参考になりました。
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