Warning: file_get_contents(/data/phpspider/zhask/data//catemap/6/multithreading/4.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Ruby on rails 访问rails线程中的变量_Ruby On Rails_Multithreading_Redis_Em Websocket - Fatal编程技术网

Ruby on rails 访问rails线程中的变量

Ruby on rails 访问rails线程中的变量,ruby-on-rails,multithreading,redis,em-websocket,Ruby On Rails,Multithreading,Redis,Em Websocket,我正在为基于web的幻灯片放映构建一个应用程序,其中一个“主”用户可以在幻灯片之间移动,每个人的浏览器都可以跟随。为了做到这一点,我使用websockets和Redis作为一个全球频道来发送消息。每个连接的客户端都将这些信息存储在一个数组中,@clients。 然后我有一个单独的订阅Redis频道的线程,其中定义了一个“on.message”块,该块应该向@clients数组中的每个人发送消息,但该数组在该块中是空的(在模块中的任何其他地方都不是空的) 大致遵循以下示例: 自定义中间件类中的相

我正在为基于web的幻灯片放映构建一个应用程序,其中一个“主”用户可以在幻灯片之间移动,每个人的浏览器都可以跟随。为了做到这一点,我使用websockets和Redis作为一个全球频道来发送消息。每个连接的客户端都将这些信息存储在一个数组中,
@clients
。 然后我有一个单独的订阅Redis频道的线程,其中定义了一个“on.message”块,该块应该向
@clients
数组中的每个人发送消息,但该数组在该块中是空的(在模块中的任何其他地方都不是空的)

大致遵循以下示例:

自定义中间件类中的相关代码:

require 'faye/websocket'
require 'redis'

class WsCommunication
  KEEPALIVE_TIME = 15 #seconds
  CHANNEL = 'vip-deck'

  def initialize(app)
    @app = app
    @clients = []

    uri = URI.parse(ENV['REDISCLOUD_URL'])
    Thread.new do
      redis_sub = Redis.new(host: uri.host, port: uri.port, password: uri.password)
      redis_sub.subscribe(CHANNEL) do |on|
        on.message do |channel, msg|
          puts @clients.count
          ### prints '0,' no clients receive msg
          @clients.each { |ws| ws.send(msg) }
        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|
      @clients << ws
      puts @clients.count
      ### prints actual number of clients
    end

    ws.on :message do |event|
      $redis.publish(CHANNEL, event.data)
    end

    ws.on :close do |event|
      @clients.delete(ws)
      ws = nil
    end

    ws.rack_response
  else
    @app.call(env)
  end
end
end
require'faye/websocket'
需要“redis”
类无线传感器网络通信
保持有效时间=15秒
频道=‘vip甲板’
def初始化(应用程序)
@app=app
@客户=[]
uri=uri.parse(ENV['REDISCLOUD\u URL'])
新做的
redis_sub=redis.new(主机:uri.host,端口:uri.port,密码:uri.password)
redis_sub.subscribe(频道)do|on|
on.message do |频道,msg|
放置@clients.count
###打印“0”,没有客户端收到消息
@clients.each{| ws | ws.send(msg)}
结束
结束
结束
结束
def呼叫(环境)
如果Faye::WebSocket.WebSocket?(环境)
ws=Faye::WebSocket.new(env,nil,{ping:KEEPALIVE_TIME})
ws.on:开放do |事件|

@客户端@客户端应该在所有线程之间共享,您确定客户端没有意外地从阵列中删除吗?尝试将“client deleted”放在ws.on:close块中并测试它。 您还可以尝试使用互斥体,其中@client变量的使用方式如下:

在末尾更新编辑:显示工作代码。除调试代码外,未修改主模块。注意:我确实遇到过我已经注意到的关于在终止前取消订阅的问题

代码看起来是正确的。我想看看您是如何实例化它的

在config/application.rb中,您可能至少有以下内容:

require 'ws_communication'
config.middleware.use WsCommunication
然后,在JavaScript客户端中,您应该有如下内容:

var ws = new WebSocket(uri);
您是否实例化了WsCommunication的另一个实例?这会将@clients设置为一个空数组,并可能显示您的症状。类似这样的情况是不正确的:

var ws = new WsCommunication;
如果您能向客户机展示,这将对我们有所帮助。如果这篇文章没有帮助,您还可以展示config/application.rb

顺便说一句,我同意@clients在任何更新中都应该受到互斥的保护,如果不是读的话。它是一种动态结构,在事件驱动的系统中随时都可能发生变化。这是一个很好的选择。(希望链接是正确的,因为Github目前似乎在所有方面都抛出了500个错误。)

您可能还注意到,$redis.publish返回接收消息的客户端数量的整数值

最后,您可能会发现您需要确保您的频道在终止前已取消订阅。我曾经遇到过这样的情况:由于之前对同一频道的订阅没有清理,我最终会多次甚至多次发送每条消息。由于您是在一个线程中订阅频道,因此需要在同一个线程中取消订阅,否则进程将“挂起”,等待正确的线程神奇地出现。我通过设置“unsubscribe”标志然后发送消息来处理这种情况。然后,在on.message块中,我测试unsubscribe标志并在那里发出unsubscribe

您提供的模块,只需稍作调试修改:

require 'faye/websocket'
require 'redis'

class WsCommunication
  KEEPALIVE_TIME = 15 #seconds
  CHANNEL = 'vip-deck'

  def initialize(app)
    @app = app
    @clients = []
    uri = URI.parse(ENV['REDISCLOUD_URL'])
    $redis = Redis.new(host: uri.host, port: uri.port, password: uri.password)
    Thread.new do
      redis_sub = Redis.new(host: uri.host, port: uri.port, password: uri.password)
      redis_sub.subscribe(CHANNEL) do |on|
        on.message do |channel, msg|
          puts "Message event. Clients receiving:#{@clients.count};"
          @clients.each { |ws| ws.send(msg) }
        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|
        @clients << ws
        puts "Open event. Clients open:#{@clients.count};"
      end

      ws.on :message do |event|
        receivers = $redis.publish(CHANNEL, event.data)
        puts "Message published:#{event.data}; Receivers:#{receivers};"
      end

      ws.on :close do |event|
        @clients.delete(ws)
        puts "Close event. Clients open:#{@clients.count};"
        ws = nil
      end

      ws.rack_response
    else
      @app.call(env)
    end
  end
end
我提供的测试发布者代码。Publisher和Subscriber可以轻松组合,因为它们只是测试:

# encoding: UTF-8
puts "Starting client-publisher.rb"
$:.unshift File.expand_path '../lib', File.dirname(__FILE__)
require 'rubygems'
require 'eventmachine'
require 'json'
require 'websocket-client-simple'

puts "websocket-client-simple v#{WebSocket::Client::Simple::VERSION}"

url = ARGV.shift || 'ws://localhost:3000'

EM.run do
  count ||= 0
  timer = EventMachine.add_periodic_timer(5+rand(5)) do
    count += 1
    send({"MESSAGE": "COUNT:#{count};"})
  end

  @ws = WebSocket::Client::Simple.connect url

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

  @ws.on :open do
    puts "-- Publisher open"
  end

  @ws.on :close do |e|
    puts "-- Publisher close (#{e.inspect})"
    exit 1
  end

  @ws.on :error do |e|
    puts "-- Publisher error (#{e.inspect})"
    @ws.close
  end

  def self.send message
    payload = message.is_a?(Hash) ? message : {payload: message}
    @ws.send(payload.to_json)
  end
end
在机架中间件层运行所有这些功能的示例config.ru:

require './controllers/main'
require './middlewares/ws_communication'
use WsCommunication
run Main.new
这是主要的。我将其从我的运行版本中剥离出来,因此如果您使用它,可能需要对其进行调整:

%w(rubygems bundler sinatra/base json erb).each { |m| require m }
ENV['RACK_ENV'] ||= 'development'
Bundler.require
$: << File.expand_path('../', __FILE__)
$: << File.expand_path('../lib', __FILE__)

Dir["./lib/*.rb", "./lib/**/*.rb"].each { |file| require file }
env = ENV['OS'] == 'Windows_NT' ? 'development' : ENV['RACK_ENV']

  class Main < Sinatra::Base

    env = ENV['OS'] == 'Windows_NT' ? 'development' : ENV['RACK_ENV']
    get "/" do
      erb :"index.html"
    end

    get "/assets/js/application.js" do
      content_type :js
      @scheme = env == "production" ? "wss://" : "ws://"
      erb :"application.js"
    end
  end
%w(rubygems绑定器sinatra/base json-erb)。每个{m}需要m}
环境['RACK_ENV']| |='development'
捆绑机

