jueves, diciembre 04, 2008

Más allá del AJAX: Comet

Introducción:

Como todos los que esteis leyendo esto sabreis (porque los que no lo sepan el títulos les habrá disuadido de leer) HTTP es un protocolo sin estado donde un cliente (un navegador o browser) realiza peticiones contra un servidor y este le "contesta".
Gracias al objeto javascript XMLHttpRequest que implementan los navegadores más comunes (Firefox, iExplorer, Safari, Opera...) el navegador puede realizar peticiones de forma asincrona al servidor sin necesidad de hacer el clásico submit. Como en un inicio este tipo de peticiones eran contestada por el servidor mediante XML a esta técnica se le conoce como AJAX (hoy en día cada vez es más común usar JSON en lugar de XML).
Quizá para muchos es muy engorroso programar webs haciendo un uso exagerado de javascript pero el resultado es una web mucho más agradable al usuario. Para facilitarnos la vida existen cada vez más toolkits con los que hacer esto:
Bien, ahora ya podemos hacer webs como Gmail; nos ponemos manos a la obra y vemos que Gmail incorpora un pequeño chat mediante el cual podemos hablar con nuestros contactos que tengan abierto Gmail en ese momento o Gtalk ¿como hacemos eso nosotros con nuestro AJAX? ¿como podemos saber que alguien nos ha hablado? Pues como ya hemos dicho antes el HTTP (que no debemos olvidar que marca unas reglas básicas que no van a cambiar) consiste en que un cliente pide algo a un servidor y este le contesta. Pero en este caso lo que queremos es que sea el sevidor el que nos envie lo que otro usuario ha escrito en el chat... Tenemos un problema. Para solucionarlo con las herramientas que tenemos hasta este punto sólo hay una solución: la función javascript setTimeout(). Cada cierto tiempo periódico "preguntaremos" al servidor si tiene algo que decirnos. Creo que no tengo que decir que esto es un desperdicio de recursos y que da la sensación de ser una "ñapa". Pues puede ser, pero es la única forma... hasta la llegada de Comet.
En este punto debo hacer un inciso; el nombre de Comet fué acuñado por el creador de Dojo, Alex Russell, pero simultaneamente otros desarrolladores bautizaron la misma técnica con otros nombre: Server Push y Reverse AJAX. Estos tres nombres se refieren a la misma técnica, es decir, son sinónimos. Espero que el día de mañana una denominación triunfe sobre las otras ya que sino es un lio hablar con otro programador teniendo que dar los tres nombres por si este sólo se refiere con uno de ellos. Sigamos...
Comet al igual que AJAX no es un invento nuevo, como el bicarbonato :), sino una técnica de "engañar" al HTTP. Básicamente consiste en que el cliente se encargue de que siempre haya una conexión con el servidor y el servidor se encarga de "coger" la conexión y no soltarla hasta que tiene algo que decir al cliente ¿sencillo no?. Aquí el problema es más de rendimiento que de implementar. En el modelo actual de servidores (Tomcat, Jetty...) por cada petición que hace el browser se ejecuta un Thread en el servidor para responderla. Esto para una aplicación web al que se van a conectar 10 usuarios no supone un problema pero para una aplicación de 10.000 usuarios concurrentes pues nos tiraría el servidor.
Por suerte o por desgracia no somos los primeros en "inventarnos" esta técnica y tanto Tomcat como Jetty han implementado formas de solventar este problema mientras esperamos a la nueva especificación de los Servlets: la 3.0 (JSR-315).

Manos a la obra:

A continuación os muestro código de como implementar la técnica Comet en un servidor Tomcat. No es una aplicación completa, sólo pequeños pedazos con lo imprescindible.

Lo primero de todo es hacer que Tomcat use un Thread para cada Servlet en lugar de un Thead para cada petición. Tendremos que sustituir en el server.xml esta linea:

<Connector URIEncoding="utf-8" connectionTimeout="20000" port="8084" protocol="HTTP/1.1" redirectPort="8443"/>

por esta otra:

<Connector URIEncoding="utf-8" connectionTimeout="20000" port="8084" protocol="org.apache.coyote.http11.Http11NioProtocol" redirectPort="8443"/>

Con este cambio hacemos que cuando al Tomcat le llegue una petición AJAX invoque el método:


public void event(CometEvent event) throws IOException, ServletException{}

en lugar del clásico doGet o doPost.

Como estareis pensando el siguiente cambio es en el Servlet. Ya no necesitamos los doGet y doPost y debemos hacer que el Servlet implemente la interfaz CometProcessor:


public class Cometa extends HttpServlet implements CometProcessor {}

Esto va cogiendo forma, tenemos la estructura sólo nos falta rellenar la parte javascript y el método Java. Empecemos por el javascript:


