0

Real-Time Push Server in Rails

There are multiple ways to add push functionality to an application, including Pushr and Pub-Nub, which are quite elegant and simple solutions. There are also some more advanced options. In this article, I’m going to show you how to use Faye, a messaging system that runs on both Ruby and Node.js.

Step 1 Get Everything Ready

We’re going build a simple chat service. Now, Ryan Bates covered this on Railscast #260, however, we're going to take a slightly different approach in this tutorial. First, we'll create a chat service where users enter a public room, and everyone can chat with each other publicly. The second functionality we’ll be adding is private messages. Additionally, we'll integrate some security to our implementation using Ryan Bate’s private_pub gem.

Be sure you have a working setup of Ruby, and Ruby on Rails 3.1. Apart from that, you’ll need thin. Thin is a widely used Ruby webserver, and Faye requires it to run (it doesn’t work with WEBrick, Rails’ built-in server). You can install Thin, like so:

gem install thin

We should be all set, so let's create the application:

rails new faye-tutorial

Now, add the faye gem to your Gemfile:

gem 'faye'

and run bundle install to install it. Faye needs to run on a separate web server from the web application itself; to accomplish this, you need to create a Rackup config file. Add a faye.ru file to the root of the project and make sure it looks like so:

require 'faye'

bayeux = Faye::RackAdapter.new(:mount => '/faye', :timeout => 25)
bayeux.listen(9292)

This file simply tells Rackup how to start the Faye server. Try it out to ensure that it's working correctly. Run this in your Terminal:

rackup faye.ru -E production -s thin

If you don’t receive any errors, then you’re good to go!

Step 2 - Some Basic Authentication

To create a chat application, we need two basic things: users and a chat room. We’re not going to authenticate our users, but we still need to provide them with a way to set their username. Let’s create that. Run the following command to create a sessions controller so we can log them in:

rails g controller sessions new create

This will create a sessions controller and two methods: new and create. Add these routes to your routes.rb file:

get '/login' => 'sessions#new', :as => :login

post '/login' => 'sessions#create', :as => :login

These two routes should be self-explanatory to you. Go ahead and modify the app/views/sessions/new.html.erb file:

<%= form_tag login_path do |f| %>
  <%= label_tag :username %>
  <%= text_field_tag :username %>
  <%= submit_tag "Enter" %>
<% end %>

And have the create method within the sessions controller look like the following:

def create
  session[:username] = params[:username]
  render :text => "Welcome #{session[:username]}!"
end

Go ahead an try it out! Run rails server in the Terminal and point your browser to localhost:3000/login and enter a username. You should be greeted by your application after you submit the form.

Step 3 - The Chat Room

Now that we have some basic authentication, let’s add a chat room. Run the following command to create a chat controller:

rails generate controller chats room

This will generate our chats controller and one room method. We need to add some routes to make our chat work, so add this line to your routes.rb file:

get '/chatroom' => 'chats#room', :as => :chat

This route will direct our user to the chat room, and let them post messages through a simple form. Modify the room method on the chats controller and make it look like so:

def room
  redirect_to login_path unless session[:username]
end

This will ensure that users set a username if they want to chat. Now, let’s create the room itself! Add this to the app/views/chats/room.html.erb:

<div class="chat_container">
  <div id="chat_room">
    <p class="alert"> Welcome to the chat room <%= session[:username] %>! </p>
  </div>

  <form id="new_message_form">
    <input type="text" id="message" name="message">
    <input type="submit" value="Send">
  </form>
</div>

This is some simple structure for the room. The form at the end will be managed by some JavaScript code that will publish the message to the chat room.

Now, to post messages to the room, we need to add some JavaScript to our view. First, add Faye’s library to app/views/layouts/application.html.erb:

<%= javascript_include_tag "http://localhost:9292/faye.js" %>

Then, add the following to the beginning of the room.html.erb view:

<script>
  $(function() {
    // Create a new client to connect to Faye
    var client = new Faye.Client('http://localhost:9292/faye');

    // Handle form submissions and post messages to faye
    $('#new_message_form').submit(function(){
      // Publish the message to the public channel
      client.publish('/messages/public', {
        username: '<%= session[:username] %>',
        msg: $('#message').val()
      });

      // Clear the message box
      $('#message').val('');

      // Don't actually submit the form, otherwise the page will refresh.
      return false;
    });
  });
