Hoy vamos a ver cómo crear una sencilla aplicación de chat con Flex. No debe tomarse este ejemplo como una aplicación completa sino simplemente como un método para comprender cómo funcionan los distintos elementos que intervienen. Tampoco debe compararse este proyecto con el cliente IRC que os presenté hace unos meses, aunque ámbos son aplicaciones de chat, son sistemas distintos, como veréis más adelante.
Flash Media Server (FMS, antiguamente conocido Flash Communication Server) es el software de Adobe que permite crear aplicaciones multiusuario de una manera sencilla y bajo tecnología Flash. FMS permite no sólo intercambiar y sincronizar mensajes de texto entre los clientes sino tambien audio y vídeo. Si flv es el sistema de vídeo por excelencia en Internet, FMS es la tecnología para crear sistemas multimedia multiusuario. La principal pega de FMS es que es de pago, y no precisamente barato, sin embargo existe una licencia “developer” que te permite utilizar el 100% de las posibilidades del servidor, la única limitación es la cantidad de usuarios concurrentes que puedes tener, diez en este caso. Existe una alternativa Open Source a FMS, Red5, pero no lo he utilizado nunca así que no puedo opinar. La teoría con Red5 es exactamente la misma que con FMS, sólo cambia el medio (FMS se programa con ActionScript y Red5 con Java). Como ejemplo de aplicaciones que utilizan FMS está Yahoo Live, la aplicación de grabación directa desde tu webcam de Youtube o todos, absolutamente todos los videochats que puedes encontrar en Internet.
Como lo más probable es que no tengas una licencia de FMS, trabajaremos con la versión developer que hemos indicado ya que nos permitirá poner en práctica todos los conceptos necesarios. Lo primero que debes hacer es, por tanto, descargar e instalar FMS, hay versiones para servidores Linux y Windows, escoge la que mejor te venga. La instalación no tiene ningún secreto así que lo dejaré en vuestras manos. En mi caso, como siempre, un CentOS5.
Los proyectos creados sobre FMS son sistemas cliente/servidor y necesitan dos aplicaciones para poder funcionar:
- Aplicación cliente, sería la parte Flash pura y dura, la que utilizarán los usuarios.
- Aplicación servidor, sería lo que corre bajo FMS y a lo que se conecta la aplicación cliente.
Digamos que FMS es la varita mágina del conjunto ya que hace completamente transparente al desarrollador la conexión entre usuarios y la inyección de mensajes entre ellos. Para entenderlo pensemos en un chat donde los usuarios se conectan al servidor y cada vez que uno de ellos escribe un mensaje éste se envía automáticamente a los demás usuarios. Imagina, además, que no sólo hablamos de texto sino que cuando un usuario enciende su webcam los demás usuarios pueden verla. Todo esto lo hace FMS por ti, sólo debes preocuparte de construir tu aplicación.
Programando una aplicación FMS
Empezaremos la parte práctica creando el código necesario en el servidor. Por defecto las aplicaciones FMS de instalan bajo /opt/adobe/fms/applications creando un nuevo directorio con el nombre de tu aplicación, en nuestro caso la llamaremos textchat.
Las aplicaciones se cargan y se mantienen en memoria una vez algún usuario accede por primera vez a la misma y, cuando pasado un tiempo prudencial no hay usuarios, se descarga para liberar recursos esperando a que otro usuario vuelva a acceder. Cada vez que se hacen cambios a la parte servidor de la aplicación hay que reiniciarla, pero no es necesario reiniciar el servidor FMS para reiniciar una aplicación, basta con descargar y volver a cargar esa aplicación desde la consola de administración como veremos más adelante. Desde esta misma consola podremos ver también la salida de los comandos trace que ocurran en el servidor y los objetos creados y su contenido, ayuda imprescindible mientras estás desarrollándola. Veremos también como activar la salida de estas opciones puesto que por defecto (por seguridad) vienen deshabilitadas.
Por defecto todas las aplicaciones deben tener un archivo main.asc con el código necesario. Crea tu main.asc de esta manera:
load("netservices.asc"); application.onAppStart = function(){ trace("inicia text chat"); this.users_so = SharedObject.get("users_so", false); this.chat_so = SharedObject.get("chat_so", false); this.userList = {}; this.uid = 1; } application.onConnect = function(client, nick, clave, sala){ trace(nick+" "+clave+" "+sala); var valido=1; for( i in this.userList){ if(String(this.userList[i].nick).toLowerCase()==String(nick).toLowerCase()){ valido=0; break; } } if(valido==1){ client.uid = this.uid; client.nick = nick; client.sala = sala; this.acceptConnection(client); this.userList[client.uid] = client; this.users_so.setProperty(client.uid, client); var msg = "<font color='#666666'><b>*** Entra " + client.nick + " ***</b></font><br>"; this.chat_so.send("onMsgSend", msg); client.call("onUserid", null, client.uid, client.nick); this.uid++; }else{ var err = new Object(); err.message = "Ya hay un usuario con ese nick"; trace(err.message); application.rejectConnection(client, err); } } application.onDisconnect = function(oldClient){ if(oldClient.uid!=undefined){ trace("Descontecta cliente. UID: " +oldClient.uid); var msg = "<font color='#666666'><b>*** " + oldClient.nick + " sale ***</b></font><br>"; this.chat_so.send("onMsgSend", msg); for( i in application.userList){ if(Number(application.userList[i].uid)==Number(oldClient.uid)){ application.userList[i]=undefined; } k++; } application.disconnect(oldClient); application.users_so.setProperty(oldClient.uid, null); } }
El funcionamiento es muy sencillo. Nuestra aplicación maneja tres eventos:
- onAppStart: Se dispara cuando la aplicación se carga en memoria por primera vez.
- onConnect: Se lanza cuando un usuario se conecta a la aplicación.
- onDisconnect:Cuando un usuario se desconecta.
Veamos la teoría de un chat y comprenderemos rápidamente como funciona FMS en su lado servidor. En una aplicación chat típica, el servidor debe manejar una lista de los usuarios conectados actualmente (los nicks) y debe poder ser capaz de enviar mensajes a todos los usuarios. Por ejemplo, cuando un nuevo usuario se conecta y sin que éste escriba nada, el servidor tiene que poder enviar un mensaje tipo “Nick se ha conectado al chat” a todos los demás usuarios. Para solucionar todo esto FMS tiene lo que se conocen como objetos compartidos (SharedObjects), objetos a los que todos los clientes pueden acceder.
Si analizas ahora el código de nuestro main.asc verás que el evento onAppStart crea las variables que se utilizarán en la aplicación:
- userList: array que contendrá la lista de usuarios conectados al sistema.
- uid: identificador del último usuario conectado para poder localizar de manera inequívoca a un cliente.
- users_so: objeto compartido de la lista de usuarios
- chat_so: objeto compartido del chat propiamente dicho, a él se envíen los mensajes que escriben los usuarios.
Creo que sobran más explicaciones ya que es tan sencillo que se entiende sólo.
Veamos ahora la teoría de qué ocurre cuando se conecta un usuario, evento onConnect. Cada vez que un cliente accede a nuestro chat debemos comprobar, antes de nada, siya hay otro usuario con el mismo nick, en cuyo caso rechazamos su conexión. Si todo va bien y su nick es único debemos guardar al usuario en la lista de usuarios y avisar a todos los clientes ya conectados que hay un nuevo usuario en el chat.
Compara ahora la teoría con el código del evento. El parámetro cliente es automático y representa al cliente que se conecta. Es un objeto con distintos parámetros predefinidos. Los demás parámetros del evento se los pasaremos nosotros desde nuestro cliente flash. Para ver si un nick ya existe simplemente lo comparamos contra todos los demás del array userList que ya hemos visto que contiene a todos los usuarios conectados. El evento onConnect puede aceptar un cliente (acceptConnection) o rechazarlo (rejectConnection).
Si el usuario ha sido admitido, le damos el siguiente UID y tendremos entonces que avisar a los demás usuarios de que tenemos un usuario nuevo. Si recordamos las variables que teníamos, había un objeto compartido al que tienen acceso todos los clientes, users_so, solo tendremos que añadir nuestro nuevo cliente a este objeto:
this.users_so.setProperty(client.uid, client);
Cuando veamos la aplicación cliente veremos como detectamos los cambios en ese objeto, pero es sumamente sencillo.
Espera!. Nos falta enviar el mensaje de “Nick se ha conectado al chat”. Tan sencillo como:
this.chat_so.send("onMsgSend", msg);
Magia!. Como chat_so era un objeto compartido, todos los clientes y el servidor tienen acceso al mismo y lo que uno escriba lo recibirán automáticamente todos los demás. Esta es la verdadera magia de FMS, no tienes que preocuparte de cómo enviar los mensajes a los usuarios uno a uno, se sincronizan sólos.
Como último detalle, cuando aceptamos la conexión de un cliente llamamos remotamente al método onUserid de la aplicación cliente. Si! has leído bien!, FMS llama a una función de tu SWF!. Más adelante veremos para qué, simplemente recuerda que cuando te conectas al chat el servidor llama a un método de la aplicación pasándole su propio UID.
Finalmente veamos el último evento del servidor, bastante obvio si has seguido lo explicado anteriormente y lo has entendido, la desconexión de un usuario.
Sin pensar mucho más entenderemos que habrá que eliminar al cliente de userList y de users_so. Pues eso mismo, no hay mucha más ciencia.
Ya tenemos la parte del servidor terminada. Sencillo ¿no?. Es posible que aún no entiendas algunas cosas. Tranquilo, cuando veamos el cliente lo entenderás todo y verás que realmente es muy sencillo crear aplicaciones de este tipo con FMS.
La consola de administración
Es una aplicación Flash desde la que podemos conectarnos como administradores al servidor FMS y realizar operaciones de mantenimiento, entre ellas cargar y descargar instancias de aplicación, ver el log del sistema (los trace), examinar objetos (en modo debug)…
Cuando instalaste FMS te pidió un usuario y una clave además de un puerto donde instalar esta consola de administración. No tienes más que conectarte a ella y comenzarás a ver estos datos.
Hay, sin embargo, algunas particularidades de administración que conviene tener en cuenta. Para habilitar las opciones de debug de objetos habrá que modificar el fichero conf/_defaultRoot_/_defaultVHost_/Application.xml cambiando:
<AllowDebugDefault>false</AllowDebugDefault>
a true. Reinicias el servidor para que el cambio se active y verás algo como esto que te servirá para depurar tu aplicación.
En la barra de opciones de aplicación (Live log, clients…) tienes dos pequeños botones que te permitirán recargar una instancia de la aplicación o descargarla por completo, las opciones que comentábamos antes que te ayudarán durante el desarrollo para no tener que reiniciar todo el servidor.
La aplicación cliente
Y llegamos al último punto. La aplicación, el chat, lo que ve el usuario.
Ya os he puesto una captura del resultado final. Veremos ahora los entresijos. Abajo de todo tenéis el código fuente de todo el proyecto de demostración.
Me meteré directamente al meollo de conectar con FMS y enviar y recibir datos, la interfaz de usuario y la pantalla inicial de login la dejo para vosotros. Como veréis hay una clase Chat.as que centraliza casi todo el proceso de comunicación con el servidor.
chat = new Chat(); chat.addEventListener(UserIDEvent.USER_ID, onUID); chat.addEventListener(NCEvent.STATUS, onStatus); chat.addEventListener(NCEvent.NONICK, onStatus); salaSeleccionada=sala.selectedItem.toString(); chat.init("rtmp://"+host+"/textchat/"+salaSeleccionada, StringUtil.trim(nick.text), StringUtil.trim(clave.text), salaSeleccionada); private function onUID( event:UserIDEvent ):void{ miuid=event.userid; currentState = "chatState"; msg.setFocus(); CursorManager.removeBusyCursor(); this.addEventListener(KeyboardEvent.KEY_UP,keyHandler); picker.addEventListener(DropdownEvent.CLOSE, pickerClose); } private function onStatus( event:NCEvent ):void{ CursorManager.removeBusyCursor(); if (event.status == "closed"){ status.text = "Desconexión completa."; } if (event.status == "nickexiste"){ status.text = "Usuario o contraseña incorrectos."; } currentState = ""; }
Al crear la instancia de Chat haremos que se detecten un par de eventos que ejecutarán:
- onUID: se dispara cuando el servidor, como hemos visto antes, llama al método cliente onUserid. Nos sirve para cambiar el estado de la interfaz de usuario desde la ventana de login a la de chat ya que sólo si hemos conectado recibiremos el UID.
- onStatus: nos ayudará adetectar conexiones rechazadas y tu propia desconexión.
Para conectar véis que llamamos al método init de Chat con cuatro parámetros, la url de la instancia de nuestra aplicación (definida como host+aplicacion+sala de chat), el nick, la clave (para un futuro) y la propia sala de chat. Si examinas este método verás qué sencillo es iniciar una conexión. El método más importante es
private function netStatus( event:NetStatusEvent ):void{ var info:Object = event.info; switch(info.code) { case "NetConnection.Connect.Success": users_so = SharedObject.getRemote("users_so", nc.uri, false); users_so.addEventListener(SyncEvent.SYNC, usersSync); users_so.addEventListener(NetStatusEvent.NET_STATUS, onSOnetStatus); users_so.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSOSecurityError); users_so.client = this; users_so.connect(nc); chat_so = SharedObject.getRemote("chat_so", nc.uri, false); chat_so.addEventListener(NetStatusEvent.NET_STATUS, onSOnetStatus); chat_so.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSOSecurityError); chat_so.client = this; chat_so.connect(nc); break; case "NetConnection.Connect.Rejected": dispatchEvent( new NCEvent(NCEvent.NONICK, "nickexiste")); break; case "NetConnection.Connect.Closed": dispatchEvent( new NCEvent(NCEvent.STATUS, "closed")); break; } }
Es el que detecta el estado de la conexión, si el servidor nos acepta o nos rechaza tal como veíamos antes e incluso si nos desconecta. Fíjate que si somos aceptados estaremos conectándonos a los SharedObjects que creamos antes en el servidor 😉 . Ahora es Chat quien hará absolutamente todo el trabajo, la aplicacion mxml es la interface de usuario que transmite a Chat los comandos necesarios. Veamos por ejemplo como indicamos al chat que envíe a los demás usuarios un mensaje que hayamos escrito nosotros.
private function sendMsg():void{ if ( msg.text.length > 0 ){ chat.sendMsg(msg.text); msg.text = ""; } }
Vale sí, pero has hecho trampas, aquí estás dejando el meollo en manos de Chat 😛 . Bien, veamos qué ocurre en sendMsg.
public function sendMsg( color:String, msg:String ):void{ var outmsg:String = ""; outmsg = "<font color='#" + color + "'><b>" + this.username+":</b> " + msg + "</font><br>"; chat_so.send("onMsgSend", outmsg); }
¡ Sólo eso ! Como ya estamos conectados al SharedObject chat_so, le decimos a éste que llame al método onMsgSend de sus clientes y le pase el texto que hemos escrito.
De acuerdo, entonces ¿cómo leemos los mensajes que escriben los demás usuarios?. Te lo acabo de decir 😛 . Cuando un usuario escribe algo, llama (da igual cómo) a un método local de cada usuario que se encarga de escribir el texto en su ventana de chat.
public function onMsgSend( msg:String ):void{ chatHistory += msg; dispatchEvent(new Event("histChange")); }
Obviamente este método tiene que ser público. Al despachar ese evento simplemente hace que se actualice el texto del textarea.
Hasta ahora creo que lo voy entendiendo, pero no has explicado como detectamos las entradas y salidas de usuarios. Cierto, veámoslo pues. Acabamos de ver que cuando entramos al chat y somos admitidos conectamos con los SharedObjects que creamos en el servidor. Si te has fijado (y si no hazlo ahora), al de usuarios le poníamos un listener distinto:
users_so.addEventListener(SyncEvent.SYNC, usersSync);
Esto significa que cada vez que haya un cambio en el objeto users_so todos los clientes serán notificados de ello. Por ejemplo, cada vez que entra o sale un usuario vimos que añadíamos o quitábamos un elemento de este objeto que representaba a ése usuario. Cada una de estas operaciones hará que nuestra aplicación cliente reciba una llamada a usersSync.
private function usersSync(event:SyncEvent):void{ var results:Object = event.target.data; var usersArray:Array = new Array(); var existe:Boolean=false; //debo actualiza solo los datos for( var i:String in results ){ if(results[i].sala==this.sala){ existe=false; for(var j:Number=0; j<dpUsers.length; j++){ if(dpUsers[j].uid==results[i].uid){ existe=true; break; } } if(existe){ //por si se ha cambiado el nick dpUsers[j].nick=results[i].nick; }else{ var clientObj:Object=new Object(); clientObj.nick = results[i].nick; clientObj.uid = results[i].uid; dpUsers.addItem(clientObj); } } } //ahora tengo que eliminar las bajas //elimino empezando por la ultima para no cambiar los indices for(j=dpUsers.length-1; j>=0; j--){ existe=false; for(i in results ){ if(dpUsers[j].uid==results[i].uid && results[i].sala==this.sala){ existe=true; } } if(!existe){ dpUsers.removeItemAt(j); } } //finalmente ordenamos los usuarios var sort:Sort = new Sort(); sort.fields = [new SortField("nick",true)]; dpUsers.sort = sort; dpUsers.refresh(); dispatchEvent(new Event("dpChange")); }
Esta método repasará todos los usuarios del SharedObject comparándolos con los de la lista de usuarios del canal donde nos encontramos chateando y añadiendo los nuevos usuarios y eliminando los que hayan salido. Quizás no sea el mejor método para manejar los usuarios y habría sido mucho más sencillo tener una pareja de métodos addUsuario y delUsuario que manejen todo automáticamente con llamadas remotas igual que con los mensajes, pero quería que viésemos el modo de recibir avisos de cambios en un SharedObject.
Conclusiones
Hemos visto cómo de una manera sencillísima podemos crear nuestro propio chat sin excesivas complicaciones. Piensa además que esto es, seguramente, lo más sencillo, el primer paso con FMS, las posibilidades reales son infinitas.
Quizás en otros artículos ampliemos las posibilidades de este pequeño ejemplo añadiendo vídeo y audio o, incluso, hagamos un pequeño sistema de grabación de vídeo con tu webcam similar al de Youtube.
Como siempre, aquí os dejo el código completo del proyecto. Recuerda que es un ejemplo de lo que se puede hacer, no debes usarlo tal cual sino aplicar lo que hemos explicado. Del mismo modo habrá cosas que no sean perfectas y se puedan hacer mejor, no lo dudo, mi intención era explicar los conceptos básicos de FMS y que se vea su potencial.