2015年3月 3日

Node.js

Node.js+Socket.IOで作る、通信対戦ができるHTML5ゲームシステムの作り方

  • このエントリーをはてなブックマークに追加

おしゃべりリバーシトップ

はじめまして、Yahoo!きっず開発担当です。
昨年Yahoo!きっずにてブラウザオンラインゲーム「おしゃべりリバーシ」をリリースしました。

※おしゃべりリバーシは2015年10月をもってサービス終了しました。ご利用いただきまして、ありがとうございました。

このゲームは、通信対戦部分の通信プロトコルWebSocketを採用し、サーバミドルウェアにNode.js+Socket.IOを採用しました。この結果、ブラウザゲームでありながらマルチデバイスでの対戦を実現しました。

今回は、Node.js+Socket.IOによるWebSocketサーバのシステム構築方法について考慮すべきポイントを、「おしゃべりリバーシ」の実例を紹介しながら説明いたします。

システム構築の話をする前に 

システム構築の話に入る前に、そもそもWebSocketやSocket.IOを知らない人向けに簡単な概略を紹介します。

WebSocketとは

ブラウザ上での双方向通信を可能にした通信規格です。
Flashなどのプラグインを除くと、従来のブラウザによる通信規格はPull形式の通信しかできず、サーバからブラウザに対してPush通信をすることはCometやAjaxといった方法で疑似的に実現する方法しかありませんでした。

WebSocketはプラグインなしでの双方向通信を可能にし、大きなメリットとしてマルチデバイス・マルチブラウザでのサービス利用が可能になりました。

Socket.IOとは

Socket.IOは、Node.js用の双方向通信サポートライブラリで以下のような特徴があります。

  • 部屋分け機能、通信処理ごとのパス変更などのライブラリが豊富に用意されており、大規模なリアルタイム通信サーバ開発が楽になるためのフレームワークとなっている。

  • Java、Objective-C向けクライアントサイドライブラリもあり、アプリの通信ライブラリとしても利用可能。

  • レガシーブラウザでのComet切り替えやFlashによる通信など、他通信技術との併用が可能。

Socket.IOの導入サンプル:チャットアプリの例

簡単なチャットアプリのソースを紹介します。

実際に動かすためには、事前にNode.jsセットアップ、Socket.IOのインストールが必要です。
Node.jsのセットアップ・Node.jsのパッケージ管理システムのnpmの詳細な使い方は割愛しますので、Node.js公式ドキュメントなどを参考にして導入してください。

なお、Socket.IOはこちらのコマンドでインストールできます。必要に応じてグローバルインストールオプション(-g)をつけてください。

npm install socket.io
サーバサイドサンプル
//サーバ作成
var io = require('socket.io')(8080);

//クライアント接続があると、以下の処理をさせる。
io.on('connection', function (socket) {

  //接続通知をクライアントに送信
  io.emit("sendMessageToClient", {value:"1人入室しました。"});

  //クライアントからの受信イベントを設定
  socket.on("sendMessageToServer", function (data) {
      io.emit("sendMessageToClient", {value:data.value});
  });     

  //接続切れイベントを設定
  socket.on("disconnect", function () {
      io.emit("sendMessageToClient", {value:"1人退室しました。"});
  });
});
クライアントサイドサンプル

Ajaxと違い、WebSocketはクロスオリジンでも動作します。HTML配信はApacheなどでも問題ありません。

<!DOCTYPE html> 
<html>
<head></head>
<body>
  <ul id="msg_list"></ul>
  <form action="" method="post" onsubmit="return false;">
    <input type="text" class="text" id="message"/>
    <input type="submit" class="button" id="send" value="送信" />
  </form>
</body>
<script type="text/javascript" src="http://sample-domain.yahoo.co.jp/jquery.min.js"></script>
<!-- socket.ioのクラインアントライブラリを取得 -->  
<script src="http://sample-domain.yahoo.co.jp/socket.io.js"></script>
<script type="text/javascript">

//接続先の指定  
var url = "sample-websocket-domain.yahoo.co.jp:8080";

//接続
var socket = io.connect(url);   

//サーバから受け取るイベントを作成
socket.on("sendMessageToClient", function (data) {
    $("#msg_list").prepend("<li>" + data.value + "</li>");
});

//ボタンクリック時に、メッセージ送信
$("input#send").click(function(){
    var msg = $("#message").val(); 
    $("#message").val(""); 
    //サーバへ送信
    socket.emit("sendMessageToServer", {value:msg}); 
});
</script>
</html>
動作例

ブラウザを2つ立ち上げて見ると確認できます。

動作例

Node.js+Socket.IOによるシステム構築

それでは、実際にシステム設計やサービスとして運営するために考慮するべきものご紹介します。
なお、サーバのOSはCentOSを想定しています。

おしゃべりリバーシのアーキテクチャ実例

まずはじめに、おしゃべりリバーシでのアーキテクチャの概略をご紹介します。
下記が概略となります。

アーキテクチャ

大きく分類すると、ユーザーからの通信を受け付けるサーバ(Webサーバ・WebSocketサーバ)とデータ管理をするサーバ(MySQL、Redis)に分かれます。