<script>
function peticionAjax(){
$.ajax({
url: 'http://localhost:8084/CometAbel/Cometa',
type: 'post',
data: { message: "enviando cosas al servidor..." },
beforeSend: function(oXhr){ oXhr.setRequestHeader('Connection', 'Keep-Alive'); },
success: function(data){
alert(data);
setTimeout('peticionAjax()', 1000);
}
});
}
</script>

Ese simple código javascript, que hace uso de jQuery, hace una llamada al servidor y cuando este le conteste mostrará un alert() con la respuesta. Acto seguido vuelve a abrir una conexión con el servidor para esperar la siguiente respuesta. Para enrevesar la cosa el cliente envia datos en el campo "data", en una aplicación normal podría mandar un código, identificardor...

Vale pues, ya tenemos casi todo pero eso era la fácil, vamos a la miga del asunto: el Servlet.

Para nuestro ejemplo usaremos dos variables dentro del Servlet. Una primera que almacenará todas las peticiones que le van llegando y otra que será un simple hilo que simulara estar haciendo el trabajo (las consultas en función de las cuales contestaremos a los navegadores):


// Array con las peticiones
protected ArrayList<HttpServletResponse> connections = new ArrayList<HttpServletResponse>();
// Hilo que simula la tarea
protected MessageSender messageSender = null;

Vamos a implementar dos métodos del Servlet:

public void init() throws ServletException {
messageSender = new MessageSender();
Thread messageSenderThread = new Thread(messageSender, "MessageSender[" + getServletContext().getContextPath() + "]");
messageSenderThread.setDaemon(true);
messageSenderThread.start();
}

public void destroy() {
connections.clear();
messageSender.stop();
messageSender = null;
}

Y ahora lo importante, el método event:

public void event(CometEvent event) throws IOException, ServletException {
// conseguimos la request y la response
HttpServletRequest request = event.getHttpServletRequest();
HttpServletResponse response = event.getHttpServletResponse();

if (event.getEventType() == CometEvent.EventType.BEGIN) {
// si nos llega una nueva petición la "pillamos"
synchronized(connections) {
connections.add(response);
}
} else if (event.getEventType() == CometEvent.EventType.ERROR) {
// si ocurre un error desde el cliente "soltamos" la petición
synchronized(connections) {
connections.remove(response);
}
event.close();
} else if (event.getEventType() == CometEvent.EventType.END) {
// si la "conversación" ha terminado "soltamos" la petición
synchronized(connections) {
connections.remove(response);
}
PrintWriter writer = response.getWriter();
writer.println("['error']");
event.close();
} else if (event.getEventType() == CometEvent.EventType.READ) {
// si el cliente nos "envía" datos, los procesamos
InputStream is = request.getInputStream();
byte[] buf = new byte[512];
do {
int n = is.read(buf); //can throw an IOException
if (n > 0) {
System.out.println(new String(buf, 0, n));
} else if (n < 0) {
System.out.println("Este error no debería ocurrir.");
return;
}
} while (is.available() > > 0);
}
}

Bueno, pues ya está casi todo hecho. En estos momentos tenemos en el servidor un Array que almacena sin desbordarnos la memoria las conexiones. Cuando ocurra "algo": un evento, una consulta que realizamos a la Base de Datos cada cierto tiempo, etc... por lo que tengamos que avisar a los navegadores sólo tenemos que recorrer ese Array y enviarles la información. Un ejemplo un tanto inútil sería una clase que implemente Runnable con el siguiente código en el método run:


public void run() {
while (true) {
try {
Thread.sleep(9000);
} catch (InterruptedException ex) {
System.out.println("InterruptedException");
}
int algoImportanteHaOcurrido = 0;
// en lugar de ese 1 iría la llamada a la lógica de negocio
// que devolverá un código
algoImportanteHaOcurrido = 1;

if(algoImportanteHaOcurrido > 0){
synchronized (connections) {
for (HttpServletResponse connection : connections) {
try {
PrintWriter writer = connection.getWriter();
writer.println("Informo a los navegadores con este texto" + algoImportanteHaOcurrido;
writer.flush();
writer.close();
} catch (IOException e) {
System.out.println("IOException");
}
}
}
}
}
}

Y eso es todo. Con esto podreis ver más o menos en que consiste esa palabra rara que oireis cada vez más: Comet.

Apuntes finales:
  • No os olvideis los synchronized al acceder a los objetos compartidos, en este caso el Array de conexiones..
  • Si quereis saber más sobre NIO aquí teneis un tutorial en inglés.
  • Aquí encontrareis un ejemplo comet de IBM.
  • Y aquí la especificación de Tomcat para NIO.

4 comentarios:

  1. Parece una buena currada. Cuando tenga tiempo me lo leo.

    ResponderEliminar
  2. Justo lo que andaba buscando... Gracias.

    ResponderEliminar
  3. Muy buena entrada!!
    Me va a ser de mucha utilidad. A ver si consigo hacer alguna pruebecilla y te comento que tal ha salido

    ResponderEliminar
  4. Gracias, para cualquier cosa aqui me tienes.

    ResponderEliminar