Manuales

 comet diagram

Vamos a ver como PHP nos ayuda en nuestra tarea de implementar un chat asíncrono con tecnología COMET.

Lo primero sería la explicación teórica de la parte servidor (la parte cliente ya se trató en la entrega anterior). Ya se comentó el problema de compartir datos entre clientes. Joseba me comentó que era un ejemplo más del típico problema de los lectores y escritores. Los lectores serían los procesos que van leyendo los mensajes que producen otros usuarios y los reenvian al navegador. Los escritores serían los procesos que se ejecutan cuando un usuario quiere mandar un mensaje. Vale, entendido el problema, sigue presente la cuestión ¿cómo comunicar los procesos?.

The cool way

El caso era que hay que utilizar un area de datos común para que ambos (lectores y escritores) puedan comunicarse. Yo me decidí por un area de memoria compartida. ¿Uh?. Si, PHP implementa una serie de funciones para la gestión de memoria compartida y semáforos. Tal memoria es un area de datos identificada por una clave de tal forma que cualquier proceso que conozca la clave, puede acceder (leer y escribir) en ese area de memoria. Es ultrarápida y gracias a los semáforos puedes controlar múltiples accesos para no corromper los datos.

Ya está, descubierta la forma de implementarlo, vamos a reunir todos los pedacitos en un código PHP que realice la tarea.

 
 1  <?php 
 2  require_once("eventmanager.php");
 3  $id_semaforo = 666;
 4  $id_memoria = 123;
 5 
 6  session_start();
 7  $_SESSION['nick'] = $_GET['who'];
 8 
 9  header("Expires: Mon, 26 Jul 1997 05:00:00 GMT" );
10  header("Last-Modified: ".gmdate("D, d M Y H:i:s")."GMT");
11  header("Cache-Control: no-cache, must-revalidate" );
12  header("Pragma: no-cache");
13 
14  if( $_GET['accion'] == "send") { 
15          $sem = sem_get( $id_semaforo );
16          if( $sem !== false) { 
17              $mem = shm_attach( $id_memoria );
18              $em = @shm_get_var( $mem, 1 );
19              if( $em === false ) $em = new EventManager();
20              $em->post( new Event(time(), $_GET['msg'] ) );
21              sem_acquire( $sem );
22              shm_put_var( $mem, 1, $em );
23              sem_release( $sem );
24              shm_detach( $mem );
25              return true;
26          } else { 
27              echo "fallo al crear el semáforo.";
28              return;
29          } 
30  } 
31  elseif( $_GET['accion'] == "listen" ) { 
32      if( !isset($_SESSION['last_time']) ) $_SESSION['last_time'] = time();
33      $time_start = time();
34      $last_time = $_SESSION['last_time'];
35      $_SESSION['last_time'] = $last_time+30;
36 
37      session_write_close();
38      while( ($time_start+30) > time() ){ 
39          $mem = shm_attach( $id_memoria );
40          $em = @shm_get_var( $mem, 1 );
41 
42          if($em){ 
43              $eventos = $em->last_from( $last_time );
44              foreach( $eventos as $evento ){ 
45                  $last_time = $evento->time;
46                  echo $evento->toJSON()."\n";
47                  flush();
48              } 
49          } 
50          shm_detach( $mem );
51          usleep(1000);
52      } 
53  } 
54  ?> 

Vayamos pasito a pasito. Lo primero que hacemos es importar un archivo llamado 'eventmanager.php'. Lo veremos después.

Las variables de las lineas 3 y 4 son los identificadores que utilizaremos para referirnos al semáforo y al área de memoria compartida respectivamente. Como veis, he utilizado números enteros, pero he visto sitios por ahí que utilizan ficheros para sacar identificadores únicos. De todas formas, eso trasciende los objetivos de este texto.

Lineas 6 y 7: comenzamos la sesión y nos guardamos el nick del usuario. Cutre pero no quería liarme la manta a la cabeza para tan poca cosa.

Lineas de la 9 a la 12:Estas son una serie de cabeceras que encontré en internet para asegurarme que el navegador no intentaba cachear la página. Puede ser útil para otros usos.

El escritor

Linea 14: En este if tenemos el control principal de nuestra aplicación. Las dos únicas acciones posibles son 'listen' y 'send'.

Lineas 15 a 17: Adquirimos las referencias al semaforo y la memoria compartida.

