0

sinatra/rocketio/clientとem-rocketio-client作った

Sinatra RocketIOのRubyクライアントを作った。
Thread版とEventMachine版がある。
コレを使うとサーバーにwebブラウザ以外からデータを投入したり取得したりできて便利。
ワーカーに作業を任せるのにも使えなくもないかも。


Thread版

sinatra-rocketio gemに付属してる。

Sinatra::RocketIO::Client · shokai/sinatra-rocketio Wiki


例えばhttp://rocketio-chat.herokuapp.com/のCUIクライアントを作る例

#!/usr/bin/env ruby
require 'rubygems'
require 'sinatra/rocketio/client'

io = Sinatra::RocketIO::Client.new('http://rocketio-chat.herokuapp.com').connect

io.on :connect do |session|
puts "#{io.type} connect!! (session_id:#{session})"
end

io.on :chat do |data|
puts "<#{data['name']}> #{data['message']}"
end

io.on :error do |err|
STDERR.puts err
end

io.on :disconnect do
puts "disconnected!!"
end

loop do
line = STDIN.gets.strip
next if line.empty?
io.push :chat, {:message => line, :name => name}
end
Threadなので、最後にloopで止めないとRubyが終了する。


デフォルトでwebsocketを先に試すので、最初からcometを使いたいのならtypeオプションを渡す。
io = Sinatra::RocketIO::Client.new('http://rocketio-chat.herokuapp.com', :type => :comet).connect


仕様

コンストラクタにアプリのURLを渡すと、 /rocketio/settingsというJSONを読みにいってそのアプリでcometとwebsocketが使えるか確認する(RocketIOは設定でcometのみとかできる)

settingsの読み込みに失敗すると10秒ぐらい待ってからまた見に行く。

websocketとcometが両方使えるなら、websocketから接続を試す。接続できなかったらcometを使う。

cometにはHTTParty、websocketにはwebsocket-client-simpleを使っている。
どちらも切断されても自動的に再接続しにいく。


EventMachine版


EM::RocketIO::Client · shokai/sinatra-rocketio Wiki
shokai/em-rocketio-client · GitHub

eventmachine版の方が高速に動作する。
通信にem-http-clientとem-websocket-clientを使っているが、thread版と動作は同じ。


インストール
gem install em-rocketio-client


同様にチャットクライアントを作ってみる

require 'rubygems'
require 'eventmachine'
require 'em-rocketio-client'

url = 'http://rocketio-chat.herokuapp.com'
EM::run do
io = EM::RocketIO::Client.new(url).connect

io.on :* do |event_name, data|
puts "(#{io.type}) #{event_name} - #{data.inspect}"
end

EM::defer do
loop do
line = STDIN.gets.strip
next if line.empty?
io.push :chat, {:message => line, :name => name}
end
end
end

*で全イベントを取得できる。


こっちは下の2つのgemを合体させて実装している。
shokai/em-cometio-client · GitHub
shokai/em-websocketio-client · GitHub

0

websocket-client-simple 作った

eventmachineに依存していないruby用websocketクライアントでまともなのが無いので作った。

shokai/websocket-client-simple · GitHub

既にwebsocket-clientというgemがあるけど、ドラフト0しか実装されていないしpull requestできるリポジトリが無かったので作った。
名前はwebsocket-client2というnode臭がするのと後ろにliteとかsimpleとか付けるperl臭がするのと、わけわからない名前付けるruby臭で迷った。


インストール


gem install websocket-client-simple


使い方


JavaScriptのWebSocketと同じように使える
require 'rubygems'
require 'websocket-client-simple'

ws = WebSocket::Client::Simple.connect 'http://example.com:8888'

ws.on :message do |msg|
puts msg.data
end

ws.on :open do
ws.send 'hello!!!'
end

ws.on :close do |e|
p e
exit 1
end

loop do
ws.send STDIN.gets.strip
end
threadで動くので、最後にloopしておかないとRubyが終了してしまいます


