カードゲーム対戦サーバ構築への道 2 -Node.jsでP2Pの再現実験-


2024年12月28日
前回は準備ないし確認のような回でしたが、今回はP2Pサーバ構築に向けて本格的に作業を行います。
何故P2P技術を扱うか?
本プロジェクトの目標は対戦型カードゲームがいつでもどこでもできるようにすることです。
最近流行りのPokémon Trading Card Game Pocketやゴッドフィールドのようなゲーム環境を整えると言えばイメージが掴みやすいかと思います。
ここで問題になるのがサーバへの負荷です。
単純にサーバが全てのプレイヤーと繋がっている場合、サーバの負荷がプレイヤー数に比例して(もしくは指数関数的に)大きくなります。
この影響を少しでも軽減しようと考えた場合に考えつくのが、サーバを経由しないP2P形式です。
P2P形式でクライアント同士が接続すれば、サーバが対戦の橋渡しをする必要はなくなり、負荷も軽減します。
しかし、P2Pのサーバにもデメリットはあります。通信相手が増えると今度は"クライアントの負荷"が爆増します。当たり前ですがP2Pはクライアントがサーバの代わりに通信を担います。
例を挙げると、MinecraftなどMMO形式のゲームにはP2Pは向かないということです。(Minecraftの場合ワールドを保存する場所が必須な上、頻繁に更新されるという問題がある)
ただ、私が今作ろうとしているのは「カードゲーム」です。対戦は1対1で行われます。つまり、クライアントとしての通信負荷はサーバを相手している時と大して変わりません。
なので、P2Pに最適な環境であると言っても過言では無いでしょう。
P2Pの前提
P2Pと言っても「サーバが無い」状態を作ることはできません。マッチングサーバが必要です。
例えば同じゲームで対戦する際、どこかで相手に会って同意を取り、お互いのアドレスを交換しなければ、"そもそも何処と通信すれば良いか分からない"からです。
通常、ルータないしアクセスポイントを経由してインターネットに接続するクライアントはNAT(Network Address Translation)という機能により、窓口となるipアドレスがルータ(デフォルトゲートウェイ)のものに変更されています。
つまり、同じアクセスポイントに接続した2つのPCは外から見ると同じipアドレスに見えるということです。
これでは、この2台を繋げる事ができません。
この為、P2Pには往々にしてNAT越え(NAT Traversal)という技術が必要になります。
これを実現する方法がSTUN (Session Traversal Utilities for NATs)やTURN(Traversal Using Relays around NAT)といった技術です。
STUN
STUNは、外から見た自分のipアドレスを知るために利用します。
これにより、クライアントがNAT越えを必要とするネットワークに居るか否かを判別することができます。
STUNが返すipアドレスが自分自身に割り当てられたipアドレスと異なる場合は、NATが噛んでいるということになります。[1]
Node.jsによるP2Pビデオアプリを再現する(環境の確認)
WebサイトでP2P通信を行うため、Googleが開発を牽引しているWebRTCの仕組みを扱います。
WebRTCとは、Web Real-Time Communicationsの略称であり、リアルタイム通信を提供するオープンソースのプロジェクトです。[3]
WebRTCをNode.jsから利用し、P2P通信の実験をします。
今回は、Turtle-child-No2様の記事内容からビデオ通話アプリを再現しました。[4]
signalingサーバをNode.jsで立て、クライアントをnginxから立ち上げます。
Node.jsとnginxをつなぐ方法は前回と同様です。
再現内容は当該記事[4]のほぼコピペなので、変更した箇所以外の記載は省きます。
以下はNode.js側のsignaling.jsの変更箇所です。
'use strict';
var port = XXXX(ポート番号);
var fs = require('fs');
var app = require('express')();
var https = require('https');
const socketIO = require('socket.io');
const options = {
key: fs.readFileSync('鍵のファイルパス'),
cert: fs.readFileSync('鍵のファイルパス')
}
const server = https.createServer(options, app);
// nginxとNode.jsを接続する 詳しくは前回の記事へ
const socketOptions = {
cors: {
origin: ["https://flying-object-works.com","https://flying-object-works.com:XXXX(ポート番号)","http://flying-object-works.com", "http://flying-object-works.com:XXXX(ポート番号)"],
credentials: true
}
};
const io = socketIO(server, socketOptions);
server.listen(port, () => {
console.log(`Server running on port ${port}`);
});
/* 以下参考記事よりコピペなので記載なし */
変更したのは、Socket.IOのメソッドが一部利用できなくなっている[5][6]箇所です。
実験時の構築の概略図はこのようになっています。

実際に起動し、Webサイトからアクセスした結果としては、以下のようになりました。

Wi-FiにつないだノートPCとモバイル通信を用いたスマホからアクセスしています。
ipadのストップウォッチを映していますが、結構ラグがあるのが分かります。
これでサーバ構築の土台としては問題がないことが分かりました。
まとめ・次回予告
今回はNode.js・現在のサーバを用いてP2Pが作成可能であることを確認しました。
次回は細かい部分の仕様と利用する技術を考えなおします。
最後に予防線を張る形ですが、私自身勉強中のためもし表現や内容に誤りを発見した場合は連絡をよろしくお願いします。
参考
[1]:Qiita@okyk: 【初心者向け】STUN/TURNサーバをざっくり解説してみた
[2]:wikipedia: TURN(本記事上で参照なし)
[4]:Qiita@Turtle-child-No2: WebRTCを実装してみた
[5]:Qiita@toshikisugiyama(杉山 俊樹): メモ: Socket.IO の set と get メソッドは廃止されている
[6]:stackoverflow: socket.io socket.set and socket.get - what is the callback argument for?