Lineas 18 a 19: Extraemos de la memoria compartida la variable $em. Esta es una instancia de la clase EventManager que veremos después. Notar que en la linea 19 nos creamos una instancia si detectamos que no había ninguna creada.

Linea 20: Esta linea la explicaré posteriormente.

Lineas 21 a 24: Ahora viene lo bueno. En la 21 adquirimos el semáforo. Hay que notar que la función sem_acquire es bloqueante, es decir, detiene la ejecución hasta que consigue hacerse con el semáforo (porque otro proceso lo libera). En la 22 escribimos en la memoria e inmediatamente después, liberamos el semáforo.

Tened en cuenta que una vez que adquirimos el semáforo, estramos en la 'zona crítica'. Todo el tiempo que el proceso permanezca en esa zona, es tiempo que el resto de procesos está a la espera. Salimos de esa zona liberando el semáforo. SIEMPRE hay que liberar el semáforo.

El lector

Linea 33: Nos vamos a definir una variable de sesión para guardarnos la hora de la última lectura. Con esto evitaremos perder mensajes (espero). En la variable $time_start guardaremos la hora actual, mientras que en $last_time guardamos la última vez que se ejecutó el lector.

Linea 37: Cerramos la sesión. Es necesario puesto que las sesiones PHP son bloqueantes para el usuario, es decir, un mismo usuario no puede abrir más de una sesión al mismo tiempo. Si no cerraramos la sesión nos encontraríamos que el proceso lector se ejecutaría durante 30 segundos bloqueando los posibles escritores hasta el fin de su ejecución. Eso no estaria nada bien, ¿verdad?.

Linea 38: Entramos en un while durante 30 segundos. Lineas 39 y 40: Leemos la variable de la memoria compartida. Por lo que he leido, no es necesario utilizar semáforos en operaciones de lectura.

Lineas 43 a 48: Utilizamos el método last_from para obtener los últimos eventos a partir de $last_time. Recorremos los eventos y los vamos escribiendo en JSON. Hacemos un flush para asegurarnos que el PHP vacia el buffer de salida.

Linea 51: Dormimos el proceso durante un segundo para no quemar el procesador.

Como veis, no tiene mayor dificultad. Lo único que me falta por comentar es la clase EventManager, que os muestro a continuación.

 
 1  <?php 
 2  require_once("eventmanager.php");
 3  $id_semaforo = 666;
 4  $id_memoria = 123;
 5 
 6  session_start();
 7  $_SESSION['nick'] = $_GET['who'];
 8 
 9  header("Expires: Mon, 26 Jul 1997 05:00:00 GMT" );
10  header("Last-Modified: ".gmdate("D, d M Y H:i:s")."GMT");
11  header("Cache-Control: no-cache, must-revalidate" );
12  header("Pragma: no-cache");
13 
14  if( $_GET['accion'] == "send") { 
15          $sem = sem_get( $id_semaforo );
16          if( $sem !== false) { 
17              $mem = shm_attach( $id_memoria );
18              $em = @shm_get_var( $mem, 1 );
19              if( $em === false ) $em = new EventManager();
20              $em->post( new Event(time(), $_GET['msg'] ) );
21              sem_acquire( $sem );
22              shm_put_var( $mem, 1, $em );
23              sem_release( $sem );
24              shm_detach( $mem );
25              return true;
26          } else { 
27              echo "fallo al crear el semáforo.";
28              return;
29          } 
30  } 
31  elseif( $_GET['accion'] == "listen" ) { 
32      if( !isset($_SESSION['last_time']) ) $_SESSION['last_time'] = time();
33      $time_start = time();
34      $last_time = $_SESSION['last_time'];
35      $_SESSION['last_time'] = $last_time+30;
36 
37      session_write_close();
38      while( ($time_start+30) > time() ){ 
39          $mem = shm_attach( $id_memoria );
40          $em = @shm_get_var( $mem, 1 );
41 
42          if($em){ 
43              $eventos = $em->last_from( $last_time );
44              foreach( $eventos as $evento ){ 
45                  $last_time = $evento->time;
46                  echo $evento->toJSON()."\n";
47                  flush();
48              } 
49          } 
50          shm_detach( $mem );
51          usleep(1000);
52      } 
53  } 
54  ?> 

Las dos clases que nos definimos aquí son EventManager y Event. La primera tiene como propósito servir de almacén para los últimos 20 eventos producidos. La segunda es una representación de un evento con las propiedades básicas como time o id.

El evento puede autorepresentarse en notación JSON mediante el método toJSON. Esta representación será la que se envíe a los clientes y se evaluará en javascript dando lugar a un objeto.

