huzefa biyawarwala
huzefa biyawarwala

Reputation: 657

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 .

Upvotes: 0

Views: 673

Answers (1)

Myst
Myst

Reputation: 19221

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.

Upvotes: 2

Related Questions