homeASCIIcasts

260: Mensajería asíncrona con Faye 

(view original Railscast)

Other translations: En Ja

Written by Juan Lupión

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 .

La página de conversación.

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.

Los mensaje sólo se añaden en una ventana.

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.

/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.

/app/views/messages/index.html.erb

<% 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

.

/app/controllers/messages_controller.rb

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.

/app/views/messages/create.js.erb

$("#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:

/faye.ru

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.

/app/views/layouts/application.html.erb

<%= 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.

/public/javascripts/application.js

$(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.

La alerta aparece tan pronto como se envía la orden.

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.

/app/helpers/application_helper.rb

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.

/config/application.rb

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.

/app/views/messages/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.

En la alerta aparece el JavaScript devuelto.

Para que el navegador evalúe el JavaScript en lugar de mostrarlo tan sólo tenemos que cambiar alert por eval.

/public/javascripts/application.js

$(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.

Los mensajes enviados aparecen en ambas ventanas.

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.

/config/initializers/faye_token.rb

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.

/app/helpers/application_helper.rb

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.

/faye.ru

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.

Nuestro mensajes se envían porque validan correctamente.

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.