Socket.IO-Client for Unity3Dの紹介

はじめに

Socket.IO

ウェブ環境では、クライアントであるブラウザとサーバー間でプッシュやリアルタイムデータを処理する際、複数の方法を利用しています。その中で、Ajaxを利用したpolling、Long pollingとストリーミングの特徴は、下図のように比較できます。


出典:https://blogs.oracle.com/theaquarium/entry/slideshow_example_using_comet_dojo

このような方式を活用しているときに登場した標準が、WebSocketです。WebSocketは、既存のゲーム開発者が活用していたTCP Socketと同様にデータの送受信ができ、Pollingよりも自由にデータの送受信ができます。しかし、WebSocketの欠点は対応ブラウザがないことです。そこでブラウザに関係なくウェブでリアルタイムにデータを処理できるようにサポートするのがSocket.IOです。Socket.IO(http://socket.io)は、LearnBoostが開発したMITライセンスのオープンソースです。

Socket.IOは、WebSocket、FlashSocket、JSONPとLong Pollingを1つにまとめ、ブラウザでWebSocketに対応していなくても、リアルタイムのデータ処理をサポートします。さまざまなブラウザでデータを処理するだけでなく、データを送受信するためのメッセージプロトコルや、接続を維持するピンポンなどのように、Socket接続の基本的な機能を実装しており、開発者はリアルタイムデータ処理だけに集中することができます。Socket.IOのプロトコル定義は下記から参照できます。
https://github.com/socketio/socket.io-protocol
https://github.com/socketio/engine.io-protocol

Socket.IO-Client

近年、ゲームでもリアルタイムでデータを処理するためにWebSocketを多く活用する傾向にあります。なぜならTCP Socketより単純に実装できるからです。Unity3DでWebGLを構築する際にも、データの送受信にはWebSocketが利用されることがあります。Socketより実装しやすいWebSocketですが、実際にはデータ処理を用意するだけで、接続を締結したり、維持したりするネットワークレイヤーは開発者が実装する必要があります。しかしSocket.IOを利用すると、接続と基本的なプロトコルが実装されているので、開発者はコンテンツデータのやり取りだけに集中することができます。

Socket.IOはさまざまな言語に対応しており、Javascriptはもちろん、Java、SwiftとCPPをサポートしています。対応言語は以下のとおりです。

開発するまで

当社では主にウェブゲームを開発するとき、データをプッシュするのにSocket.IOを利用しています。既存のSocket.IO実装体はNode.jsを活用していましたが、Javaを活用して大容量のトラフィック処理とセキュリティ認証を追加したプラットフォームとして実装しました。このプラットフォームを社内共通としてプラットフォーム化し、ウェブゲームだけでなく、他のサービスにも適用して、ウェブとネイティブアプリ間のデータ通信に活用しています。

既存のSocket.IOクライアントは、さまざまなネイティブ言語に対応していましたが、ゲーム開発に多用されているUnity3Dはサポートしていません。サーバーは当社のプラットフォームもあり、オープンソースであるSocket.IO Node.js用サーバーもあるので、クライアントさえあればゲーム開発者も簡単に開発ができるでしょう。しかし、従来のUnity3D用のライブラリは、0.9バージョンであったり、またはメンテナンスされていない状態であったため、Socket.IO-Client Unity3Dライブラリを開発しました。

Socket.IOの使い方

実際にNode.js+Socket.IO-Client Unity3Dを使って実装されたサンプルコードを見ながら、Socket.IOの使い方を紹介します。サーバー/クライアントコードで構成されており、サーバーコードは、Socket.IO公式ホームページから抜粋/変更しました。

さらにNode.jsで直接サーバーコードを実行してみたい方は、下記リンクを参照するとサーバー開発環境を設定できます。
Node.js and Visual Studio Code End to End

サンプルを実装するソースは、下記リンクから確認できます。

サーバー接続

コネクションを結ぼう。サーバーはまずHTTPサーバーをオープンさせる必要があります。これはSocket.IOクライアントが最初はPollingモードで接続を試みるように実装されているためです。

サーバーコード[https://github.com/nhnent/socket.io-client-unity3d/blob/master/Assets/__Sample/Server~/connection.js]

var app = require('http').createServer(handler)
var fs = require('fs');

app.listen(4444);

function handler (req, res) {
  fs.readFile(__dirname + '/index.html',
  function (err, data) {
      if (err) {
          res.writeHead(500);
          return res.end('Error loading index.html');
      }

      res.writeHead(200);
      res.end(data);
  });
}

// socket.io スタート
var io = require('socket.io')(app);

// クライアントコネクションイベント処理
io.on('connection', function (socket) {
  console.log('Hello, socket.io~');
});

Socket.IOをスタートすると「Socket.IO」モジュールをインポートするとき、ワンショットで処理されます。このとき、引数の値にHTTPサーバーのインスタンス(app変数)を追加すると、HTTPサーバーのアドレスにSocket.IOがBindされ、自動でListenを開始します。このとき、リターンするio変数は一種のマネージャーオブジェクトとして、これを通じて実際のセッションに対応するSocketオブジェクトを取得できます。io.on()メソッドで「connection」というイベントを処理するように実装し、クライアントで接続が完了したら、コールバック関数を通じてSocketを獲得できます。Socketは一種のセッションオブジェクトと考えてもよいでしょう。Socketを通して、今後さまざまなパケットを送受信できます。

クライアントコード[https://github.com/nhnent/socket.io-client-unity3d/blob/master/Assets/__Sample/Client/Src/Connection.cs]

using UnityEngine;
using socket.io;

namespace Sample {

    public class Connection : MonoBehaviour {

        void Start() {
            // 接続URL
            var serverUrl = "http://localhost:4444";

            // サーバーで接続を試みる
            var socket = Socket.Connect(serverUrl);

            // 接続完了イベント処理
            socket.On("connect", () => {
                Debug.Log("Hello, socket.io~");
            });
        }

    }

}

クライアントは、Socket.IO.Socketクラスのconnect()メソッドを呼び出します。このとき、引数の値はサーバーのURLを入力する。接続が成功すればSocket変数を返して、サーバーと同じようにさまざまなパケットを送受信できます。サンプルコードでは「connect」イベントをキャッチしてウェルカムメッセージを出力するように実装してみました。

イベントハンドリング

Socket.IOはイベント基盤で、すべての通信処理は「イベント登録+コールバック関数の実装」形式で実装されます。イベントをSendするメソッドはEmit()、イベントをReceiveするメソッドはOn()である。次のサンプルで詳しく見てみよう。

サーバーコード[https://github.com/nhnent/socket.io-client-unity3d/blob/master/Assets/__Sample/Server~/events.js]

// HTTPサーバーコードは省略 (...)

// socket.ioスタート
var io = require('socket.io')(app);

// クライアントコネクションイベント処理
io.on('connection', function (socket) {

    // 'news' イベントsend
    socket.emit('news', { hello: 'world' });

    // 'my other event'イベントreceive
    socket.on('my other event', function(data) {
        console.log(data);
    });

});

クライアントコード[https://github.com/nhnent/socket.io-client-unity3d/blob/master/Assets/__Sample/Client/Src/Events.cs]

using UnityEngine;
using socket.io;

namespace Sample {

    public class Events : MonoBehaviour {

        void Start() {
            var serverUrl = "http://localhost:4444";
            var socket = Socket.Connect(serverUrl);

            // "news" イベント処理Receive
            socket.On("news", (string data) => {
                Debug.Log(data);

                // "my other event" イベントSend
                socket.Emit(
                    "my other event",       // イベント名
                    "{ \"my\": \"data\" }"  // データ (Jsonテキスト)
                    );
            });
        }

    }

}

2つのイベントを定義して、サーバーとクライアント間のパケットの送受信を実装しました。サーバーは「news」イベントをSendして「my other event」をReceiveします。クライアントは反対です。

実行結果

  • サーバー
  • クライアント

Emit()メソッド

Sendを担当するEmit()メソッドは、次のような形式です。

public void Emit(string evtName, string data);
  • Emit()パラメータ
evtName data
イベント名 イベントデータ(主にJsonオブジェクト)

On()メソッド

Receiveを担当するOn()メソッドの定義は次のとおりです。

public void On(string evtName, Action<string> callback);
  • On()パラメータ
evtName callback
イベント名 イベントハンドラ関数

システムイベント

Emit()とOn()メソッドで宣言したイベント名の一部の文字列は、システム予約語のため、使用を禁止します。
ここでクライアントのシステムイベントのみ整理します。

イベント名 コールバックパラメータ 備考
connect なし 接続完了
connectTimeOut なし タイムアウトによる接続失敗
reconnectAttempt なし 再接続
reconnectFailed なし 再接続失敗
reconnect int(再接続回数) 再接続完了
reconnecting int(再接続回数) 再接続中
connectError Exception 接続失敗
reconnectError Exception 再接続失敗

コールバックパラメータに基づいて、On()メソッドの呼び出し形式が変わります。たとえば「reconnect」イベントをハンドリングしたい場合は、次のようなコードを記述する必要があります。

socket.On("reconnect", (int attempt) => {
    Debug.LogFormat("Reconnected after {0} trials, attempt);
});

Event Ack

次にイベントをSendして、その応答をReceiveする方法を説明します。

サーバーコード[https://github.com/nhnent/socket.io-client-unity3d/blob/master/Assets/__Sample/Server~/acks.js]

// HTTPサーバーコードは省略 (...)
// socket.ioスタート
var io = require('socket.io')(app);

// クライアントコネクションイベント処理
io.on('connection', function (socket) { 

   socket.on('ferret', function (name, fn) {
        // 'woot'文字列をAckメッセージで送る
        fn('woot');
    }); 
});

クライアントコード[https://github.com/nhnent/socket.io-client-unity3d/blob/master/Assets/__Sample/Client/Src/Acks.cs]

using UnityEngine;
using socket.io;

namespace Sample {

    public class Acks : MonoBehaviour {

        void Start() {
            var serverUrl = "http://localhost:4444";
            var socket = Socket.Connect(serverUrl);

            socket.On("connect", () => {

                // "ferret" イベント Send
                socket.Emit(
                    "ferret", "\"toby\"", 
                    (string ackData) => { Debug.Log(ackData); } // 3番目の引数にコールバックをセッティングすると、Ackモードで動作し、Ackのときにコールバックが呼び出される
                    );
            });
        }

    }

}

先ほど紹介したイベントハンドリングのサンプルと異なる点は、クライアントコードでEmit()メソッドに3番目の引数としてコールバックを追加したことです。このように呼び出すとAckモードでEmit()メソッドが動作するようになります。上記サンプルでは、サーバーコードからAckメッセージで「woot」の文字列を返し、クライアントはAckコールバックから「woot」の文字列を引数の値として受信します。

Namespace

1つのソケットを論理的な処理単位に区分できます。Multiplexingとも呼ばれるこれらのパターンを通じて1つのコネクション(ソケットインスタンス)で複数の独立したコードを開発できます。

サーバーコード[https://github.com/nhnent/socket.io-client-unity3d/blob/master/Assets/__Sample/Server~/namespace.js]

// HTTPサーバーコードは省略 (...)
// socket.ioスタート
var io = require('socket.io')(app);

// chatネームスペース
var chat = io
  .of('/chat')
  .on('connection', function (socket) {
    chat.emit('a message', { everyone: 'in', '/chat': 'will get' });
    socket.emit('a message', { that: 'only', '/chat': 'will get' });
  });

// newsネームスペース
var news = io
  .of('/news')
  .on('connection', function (socket) {
    socket.emit('item', { news: 'item' });
  });

クライアントコード[https://github.com/nhnent/socket.io-client-unity3d/blob/master/Assets/__Sample/Client/Src/Namespace.cs]

using UnityEngine;
using socket.io;

namespace Sample {

    public class Namespace : MonoBehaviour {

        void Start() {
            var serverUrl = "http://localhost:4444";

            // newsネームスペース
            var news = Socket.Connect(serverUrl + "/news");
            news.On("connect", () => {
                news.Emit("woot");
            });
            news.On("a message", (string data) => {
                Debug.Log("news => " + data);
            });
            news.On("item", (string data) => {
                Debug.Log(data);
            });

            // chatネームスペース
            var chat = Socket.Connect(serverUrl + "/chat");
            chat.On("connect", () => {
                chat.Emit("hi~");
            });
            chat.On("a message", (string data) => {
                Debug.Log("chat => " + data);
            });
        }

    }

}

 

サーバーはof()メソッドを通じてネームスペースを宣言します。クライアントは単純にURLにアドレスを追加することで宣言が可能です。上記サンプルでは、”http://localhost:4444/chat”と、”http://localhost:4444/news”のように、chatとnewsのネームスペースを宣言しました。その他、残りは一般的なイベント処理と同じです。

実行結果

上記サンプルのポイントとしては、クライアントコードでchatとnewsを「a message」のイベントとして登録しましたが、chatソケットだけが「a message」のイベントを処理することになるという点です。ネームスペースに区分してイベント名を重複使用できることを示唆し、これを通じてチャネルあるいはコンテンツ開発のスコープごとに、それぞれのネームスペースを割り当てて作業すると、1つのソケットコネクションの中でも、イベント名が衝突せずにコードを書くことができます。

結論

Socketを利用してデータを送受信するには、ネットワークの連結維持とデータ転送のために、あまりにも多くの努力が必要となります。ゲーム開発において、困難なネットワークの実装を省き、コンテンツ開発に注力できれば、より良いゲームが出てくることでしょう。ウェブのように、さまざまなプラットフォームに対応したゲームを開発する上で、ネットワークレイヤーごとに開発が必要であれば、さらにゲームを開発できる時間を奪われてしまいます。そこで私たちは、ゲーム開発者がデータ通信に多くの時間を割くことなくゲームを開発できるように、Socket.IOのUnity3DをMITライセンスで公開することにしました。Socket.IO-Client-Unity3Dを利用して、コンテンツ開発にもっと時間を投資できるようにサポートできればうれしいです。

ライブラリに興味がある方には、https://github.com/nhnent/socket.io-client-unity3dを紹介したいと思います。また、参加したい方は、https://github.com/nhnent/socket.io-client-unity3d/blob/master/CONTRIBUTING.mdをご覧ください。

TOAST Meetup 編集部

TOASTの技術ナレッジやお得なイベント情報を発信していきます
pagetop