Sinatra + WEBrick + Comet で簡単なチャット

サンプルチャット

試しにということで最低限の骨組みだけのものを書いてみました。チャット初めて書きました。Thread::Queue も初めて使いました。

sonota88/sinatra-webrick-samplechat
https://github.com/sonota88/sinatra-webrick-samplechat

現時点( タグ=20190327 )では サーバ側が 38 行、 JavaScript が 109 行。

短いのでサーバ側だけ貼っておきます。

require 'sinatra'
require 'sinatra/reloader'

class ConnectionManager
  def initialize
    @map = {} # session id => queue
  end

  def broadcast(msg)
    @map.each_value { |queue| queue.enq(msg) }
  end

  def deq(session_id)
    unless @map.key?(session_id)
      @map[session_id] = Thread::Queue.new
    end

    @map[session_id].deq
  end
end

$conn_manager = ConnectionManager.new

get "/" do
  send_file "index.html"
end

post "/comet/open" do
  $conn_manager.deq(params[:sessionid])
end

post "/messages" do
  $conn_manager.broadcast(
    params[:sessionid] + ": " + params[:body]
  )
  "ok"
end

見ての通りですが、サンプルなので

  • エラーハンドリングは適当
  • タイムアウト、再接続などのハンドリングなし
  • 離脱・リロードで発生する幽霊接続の後始末なし
  • 永続化なし

経緯

  • プロトタイプ、自分だけ or チーム内の数人でしか使わないちょっとしたツールをシュッと作りたいという場合に、いつも Sinatra を使っている
    • べんり
  • その延長でサーバからの push が必要なものが作りたくなって WebSocket を使おうとした
  • WEBrick だとできないらしい
  • Thin とかを使うらしい
    • でもほんとにちょっとしたやつなんだけどな……
  • Sinatra(+WEBrick)だけでなんとかできない?
  • そういえば Comet(ロングポーリング)というのがありましたね

要件

上に書いたことと被りますが。 ここらへんが合わない場合はおとなしく WebSocket とかを使った方がよいはず。

  • 接続数
    • 多くないというか少ない。自分だけ or 数人程度
  • レイテンシも別に…
    • ゲームの画面を 60 FPSレンダリングしたいとかじゃない
    • ちょっともっさり程度でもいい。動けばいい
    • でも普通のポーリングよりは全然まし、体感的に違いが分かって嬉しい、という程度でOK
  • メンテナンス性が良いというか、メンテナンスフリーなのが望ましい
    • こういうごくごく小規模はツールは環境が変化して動かなくなることがツール自体を使わなくなる契機になりがち(けっこうばかにできない)なので、枯れてないものになるべく依存したくない……

実用するには

実用しようとするといろいろやらないといけないっぽくて、以下、ちょっと試してみたレベルで得た知見+適当な思いつきのメモ。ここらへんが膨らんできたらライブラリとかありものの利用を検討する。

  • @map への追加だけやっていて削除がないので保持する接続(@map の要素)が増え続ける
    • ブラウザのタブ開く・閉じる or リロード
    • 不要になったものは消さないといけない
      • Queue#num_waiting == 0 のもの
        • だけではダメかも。接続ごとに最終接続日時を持っておいてそれと合わせて判断?
      • キューに溜まり続けるパターンもありそう
        • これも適当な閾値と最終接続日時を合わせて判断?
  • タイムアウト・再接続
    • HTTP的にタイムアウトどう扱うのか詳しくない……
    • サーバ側 and/or クライアント側
      • クライアント側: タイムアウトしたら comet のループを止めてサーバにポーリングするループに入る。サーバが復活したらポーリング止めて comet のループを再開、みたいな?
    • アプリケーションの要件によっても変わる
  • enqueue が高頻度な場合
    • Thread::SizedQueue も検討(enqueue をブロックする)
    • 上記のサンプルだとキューにたくさん溜まっていても deq で 1個だけ取ってクライアント側にすぐ戻していて、接続がもったいない
      • 複数個 dequeue してまとめて返す
    • とはいえチャット程度なら心配する必要なさそう

関連