データ管理に関して、MySQLとは別にRedisも採用しています。

Redisとは、NoSQLの流れをもつKey-Valueデータストアです。
詳細な機能の紹介は割愛しますが、メモリベースで管理できて入出力が高速なだけでなく多様な機能を持つため、多用途で利用できるのが特徴です。

「おしゃべりリバーシ」でRedisを採用した背景は下記の2つで、WebSocketサーバとともにシステムにおける大黒柱のような存在となっています。

  • 1:対戦情報のように永続性は低いものの更新頻度が高いデータを管理するため。
    Redisのようなメモリベースで管理して高速な入出力ができるものでないと、大幅な通信遅延や高負荷が発生しリアルタイムな対戦を実現ないためです。

  • 2:WebSocketサーバの負荷分散のため。
    ※こちらについては次の項目で後述します。

RedisでWebSocketサーバの負荷分散する

Node.jsはシングルプロセスでしか動作しないという特徴があります。
そのため、複数のCPUを積んだサーバの場合、性能を発揮しきれないという欠点があります。

一般的に複数CPUを積んだサーバの場合、Node.jsを複数プロセス起動することが対策として一般的です。
しかし、Socket.IOを利用する場合そのままの対策ではうまく行かないケースがあります。

そのうまくいかない仕組みとは通信確立(=ハンドシェイク)における独自のプロセスであり、それが下図になります。

Socket.IOでのハンドシェイクの仕組み

図のように、Socket.IOはWebSocket通信確立のハンドシェイク中、クライアントがサーバに対して断続的な通信を行いながら、アクセス先のNode.jsプロセスが必要な情報を集めセッションデータを作成することで通信を確立します。

これが、マルチプロセスの場合、下の図のようなトラブルが発生します。

Node.jsをマルチプロセスで動かしたとき、ハンドシェイクが失敗してしまう仕組み

図のように、Node.jsをマルチプロセスとして実行してしまうと、ハンドシェイク中の通信でさえもプロセス単位でのアクセス振り分けがされてしまいます。
つまり、ハンドシェイク時のクライアントからサーバへの2回目以降の通信の際、セッションを作成していないプロセスにアクセスしてしまう可能性が発生します。結果としてセッションを作成していないプロセスにアクセスしてしまうと、セッション情報が取得できず通信失敗となってしまいます。

この事象対策として登場するのが、Redisによるセッション管理です。それが下の図のようになります。

Redisによるスケールアウトのイメージ図

図のように、Redisにセッション管理を一元することで、別プロセスが作成したセッションでも管理することができ、ハンドシェイク失敗を防ぐことができます。 さらに、このスケールアウト機能は、プロセス間だけでなく、サーバー間でも行えるため、サーバー単位の負荷分散も実現できます。

現実に、Socket.IOではRedisにセッション情報を管理させることでスケールアウトする方法が公式的に奨励され、ライブラリとして存在しています。
※実際の方法については、利用ライブラリ、利用するSocket.IOのバージョンによって異なりますので、詳しくは公式ドキュメントを参照ください。

ファイルディスクリプタの設定による最大接続数の調整

サービスとして運用する場合、スケールアウトした場合であっても1つのサーバに大量の同時接続セッションが生まれることになります。
そのため大量接続を維持するためにはファイルディスクリプタ数の設定を変更する必要があります。

ファイルディスクリプタ数について、検証などで一時的に変更するケースと、サービス実運用として永続的に設定する方法があります。
また、現在の設定値はulimitコマンドで確認できます。

○実行例

$ ulimit -n
1024

一時的に変更する場合

一時的な変更はulimitコマンドで設定できます。ただし、実行中のシェルが終了するとこの設定は消えてしまいます。

#設定するためにrootユーザになります。
$ sudo su

$ ulimit -n 8192

サービス運用のために、設定値として維持したい場合

永続的に設定する場合、下記のファイルを修正する必要があります。

/etc/security/limits.conf

root、nobodyの上限を変えることで設定できるようになります。
※設定後はOSを再起動してください。

nobody  soft    nofile  8192
nobody  hard    nofile  8192

root    soft    nofile  8192
root    hard    nofile  8192

長期的な運用のためメモリリーク対策をする

Node.jsとSocket.IOを長期的に運用していると、メモリの増加が見られるケースがあります。
Socket.IOのメモリ管理については、バージョンごとにメモリ管理の改善をおこなっておりますがまだまだ課題となる部分でもあります。

「おしゃべりリバーシ」に関して、夜11時〜朝6時のメンテナンス時間にNode.jsを再起動するという方法で解決していますが、
負荷分散時に休眠サーバを作る・Node.jsのガーベージコレクタを手動実行するなど対策を考える必要があります。

終わりに

リアルタイム通信技術としてのWebSocketはまだまだ課題も多く、導入も難しい技術ですが、本記が皆様のWebsocket導入の一助になれば幸いです。

Appendix

Yahoo! JAPANでは情報技術を駆使して人々や社会の課題を一緒に解決していける方を募集しています。詳しくは採用情報をご覧ください。

  • このエントリーをはてなブックマークに追加