$:@kfrz这是ruby,不是python;你为什么在一个单独的线程中这样做?还有一个问题,你为什么要用手做这个?如果您使用Rails 5.0,您可以使用
actioncable
,它可以解决您的所有问题。请查看这是否有帮助:@RaVeN Redis代码正在阻塞。它永远不会返回控制权。因此,线程是其操作所必需的。ActionCable是一个不错的解决方案,如果所有客户端都是JavaScript的话,它可以在这种情况下工作。不过,这个解决方案更通用,应该能够处理任何支持WebSocket的客户端。嘿,感谢所有这些信息!我会把这个标记为答案,因为即使我的项目的需求发生了变化,我能够以不同的方式做我需要的事情,这是非常有用的。
%w(rubygems bundler sinatra/base json erb).each { |m| require m }
ENV['RACK_ENV'] ||= 'development'
Bundler.require
$: << File.expand_path('../', __FILE__)
$: << File.expand_path('../lib', __FILE__)

Dir["./lib/*.rb", "./lib/**/*.rb"].each { |file| require file }
env = ENV['OS'] == 'Windows_NT' ? 'development' : ENV['RACK_ENV']

  class Main < Sinatra::Base

    env = ENV['OS'] == 'Windows_NT' ? 'development' : ENV['RACK_ENV']
    get "/" do
      erb :"index.html"
    end

    get "/assets/js/application.js" do
      content_type :js
      @scheme = env == "production" ? "wss://" : "ws://"
      erb :"application.js"
    end
  end