Conclusiones

Las posibilidades de esta amalgama de tecnologías es desbordante. Su implementación, salvados algunos escollos iniciales, es sencilla y fácil de adaptar. Por contra, la parte servidor sufre una carga adicional en peticiones y sobre todo, en la duración de estas. No es un obstaculo insalvable, pero la cantidad de CPU y las conexiones máximas permitidas por el Apache deberían ser ajustadas para obtener el mejor rendimiento.

En resumen, que mola!.

Primera parte de este tutorial..

Artículo escrito por Francisco Javier Nieto Borrallo.

 comet chat logo

Tenía curiosidad al respecto de esa nueva tecnología que llaman COMET y que básicamente consiste en mantener una comunicación constante entre el cliente y el servidor web. Para explorar un poco, me planteé un proyecto sencillo: un chat asíncrono web.

La estructura es la siguiente:

  • Una página html con el código mínimo imprescindible para presentar los mensajes en un área de texto y poder enviar mensajes al chat.
  • Un código javascript que se encargue de mantener un hilo de escucha abierto con el servidor (para recibir los mensajes de la gente) y que mande un mensaje al servidor cada vez que escribamos.
  • Un código PHP que reciba y envíe los mensajes de los usuarios.

Los principales problemas de esta aproximación son los siguientes:

  • ¿Cómo mantener un hilo de escucha con el servidor?. Hasta ahora, lo que sabía hacer es hacer peticiones XMLHttpRequest desde el cliente hasta el servidor, pero no veía la forma en la que el servidor podría distribuir los mensajes a los clientes. Si, podemos recargar la página cada cierto tiempo, pero eso no mola nada, es tecnología del siglo pasado.
    La luz me la dió esta página, que discutía varias maneras de implementar streaming sobre HTTP. Una de ellas (la mejor de todas) consiste en hacer desde el cliente una petición XMLHttpRequest y mantenerla abierta durante varios segundos. ¿Cómo?¿Mande? Si, haces una petición a un script php que mediante un bucle while y un uso adecuado de la función time() se ejecute, digamos, unos 30 segundos y en cada iteración del bucle compruebe si hay mensajes nuevos. Evidentemente en cliente tienes que tener una función que se ejecute cada cierto tiempo para leer los mensajes que vayan llegando (típico polling).
  • En el servidor, ¿Cómo compartir mensajes entre distintas sesiones de usuarios?. No podemos hacerlo a traves del típico $_SESSION, porque cada usuario sólo accede a los datos de su propia sesión. No deberiamos hacerlo a través de un fichero porque no es nada escalable y muy lento, y lo más seguro es que el fichero acabara corrompiendose si no controlamos bien los accesos. ¿Quizás con una base de datos?, puede valer, pero me seguía pareciendo lento y un poco aburrido. Más adelante os mostrará como lo he hecho yo (the cool way).

Veamos un poco de código. Lo siguiente sería el html del chat, muy sencillo y que no necesita explicación (creo).

 
 1  <htmlgt; 
 2  <head> 
 3  <scriptlanguage="javascript"src="chat.js"></script> 
 4  <title>Chat con eventos compartidos en servidor COMET</title> 
 5  </head> 
 6  <bodyonload="javascript: main()"> 
 7  <textareaid="chatPanel"rows="10"cols="90"disabled="true"></textarea> 
 8  <inputtype="text"id="nick"disabled="true"/> 
 9  <formonsubmit="javascript:return send();"> 
10      <inputtype="text"id="chatBox"value=""style="width:90%"/> 
11  </form> 
12  </body> 
13  </html> 

Como veis, lo único que hacemos es traernos un archivo js (chat.js) y definirnos un par de campos para presentar datos. El campo 'chatBox' llama a una función llamada 'send()' para enviar al servidor lo que se escriba en él.

Una cosa más, en el onload llamamos a una función 'main()' que veremos a continuación.

 
 1  var nick = "";
 2  var x;
 3  var x_Send;
 4  var intervalo;
 5  var last_id=0;
 6 
 7  function main(){ 
 8      while(nick == "") nick = prompt("Nick:");
 9      document.getElementById("nick").value=nick;
10      openStream();
11  } 
12  function openStream(){ 
13      x = new XMLHttpRequest()
14      x.open("GET", "chatServer.php?accion=listen", true);
15      x.send(null);
16      intervalo = setInterval(poll, 1000);
17  } 
18  function closeStream(){ 
19      clearInterval( intervalo );
20      x.abort();
21      openStream();
22  } 
23  function poll(){ 
24      var r = new Array();
25      r= x.responseText.split("\n");
26          for(var f=0; f< r.length; f++){ 
27              if(r[f]=="") continue;
28              var evento = eval("("+ r[f] + ")" );
29              if(evento.id> last_id){ 
30                  last_id = evento.id;
31                  echo(evento.nick+": "+evento.msg+"\n");
32              } 
33          } 
34      if(x.readyState == 4) closeStream();
35  } 
36  function echo( str ){ 
37      document.getElementById("chatPanel").value += str;
38      document.getElementById("chatPanel").scrollTop = document.getElementById("chatPanel").scrollHeight;
39  } 
40  function send(){ 
41      x_Send = new XMLHttpRequest();
42      var msg = document.getElementById("chatBox").value;
43      x_Send.open("GET", "chatServer.php?accion=send&msg="+msg+"&who="+nick, true);
44      x_Send.send(null);
45      document.getElementById("chatBox").value = "";
46      return false;
47  } 

Aquí si que hay jugo. Vamos función por función.

main(): Nos pide un 'nick' y llama a openStream().

openStream(): Abre una petición XMLHttpRequest al archivo chatServer.php con el código de acción 'listen' que como veremos posteriormente, indica al php que se quede en espera y nos vaya enviando mensajes. ¿Cuando leemos esos mensajes? pues para ello nos definimos un intervalo cada segundo (podría ser menos si necesitamos mñas interactividad) para que llame a la función 'poll()'.

closeStream(): Cierra la petición XMLHttpRequest y vuelve a crear otra llamando a openStream.

poll(): Esta función recoge el contenido actual del responseText de la petición de escucha, la divide en lineas y si corresponde, imprime la línea por pantalla con 'echo()'.

echo(): Escribe lo que le mandes como parametro en el textarea y mueve el scroll.

send(): Envía el texto al servidor mediante un XMLHttpRequest normalito.

Hasta aquí la parte del cliente. Hemos visto como implementar mediante javascript hilos de escucha con el servidor. ¿Utilidad? Para todas aquellas aplicaciones en la que la colaboración entre usuarios es fundamental o crítica. Se me ocurre, a bote pronto, edición multiusuario de documentos online, control de concurrencia, etc...

Mientras escribo la segunda parte de este mini tutorial, podeis entreteneros probando el chat en la siguiente url: chat web. Eso si, si solo estás tú conectado, no vas a ver nada espectacular. Lo suyo es probarlo con muchos usuarios.

Actualización: Ya tengo lista la segunda parte.

Artículo escrito por Francisco Javier Nieto Borrallo.

apacheUn manual práctico para el administrador de servidores web Apache.

Incluye instalación, configuración básica, hosts virtuales, autenticación, negociación de contenidos, índices, cgi, Server Side Includes, logs, php, aplicaciones LAMP, SSL, seguridad y optimización de rendimiento.

Podrás descargrlo aquí.

 

 

 

nirvanaO "Como vivir con GNU/Linux". Un manual sobre GNU/Linux.

Aborda muchos temas: instalación, bash, vim, apt, sistemas de ficheros, gestión de usuarios, configuración de redes, arranque del sistema, gestión de procesos, compilación del núcleo, configuración de hardware, ofimática, seguridad, firewall, LDAP y un anexo sobre el protocolo de red TCP/IP.

Está basado en Debian, pero casi todos los capítulos son independientes de la distribución.

Podrás descargarlo aquí.

Lo que necesitas:

logo transp

Sobre Ilke Benson

Dónde estamos     

C/ Donoso Cortés, 6 - 3º. Oficina 10
06002 Badajoz (Extremadura)

     
Teléfono  

telf: +34 924 98 34 19

fax: +34 924 98 34 19

Email  

  

info@ilkebenson.com

  

Pídenos

Es importante estar conectado con nuestros clientes, según nuestras metodologías de desarrollo, ellos se hacen parte indispensable en el ciclo de vida del proyecto.

Es por este motivo por el cual disponemos de un sistema que permite a nuestros clientes informar de incidencia o solicitar modificaciones de manera priorizada (peticiones de tareas).

Sistema de Gestión de Peticiones de Ilke Benson

Ilke Benson  ©2024 Ilke Benson. All Rights Reserved. Aviso Legal. Diseñado por Ilke Benson

¿Quieres algo concreto?