Ruby製WebSocketアプリを最速でHerokuにデプロイする5つのステップ Sinatra-websocket-templateの紹介
「テレビ放送が駄目になった」と言われて久しいですがその理由ははっきりしています。それは放送というものがリアルタイム・コンテンツを扱う媒体だからです。リアルタイム・コンテンツはユーザの自由を奪います。ある番組を見るためにユーザはその時間テレビの前に固定化されます。録画放送番組は字義的にはバッファード・コンテンツ1と言えますが、ユーザがそのコントロール権を持っていないつまりその視聴タイミングの制御を製作者側が持っているので、これはリアルタイム・コンテンツなのです。ユーザの唯一の武器はDVDレコーダによる制約の中のローカルバファリングのみです。
現在のWebは主としてバッファード・コンテンツを扱う媒体です。バッファード・コンテンツの世界ではユーザは好きな時間に好きなだけコンテンツを視聴できるという自由が与えられます。コンテンツの製作者側・提供者側にそのタイミングをコントロールする自由はありません。ユーザがコンテンツのコントロール権を持っている世界、それがWebなのです。
その一方でWebにも変化が訪れています。体験の共有に対する回帰です。リアルタイム・コンテンツの消費からバッファード・コンテンツの消費への移行でユーザが失ったもの、それが体験の共有です。体験の共有はコンテンツに対する消費の同時性つまりリアルタイム性を要求します。前述のようにこれはユーザにとっての制約です。しかしユーザは今、この制約の一部を受け入れてでもWeb上での体験の共有を求めるようになってきています。その結果としてWeb上のコンテンツもバッファード・コンテンツから偽リアルタイム・コンテンツにその一部が変質しつつあります。テキストコンテンツは頁という概念を失い、リアルタイム・コミュニケーションのために140文字の言葉のかけらに圧縮されました。
この変化をWebがすべてを飲み込んでいると見ることもできるでしょう。結局、この世界では誰もコントロール権など持っていない(いらない)、そう理解するのが正しいのかもしれません。
とか。
そんなわけで、WebSocketです。WebSocketはWebの世界で真のリアルタイムを実現します。そしてその準備は整いつつあります。
まずは体験です。さあ、次のリンクをクリックして真のリアルタイムの世界を、体験の共有を味わってください!
使い方の説明はこちら。
スイマセン…遊びに来てくれる人が少なくて…宣伝…。
まあ、でもこれがWebSocketによるリアルタイムWebの一サンプルです。リアルタイムWebの世界は未開拓の地です。発想次第でユニークなものが生まれる可能性があります。
Ruby製WebSocketアプリを最速でHerokuにデプロイする5つのステップ
さて、ここからが本題です。ふぅ。
Herokuでは現在試験的機能として、WebSocketを可能にするオプションが用意されています。これを使うとPusherなどの外部サービスに頼らずに、HerokuでホスティングされるWebアプリ上でWebSocketの機能を実現できるようになります。
加えて、HerokuはRuby(Sinatra)でWebSocketを使ったチャットシステムのサンプル&デモも公開しています。
で、この記事に従えばWebSocketのアプリを作って公開することが簡単にできるのですが、それでもやっぱり定型的なコードを一つずつ用意するのには骨が折れます。SinatraにはRailsのようなGeneratorもないですし。
そんなわけで…。
SinatraでWebSocketを作る際のスケルトンを生成するSinatra-websocket-template
というGemを作りました:-)
sinatra_websocket_template | RubyGems.org | your community gem host
Sinatra-websocket-template
は、bundle gem PROJECT
で生成されるファイル群に加えて、上記記事を参考にしたWebSocketに必要なファイル群を生成、出力します。ファイルにはWebSocketを実現するサンプルコードが載っているので、コードを一行も書くこと無くWebSocketの体験ができます。
以下では、このGemを使ってWebSocketアプリを作る手順および作ったWebアプリをHerokuにデプロイする手順を説明します。
STEP1: Sinatra-websocket-templateのインストール
Sinatra-websocket-template
をインストールします。
% gem install sinatra_websocket_template
Fetching: sinatra_websocket_template-0.0.1.gem (100%)
Successfully installed sinatra_websocket_template-0.0.1
1 gem installed
STEP2: プロジェクトの生成
sinatra_websocket_template
コマンドを実行してサブコマンドを確認します。
% sinatra_websocket_template
Commands:
sinatra_websocket_template help [COMMAND] # Describe available commands or one specific command
sinatra_websocket_template new PROJECT # Creates a skeleton for your project
sinatra_websocket_template version # Prints the SinatraWebsocketTemplate's version
new
サブコマンドにプロジェクトの名前を渡して、プロジェクトのスケルトンを生成します。
$ sinatra_websocket_template new hello_chat
create hello_chat/Procfile
create hello_chat/config.ru
create hello_chat/lib/app.rb
create hello_chat/lib/views/index.haml
create hello_chat/lib/public/css/main.css
create hello_chat/lib/public/js/application.js
create hello_chat/lib/hello_chat/backend.rb
create hello_chat/Gemfile
create hello_chat/Rakefile
create hello_chat/LICENSE.txt
create hello_chat/README.md
create hello_chat/.gitignore
create hello_chat/hello_chat.gemspec
create hello_chat/lib/hello_chat.rb
create hello_chat/lib/hello_chat/version.rb
Initializating git repo in /Users/keyes/Dropbox/playground/hello_chat
プロジェクトのディレクトリに移動して、生成されたファイル群を確認します。
% cd hello_chat
/hello_chat% tree
.
├── Gemfile
├── LICENSE.txt
├── Procfile
├── README.md
├── Rakefile
├── config.ru
├── hello_chat.gemspec
└── lib
├── app.rb
├── hello_chat
│ ├── backend.rb
│ └── version.rb
├── hello_chat.rb
├── public
│ ├── css
│ │ └── main.css
│ └── js
│ └── application.js
└── views
└── index.haml
6 directories, 14 files
折角だから、「dir_friend」を使って、ビジュアライズもします:-)
% dir_friend dot .
'a.dot' created in the current directory.
(参考:あなたはファイルシステムに美を見るか?)
この中でWebSocketを実現する重要なファイルは、lib/hello_chat/backend.rb
とlib/public/js/application.js
です。中身は後で確認するとして、先に進みます。
STEP3: ローカルでのプロジェクトの起動
まずはbundle install
します。
/hello_chat% bundle install
Fetching gem metadata from https://rubygems.org/...........
Fetching gem metadata from https://rubygems.org/..
Resolving dependencies...
Using rake (10.1.0)
Using bundler (1.3.5)
Using dotenv (0.9.0)
Using eventmachine (1.0.3)
Using websocket-driver (0.3.1)
Using faye-websocket (0.7.1)
Using thor (0.18.1)
Using foreman (0.63.0)
Using tilt (1.4.1)
Using haml (4.0.4)
Using rack (1.5.2)
Using puma (2.7.1)
Using rack-protection (1.5.1)
Using sinatra (1.4.4)
Using hello_chat (0.0.1) from source at /Users/keyes/Dropbox/playground/hello_chat
hello_chat at /Users/keyes/Dropbox/playground/hello_chat did not have a valid gemspec.
This prevents bundler from installing bins or native extensions, but that may not affect its functionality.
The validation message from Rubygems was:
"FIXME" or "TODO" is not a description
Your bundle is complete!
Use `bundle show [gemname]` to see where a bundled gem is installed.
アプリケーションに必要なGemsがインストールされました。プロセスマネージャのforemanを通してアプリを起動します。
/hello_chat% bundle exec foreman start
13:37:24 web.1 | started with pid 18722
13:37:27 web.1 | Puma starting in single mode...
13:37:27 web.1 | * Version 2.7.1, codename: Earl of Sandwich Partition
13:37:27 web.1 | * Min threads: 0, max threads: 16
13:37:27 web.1 | * Environment: development
13:37:27 web.1 | * Listening on tcp://0.0.0.0:5000
13:37:27 web.1 | Use Ctrl-C to stop
5000番ポートでサーバが起動したので、2つのブラウザを起動してアクセスします。
それぞれのブラウザでページ上のHello!ボタンを交互に押してみます。
WebSocketによる通信が成功しています。これでSinatra-websocket-templateで生成されるスケルトンで一応WebSocketが実現できることが確認できました。
STEP4: プロジェクトのコードを書く
生成されたスケルトンを土台にしてプロジェクトに必要なコードを書きます。あなたの出番です。
git commit
で準備が完了しました。
STEP5: Herokuへデプロイ
プロジェクトが完成したのでHerokuにアプリを作ります。
% heroku create hellochat
Heroku上のWebSocketの機能を有効にします。
% heroku labs:enable websockets
デプロイです。
% git push heroku master
% heroku open
これで完了です。
backend.rbとapplication.jsの中身
スケルトンにおけるbackend.rb(WebSocketにおけるサーバー側)
とapplication.js(クライアント側)
の中身だけ確認してみます。
backend.rbはRackのミドルウェアとして構成されています。
# backend.rb
require 'faye/websocket'
require 'json'
module HelloChat
class Backend
KEEPALIVE_TIME = 15
def initialize(app)
@app = app
@clients = []
end
def call(env)
if Faye::WebSocket.websocket?(env)
ws = Faye::WebSocket.new(env, nil, ping: KEEPALIVE_TIME)
ws.on :open do |event|
p [:open, ws.object_id]
@clients << ws
ws.send({ you: ws.object_id }.to_json)
@clients.each do |client|
client.send({ count: @clients.size }.to_json)
end
end
ws.on :message do |event|
p [:message, event.data]
@clients.each { |client| client.send event.data }
end
ws.on :close do |event|
p [:close, ws.object_id, event.code]
@clients.delete(ws)
@clients.each do |client|
client.send({ count: @clients.size }.to_json)
end
ws = nil
end
ws.rack_response
else
@app.call(env)
end
end
end
end
クライアント側からの通信がWebSocketに係る場合、Backend#callでそのイベントをソケット側にハイジャックし(Faye::WebSocket.websocket?(env))、ここでそのイベントに応じた処理を返します。例えば、クライアント側からWebSocketに係るメッセージが送信されてきた場合は、これをws.on :message
で受けて、そのメッセージを全クライントにブロードキャストしています。また、クライアントが立ち上げられたときは、これをws.on :open
で受けて、そのクライアントに自身の識別ID(object_id)を返すと共に、クライアントの総数をブロードキャストしています。
次に、application.js
のコードを見ます。
// application.js
function counterRefresh (count) {
$("#user-counter").val(count);
}
function appendMessage (userid, message) {
$("#message-box").append("<div class='message'><span class='user-id'>" + userid + ":</span> " + message + "</div>");
}
$("#count-button").click(function(event) {
var text = this.innerHTML;
var data = JSON.stringify({ userid: myid, text: text });
ws.send(data);
});
var myid;
var ws = new WebSocket(location.origin.replace(/^http/, 'ws'));
ws.onmessage = function(msg) {
var data = JSON.parse(msg.data);
if (data.you) { myid = data.you; }
else if (data.text) {
var id;
myid==data.userid ? id = 'my-message' : id = data.userid;
appendMessage(id, data.text);
}
else if (data.count) { counterRefresh(data.count); }
}
クライアント側では、WebSocketオブジェクトを生成してws.onmessage
にサーバー側からのメッセージを受けたときの処理を登録しています。そして受信データに含まれるプロパティに応じて処理を切り分けています。また、Hello!
ボタンがクリックされたときに,ws.send(data)
でサーバー側にメッセージを送信します。
以上で、説明は終わりです。
是非ともSinatra-websocket-template
をベースに何か面白いものを作ってくださいね。
=== Ruby関連電子書籍100円で好評発売中! ===
- 今勝手に考えた造語です。ユーザがランダムアクセス可能なリソースをイメージしています。 ↩
blog comments powered by Disqus