Heroku チュートリアルを使用して websocket を実装しました。
Thin では正常に動作しますが、Unicorn と Puma では動作しません。
また、クライアントのメッセージに応答するエコー メッセージも実装されています。各サーバーで適切に動作するため、websockets の実装に問題はありません。
Redis のセットアップも正しいです (すべてのメッセージをキャッチし、subscribe
ブロック内のコードを実行します)。
今はどのように機能しますか:
サーバーの起動時に、空の@clients
配列が初期化されます。次に、Redis をリッスンし、そのメッセージを @clients 配列から対応するユーザーに送信するための新しいスレッドが開始されます。
ページの読み込み時に、新しい websocket 接続が作成され、@clients 配列に保存されます。
ブラウザーからメッセージを受信すると、同じユーザーに接続されているすべてのクライアントにメッセージを送り返します (その部分は、Thin と Puma の両方で適切に機能しています)。
Redis からメッセージを受信した場合、@clients 配列に保存されているすべてのユーザーの接続も検索します。ここで奇妙なことが起こります:
Thin で実行している場合、@clients 配列で接続を検出し、それらにメッセージを送信します。
Puma/Unicorn で実行している場合、@clients 配列は、その順序で試しても (ページのリロードなどを行わずに) 常に空です。
- ブラウザからメッセージを送信 ->
@clients.length
が 1 の場合、メッセージが配信されます - Redis 経由でメッセージを送信 ->
@clients.length
は 0、メッセージは失われます - ブラウザからメッセージを送信 ->
@clients.length
は 1 のままで、メッセージは配信されます
- ブラウザからメッセージを送信 ->
誰かが私に欠けているものを明確にしてもらえますか?
Puma サーバーの関連構成:
workers 1
threads_count = 1
threads threads_count, threads_count
関連するミドルウェア コード:
require 'faye/websocket'
class NotificationsBackend
def initialize(app)
@app = app
@clients = []
Thread.new do
redis_sub = Redis.new
redis_sub.subscribe(CHANNEL) do |on|
on.message do |channel, msg|
# logging @clients.length from here will always return 0
# [..] retrieve user
send_message(user.id, { message: "ECHO: #{event.data}"} )
end
end
end
end
def call(env)
if Faye::WebSocket.websocket?(env)
ws = Faye::WebSocket.new(env, nil, {ping: KEEPALIVE_TIME })
ws.on :open do |event|
# [..] retrieve current user
if user
# add ws connection to @clients array
else
# close ws
end
end
ws.on :message do |event|
# [..] retrieve current user
Redis.current.publish({user_id: user.id, { message: "ECHO: #{event.data}"}} )
end
ws.rack_response
else
@app.call(env)
end
end
def send_message user_id, message
# logging @clients.length here will always return correct result
# cs = all connections which belong to that client
cs.each { |c| c.send(message.to_json) }
end
end