サンプルに、
  • rubyで書いたwebsocketチャットサーバー
  • webブラウザ用チャットクライアント
  • websocket-client-simpleで書いたチャットクライアント
を入れておいた。



websocketのデータのparseにはimanel氏のwebsocketというgemを使った。
とても良くテストなども書かれていてメンテされているので、TCPSocketをそれに食わせるだけでクライアント書けた。

あとちゃんと試してないけど多分
ws.send binary_data, :type => :binary
でバイナリ送信できると思う。

0

Sinatra RocketIOで家のRaspberry PiのArduinoをリアルタイムで見る

RocketIOのサンプルとして、それなりに激しい処理が低スペックマシンで動くかテストするために作った。

http://status.shookai.org

ソースコード
https://github.com/shokai/rocketio-arduino-sample


こういうのが表示されてるはず


物理的な意味ではこうなってる
R0020611


Arduino Firmata on Rubyで、ADコンバータに温度センサと明るさセンサを付けたArduinoを読んで0.3秒毎にSinatra RocketIOでブラウザにpushしている。

5秒毎にCPUとメモリ使用量もpushしてる。
あと新規接続/切断がある度に全クライアントに接続数を通知してる。


60行ぐらいのSinatra40行ぐらいのJavaScriptでできてる。


JavaScript側を抜粋すると、こんな感じでそれぞれイベント待ちしておいて

io.on("arduino", function(data){
$("#temp").text("気温:"+data.temp+"度");
$("#light").text("明るさ:"+data.light);
});

io.on("stat", function(data){
$("#cpu").text("CPU:"+data.cpu+"%");
$("#mem").text("メモリ:"+data.mem+"%");
});

io.on("clients", function(data){
$("#websocket").text("websocket:"+data.websocket);
$("#comet").text("comet:"+data.comet);
});


サーバー側はsinatraの中にArduinoでセンサーを読むEventMachineのタイマーをベタ書き。
io = Sinatra::RocketIO

## Arduino sensors
io.once :start do
begin
arduino = ArduinoFirmata.connect ENV['ARDUINO'], :eventmachine => true
rescue => e
STDERR.puts "#{e.class} - #{e}"
next
end
EM::add_periodic_timer 0.3 do
light = arduino.analog_read 0
$logger.debug "light : #{light}"
temp = arduino.analog_read(1).to_f*5*100/1024
$logger.debug "temperature : #{temp}"
io.push :arduino, :temp => temp, :light => light
end
end
CPUとメモリは同じ要領で、psコマンドの結果をparseして送ってる。


Sinatra側で定期的に行う処理は、 Sinatra::RocketIO.once :start の中で登録すると良いです。
“start”はeventmachineとwebsocketサーバーの準備が整ったら発火するイベント。
onではなくonceなのは1回目だけ呼ばれるという意味。


あと、Arduino Firmataの最近のバージョンからeventmachine用の起動オプションを付けた。
ArduinoFirmata.connect "/dev/tty.usbデバイス名", :eventmachine => true
EM::runの中でこうやって起動するとThreadのかわりにeventmachineで動くようになった。
Sinatraの中で動かすときはこうしないと、タスク切り替えがうまくいかない。

0

Sinatraのroutesを直接操作する

get ‘/’とかpost ‘/message’ とかDSLで登録したものをroutesという。

Sinatra::Base#routesにHashがあって、これを直接のぞける。
logoutとかloginとか、すでにrouteとして登録されている名前でユーザ名を作れないようにするとかに使えると思う。



リクエストメソッドをkeyとしたhashの子要素に配列の配列があって、1つ目の要素がパスにマッチするかどうか判定する正規表現。

