チャットやオンラインゲームなどのアプリではリアルタイム通信機能が必要になります。実装の難易度が高そうに感じられるかもしれませんが、Monacaアプリで使われるHTML5であれば、WebSocketという技術を使うことで比較的簡単に実現できます。

こちらの記事ではWebSocketの概要と、その導入方法について紹介します。

WebSocketとは

WebSocketは読んで字のごとく、Web上でソケット通信を実現する技術になります。これまでのHTTP通信では一回の接続ごとに情報が送受信されて切断される仕組みでした。そのため、サーバ側からクライアントに対して情報を送る(プッシュする)のが困難でした。

サーバ側から情報をプッシュする技術として、WebSocket登場以前にも幾つかの方法がありました。

multipart/x-mixed-replace

Firefoxの前身であるWebブラウザ、Netscapeを開発していたNetscape社が実装していたサーバプッシュ方式です。Webカメラの映像を書き換える際など、MJPEG over HTTPに使われています。IEでは実装されていません。

Java/Flash

JavaやFlashといったプラグインを使った技術でもリアルタイム通信機能が実現されていました。ともあれ、HTML5の時代になってプラグインは使われないようになっていますので過去の技術になってしまっています。

Comet

ロングポーリングと呼ばれる手法でHTTP接続を長時間持続させつつサーバ側からメッセージを送信、さらに再接続します。ただしコネクションが張り続けられることになるので大量のクライアントからの接続に対応するのは難しいと言えます。

実装方法について

こうした歴史がある中で、HTML5の時代になって生み出されたのがWebSocketになります。プロトコルはwsまたはwss(WebSockets over SSL/TLS)を指定します。JavaScriptで書くと次のようになります。

let connection = new WebSocket('ws://example.com/');

処理はすべてコールバックで実装していきます。

// 接続した際のコールバック
connection.onopen = function () {
  // メッセージの送信
  connection.send('こんにちは世界');
};

// エラーが出た場合
connection.onerror = function (error) {

};

// メッセージを受け取った場合
connection.onmessage = function (e) {

};

基本的にはテキストメッセージを送るかと思いますが、最新の仕様ではバイナリも送受信できるようになっています(参考:WebSocket の導入: ウェブにソケットを実装する - HTML5 Rocks)。

// Canvasタグの内容をArrayBufferとして送る場合
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var i = 0; i < img.data.length; i++) {
  binary[i] = img.data[i];
}
connection.send(binary.buffer);

// ファイルで指定された内容をBlobとして送る場合
var file = document.querySelector('input[type="file"]').files[0];
connection.send(file);

// 受け取り側。arraybufferまたはblobが指定できます
connection.binaryType = 'arraybuffer';
connection.onmessage = function(e) {
  console.log(e.data.byteLength); // ArrayBuffer object if binary
};

JSONを送る場合には文字列に変換する必要があります。

connection.send(JSON.stringify(json_object));

逆にメッセージを受け取った方ではJSON.parseを使って文字列からJSONオブジェクトに変換します。

connection.onmessage = function(message) {
  var data = JSON.parse(message.data);
}

WebSocketサーバを用意する

WebSocket通信を行う際には通常のWebサーバに変わる、WebSocketサーバを用意しなければなりません。有名なところとしては下記があります。

元々Node.jsが出てきた時にチャット実装が目立っていたこともあってNode.jsによる実装がよく見かけられます。ついでEventMachineを使ったRubyベースのWebSocketサーバも数多くあります。なお、Ruby on Rails5からはAction Cableという機能によって簡単にWebSocketが使えるようになっています。セッションデータ(実際にはCookieを利用)も使えるので、認証情報をWebサーバ側と共通で利用できます。もしRuby on Railsでシステムを構築しているならば別途WebSocketサーバを立てるのではなくAction Cableを使うのが手軽でしょう。

WebSocketを別サーバで立てる場合、最も簡単に立てられるのがHerokuを使った方法になります。今回は heroku-examples/ruby-websockets-chat-demo を使って紹介します。これはRubyベース、faye-websocketを使ったWebSocketサーバになります。

本記事公開時点で、Ruby 2.1が非サポートとなったことにより上記アプリケーションがデプロイできなくなったようです。
Ruby 2.2系にアップデートしたものを用意しましたので、こちらをご利用ください。

デプロイについて

ruby-websockets-chat-demo のデプロイはとても簡単です。goofmint/ruby-websockets-chat-demoに行き、[Deploy to Heroku] をクリックするだけです。

こちらにそのライブデモサーバがあります

すぐにメッセージを送ることができます。

フロントエンドについて

まずこの仕組みですが、トップページにてapplication.jsが読み込まれています。この中でWebSocketのコネクションを有効にしています。

var scheme   = "wss://";
var uri      = scheme + window.document.location.host + "/";
var ws       = new WebSocket(uri);

後はメッセージを受け取った時に画面に描画するコードがあります(分かりやすいように一部修正)。

ws.onmessage = function(message) {
  var data = JSON.parse(message.data);
  // 画面に描画
  $("#chat-text").append(`<div class="panel panel-default">
    <div class="panel-heading">${data.handle}</div>
    <div class="panel-body">${data.text}</div></div>`);
};

さらにチャットの送信イベントがあります。

$("#input-form").on("submit", function(event) {
  event.preventDefault();
  var handle = $("#input-handle")[0].value;
  var text   = $("#input-text")[0].value;
  ws.send(JSON.stringify({ handle: handle, text: text }));
  $("#input-text")[0].value = "";
});

これらの処理をMonacaアプリで実装すれば同じようにチャット機能が使えます。より適切な実装としては ws.onclose(接続が閉じられた際のイベント)が呼ばれた際にはsetIntervalを使って定期的に接続し直すのが良いでしょう。

バックエンドについて

次にバックエンドの実装は ruby-websockets-chat-demo/chat_backend.rb at master · heroku-examples/ruby-websockets-chat-demo が参考になります。

この例では faye-websocket を使っていますので、Faye::WebSocket.new にてWebSocketサーバを立てます。後はJavaScript側と同じようにopen/message/closeといった各イベントについて処理を実装していきます。このチャットデモではデータをRedisに保存しています。

KEEPALIVE_TIME = 15
CHANNEL        = "chat-demo"

ws = Faye::WebSocket.new(env, nil, {ping: KEEPALIVE_TIME })

# WebSocketサーバが立ち上がった時のイベント
ws.on :open do |event|
  p [:open, ws.object_id]
  @clients << ws
end

# メッセージを受け取った時のイベント
ws.on :message do |event|
  p [:message, event.data]
  @redis.publish(CHANNEL, sanitize(event.data))
end

# 接続が閉じた時のイベント
ws.on :close do |event|
  p [:close, ws.object_id, event.code, event.reason]
  @clients.delete(ws)
  ws = nil
end

ws.rack_response

このチャットデモはHerokuにデプロイしていますが、もちろん自社のサーバにデプロイもできます。その際にはRedisのインストールをし、次のようにコマンドを実行します。 .env.sample を参考に、RedisのURLやWebサーバのポート番号の変更を忘れずに行ってください。

$ bundle
$ foreman start

WebSocketサーバはWebサーバとは別で立ち上げる必要があるので敷居が高いと感じるかも知れません。また、安定的に稼働し続けるためには運用上の慣れも必要です。しかしリアルタイム通信機能を組み込むことでより発展的なアプリを開発することができます。今回の記事が皆様のお役に立てば幸いです。

次回は、WebSocketサーバを利用してMonacaでチャットアプリを作る内容を予定しています。今回軽く触れたフロントエンドの処理について詳しく解説しますので、ご期待ください。