#260 Messaging with Faye
- Download:
- source codeProject Files in Zip (266 KB)
- mp4Full Size H.264 Video (19.2 MB)
- m4vSmaller H.264 Video (13.5 MB)
- webmFull Size VP8 Video (36.7 MB)
- ogvFull Size Theora Video (26.5 MB)
En este episodio vamos a añadir funcionalidad de mensajería instantánea a una aplicación Rails ya existente. Ya tenemos una parte de la funcionalidad adelantada: se trata de una página con un campo de texto en el cual se puede escribir un mensaje, que luego se añade a la ventana de conversación cuando se hace clic en ‘send’ utilizando AJAX .
Por ahora todo va bien, pero la aplicación tiene un problema tal y como está. Si se abre otra ventana del navegador para que funcione como si fuese otro cliente de la conversación, los mensajes que se tecleen en una ventana no aparecerán en la otra.
Necesitamos enviar notificaciones para decirles a los otros clientes que se ha añadido un mensaje nuevo y mostrarlo. Hay varias formar de implementar esto pero antes de hacerlo veamos el código que ya tenemos. Se trata de un sencillo formulario que utiliza AJAX y jQuery. Esto no tiene nada de complicado, pero antes de seguir podemos familiarizarnos con jQuery en el episodio 136 [verlo, leerlo].
Primero como estamos usando jQuery en nuestra aplicación hemos añadido la gema jquery-rails al Gemfile
.
source 'http://rubygems.org' gem 'rails', '3.0.5' gem 'sqlite3' gem 'nifty-generators' gem 'jquery-rails'
Veamos ahora el código de la vista de la página de charla. Tiene una lista con un id
igual a chat
que muestra los mensajes y un formulario con :remote => true
para que se envíe por AJAX.
<% title "Chat" %> <ul id="chat"> <%= render @messages %> </ul> <%= form_for Message.new, :remote => true do |f| %> <%= f.text_field :content %> <%= f.submit "Send" %> <% end %>
El formulario se envía a la acción create
de MessagesController
class MessagesController < ApplicationController def index @messages = Message.all end def create @message = Message.create!(params[:message]) end end
La acción create
tiene una plantilla JavaScript que agrega el nuevo mensaje a la lista y luego limpia el formulario.
$("#chat").append("<%= escape_javascript render(@message) %>");
$("#new_message")[0].reset();
Se trata de un código JavaScript bastante sencillo. Lo que tenemos que hacer es cambiar la primera línea del código anterior para que se propague el nuevo mensaje a todos los clientes.
¿Cómo hacerlo? La realidad es que Rails no lleva muy bien la gestión de eventos asíncronos, dado que no podemos mantener un socket abierto contra una aplicación Rails. Podríamos considerar cambiar por completo de framework, porque hay varios diseñados específicamente para este tipo de problemas. Se trata de frameworks como Node.js con Socket.IO, o, si queremos seguir usando Ruby, Cramp, async_sinatra, o el nuevo Goliath. Todas son soluciones igualmente válidas, pero ¿y si queremos seguir usando Rails? Sería muy interesante poder seguir usando Rails para la lógica de nuestra aplicación y disfrutar a la vez de los beneficios de algún tipo de gestión de eventos asíncronos para publicar y suscribir cuando sea necesario.
Aquí es donde entra en juego Faye. Faye es un servidor que gestiona de manera asíncrona el patrón de publicación-suscripción. Podemos usarlo junto con nuestra aplicación Rails e invocarlo cuando necesitemos dicha funcionalidad. Faye viene en dos variantes: un servidor en Node.js y un servidor en Ruby. Ambos utilizan el mismo protocolo así que podemos escoger nuestro lenguaje favorito. No hace falta decir que nosotros nos quedamos con el servidor Ruby.
Primero tenemos que empezar instalado la gema Faye.
$ gem install faye
A continuación tenemos que crear un fichero para Rackup en la raíz de nuestra aplicación Rails, al que llamaremos faye.ru
. En este fichero crearemos una nueva aplicación Rack con una línea de código copiada de la documentación de Faye:
require 'faye' faye_server = Faye::RackAdapter.new(:mount => '/faye', :timeout => 45) run faye_server
Podemos arrancar el servidor con la orden rackup
, añadiendo las opciones necesarias para arrancarlo en modo de producción y con Thin como servidor.
$ rackup faye.ru -s thin -E production >> Thin web server (v1.2.11 codename Bat-Shit Crazy) >> Maximum connections set to 1024 >> Listening on 0.0.0.0:9292, CTRL+C to stop
Nuestra aplicación Faye se encuentra en ejecución en el puerto 9292. El servidor dispone de un fichero Javascript que tenemos que incluir en el layout de nuestra aplicación. Dicho fichero se encuentra en http://localhost:9292/faye.js
, estando el nombre basado en la opción :mount
que se ha pasado anteriormente.
<%= javascript_include_tag :defaults, "http://localhost:9292/faye.js" %>
Por supuesto en producción tendremos que cambiar la URL para que apunte al servidor adecuado.
El sitio web de Faye incluye documentación sobre su uso donde se explica que una vez incluido el JavaScript de Faye tenemos que crear un nuevo cliente. Para ello, añadiremos el siguiente código en nuestro fichero application.js
.
/public/javascripts/application.js
$(function() { var faye = new Faye.Client('http://localhost:9292/faye'); });
Nótese que usamos la función $
para garantizar que el código no se ejecuta hasta que el DOM de la página no se haya cargado por completa. Nuevamente tendremos que cambiar la URL una vez que tengamos la aplicación en producción.
Una vez que tenemos configurado el cliente Faye podemos suscribirnos a canales. Dado que sólo tenemos una página en nuestra aplicación sólo existirá un único canal, al que llamaremos /messages/new
. Para suscribirnos a un canal invocamos la función subscribe
, pasándole el nombre del canal al que nos queremos suscribir y una función de vuelta. Esta función callback será llamada cuando el canal reciba un mensaje, y se le pasarán ciertos datos. Por ahora, simplemente mostraremos un alert
para ver qué datos nos han llegado.
$(function() { var faye = new Faye.Client('http://localhost:9292/faye'); faye.subscribe('/messages/new', function (data) { alert(data); }); });
Podemos probarlo todo ahora mismo arrancando nuestra aplicación Rails y visitando la página de chat. Se carga el JavaScript y el cliente Faye comienza a escuchar en espera de mensajes. Podemos hacer que la función de vuelta se dispare manualmente utilizando curl
para enviar un mensaje al canal.
$ curl http://localhost:9292/faye -d 'message={"channel":"/messages/new", "data":"hello"}'
Para que todo esto funcione tenemos que enviar datos POST en cierto formato: un parámetro de mensaje con ciertos datos en JSON, que tienen que incluir claves llamadas channel
y data
.
Al ejecutar el comando curl
recibiremos instantáneamente un mensaje de alerta en el navegador que abrimos antes con los datos que hemos enviado.
Esto significa que podemos enviar notificaciones a través de nuestra aplicación Rails enviando una petición POST a Faye.
Difusión de mensajes
Podemos ya plantearnos modificar nuestro fichero create.js.erb
para que cuando se envíe un mensaje se retransmita por Faye a todos los navegadores suscritos al canal. Sería muy cómodo tener un método llamado por ejemplo broadcast
que recibiese un parámetro channel
y un bloque de forma que cualquier cosa que el bloque devolviese sería retransmitida a todo el canal.
Creemos este método en ApplicationHelper
, y en él construiremos el mensaje a partir del parámetro channel
y lo que devuelva el bloque y luego utilizaremos Net::HTTP.post_form
para hacer un POST con estos datos hacia el servidor Faye.
module ApplicationHelper def broadcast(channel, &block) message = {:channel => channel, :data => capture(&block)} uri = URI.parse("http://localhost:9292/faye") Net::HTTP.post_form(uri, :message => message.to_json) end end
Tenemos que requerir Net::HTTP
en el fichero /config/application.rb
dado que Rails no lo incluye por defecto.
require File.expand_path('../boot', __FILE__) require 'rails/all' require 'net/http' # resto del archivo...
Ya podemos usar nuestro nuevo método broadcast
en create.js.erb
.
<% broadcast "/messages/new" do %> $("#chat").append("<%= escape_javascript render(@message) %>"); <% end %> $("#new_message")[0].reset();
Podemos probarlo ahora mismo. Si volvemos a la aplicación e introducimos un mensaje, Faye nos lo devuelve y vemos el JavaScript que tenemos que evaluar para añadir el nuevo mensaje a la lista.
Para que el navegador evalúe el JavaScript en lugar de mostrarlo tan sólo tenemos que cambiar alert
por eval
.
$(function() { var faye = new Faye.Client('http://localhost:9292/faye'); alert('subscribing!') faye.subscribe('/messages/new', function (data) { eval(data); }); });
Podemos probar esta funcionalidad recargando la página y abriendo una nueva ventana del navegador. Cuando escribamos un mensaje en una ventana de conversación, éste aparecerá inmediatamente en la otra.
Con esto podemos tomar cualquier peticion AJAX y retransmitir cualquier código JavaScript a todos los clientes suscritos utilizando el bloque de broadcast
. Si en lugar de ejecutar JavaScript preferimos trabajar con JSON podemos hacerlo con un enfoque similar devolviendo JSON en lugar de JavaScript.
Seguridad
El código que llevamos escrito funciona bien, pero no es seguro. Anteriorme enviamos un mensaje simplemente usando curl
desde la línea de órdenes, y tal y como está el código cualquiera podría hacer lo mismo y enviar código JavaScript para ser evaluado en todos los clientes que se encuentren escuchando un canal dado.
Con Faye podemos resolver esto utilizando extensiones, lo que se explica en la documentación pero que repasaremos rápidamente aquí. Para definir estas extensiones tenemos que crear una clase ruby y hacer que implemente un método llamado incoming
o bien un método outgoing
. Nuestro método leerá un código de autenticación y devolverá un error si dicho código no es el esperado. Se puede utilizar el método add_extension
en el fichero Rackup para añadir la clase como una extensión del propio servidor Faye.
Tendremos que generar un código secreto compartido entre el servidor Faye y la aplicación Rails para que sea verificado antes de aceptar ningún mensaje. Para ello tenemos que añadir un nuevo fichero de inicialización en nuestra aplicación Rails, al que llamaremos faye_token.rb
. Nótese que no queremos incluir este fichero en un repo Git porque tiene que ser único en cualquier sistema domde utilicemos esta aplicación. En el fichero crearemos una constante llamada FAYE_TOKEN
que puede tener prácticamente cualquier valor.
FAYE_TOKEN = "anything_here"
A continuación actualizaremos el método broadcast
para que incluya este secreto junto con cualquier mensaje enviado. Los datos de las extensiones se incluyen con el parámetro :ext
y en dicho parámetro es en el que enviaremos el secreto con el nombre :auth_token
.
module ApplicationHelper def broadcast(channel, &block) message = {:channel => channel, :data => capture(&block), :ext => {:auth_token => FAYE_TOKEN}} uri = URI.parse("http://localhost:9292/faye") Net::HTTP.post_form(uri, :message => message.to_json) end end
Por último tenemos que modificar el fichero faye.ru
para añadir la extensión que gestiona este esquema de autenticación.
require 'faye' require File.expand_path('../config/initializers/faye_token.rb', __FILE__) class ServerAuth def incoming(message, callback) if message['channel'] !~ %r{^/meta/} if message['ext']['auth_token'] != FAYE_TOKEN message['error'] = 'Invalid authentication token.' end end callback.call(message) end end faye_server = Faye::RackAdapter.new(:mount => '/faye', :timeout => 45) faye_server.add_extension(ServerAuth.new) run faye_server
Cargamos el secreto para Faye desde el fichero de inicialización que escribimos antes y luego creamos una nueva clase llamada ServerAuth
que implementa el método incoming
. En este método nos aseguramos que el nombre del canal no empiece por “meta” porque Faye utiliza internamente nombres de ese estilo y no queremos autenticar ninguno de estos canales. A continuación compraremos que el auth_token
es correcto y si no lo es enviamos un mensaje. Luego llamamos a la función de vuelta. Por último, al final del fichero añadimos el método de extensión justo después de crear el servidor Faye.
Si ahora reiniciamos ambos servidores e intentamos enviar el comando curl
otra vez recibiremos un error de petición incorrecta porque la petición no ha sido validada.
$ curl http://localhost:9292/faye -d 'message={"channel":"/messages/new", "data":"hello"}' HTTP/1.1 400 Bad Request Content-Type: application/json Connection: close Server: thin 1.2.11 codename Bat-Shit Crazy Content-Length: 11
Sin embargo nuestra aplicación Rails sigue funcionando igual que antes porque los mensajes envían el código secreto de autenticación correcto.
Con esto concluimos este episodio sobre Faye. Es una solución excelente para gestionar las notificaciones push sin tener que cambiar de framework por completo. Podemos mantener toda la lógica dentro de Rails y a la vez disfrutar de las ventajas que ofrecen las notificacione asíncronas.
Aquellos que no estén interesados en gestionar su propio servidor Faye, pueden considerar echar un vistazo a un servicio llamado Pusher que nos libera de llevar la gestiónn del servidor de eventos.