</script>

This method takes the message in the form we added previously (when it’s submitted), and sends the author’s username and message to the “/messages/public” channel in a JSON object. Channels are Faye’s way of sending messages. A user subscribes to a channel and receives all messages that are sent to it. In our case, there is only one channel, which is the public one. Let’s dissect the code a bit more:

  1. First we instantiate a new Faye Client, and have it connect to our Faye server.
  2. Next, we handle the form’s submission. When the user hits the enter key or clicks “Send”, we’ll publish the aforementioned JSON object containing the message’s sender and the message itself to the public channel.
  3. Following that, we clear the message box and return false, as to avoid the form from actually being submitted and refreshing the page.

Now, this will publish messages to the chat room, but our connected users won’t be able to receive them, because their browsers are not subscribed to the channel. This is achieved through a little more JavaScript. Make the JavaScript block on app/views/chats/room.html.erb look like so:

<script>
  $(function() {
    // Create a new client to connect to Faye
    var client = new Faye.Client('http://localhost:9292/faye');

    // Subscribe to the public channel
    var public_subscription = client.subscribe('/messages/public', function(data) {
      $('<p></p>').html(data.username + ": " + data.msg).appendTo('#chat_room');
    });

    // Handle form submission to publish messages.
    $('#new_message_form').submit(function(){
      // ...
      // Leave this part as it is
      // ...
    });
  });
</script>

This simply connects to Faye's server, and subscribes to the “/messages/public” channel. The callback we provide receives the messages sent. data will be the JSON object we published before, so we simply use that to create a tag with the message inside, and append it to the chat room container.

You should now have a simple chat! Run both Faye and the Rails server, and open two browsers (or an Incognito Window on Chrome for example). You can enter two different usernames and test your chat. Messages should appear almost instantly on the chat room once you send them.

Step 4 - Adding Private Messages

Right now, users are able to chat with one another, but all messages are public. Let’s add some simple functionality where people can send someone private messages, by mentioning the recipient’s username - sort of like in Twitter. For this to work, we’re going to subscribe our users to their own channels, so they’re the only ones who are able to receive messages from it. Make your app/views/chats/room.html.erb JavaScript look like this:

<script>
  $(function() {
    // Subscribe to receive messages!
    var client = new Faye.Client('http://localhost:9292/faye');

    // Our public subscription
    var public_subscription = client.subscribe('/messages/public', function(data) {
      $('<p></p>').html(data.username + ": " + data.msg).appendTo('#chat_room');
    });

    // Our own private channel
    var private_subscription = client.subscribe('/messages/private/<%= session[:username] %>', function(data) {
      $('<p></p>').addClass('private').html(data.username + ": " + data.msg).appendTo('#chat_room');
    });

    // Handle form submission to publish messages.
    $('#new_message_form').submit(function(){
      // Is it a private message?
      if (matches = $('#message').val().match(/@(.+) (.+)/)) {
        client.publish('/messages/private/' + matches[1], {
          username: '<%= session[:username] %>',
          msg: matches[2]
        });
      }
      else {
        // It's a public message
        client.publish('/messages/public', {
          username: '<%= session[:username] %>',
          msg: $('#message').val()
        });
      }

      // Clear the message box
      $('#message').val('');

      return false;
    });
  });
</script>

As you can see, we’re subscribing to two Faye channels: one is the public channel, and the second is a channel, called “/messages/private/USERNAME” (notice that we use the username on the Rails session). This way, when someone mentions that user, instead of sending the message through the public channel, we send it through the recipient’s private channel, so only that person can read it. We also add some simple styles to it, so it displays in bold.

Another thing that’s changed is the code that publishes messages. We first check if the message is a private one, by applying a simple regular expression that looks for a mention. If it is, we publish a message to the recipient’s specific channel. If not, we do exactly as we did before - publish it to the public channel.

Now try it out! Send messages to other users by mentioning them, you should only see them in the recipient’s chat window.

Step 5 - Some Caveats

This implementation has its flaws. First, we’re not checking to see if the username a person chooses is already in use. This would mean that anyone could enter the same username as someone else and send messages pretending to be them, and even receive their private messages! This is easily solved by adding some sort of authentication system or by storing the usernames, that are currently in use, within a database. I’m not going cover this in today's tutorial, but it should be fairly easy for you to implement. If you need assistance, leave a comment, and we'll help!

