#266 HTTP Streaming
- Download:
- source codeProject Files in Zip (90.8 KB)
- mp4Full Size H.264 Video (17.3 MB)
- m4vSmaller H.264 Video (10.1 MB)
- webmFull Size VP8 Video (11.1 MB)
- ogvFull Size Theora Video (24 MB)
Continuamos en este episodio con la serie dedicada a algunas de las novedades que se incluyen en la primera versión beta de rails 3.1 Esta vez veremos lo que se conoce como emisión continua (streaming) de HTTP explicado en este post del blog oficial de Ruby on Rails; merece la pena leer esa anotación primero. Hoy trataremos de configurarlo en una aplicación Rails y repasaremos algunos de los potenciales problemas que nos podemos encontrar.
Para demostrar en qué consiste la emisión continua de HTTP utilizaremos la sencilla aplicación de tareas que vimos en el último episodio. Esta funcionalidad puede activarse a nivel de controlador o acción pero no viene habilitada por defecto.
Para activar la emisión continua utilizamos render pasando la opción :stream => true
.
def index @projects = Project.all render :stream => true end
Y para habilitar la emisión continua en todo el controlador utilizaremos el método de clase stream
.
class ProjectsController < ApplicationController stream def index # rest of controller code end end
Pero WEBrick, el servidor web por defecto en Rails 3, no soporta la emisión continua por lo que para verla en acción tenemos que cambiar a otro que sí lo haga como por ejemplo Unicorn.
En el Gemfile
de la aplicación figura la gema Unicorn pero por defecto se encuentra comentada. Para utilizar esta gema descomentaremos la línea y ejecutaremos bundle para instalarla.
# Use unicorn as the web server gem 'unicorn'
Para que Unicorn realice la emisión continua de HTTP debemos configurarlo, Rails dispone de de documentación interna sobre la emisión continua de HTTP donde se explica cómo hacerlo; de momento nosotros tan sólo tendremos que crear un fichero de configuración que pondremos en el directorio config
.
listen 3000, :tcp_nopush => false
Tras esto podemos arrancar Unicorn diciéndole que use dicho fichero de configuración.
$ unicorn_rails --config-file config/unicorn.rb
Si tenemos problemas al lanzar esta orden debemos asegurarnos de haber ejecutado bundle
después de descomentar la línea que incluye gem 'unicorn'
en el Gemfile
. Si aún así sigue sin funcionar, podemos probar a poner delante bundle exec
.
Ya podemos abrir nuestra aplicación con el navegador en el puerto 3000, como lo haríamos si estuviésemos con el servidor de Rails por defecto.
Simulación de la emisión continua
No habremos podido notar que el HTML de la pagína se emite de forma continua, para apreciarlo tenemos que simular algo que haga que la página tarde algunos segundos en ejecutarse en la capa de vista para así ver que la respuesta se está devolviendo a trozos. La forma más sencilla de hacer esto es añadiendo una llamada a sleep
en una de las vistas.
<% sleep 5 %> <h1>Listing projects</h1> <table> <tr> <th>Name</th> <th></th> <th></th> <th></th> </tr> <% @projects.each do |project| %> <tr> <td><%= project.name %></td> <td><%= link_to 'Show', project %></td> <td><%= link_to 'Edit', edit_project_path(project) %></td> <td><%= link_to 'Destroy', project, confirm: 'Are you sure?', method: :delete %></td> </tr> <% end %> </table> <br /> <%= link_to 'New Project', new_project_path %>
Podemos ver la respuesta troceada recuperando la página con curl
. Si cargamos la página principal de la aplicación veremos que parte de la respuesta se devuelve inmediatamente.
$ curl -i localhost:3000 HTTP/1.1 200 OK Date: Wed, 18 May 2011 08:18:56 GMT Status: 200 OK Connection: close Cache-control: no-cache Transfer-Encoding: chunked Content-Type: text/html; charset=utf-8 X-UA-Compatible: IE=Edge X-Runtime: 0.023745 <!DOCTYPE html> <html> <head> <title>Todo</title> <link href="/assets/application.css" media="screen" rel="stylesheet" type="text/css" /> <script src="/assets/application.js" type="text/javascript"></script> <meta content="authenticity_token" name="csrf-param" /> <meta content="0eBxvhbMH6HA8ocRLw06uNnmh7zqWo5dGSeFIA8sfj8=" name="csrf-token" /> </head> <body>
Habrá algunos segundos de retraso antes de que vuelva el resto. Nótese en la información de la cabecera que Transfer-Encoding
vale chunked
y no hay cabecera Content-Length
porque el servidor no puede saber el tamaño de la página completa.
<h1>Listing projects</h1> <table> <tr> <th>Name</th> <th></th> <th></th> <th></th> </tr> <tr> <td>Housework</td> <td><a href="/projects/1">Show</a></td> <td><a href="/projects/1/edit">Edit</a></td> <td><a href="/projects/1" data-confirm="Are you sure?" data-method="delete" rel="nofollow">Destroy</a></td> </tr> </table> <br /> <a href="/projects/new">New Project</a> </body> </html>
Si no hubiésemos habilitado la emisión continua se habría producido un retraso de cinco segundos antes de ver cualquier respuesta desde el servidor. Si somos capaces de devolver la parte de la cabecera de la página de forma inmediata el navegador podrá empezar a procesar esta sección y cargará el JavaScript y los archivos CSS que necesite mientras espera que el resto de la página sea devuelto por el servidor.
Para aprovechar al máximo la emisión continua tenemos que mover todo el procesamiento que podamos a la capa de las vista para que el servidor pueda empezar a emitir la página tan pronto como le sea posible. En nuestra acción index
usamos Project.all
para recuperar los proyectos que aparecen en la página pero si lo hacemos así el servidor no puede empezar a emitir esta página hasta que dicha consulta haya terminado. Si la cambiamos por algo basado en scoped
, que realiza una carga perezosa, la llamada a base de datos no se hará hasta que la capa de vista empiece a iterar sobre la colección de proyectos.
def index @projects = Project.scoped end
Problemas potenciales con la emisión continua
Todo esto pinta muy bien pero hay algunos inconvenientes que deberemos tener en cuenta antes de usarlo. El primero es que se invierte el orden de procesamiento entre layout y plantilla. En una petición Rails normal primero se procesa la plantilla de la acción y a continuación el layout. En una petición con emisión continua tenemos que mostrar primero el contenido del layout tan pronto como se pueda porque por lo general esta es la parte que contiene la sección head
. La plantilla de la acción no se procesará hasta que lleguemos al comando yield
del layout.
Todo esto quiere decir que si intentamos hacer algo como establecer una variable de instancia en la plantilla no tendremos acceso a ella en el layout porque la plantilla todavía no habrá sido procesada. Por ejemplo, intentemos establecer una variable de instancia llamada @title
en la acción index
.
<% @title = "Projects "%> <% sleep 5 %> <h1>Listing projects</h1> <table> <!-- Se omite el resto del archivo. -->
Y tratemos a continuación de utilizar esta variable en el layout:
<!DOCTYPE html> <html> <head> <title><%= @title %></title> <%= stylesheet_link_tag "application" %> <%= javascript_include_tag "application" %> <%= csrf_meta_tags %> </head> <body> <%= yield %> </body> </html>
Si recuperamos la página con curl
y miramos la cabecera head
el elemento title
aparecerá vacío porque el layout se procesa antes que la plantilla y por lo tanto la variable @title
aún no habrá sido definida cuando la pide el fichero de layout. Esto no ocurre en las peticiones normales (que no usan emisión continua) porque la plantilla se procesa primero.
<head> <title></title> <link href="/assets/application.css" media="screen" rel="stylesheet" type="text/css" /> <script src="/assets/application.js" type="text/javascript"></script> <meta content="authenticity_token" name="csrf-param" /> <meta content="x0CtIY+0vEbfkh6gohZp/WdOd0ZanobQHZT8+HUC/OE=" name="csrf-token" /> </head>
Por supuesto la forma correcta de pasar información entre la plantilla y el layout es utilizar content_for
, pero esto tampoco funciona. Si lo intentamos y luego utilizamos curl
para ver la página veremos que la salida se detendrá justo antes del elemento title
.
Este fallo se debe al funcionamiento de content_for
. Si tenemos varias llamadas a content_for
para el mismo elemento Rails las concatena todas. Por tanto, cuando Rails ve content_for :title
al principio no puede saber que no hay más llamadas en el resto de la página.
Rails 3.1 proporciona un nuevo método llamado provide
que se comporta exactamente igual que content_for
excepto que no concatena los valores y es el que debemos utilizar para establecer el título de la página.
<% provide :title, "Projects" %>
En el layout ya podemos utilizar yield
igual que haríamos si usásemos content_for
.
<title><%= yield :title %></title>
Si ahora abrimos la página veremos que el elemento title
contiene el título deseado.
<!DOCTYPE html> <html> <head> <title>Projects</title> <link href="/assets/application.css" media="screen" rel="stylesheet" type="text/css" /> <script src="/assets/application.js" type="text/javascript"></script> <meta content="authenticity_token" name="csrf-param" /> <meta content="NzdFt92dDBSXRRgFR0pRZRizirN87Qb5CVdqgGEAvTU=" name="csrf-token" /> </head> <!-- rest of page -->
Otro potencial problema a la hora de utilizar la emisión continua es lo que ocurre cuando se eleva una excepción. Podemos ver esto añadiendo una llamada a un método inexistente en la plantilla index
.
<% provide :title, "Projects" %> <% fall_over %> <% sleep 5 %> <h1>Listing projects</h1> <!-- resto de la página -->
Podemos ver la interesante salida que devuelve la página.
$ curl -i http://localhost:3000/ (omitimos la información de la cabecera) <!DOCTYPE html> <html> <head> <title>Projects</title> <link href="/assets/application.css" media="screen" rel="stylesheet" type="text/css" /> <script src="/assets/application.js" type="text/javascript"></script> <meta content="authenticity_token" name="csrf-param" /> <meta content="rhFAQuK2s5Rxi6jjC3jA12k07GjD75VeWlbsyf47bLc=" name="csrf-token" /> </head> <body> "><script type="text/javascript">window.location = "/500.html"</script></html>
Si se eleva una excepción Rails devuelve un elemento script al navegador que contiene una línea JavaScript para redirigir el navegador a la página 500.html
. Si vemos esta página en el navegador seremos llevados a la página de error estándar que muestran las aplicaciones Rails en producción.
Esto quiere decir que no podemos ver la información de depuración ni tan siquiera en modo de desarrollo, por lo que tendremos que buscar los errores en el log de desarrollo.
Tampoco podremos establecer información de sesión y cookies dentro de la plantilla en emisión continua. Si intentamos establecer una variable de sesión en la plantillas Rails ya habrá enviado la cabecera HTTP al navegador por lo que no podremos ampliar dicha información desde la plantilla. Esto se aplica también a cookies y mensajes flash, que utilizan la sesión. Deberemos establecer esta información siempre desde el controlador.
Los dos últimos problemas potenciales consisten en que la emisión continua de HTML utiliza la funcionalidad de fibras en Ruby, por lo que necesitamos estar en Ruby 1.9 y que además es incompatible con ciertos tipos de middleware. Si el middleware modifica por su cuenta la respuesta de la aplicación no funcionará en emisión continua.
Con todo esto ya podemos entender por qué la emisión continua de HTTP no viene activada por defecto: hay un gran número de problemas potenciales que deberemos tener en cuenta antes de su uso, por lo que lo mejor es restringirlo a sólo aquellas páginas en las que necesitemos sacar el máximo rendimiento. Pero a pesar de todos estos problemas Merece la pena considerar la emisión continua de HTTP porque podemos mejorar la experiencia final de usuario, especialmente en páginas que incluyen un gran número de ficheros JavaScript y CSS que el navegador pueda ir cargando y procesando tan pronto como sea posible.