I'm writing a Rails application that uses WebSockets to communicate with other machines (no browser and client side logic in this process). I have a channel:
class MachinesChannel < ApplicationCable::Channel
def subscribed
...
end
def unsubscribed
...
end
def handle_messages
...
end
end
To receive the data the only way I know about is the JavaScript client:
ActionCable.createConsumer('/cable').subscriptions.create 'MachinesChannel',
received: (message) ->
#perform('handle_messages')
I can call server side methods from JS via #perform() method.
Is there any way to omit the JS part and somehow directly handle the incoming data in MachinesChannel?
The ideal situation would be to have the handle_messages method accept a data argument and have this metod called on incoming data.
After looking into ActionCable source code I got the following solution. You just have to create a method in MachinesChannel that you want to be called, e.g. handle_messages(data). Then, in the client that connects to your websocket, you need to send a message in the following format (example in ruby):
id = { channel: 'MachinesChannel' }
ws = WebSocket::Client::Simple.connect(url)
ws.send(JSON.generate(command: 'message', identifier: JSON.generate(id), data: JSON.generate(action: 'handle_messages', foo: 'bar', biz: 'baz')))
action has to be the name of the method you want to be called in MachinesChannel. The rest of key-values are whatever you want. This the date you can receive in the ActionCable channel.
Recently a gem action_cable_client has been release which seems exactly perfect for this kind of usage. I haven't used it, so I don't know how it really works.
Instead of:
def handle_messages
...
end
This works for me:
def receive(data)
puts data
...
end
Related
We've successfully implemented real time updates in our app using ActionCable in Rails and implemented the consumer as a client service in Ember CLI, but am looking for a better, less-expensive approach.
app/models/myobj.rb
has_many :child_objs
def after_commit
ActionCable.server.broadcast("obj_#{self.id}", model: "myobj", id: self.id)
self.child_objs.update_all foo: bar
end
app/models/child_obj.rb
belongs_to :myobj
def change_job
self.job = 'foo'
self.save
ActionCable.server.broadcast("obj_#{self.myobj.id}", model: "child_obj", id: self.id)
end
frontend/app/services/stream.js
Here we're taking the model and id data from the broadcast and using it to reload from the server.
import Ember from 'ember';
export default Ember.Service.extend({
store: Ember.inject.service(),
subscribe(visitId) {
let store = this.get("store")
MyActionCable.cable.subscriptions.create(
{channel: "ObjChannel", id: objId}, {
received(data) {
store.findRecord(data.model, data.id, {reload: true});
}
}
);
},
});
This approach "works" but feels naïve and is resource intensive, hitting our server again for each update, which requires re-authenticating the request, grabbing data from the database, re-serializing the object (which could have additional database pulls), and sending it across the wire. This does in fact cause pool and throttling issues if the number of requests are high.
I'm thinking we could potentially send the model, id, and changeset (self.changes) in the Rails broadcast, and have the Ember side handle setting the appropriate model properties. Is this the correct approach, or is there something else anyone recommends?
You should be fine with sending whole entity payload with your change event via sockets. Later you can push payload to the store - create new records or update existing. This way you'll avoid additional server requests.
Maybe it is a good example for server push system. There are many users in the system, and users can talk with each other. It can be accomplished like this: one user sends message(through websocket) to the server, then the server forward the message to the other user. The key is to find the binding between the ws(websocket object) and the user. The example code like below:
EM.run {
EM::WebSocket.run(:host => "0.0.0.0", :port => 8080, :debug => false) do |ws|
ws.onopen { |handshake|
# extract the user id from handshake and store the binding between user and ws
}
ws.onmessage { |msg|
# extract the text and receiver id from msg
# extract the ws_receiver from the binding
ws_receiver.send(text)
}
end
}
I want to figure out following issues:
The ws object can be serialized so it can be stored into disk or database? Otherwise I can only store the binding into memory.
What the differences between em-websocket and websocket-rails?
Which gem do you recommend for websocket?
You're approaching a use case that websockets are pretty good for, so you're on the right track.
You could serialize the ws object with Marshal, but think of websocket objects as being a bit like http request objects in that they are abstractions for a type of communication. You are probably best off marshaling/storing the data.
em-websocket is a lower(ish) lever websocket library built more or less directly on web-machine. websocket-rails is a higher level abstraction on websockets, with a lot of nice tools built in and pretty ok docs. It is built on top of faye-websocket-rails which is itself built on web machine. *Note, action cable which is the new websocket library for Rails 5 is built on faye.
I've use websocket-rails in the past and rather like it. It will take care of a lot for you. However, if you can use Rails 5 and Action Cable, do that, its the future.
The following is in addition to Chase Gilliam's succinct answer which included references to em-websocket, websocket-rails (which hadn't been maintained in a long while), faye-websocket-rails and ActionCable.
I would recommend the Plezi framework. It works both as an independent application framework as well as a Rails Websocket enhancement.
I would consider the following points as well:
do you need the message to persist between connections (i.e. if the other user if offline, should the message wait in a "message box"? for how long should the message wait?)...?
Do you wish to preserve message history?
These points would help yo decide if to use a persistent storage (i.e. a database) for the messages or not.
i.e., to use Plezi with Rails, create an init_plezi.rb in your application's config/initializers folder. use (as an example) the following code:
class ChatDemo
# use JSON events instead of raw websockets
#auto_dispatch = true
protected #protected functions are hidden from regular Http requests
def auth msg
#user = User.auth_token(msg['token'])
return close unless #user
# creates a websocket "mailbox" that will remain open for 9 hours.
register_as #user.id, lifetime: 60*60*9, max_connections: 5
end
def chat msg, received = false
unless #user # require authentication first
close
return false
end
if received
# this is only true when we sent the message
# using the `broadcast` or `notify` methods
write msg # writes to the client websocket
end
msg['from'] = #user.id
msg['time'] = Plezi.time # an existing time object
unless msg['to'] && registered?(msg['to'])
# send an error message event
return {event: :err, data: 'No recipient or recipient invalid'}.to_json
end
# everything was good, let's send the message and inform
# this will invoke the `chat` event on the other websocket
# notice the `true` is setting the `received` flag.
notify msg['to'], :chat, msg, true
# returning a String will send it to the client
# when using the auto-dispatch feature
{event: 'message_sent', msg: msg}.to_json
end
end
# remember our route for websocket connections.
route '/ws_chat', ChatDemo
# a route to the Javascript client (optional)
route '/ws/client.js', :client
Plezi sets up it's own server (Iodine, a Ruby server), so remember to remove from your application any references to puma, thin or any other custom server.
On the client side you might want to use the Javascript helper provided by Plezi (it's optional)... add:
<script src='/es/client.js' />
<script>
TOKEN = <%= #user.token %>;
c = new PleziClient(PleziClient.origin + "/ws_chat") // the client helper
c.log_events = true // debug
c.chat = function(event) {
// do what you need to print a received message to the screen
// `event` is the JSON data. i.e.: event.event == 'chat'
}
c.error = function(event) {
// do what you need to print a received message to the screen
alert(event.data);
}
c.message_sent = function(event) {
// invoked after the message was sent
}
// authenticate once connection is established
c.onopen = function(event) {
c.emit({event: 'auth', token: TOKEN});
}
// // to send a chat message:
// c.emit{event: 'chat', to: 8, data: "my chat message"}
</script>
I didn't test the actual message code because it's just a skeleton and also it requires a Rails app with a User model and a token that I didn't want to edit just to answer a question (no offense).
How do I send update notifications to clients using server sent event?
What I want to accomplish is that when a client ajax calls an action, the server then would send relevant data to all connecting clients through my stream action.
I'm trying to know if this would be possible without websockets or pub/subs.
From what I can gather, you're looking for a generalized approach, rather than specific code?
--
SSE's
Server Sent Events are an HTML5 technology, meaning that if you do it correctly, it shouldn't matter whether you use Rails or another framework -- they should just work
One drawback to SSE's is they act very similar to Ajax long-polling, meaning they send constant "pings" / requests to your server, relaying back any response they find. And they'll still use the pub/sub pattern too
-
Simply, SSE's are when you have a Javascript "event listener", which will listen to an "endpoint" (URL). The endpoint, in the case of Rails, will be a controller#action, from which you can send the relevant text/event-stream updates, which is what ActionController::Live::SSE is there to do
--
Setup
#config/routes.rb
resources :your_controller do
collection do
get :endpoint
end
end
#app/assets/javascripts/application.js
var source = new EventSource('your_controller/endpoint');
source.addEventListener('message', function(e) {
console.log(e.data);
}, false);
#app/controllers/your_controller.rb
Class YourController < ActionController::Base
include ActionController::Live
def endpoint
response.headers['Content-Type'] = 'text/event-stream'
sse = SSE.new(response.stream, retry: 300, event: "event-name")
sse.write({ name: 'John'})
ensure
sse.close
end
end
This will send the relevant updates for you every time
I am trying to create sort of Whatsapp like messaging app server side in Rails, with private conversations. now, I am trying to implement the realtime part of the app - I am using websocket-rails - and I am not sure how to send a message only to the users in the private message - I saw a feature called private channels in websocket-rails - but after reading the documentation, I got under the impression that each private channel needs to be defined statically, and I cannot create channels realtime.
Do you know how can I implement private conversations in websocket-rails, like a guide or a direction? or any other websocket service I can use to implement it?
You can pass parameters from the client side to the server side when creating a subscription. For example:
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_#{params[:room]}"
end
end
An object passed as the first argument to subscriptions.create becomes the params hash in the cable channel. The keyword channel is required:
# app/assets/javascripts/cable/subscriptions/chat.coffee
App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
received: (data) ->
#appendLine(data)
appendLine: (data) ->
html = #createLine(data)
$("[data-chat-room='Best Room']").append(html)
createLine: (data) ->
"""
<article class="chat-line">
<span class="speaker">#{data["sent_by"]}</span>
<span class="body">#{data["body"]}</span>
</article>
"""
Somewhere in your app this is called, perhaps
ActionCable.server.broadcast(
"chat_#{room}",
sent_by: 'Paul',
body: 'This is a cool chat app.'
)
The good folks at LayerVault posted a easy way to add 'real-time' features on existing Ruby application without resorting to client side MCV or rewrite the entire app in Node.js, a while back. Their post show that you can update to all clients whenever your rails Models are updated [Reference 1] through delay_job & Active:Record Observer:
class FileObserver < ActiveRecord::Observer
observer :lv_file
def after_commit(record)
record.delay.report_updated
end
end
and
class LVFile < ActiveRecord::Base
def report_updated
Messenger.publish_message('file_updated', "file/#{self.id}")
end
end
[Reference 1] http://layervault.tumblr.com/post/31462727280/rails-in-realtime-part-2
Connect to socket.io server from client side javascript is easy. however I'm at lost how to implement Messenger.publish_message('file_updated', "file/#{self.id}") on rails server side. In the post they do mentioned that
The report_updated method makes a class method call to a separate Messenger class (our Socket.IO interface on the web app side) and reports that a file has changed to the appropriate room.
But I'm still not very sure how to implement Messager class in Rails so that Active:Record Observer will be able to connect to my socket.io server and send updates.
[PS] A conversation on HN indicates that they utilized this gem
The gem's wiki even included a brief demo:
require 'SocketIO'
client = SocketIO.connect("http://localhost", sync: true) do
before_start do
on_message {|message| puts message}
on_disconnect {puts "I GOT A DISCONNECT"}
end
after_start do
emit("loadLogs", "/var/www/rails_app/log/production.log")
end
end
puts "socket still running"
loop do
sleep 10
puts 'zzz'
end
The Messenger class doesn't have much to it. All you need is to create the connection, fire off the message and then wrap things up. This is the entirety of our messenger.rb:
require 'socketio-client'
class Messenger
def self.send_message!(name, args)
begin
SocketIO.connect(MESSENGER_SOCKETIO_ADDRESS, sync: true) do
after_start do
emit(name, args)
disconnect
end
end
rescue
end
end
end