{"GET"=>[[/^\/cometio\/cometio(?:\.|%2E)js$/, [], [], #<Proc:0x007fbd5cab8d78@/Users/sho/.rvm/gems/ruby-1.9.2-p318/gems/sinatra-1.3.6/lib/sinatra/base.rb:1293>], [/^\/cometio\/io$/, [], [], #<Proc:0x007fbd5cab6d98@/Users/sho/.rvm/gems/ruby-1.9.2-p318/gems/sinatra-1.3.6/lib/sinatra/base.rb:1293>], [/^\/websocketio\/websocketio(?:\.|%2E)js$/, [], [], #<Proc:0x007fbd5a1d8598@/Users/sho/.rvm/gems/ruby-1.9.2-p318/gems/sinatra-1.3.6/lib/sinatra/base.rb:1293>]], "HEAD"=>[[/^\/cometio\/cometio(?:\.|%2E)js$/, [], [], #<Proc:0x007fbd5cab7608@/Users/sho/.rvm/gems/ruby-1.9.2-p318/gems/sinatra-1.3.6/lib/sinatra/base.rb:1293>], [/^\/cometio\/io$/, [], [], #<Proc:0x007fbd5cab6500@/Users/sho/.rvm/gems/ruby-1.9.2-p318/gems/sinatra-1.3.6/lib/sinatra/base.rb:1293>], [/^\/websocketio\/websocketio(?:\.|%2E)js$/, [], [], #<Proc:0x007fbd5a227a80@/Users/sho/.rvm/gems/ruby-1.9.2-p318/gems/sinatra-1.3.6/lib/sinatra/base.rb:1293>]], "POST"=>[[/^\/cometio\/io$/, [], [], #<Proc:0x007fbd5cab5d80@/Users/sho/.rvm/gems/ruby-1.9.2-p318/gems/sinatra-1.3.6/lib/sinatra/base.rb:1293>]]}



たとえば /cometio/cometio.js を削除するなら

Sinatra::Application.routes["GET"].delete_if{|route|
"/cometio/cometio.js" =~ route[0]
}

0

Bundlerとgem.dependencyの順序

bundler使う時、依存関係の根っこのgemほど下に書くべきらしい。

sinatra-rocketio -> (sinatra-cometio/sinatra-websocketio) -> sinatra-contrib -> sinatra -> rack
みたいな依存の階層関係があるgemから、複数のgemをGemfile/.gemspecに書いてしまうと、親のgemの方がメジャーバージョンアップした時に依存解決できなくなる事がある。


sinatra-rocketioのリポジトリで

bundle install
するとこういうエラーがでてた(もう直った)
Fetching gem metadata from https://rubygems.org/........
Fetching gem metadata from https://rubygems.org/..
Resolving dependencies...
Bundler could not find compatible versions for gem "sinatra":
In Gemfile:
sinatra-rocketio (>= 0) ruby depends on
sinatra (~> 1.3.0) ruby

sinatra-rocketio (>= 0) ruby depends on
sinatra (1.4.1)
何を言っているのかわからない・・


これがsinatra-rocketio.gemspec
gem.add_dependency "rack", ">= 1.5.0"
gem.add_dependency "sinatra", ">= 1.3.6"
gem.add_dependency "eventmachine", ">= 1.0.0"
gem.add_dependency "event_emitter", ">= 0.2.3"
gem.add_dependency "sinatra-contrib", ">= 1.3.2"
gem.add_dependency "sinatra-cometio", ">= 0.3.7"
gem.add_dependency "sinatra-websocketio", ">= 0.1.5"

これはsintra1.4がリリースされたが、sinatra-contribのgemspecがsinatra1.3系統の最新版を使うようになっているため。

上の行から解釈していくっぽいので、sinatra-contribをsinatraより上に書けばsinatra1.3.6が使われる。


というわけでこういう感じで書けばいい
gem.add_dependency "sinatra-cometio", ">= 0.3.8"
gem.add_dependency "sinatra-websocketio", ">= 0.1.6"
gem.add_dependency "sinatra"
gem.add_dependency "eventmachine", ">= 1.0.0"
gem.add_dependency "event_emitter", ">= 0.2.3"

これのおかげで、bundlerでインストールするとずっと0.0.3がインストールされちゃってたけど今の最新は0.0.8です