JavaScript
Titanium
Socket.io
websocket

TitaniumでSocket.io 1.0に(無理矢理)対応する

More than 3 years have passed since last update.

Socket.ioが1.0になってしばらく経ちますが、Titaniumはまだ対応していません。いちおう、Socket.ioのGithubでもissueが上がって議論されてはいるものの、ネイティブ/JSのモジュールで対応できたという報告はありません。今のところ、0.9系で対応するしかなさそうです。

では全く使えないのかというと、そういうわけでもありません。WebViewの中では普通に動くので、それを利用するとSocket.ioのプロトコルについて悩んだりする必要もなく動作させることができます。ただし、バイナリデータをどう扱うのかといったことはまだ対応方法を考えていないので、完璧というわけではありません。

しかし、1.0から導入されたSocket.io Emitterのような便利な機能が使えるようになればサーバ側もクライアント側もどちらも幸せなので、試してみる価値はあると思います。

ざっとまとめたのでこちらに書いておきます。もう眠いから校正できない…


ローカルのHTMLファイルを準備

TitaniumのWebViewはセキュリティを考慮してローカルのHTMLファイルからのみアプリケーションレベルのイベントを呼び出すことができます。そこで、アプリのResources以下にHTMLファイルを用意します。Alloyならapp/assets以下になります。

<html><!--app/assets/local.html-->

<head>
<script src="https://production/server/socket.io/socket.io.js"></script>
<script src="https://production/server/script.js"></script>
</head>
</html>

ご覧の通り、ロードするJavaScriptがローカルにない場合でもHTMLファイルがローカルのファイルであればTi.App.addEventListener('イベント名')で設定するアプリケーションレベルのイベントを呼び出すことが可能です。サーバ上に処理の本体を設置しておくと更新が楽なので便利ですね。サーバ側のJavaScriptはこんな感じにしておきましょう。

var socket = io.connect("https://production/server:3001/", { 'forceNew':true });

socket.on('message', function(message){
Ti.App.fireEvent('ws:message', {message: message});
});

socket.on('force_reload', function(payload){
Ti.App.fireEvent('ws:force_reload', {payload: payload});
});

とりあえず2つのイベント(messageとforce_reload)に反応して、アプリケーションレベルのイベント(ws:messageとws:force_reload)を発火するようにしました。'forceNew':trueはセッションが切れた場合に再接続を試みるための設定です。

これを受けるアプリ側のJSを用意しましょう。


アプリ側の実装

//app/assets/websocket.js

function Websocket(){

var webview = Ti.UI.createWebView({
url: '/local.html',
top: 0, width: 0, height: 0
}); //見えねー

// 遠隔操作関数を用意してみる
Ti.App.addEventListener('ws:force_reload', function(){
webview.reload();
});
Ti.App.addEventListener('ws:message', function(e){
alert(e.message);
});

return webview;
}
module.exports = Websocket;

//app/alloy.js
Alloy.Globals.ws = (new (require('libs/websocket')));

//app/controllers/index.jsなど
$.index.add(Alloy.Globals.ws);

force_reloadを用意しておけば、例えばサーバ側でJavaScriptを更新した場合などに、接続中のすべての端末にブロードキャストで再読み込みするように強制することができます。

アプリ側の準備はこれで完了です。ちょっとどうしたんだというくらい何も特別なことはしていないのがおわかりいただけると思います。続いて試験用にサーバ側も準備しましょう。


Socket.io-emmiter

ところで、Websocketでサービスを提供しようとするとき、サーバ側はどうしようって悩むこと、ありませんか?例えば、Railsで運用しているウェブサービスがあって、そのクライアントアプリを作った場合、Websocketで何かしようとすると、まあ一番手っ取り早いのはwebsocket-railsだったりします(UPDATE: Rails 5にはAction Cableという機構が入るんだそうですね)。これ自体に不満はないんですが、例えばおそらくRailsアプリケーションで一番使われているであろうUnicornを既に使っている場合はUnicornをやめるか別途スタンドアローンのWS用サーバを起動しなければいけません。Unicornをやめるのは現実的でない環境もあるでしょう。かといって同じRailsのアプリケーションで2つのサーバを起動するのはちょっと気持ち悪いですよね。困ったものです。

それに、複雑なウェブアプリケーションを構築するのに、Node.jsではRailsに慣れてしまった身にはまだ面倒です。

そこで、本日登場していただくのがこちら、socket.io-emitterさんです。これはRedisのPub/Subを使ってSocket.ioのサーバとそれ以外のプロセスの間の通信を取り持ってくれるとっても便利な仕組みなんですよ。今ならなんと、RubyやPHP、ZeroMQなどからも利用できて大変お得!お支払いは月々2,400円の36回払い、分割手数料は誰かが負担します!

テレビショッピングの口調はここまでにします(あ、もちろんみんなフリーソフトウェアです)が、これを使って単純にSocket.ioのサーバをNode.jsで起動し、複雑なロジックはRailsで構築して、必要に応じてRails側からクライアントにメッセージを送信することにしましょう。

var app = require('express')();

var http = require('http').Server(app);
var redis = require('redis');
var redisAdapter = require('socket.io-redis');

app.get('/', function (req, res) {
//今は使わないけど、ほら、将来PCと連携とかもしたいじゃん?
res.sendFile(__dirname + '/index.html');
});

app.get('/script.js', function(req, res){
//上のHTMLで読み込ませていたファイルね
res.sendFile(__dirname + '/script.js');
});

var pub = redis.createClient();
var sub = redis.createClient(null, null, { detect_buffers: true });
var io = require('socket.io')(http, {
adapter: redisAdapter({ pubClient: pub, subClient: sub })
});

http.listen(3001, function () {
console.log('listening on *:3001');

io.on('connection', function (socket) {
console.log(socket.id + " is now online");
//本来はここで認証とか処理する
socket.emit('message', {message: 'よく来なさった'});

//今回はとりあえずこれだけ
socket.on('message', function (payload) {
console.log('message event', typeof(payload), payload);
socket.emit('message', payload);
});
});
});

ご覧の通り、単純に指定されたファイルの配信とSocket.ioのサーバの役割だけするスクリプトです。

Rails側はGemfileにgem 'socket.io-emitter'を追加しておきます。

class HogeController < ApplicationController

def index
emitter = SocketIO::Emitter.new
emitter.emit('message', Time.now.stftime("もんげー、今は%Y年%m月%d日の%H時%m分%S秒ずら"))
end
end

これでHogeControllerのindexにアクセスがあるたびに接続されたクライアントにmessageイベントが送信されます。

それから、面白いことにSocket.ioはチャンネルを指定してメッセージをブロードキャストする以外にも、Socketのインスタンス毎に付与されるユニークなIDを使って特定の端末だけとメッセージをやりとりする機能も用意されています。

emitter.to('-1234-your-socket-id').emit('message', DateTime.now.to_s)

もちろん、これだけでは全然意味がないアプリですが、この原理を応用してチャットやその他いろいろな機能を実装するための土台を作ることはできました。あとはアイデア次第なので、みなさんもぜひ試してみてください。