rubyruby-on-rails-4websocketwebsocket-rails

WebSockets in Rails : Do we have to create a new WebSocketController in our existing application while using the websockets?


I have a chat application here . I was going through the wiki of WebSockets . There the code is described in the ChatController like this :

class ChatController < WebsocketRails::BaseController
  def initialize_session
    # perform application setup here
    controller_store[:message_count] = 0
  end
end

My Question : how should i implement this in my chatcontroller which is exteding the ApplicationController ? . Whether i should create a new controller for using the websockets or modify the existing chatcontroller which should extend the WebsocketRails? . I am new to WebSockets so any help regarding this will really help .

Thank you .


Solution

  • The answer, with relation to the websocket-rails gem, as well as Faye and Plezi is YES, you have to create a new controller class, different than the one used by Rails.

    Your websocket-rails must inherit WebsocketRails::BaseController, while your Rails controller must inherit ActionController::Base (usually by inhering you ApplicationController, which inherits this class).

    Ruby doesn't support double class inheritance (although Mixins are possible when using modules).

    Faye, on the other hand doesn't use the Controller in the same object oriented way and there are more options there. For instance, you COULD map websocket events to your controller CLASS methods, but you might have an issue initializing a Rails controller for each websocket connection, since some of the Controller's internal mechanisms might break. Session info, for instance, will NOT be available and you would probably have to avoid any and all Rails specific methods.

    With Plezi, these inheritance issues don't exit, but Plezi automatically creates Http routes to any public methods your Controller has - so your Rails methods will be exposed in a way you didn't intend. Plezi Controllers, on the other hand, can answer both Http and Websockets.

    The reason I wrote also about Faye and Plezi is because the websocket-rails gem had last been updated (according the the CHANGELOG) on March 2014...

    I don't know how well it will survive (or had survived) the latest updates to Rails, but I would recommend moving on.

    For now, November 2015, Faye is the more common option and Plezi is a new player in the field. I'm Plezi's author.

    Edit (answering the comment)

    Both Faye and Plezi should allow one user to send a message to another.

    I think Plezi is easiyer to use, but this is because I wrote Plezi. I am sure the person that wrote Faye will think that Faye is easier.

    There are a few options about the best way(s) to implement a chat, depending on how you want to do so.

    You can look at the plezi application demo code if you install Plezi and run (in your terminal):

      $ plezi mini my_chat
    

    Here's a quick hack for adding Plezi websocket broadcasting to your existing application.

    If I had access to your database, I would have done it a little differently... but it's good enough as a proof of concept... for now.

    Add the following line to your Gemfile:

    gem 'plezi'
    

    Create a plezi_init.rb file and add it to your config/initializers folder. Here is what it hold for now (most of it is hacking the Rails cookie, because I don't have access to your database and I can't add fields):

    class WebsocketController
    
        def on_open
            # this is a Hack - replace this with a database token and a cookie.
            return close unless cookies[:_linkedchats_session] # refuse unauthenticated connections
            # this is a Hack - get the user
            @user_id = decrypt_session_cookie(cookies[:_linkedchats_session].dup)['warden.user.user.key'][0][0].to_s
            puts "#{@user_id} is connected"
        end
    
        def on_message data
            # what do you want to do when you get data?        
        end
    
        protected
    
        # this will inform the user that a message is waiting
        def message_waiting msg
            write(msg.to_json) if msg[:to].to_s == @user_id.to_s
        end
    
        # this is a Hack - replace this later
        # use a token authentication instead (requires a database field)
        def decrypt_session_cookie(cookie)
            key ='4f7fad1696b75330ae19a0eeddb236c123727f2a53a3f98b30bd0fe33cfc26a53e964f849d63ad5086483589d68c566a096d89413d5cb9352b1b4a34e75d7a7b'
            cookie = CGI::unescape(cookie)
    
            # Default values for Rails 4 apps
            key_iter_num = 1000
            key_size     = 64
            salt         = "encrypted cookie"         
            signed_salt  = "signed encrypted cookie"  
    
            key_generator = ActiveSupport::KeyGenerator.new(key, iterations: key_iter_num)
            secret = key_generator.generate_key(salt)
            sign_secret = key_generator.generate_key(signed_salt)
    
            encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON)
            encryptor.decrypt_and_verify(cookie)
        end
    end
    
    # IMPORTANT - create the Plezi, route for websocket connections
    Plezi.route '/ws', WebsocketController
    

    That's almost all the Plezi application you need for this one.

    Just add the following line in to your ChatsController#create method, just before the respond_to:

     WebsocketController.broadcast :message_waiting,
              from: @msg.sender_id,
              to: @msg.receiver_id,
              text: @msg.text,
              msg: :chat
    

    That's it for the server... Now, the client.

    Add the following script to your chat.html.erb template (or, because turbo-links might mess up your script's initialization, add the script to your application.js file... but you will be refusing a lot of connections until your users log in):

    <script type="text/javascript">
    
    // Your websocket URI should be an absolute path. The following sets the base URI.
    // remember to update to the specific controller's path to your websocket URI.
    var ws_controller_path = '/ws'; // change to '/controller/path'
    var ws_uri = (window.location.protocol.match(/https/) ? 'wss' : 'ws') + '://' + window.document.location.host + ws_controller_path
    // websocket variable.
    var websocket = NaN
    // count failed attempts
    var websocket_fail_count = 0
    // to limit failed reconnection attempts, set this to a number.
    var websocket_fail_limit = NaN
    
    
    function init_websocket()
    {
        if(websocket && websocket.readyState == 1) return true; // console.log('no need to renew socket connection');
        websocket = new WebSocket(ws_uri);
        websocket.onopen = function(e) {
            // reset the count.
            websocket_fail_count = 0
            // what do you want to do now?
        };
    
        websocket.onclose = function(e) {
            // If the websocket repeatedly you probably want to reopen the websocket if it closes
            if(!isNaN(websocket_fail_limit) && websocket_fail_count >= websocket_fail_limit) {
                // What to do if we can't reconnect so many times?
                return
            };
            // you probably want to reopen the websocket if it closes.
            if(isNaN(websocket_fail_limit) || (websocket_fail_count <= websocket_fail_limit) ) {
                // update the count
                websocket_fail_count += 1;
                // try to reconect
                init_websocket();
            };
        };
        websocket.onerror = function(e) {
            // update the count.
            websocket_fail_limit += 1
            // what do you want to do now?
        };
        websocket.onmessage = function(e) {
            // what do you want to do now?
            console.log(e.data);
            msg = JSON.parse(e.data)
            alert("user id: " + msg.from + " said:\n" + msg.text)
        };
    }
    // setup the websocket connection once the page is done loading
    window.addEventListener("load", init_websocket, false); 
    
    </script>
    

    Done.