rubyweb-applicationswebsocketsinatrafaye

How can I send messages to specific client using Faye Websockets?


I've been working on a web application which is essentially a web messenger using sinatra. My goal is to have all messages encrypted using pgp and to have full duplex communication between clients using faye websocket.

My main problem is being able to send messages to a specific client using faye. To add to this all my messages in a single chatroom are saved twice for each person since it is pgp encrypted.

So far I've thought of starting up a new socket object for every client and storing them in a hash. I do not know if this approach is the most efficient one. I have seen that socket.io for example allows you to emit to a specific client but not with faye websockets it seems ? I am also considering maybe using a pub sub model but once again I am not sure.

Any advice is appreciated thanks !


Solution

  • I am iodine's author, so I might be biased in my approach.

    I would consider naming a channel by the used ID (i.e. user1...user201983 and sending the message to the user's channel.

    I think Faye will support this. I know that when using the iodine native websockets and builtin pub/sub, this is quite effective.

    So far I've thought of starting up a new socket object for every client and storing them in a hash...

    This is a very common mistake, often seen in simple examples.

    It works only in single process environments and than you will have to recode the whole logic in order to scale your application.

    The channel approach allows you to scale using Redis or any other Pub/Sub service without recoding your application's logic.

    Here's a quick example you can run from the Ruby terminal (irb). I'm using plezi.io just to make it a bit shorter to code:

    require 'plezi'
    
    class Example
      def index
        "Use Websockets to connect."
      end
      def pre_connect
        if(!params[:id])
          puts "an attempt to connect without credentials was made."
          return false
        end
        return true
      end
      def on_open
        subscribe channel: params[:id]
      end
      def on_message data
        begin
          msg = JSON.parse(data)
          if(!msg["to"] || !msg["data"])
            puts "JSON message error", data
            return
          end
          msg["from"] = params[:id]
          publish channel: msg["to"].to_s, message: msg.to_json
        rescue => e
          puts "JSON parsing failed!", e.message
        end
    
      end
    end
    
    Plezi.route "/" ,Example
    Iodine.threads = 1
    exit
    

    To test this example, use a Javascript client, maybe something like this:

    // in browser tab 1
    var id = 1
    ws = new WebSocket("ws://localhost:3000/" + id)
    ws.onopen = function(e) {console.log("opened connection");}
    ws.onclose = function(e) {console.log("closed connection");}
    ws.onmessage = function(e) {console.log(e.data);}
    ws.send_to = function(to, data) {
        this.send(JSON.stringify({to: to, data: data}));
    }.bind(ws);
    
    // in browser tab 2
    var id = 2
    ws = new WebSocket("ws://localhost:3000/" + id)
    ws.onopen = function(e) {console.log("opened connection");}
    ws.onclose = function(e) {console.log("closed connection");}
    ws.onmessage = function(e) {console.log(e.data);}
    ws.send_to = function(to, data) {
        this.send(JSON.stringify({to: to, data: data}));
    }.bind(ws);
    
    ws.send_to(1, "hello!")