#316 Private Pub
- Download:
- source codeProject Files in Zip (80.9 KB)
- mp4Full Size H.264 Video (15.2 MB)
- m4vSmaller H.264 Video (8.28 MB)
- webmFull Size VP8 Video (10 MB)
- ogvFull Size Theora Video (17.4 MB)
En el episodio 260 vimos como se pueden actualizar los contenidos del navegador del usuario en tiempo real utilizando Faye. Pero aunque Faye es una solución muy buena su uso puede acarrear bastante trabajo, especialmente si tenemos requisitos complejos de seguridad. Esto es lo que llevó a Ryan Bates a crear una gema llamada Private Pub que funciona sobre Faye, con el propósito de hacer que fuese más fácil publicar y suscribirse a eventos en tiempo real en una aplicación Rails, y en este episodio veremos cómo funciona.
La aplicación con la que vamos a trabajar es un sencillo chat. Si introducimos un mensaje en el campo de texto del fondo de la página y hacemos clic en “Send” el navegador enviará una petición AJAX para guardar el mensaje y luego actualizará el navegador para que dicho mensaje aparezca en la ventana principal.
Pero hay un problema con nuestra aplicación. Si hay múltiples clientes de chat abiertos no se actualizarán todos en tiempo real cuando se envíe el mensaje. Sólo el cliente que envió el mensaje lo verá aparecer; el resto tendrá que recargar la página para verlo. Hay varias soluciones a este problema, una de ellas es hacer que los clientes realicen sondeos periódicos consultando si hay nuevos mensajes cada par de segundos, lo cual no es la mejor idea. Otra solución es mantener una conexión por socket con el servidor en cada cliente de forma que el servidor pueda avisar a los clientes cuando lleguen nuevos mensajes para que estos aparezcan en tiempo real. El problema con esta solución es que la arquitectura de Rails no está pensada para peticiones de larga duración.
Faye y Private Pub pueden sernos de ayuda en este escenario pero antes de empezar nuestro repaso veremos cómo funciona nuestra aplicación de chat. Empezaremos con la plantilla de la vista de la página de chat.
<h1>Chat</h1> <ul id="chat"> <%= render @messages %> </ul> <%= form_for Message.new, remote: true do |f| %> <%= f.text_field :content %> <%= f.submit "Send" %> <% end %>
En esta plantilla se usa un parcial llamado messages
para mostrar el listado de mensajes y se incluye un formulario para crear nuevos mensajes. Este formulario utiliza la opción remote:true
para que la petición se envíe vía AJAX hacia la acción create
de MessagesController
, que tiene el siguiente aspecto:
class MessagesController < ApplicationController def index @messages = Message.all end def create @message = Message.create!(params[:message]) end end
La acción create
es muy sencilla, tan sólo crea un nuevo Message
. Veamos la plantilla de JavaScript que se devolverá como respuesta a la llamada AJAX:
$("#chat").append("<%= j render(@message) %>");
$("#new_message")[0].reset();
Este fragmento de JavaScript añade el mensaje que se acaba de crear al listado del chat y borra el campo del formulario de nuevo mensaje. Lo que queremos que se haga aquí es enviar el mensaje a todos los usuarios del chat para que lo muestren, no sólo al que lo envió y es para esto para lo que vamos a usar Private Pub.
Instalación de Private Pub
Private Pub se instala de la forma habitual añadiendo la gema al fichero Gemfile
de la aplicación y ejecutando a continuación bundle
. Esto hará que se instale también Faye porque está declarado como dependencia.
source 'http://rubygems.org' gem 'rails', '3.1.3' gem 'sqlite3' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.1.5' gem 'coffee-rails', '~> 3.1.1' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' gem 'private_pub'
A continuación ejecutamos el generador para crear el fichero de configuración y un fichero Rackup para lanzar el servidor Faye.
$ rails g private_pub:install create config/private_pub.yml create private_pub.ru
El servidor Rack se arranca con esta orden:
$ rackup private_pub.ru -s thin -E production >> Thin web server (v1.3.1 codename Triple Espresso) >> Maximum connections set to 1024 >> Listening on 0.0.0.0:9292, CTRL+C to stop
Esta orden arranca Faye usando el servidor Thin en el entorno de producción (lo cual es necesario para que arranque). El último paso es poner private_pub
en fichero de manifiesto de JavaScript de nuestra aplicación.
// This is a manifest file that'll be compiled into including all the files listed below. // Add new JavaScript/Coffee code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // the compiled file. // //= require jquery //= require jquery_ujs //= require private_pub //= require_tree .
Podemos incluir este JavaScript en el layout de la aplicación si estamos con versiones de Rails anteriores a la 3.1.
Uso de Private Pub en nuestras aplicaciones
Una vez configurada la carga de Private Pub es fácil suscribirse a un canal y publicar notificaciones de actualización sobre él. Lo único que hay que hacer es suscribirse a un canal en una plantilla de la vista, por ejemplo en nuestra página de mensajes, invocando subscribe_to
y pasando el nombre del canal. El formato de un canal en Faye es como el de una ruta de archivo, nosotros vamos a usar /messages/new
.
<h1>Chat</h1> <ul id="chat"> <%= render @messages %> </ul> <%= form_for Message.new, remote: true do |f| %> <%= f.text_field :content %> <%= f.submit "Send" %> <% end %> <%= subscribe_to "/messages/new" %>
Lo siguiente que tenemos que hacer es ir a una plantilla JavaScript que se muestre como resultado de una petición AJAX (como la que tenemos que muestra los nuevos mensajes) y envolver el código en un bloque publish_to
.
<% publish_to "/messages/new" do %> $("#chat").append("<%= j render(@message) %>"); $("#new_message")[0].reset(); <% end %>
El hacer esto quiere decir que este JavaScript no sólo se devolverá al cliente que hizo la petición, sino que será publicada a todos los suscriptores del canal /messages/new
. Podemos hacer la prueba ahora mismo. Si abrimos la aplicación en dos navegadores distintos y enviamos un mensaje en uno de ellos, dicho mensaje aparecerá instantáneamente en el otro.
Puede ser que no queramos publicar sobre un canal directamente en la respuesta AJAX. Por ejemplo podríamos querer publicar desde una acción del controlador. Esto se puede hacer desde Ruby utilizando PrivatePub.publish_to
, y pasando el nombre del canal en el que queremos publicar y el código JavaScript que queremos que se ejecute en todos los clientes suscritos. Demostremos esta funcionalidad devolviendo un alert
.
def create @message = Message.create!(params[:message]) PrivatePub.publish_to("/messages/new", "alert('#{@message.content}');") end
El alert
aparecerá en ambos navegadores cuando se escriba un mensaje en cualquiera de ellos.
Manipulación de datos JSON
También se puede si lo preferimos trabajar con datos en JSON en lugar de código JavaScript. Lo único que hace falta hacer es pasar a publish_to
un objeto (como un hash) en lugar de una cadena y entonces será convertido a JSON.
def create @message = Message.create!(params[:message]) PrivatePub.publish_to("/messages/new", message: @message) end
Con este enfoque tendremos que escribir JavaScript para gestionar el JSON que estamos emitiendo. Lo haremos en el fichero CoffeeScript messages
.
PrivatePub.subscribe "/messages/new", (data, channel) -> alert data.message.content
Aquí llamamos a PrivatePub.subscribe
, que recibe un nombre de canal y una función asíncrona que recibe dos argumentos, data
y channel
. La función será invocada cada vez que haya una actualización con nuevos datos JSON en el canal. En nuestro código tan sólo hacemos alert
mostrando el campo message.content
de los datos recibidos. Si recargamos las páginas y enviamos otro mensaje veremos la alerta mostrando los datos JSON enviados desde el servidor.
Una ventaja de Private Pub es que los canales por defecto son privados. Esto quiere decir que no tenemos que preocuparnos de que haya usuarios escuchando en canales a los que no hayan sido explícitamente suscritos. Con esto es muy fácil crear, por ejemplo, una sala privada de conversación donde sólo pueden escribir y leer los usuarios que se hayan suscrito. También podemos expirar las suscripciones pasado algún tiempo de forma que los usuarios no puedan volver a escuchar mensajes después de haber terminado la sesión. Todo esto se configura en el fichero private_pub.yml
.
development: server: "http://localhost:9292/faye" secret_token: "secret" test: server: "http://localhost:9292/faye" secret_token: "secret" production: server: "http://example.com/faye" secret_token: "210eb617b6ce1c351d986a3185d34025cf42e5091a37502f18f595f7e8773853" signature_expiration: 3600 # one hour