The second caveat is not as obvious. The problem with our implementation is that anyone could manipulate the JavaScript on the fly (using Firebug for instance) to subscribe themselves to any channel they wish (even private channels), and they could publish messages to them pretending to be someone else. This is not as easily solved as the first flaw I pointed out (if we were to solve this manually), but Ryan Bates created a gem that makes this task a cinch, and our app very secure.

The gem is called private_pub; it essentially forbids any user from publishing to channels with JavaScript, meaning only the Rails app is able to publish to them. This adds some security since a malicious user would not be able to publish to private channels. Another thing that this gem solves is subscriptions. With private_pub, a user can only receive messages from channels we subscribe them to, so they’re not able to add a subscription manually, solving the entire issue.

So let’s add it to our Gemfile:

gem 'private_pub', :git => 'git://github.com/ryanb/private_pub.git'

run bundle install to install the gem and run the generator to create the configuration files:

rails g private_pub:install

You will receive a conflict warning when running this command (private_pub tries to overwrite the faye.ru file). Just type “Y” and hit enter, as it’s necessary to overwrite that file. You’ll also need to move the public/private_pub.js file to the app/assets/javascripts folder. And the last thing: remove the line that includes faye.js on application.html.erb, since Private Pub includes it automatically. Make sure you restart both servers (rails and faye) at this point.

Now, we need to make some changes. First, subscribing a user to a channel is done differently with private_pub. Edit app/views/chats/room.html.erb and add the following before the JavaScript block:

<%= subscribe_to "/messages/public" %>
<%= subscribe_to "/messages/private/#{session[:username]}" %>

This is private_pub’s way of subscribing to channels. This authorizes the user to receive messages through the channel you specify. Now, we need to change the JavaScript code to the following:

PrivatePub.subscribe("/messages/public", function(data) {
$('<p></p>').html(data.username + ": " + data.msg).appendTo('#chat_room
});

PrivatePub.subscribe("/messages/private/<%= session[:username] %>", function(data) {
$('<p></p>').addClass('private').html(data.username + ": " + data.msg).appendTo('#chat_room
});

The only difference here is that we’re using PrivatePub to subscribe to channels instead of the Faye library directly.

We’re also going to need to change the way we publish messages. With Private Pub, only the Rails application is able to publish messages. The Javascript library can’t publish messages on its own. This is a good thing, since we take full control of who publishes messages and to which channel. In order to do this, we’re going to need to change the form used to send messages to the following:

<%= form_tag new_message_path, :remote => true do %>
  <%= text_field_tag :message %>
  <%= submit_tag "Send" %>
<% end %>

This is an AJAX form, so it won’t refresh the page when submitted. It’s also going to be looking for new_message_path, so be sure you add this to routes.rb:

post '/new_message' => 'chats#new_message', :as => :new_message

You’re also going to need to create a new method on the chats controller:

def new_message
  # Check if the message is private
  if recipient = params[:message].match(/@(.+) (.+)/)
    # It is private, send it to the recipient's private channel
    @channel = "/messages/private/#{recipient.captures.first}"
    @message = { :username => session[:username], :msg => recipient.captures.second }
  else
    # It's public, so send it to the public channel
    @channel = "/messages/public"
    @message = { :username => session[:username], :msg => params[:message] }
  end

  respond_to do |f|
    f.js
  end
end

This works very much like its JavaScript counterpart. It determines if the message contains a mention, and, if it does, it sends the message to the recipient’s private channel. If not, it sends the message through the public channel. Now, this isn’t actually sending the message, it just creates two variables that we need to use from within the view in order to send a one. Private Pub doesn’t allow you to send messages through the controller (at least not with the version I used for this tutorial), so go ahead and create the file app/views/chats/new_message.js.erb and add the following:

// Clear message input
$('#message').val('');

// Send the message
<% publish_to @channel, @message %>

This will execute the first line of code, clearing the message box, and by calling publish_to, Private Pub will send @message (which will be converted to a JSON object when it arrives) to @channel. Simple, huh?

Go ahead an try it out. It should work just like before, only with new added security!

Conclusion

I hope this article gave you some insight into how to use Faye for your projects. I would advise that you always use Private Pub, since it adds an important level of security, unless you don’t really need it.

References

Pushr, Pub-Nub, Faye, Railscast #260 Messaging with Faye, private_pub


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí