En un articulo anterior he hablado sobre la librería Tornado y cómo se puede utilizar para implementar servidores y clientes TCP asíncronos. Además, también he hablado sobre algunas de las vulnerabilidades más comunes en WebSockets, una de las características más interesantes en la especificación HTML5. En esta ocasión veremos cómo utilizar los módulos disponibles en Tornado para crear un servidor web básico que soporte WebSockets para poder realizar pruebas de concepto rápidas y comprender el comportamiento tanto en clientes como servidores de los websockets.
Tornado es una librería que solamente se encuentra disponible para máquinas basadas en Unix, por este motivo, en un próximo articulo hablaré sobre otra implementación independiente de plataforma basada en Java llamada websocket4j. Otra buena solución cuando queremos realizar pruebas con un sistema basado en Windows.
-
Implementación de un cliente utilizando la API de WebSockets
Antes de comenzar a implementar el servidor, vamos a comenzar creando un cliente básico que servirá posteriormente para probar cualquier implementación de un servidor con WebSockets. Para implementar un cliente, basta con utilizar la API en Javascript que se encuentra habilitada en prácticamente todos los navegadores modernos que soportan HTML5, como es el caso de Firefox, Opera o Chrome.
test.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script type="text/javascript"> function WebSocketTest() { var ws = new WebSocket("ws://localhost:8888/ws?Id=123456789&name=Adastra&continue=Yes"); if (ws != null & ws.readyState == WebSocket.OPEN) { ws.send("Data from the client to the server!"); } ws.onopen = function() { ws.send("Opening connection!"); }; ws.onmessage = function (evt) { var received_msg = evt.data; alert("Message received... " + received_msg); }; ws.onclose = function() { alert("Connection is closed..."); }; } </script> </head> <body> <a href="javascript:WebSocketTest()">Run WebSocket</a> </body> </html>
En la página HTML se puede apreciar que la función “WebSocketTest” contiene todos los elementos necesarios para establecer una comunicación con el servidor web y posteriormente, enviar y recibir mensajes de forma asíncrona. Esto último es lo que hace tan interesantes los WebSockets, ya que después de establecer el handshake, tanto cliente como servidor pueden enviarse mensajes sin necesidad de esperar a que la otra parte conteste y el servidor, puede enviar datos sin necesidad de que exista una petición previa por parte del cliente.
Ahora bien, después de tener preparada la pieza de código correspondiente al cliente, lo siguiente consistirá en crear un servidor que se encargue de procesar las peticiones y manejar todas las conexiones utilizando el protocolo de WebSockets.
-
Implementación del un servidor utilizando la API de WebSockets y la librería Tornado
Como comentaba anteriormente, Tornado cuenta con varias clases y funciones que permiten crear diversos tipos de elementos de red, tanto síncronos como asíncronos. En este caso concreto nos centraremos en el módulo que permite la creación de servidores y aplicaciones web con Tornado. Esto será útil para realizar pruebas de concepto y entender el funcionamiento de ciertas características propias en entornos web.
El siguiente programa permitirá la creación de un servidor web básico utilizando Tornado, el cual aceptará conexiones en HTTP normales si el usuario solicita el recurso “/” y conexiones utilizando el protocolo de WebSockets si el usuario solicita el recurso “/ws”.
serverTornado.py
</pre> <pre>import tornado.ioloop import tornado.web import tornado.websocket from tornado.options import define, options, parse_command_line class Client: def __init__(self, clientId, name, cont, connection): self.id = clientId self.name = name self.cont = cont self.connection = connection clients = [] class IndexHandler(tornado.web.RequestHandler): def get(self): self.render("test.html") class WebSocketHandler(tornado.websocket.WebSocketHandler): def open(self, *args): self.id = self.get_argument("Id") self.name = self.get_argument("name") self.cont = self.get_argument("continue") newclient = True for client in clients: if client.id == self.id: client.connection.write_message("Hello Again %s !" %(client.name)) newclient = False break if newclient: clientRef = Client(self.id, self.name, self.cont, self) clients.append(clientRef) self.write_message("Hello %s !" %(self.name)) def on_message(self, message): for client in clients: if client.id == self.id: print "Message from %s received : %s" % (client.name, message) def on_close(self): for client in clients: if self.id == client.id: clients.remove(client) break define("port", default=8888, help="run on the given port", type=int) app = tornado.web.Application([ (r'/', IndexHandler), (r'/ws', WebSocketHandler), ]) if __name__ == '__main__': parse_command_line() app.listen(options.port) tornado.ioloop.IOLoop.instance().start()
Los elementos más importantes del programa anterior son los siguientes:
– Objeto del tipo “tornado.web.Application” el cual se encarga de definir las URI disponibles para el servidor web. En este caso concreto, se ha definido que el usuario podrá acceder a la ruta “/” y “/ws”. Si el usuario solicita el recurso “/” el servidor se encargará de ejecutar el handler “IndexHandler” y si el usuario solicita el recurso “/ws” el servidor se encargará de ejecutar el handler “WebSocketHandler”.
- IndexHandler: Clase que hereda de la clase “tornado.web.RequestHandler” y que se encarga de procesar las peticiones HTTP realizadas por los clientes que emplean el método GET. En este caso, la case se encarga simplemente de responder al cliente con la página “test.html”, la cual incluye el contenido que se ha explicado anteriormente en el la primera parte de este articulo, es decir, la página HTML con los elementos necesarios para interactuar con el servidor web.
- WebSocketHandler: Clase que hereda de la clase “tornado.web.WebSocketHandler” y que se encarga de procesar las peticiones entrantes que utilicen el protocolo de WebSockets. La clase incluye los métodos “open”, “on_message” y “on_close”, los cuales son invocados automáticamente cuando se abre una conexión, se recibe un mensaje y se cierra una conexión existente, respectivamente.
- Finalmente, la definición propiamente dicha del servidor web viene dada por una instancia de la clase “tornado.ioloop.IOLoop”, la cual se encarga de crear un hilo de ejecución que se mantendrá en funcionamiento de forma indefinida y que utilizará las opciones por línea de comandos que se han definido por medio de la función “tornado.options.define”.
Con todos lo anterior, ahora es posible ejecutar el servidor web y probar los métodos de los dos handlers definidos.
>python serverTornado.py
Si el usuario solicita el recurso “/”, el servidor se encargará de responder con la página “test.html” tal como se enseña en la siguiente imagen.
En enlace que se puede ver en la imagen, se encarga de invocar a una función en Javascript que permite interactuar con el servidor web y enviar mensajes utilizando el protocolo WebSockets, tal como se puede apreciar en la siguiente imagen.
Se trata de un ejemplo muy simple y que no solo permite conocer cómo funcionan los websockets, sino que también explica como utilizar Tornado para crear un servidor que los soporte. No obstante, tal como mencionaba anteriormente, Tornado solamente funciona para sistemas basados en Unix, con lo cual, en el próximo articulo hablaré sobre otra librería basada en Java llamada websockets4j.
Un Saludo y Happy Hack!
Adastra.