執筆者: いけちぃ
最終更新: 2021/08/12
プログラミングを始めた頃に構築した、Minecraftサーバのリモート制御ツールの開発体験記です。
Minecraft って、複数人でプレイすると沼ですよね。
今や大陸内には高速道路が広がり、複数の大陸間には水上橋が建築されています。(やばすぎ)
最初はローカルでサーバを建ててプレイしていましたが、
プレイヤーがどんどん増えてきたため、Google Cloud Platform(GCP)でサーバを立てることにしました。
これが、「情クラ!」という Minecraft サーバの誕生のきっかけです。
いつどこでも Minecraft サーバで遊べる環境が完成しましたが、しかし1つ大きな問題が発生しました。
Google Cloud Platform(GCP)が、まだ365日無料トライアルだった頃の話です。
当時は n1-standard-1
プラン(仮想 CPU 数: 1, メモリ: 3.75 GB)を使用していたため、大人数が長時間プレイしていると動作がもっさりしてくるのです。
サーバ管理者だった私は、毎度 SSH 接続してサーバを再起動していました。
ですがここは、ぜひ技術の力で課題解決をしよう!ということで、Minecraftサーバを誰でも簡単に制御できる、Webアプリケーションを作成することになりました。
最初に実装したのは、「ホーム」のお知らせ機能と「サーバ状況」の機能です。
モバイルファーストのデザインですが、レスポンシブ対応させています。
一応そういうブログ( ?)なので、今回 Minecraft サーバと連携したノウハウについて記述しておきます。
Minecraft サーバでは、ある特定のパケットを受け取ると、現在のサーバのステータスを返す機能が搭載されています。
これは、Minecraft のマルチサーバ 一覧画面などで使用されています。
まず、クライアント側が Handshake パケットを送信します。(以下 wiki の情報)
さらに続けてリクエストパケットを送信します。その応答パケットとして、サーバが以下のようなJSONを返します。
{
"version": {
"name": "1.16.1",
"protocol": 47
},
"players": {
"max": 12,
"online": 5,
"sample": [
{
"name": "minecraft_name",
"id": "4566e69f-c907-48ee-8d71-d7ba5aa00d20"
}
]
},
"description": {
"text": "Hello world"
},
"favicon": "data:image/png;base64,<data>"
}
また、このパケットを送信するためのライブラリとして、今回は以下のPHPライブラリを使用しました。
PHP-Minecraft-Query https://github.com/xPaw/PHP-Minecraft-Query
あまり技術的な話をすると長くなっちゃいそうなので、次に行きます。
他に実装した機能も紹介します。
今回サーバのコントロール機能が Web に公開されるので、ログイン機能を実装する必要があります。全員 Google アカウントを持っていたので、Firebase Authentication を使って Google ログインを実装します。
以下の公式ドキュメントを読むとよくわかりますよ(雑)
Firebase Authentication(Google) https://firebase.google.com/docs/auth/web/start?hl=ja
Minecraft サーバでは、Node.js + TypeScript でAPIを構築しています。
以下は Minecraft を起動するルーティングの一部抜粋です。
// --- Start Server ------------------------------------------------------------
router.post('/api/run/start', (req: express.Request, res: express.Response) => {
const schema = Joi.object({
user: Joi.string().required(),
})
const validation = schema.validate(req.body)
if (validation.error) {
post("不正なリクエストを拒否しました", "ユーザ固有IDが設定されていないリクエストが送信されました", 3)
res.status(400).send('Bad request')
return
}
statusAsync(req.body.user)
.then(() => {
startAsync(req.body.user)
.then(() => {
res.send('Success')
})
.catch((err) => {
if (err == 'failed due to run interval')
post("起動コマンドを拒否しました", "前回の処理の実行から" + process.env.WAIT_SECONDS_FROM_LAST_PROCESS + "秒経過していないため、コマンドを拒否しました。", 2)
else
post("起動コマンドを拒否しました", "サーバが既に起動しているため、起動コマンドを拒否しました。サーバとの同期ができていない恐れがあります。[Err: startAsync()]", 2)
res.status(400).send('Bad request')
})
})
.catch(() => {
post("起動コマンドを拒否しました", "既に起動しているため、起動コマンドを拒否しました。サーバとの同期ができていない恐れがあります。[Err: statusAsync()]", 2)
res.status(400).send('Bad request')
})
})
// -----------------------------------------------------------------------------
軽く説明していきます。
まず、Joi という npm パッケージを利用して、送信されたクエリパラメータなどのバリデーションを行います。
そして、post( ) という関数がありますが、これはサーバのエラーなどを Discord のサーバに通知するための関数として用意してあります。
server.jar は screen 上で走らせているのですが、screen の二重起動にならないように初めに起動してもよいかのチェックも行っています。
また、実行系はシェルにまとめてあります。
#!/bin/bash
JARFILE=/home/jokura_server/minecraft/server.jar
MEM=15000M
cd `dirname $0`
screen -UAmdS minecraft java -server -Xms${MEM} -Xmx${MEM} -jar ${JARFILE} nogui
上記のファイルは、起動用のシェルです。
他にも再起動用やバックアップ用などのシェルも用意してあります。(下記は 再起動用)
#!/bin/bash
WAIT=30
STARTSCRIPT=/home/jokura_server/minecraft/start.sh
SCREEN_NAME='minecraft'
screen -p 0 -S ${SCREEN_NAME} -X eval 'stuff "say '${WAIT}'秒後にサーバを再起動します\015"'
screen -p 0 -S ${SCREEN_NAME} -X eval 'stuff "say すぐに再接続可能になるので、しばらくお待ち下さい\015"'
sleep $WAIT
screen -p 0 -S ${SCREEN_NAME} -X eval 'stuff "stop\015"'
while [ -n "$(screen -list | grep -o "${SCREEN_NAME}")" ]
do
sleep 1
done
$STARTSCRIPT
今は残念ながら、Webサービスを終了しています。
(Minecraft の活動自体はしています!)
2021年4月から、Minecraft サーバを別のメンバーが自宅サーバで建ててくれることになりました。
やはり GCP などの従量課金制のサーバで運用するには、かなり気を使ってしまうので正直解放されました。
かといって、情クラ!での活動はまだまだ続けていく予定なので、ぜひこのブログでも活動を共有していけたらなと思います!
この人が書いた記事