Libro: Introduction to Tornado

By valbornoz Feb. 13, 2023, 5:03 p.m.

¿Qué es Tornado?

Es un Framework escrito en Python que nos proporciona un poderoso Servidor Web para resolver cerca de 10,000 conexiones simultaneas creando aplicaciones web de forma muy sencilla, fue desarrollado originalmente por FriendFeed.

Ahora, podemos hablar de su instalación, lo cual se puede hacer en un entorno, podemos instalarlo con pip como dice la documentacion que encuentras en https://pypi.org/project/tornado/.

Su principal uso lo podemos encontrar en Long Polling y Websockets, estos ultimos demandan un conexion prolongada, para lo cual Tornado nos proporciona ciertas herramientas para llevar nuestra aplicación al siguiente nivel.

Instalación

Podemos instalarlo via easy_install, por pip o descargarlo de Github. De las siguiente maneras.

curl -L -O http://github.com/downloads/facebook/tornado/tornado-2.1.1.tar.gz
tar xvzf tornado-2.1.1.tar.gz
cd tornado-2.1.1
python setup.py build
sudo python setup.py install

O por pip, como se menciono anteriormente

pip install tornado

Servicios Webs Simples

Con Tornado podemos crear aplicaciones web sencillas, como el libro lo menciona. El siguiente ejemplo se muestra la forma de levantar un web services y retorna un saludo.

Pagina 4 Example 1-1. The basics hello.py.

import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web

from tornado.options import define, options
define('port', default=8000, help='run on the given port', type=int)

class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        greeting = self.get_argument('greeting', 'Hello')
        self.write(greeting + ', friendly user!')

if __name__ == '__main__':
    tornado.options.parse_command_line()
    app = tornado.web.Application(handlers=[(r"/", IndexHandler)])
    http_server = tornado.http

Podemos observar que la sintaxis para escribir servicios web es muy parecidad a frameworks como Django, apesar de esto, Tornado no esta hecho para crear aplicaciones web robustas, dado que no contamos con un ORM, sistema de formularios, entre otras caracteristicas necesarias para sistemas más completos.

Para demostrar lo anterior podemos ver el siguiente ejemplo.

# importing the main event loop
import tornado.ioloop

# for HTTP requesthandlers ( to map the requests to request handlers)
import tornado.web

import os.path

class WeatherHandler(tornado.web.RequestHandler):

    def get(self):

        self.render("ejemplo4.html", output="")

    def post(self):
        degree = int(self.get_argument("degree"))
        output = "Hot!" if degree > 20 else "cold "
        drink = "Have some Beer!" if degree > 20 else "you need hot beverage ☕"
        self.render("ejemplo4.html", output = output, drink = drink)

def make_app():
    return tornado.web.Application([
        (r"/weather", WeatherHandler),
    ],
    template_path=os.path.join(os.path.dirname(__file__), "templates"),
    static_path=os.path.join(os.path.dirname(__file__), "static"),
    debug = True,
    autoreload = True)

if __name__ == "__main__":
    app = make_app()
    port = 8888
    app.listen(port)
    print(F'Server is listening on localhost on port {port}')
    # to start ther server on the current thread
    tornado.ioloop.IOLoop.current().start()

En la parte del HTML se declara el formulario, cuando hay frameworks que nos hacen la vida más facil en estos temas.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Welcome, Tornado</title>
</head>
<body>
    <h1>Welcome, Tornado</h1>
    <h2>Using Arguments For Data Query in Tornado </h2>

    <h3>Form</h3>

    <form action="" method="post">
        <label for="">degree</label>
        <input type="text" name="degree" value="">
        <input type="submit">
    </form>

    {% if output %}
        <hr>
    Today is ...
    {{ output }}
    <br>
    <br>
    {{ drink }}
    {% end %}
</body>
</html>

Base de datos en Tornado

A falta de un ORM, podemos conectar nuestras aplicaciones con MongoDB, solo necesitamos aprender el uso de este con Python.

Tan fácil como importar PyMongo y establecer una conexión.

import pymongo
conn = pymongo.Connection("localhost", 27017)

Para más información podemos leer la documentación oficinal https://pymongo.readthedocs.io/en/stable/

Servicios Asincronos

Con Tornado podemos crear aplicaciones como un chat hecho con un WebSocket, esto gracias a las herramientas que proporciona. Este es un claro ejemplo de aplicaciones que requieren una conexión prolongada con el servidor.

Example Chat Websocket, chatwebsocket.py.

import asyncio
import logging
import tornado.websocket
import tornado.web
import os.path
import uuid

from tornado.options import define, options

define("port", default=8888, help="run on the given port", type=int)


class Application(tornado.web.Application):
    def __init__(self):
        handlers = [(r"/", MainHandler), (r"/chatsocket", ChatSocketHandler)]
        settings = dict(
            cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
            template_path=os.path.join(os.path.dirname(__file__), "templates"),
            static_path=os.path.join(os.path.dirname(__file__), "static"),
            autoreload = True,
            xsrf_cookies=True,
        )
        super().__init__(handlers, **settings)


class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("indexwebsocket.html", messages=ChatSocketHandler.cache)


class ChatSocketHandler(tornado.websocket.WebSocketHandler):
    waiters = set()
    cache = []
    cache_size = 200

    def get_compression_options(self):
        # Non-None enables compression with default options.
        return {}

    def open(self):
        ChatSocketHandler.waiters.add(self)

    def on_close(self):
        ChatSocketHandler.waiters.remove(self)

    @classmethod
    def update_cache(cls, chat):
        cls.cache.append(chat)
        if len(cls.cache) > cls.cache_size:
            cls.cache = cls.cache[-cls.cache_size :]

    @classmethod
    def send_updates(cls, chat):
        logging.info("sending message to %d waiters", len(cls.waiters))
        for waiter in cls.waiters:
            try:
                waiter.write_message(chat)
            except:
                logging.error("Error sending message", exc_info=True)

    def on_message(self, message):
        logging.info("got message %r", message)
        parsed = tornado.escape.json_decode(message)
        chat = {"id": str(uuid.uuid4()), "body": parsed["body"]}
        chat["html"] = tornado.escape.to_basestring(
            self.render_string("messagewebsocket.html", message=chat)
        )

        ChatSocketHandler.update_cache(chat)
        ChatSocketHandler.send_updates(chat)


async def main():
    tornado.options.parse_command_line()
    app = Application()
    app.listen(options.port)
    await asyncio.Event().wait()


if __name__ == "__main__":
    asyncio.run(main())

Example Chat Websocket, templates/indexwebsocket.html.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Tornado Chat Demo</title>
    <link rel="stylesheet" href="{{ static_url("chatwebsocket.css") }}" type="text/css">
  </head>
  <body>
    <div id="body">
      <div id="inbox">
        {% for message in messages %}
          {% include "messagewebsocket.html" %}
        {% end %}
      </div>
      <div id="input">
        <form action="/a/message/new" method="post" id="messageform">
          <table>
            <tr>
              <td><input type="text" name="body" id="message" style="width:500px"></td>
              <td style="padding-left:5px">
                <input type="submit" value="{{ _("Post") }}">
                <input type="hidden" name="next" value="{{ request.path }}">
                {% module xsrf_form_html() %}
              </td>
            </tr>
          </table>
        </form>
      </div>
    </div>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js" type="text/javascript"></script>
    <script src="{{ static_url("chatwebsocket.js") }}" type="text/javascript"></script>
  </body>
</html>

Example Chat Websocket, templates/messagewebsocket.html.

<div class="message" id="m{{ message["id"] }}">{% module linkify(message["body"]) %}</div>

Example Chat Websocket, static/chatwebsocket.css.

body {
  background: white;
  margin: 10px;
}

body,
input {
  font-family: sans-serif;
  font-size: 10pt;
  color: black;
}

table {
  border-collapse: collapse;
  border: 0;
}

td {
  border: 0;
  padding: 0;
}

#body {
  position: absolute;
  bottom: 10px;
  left: 10px;
}

#input {
  margin-top: 0.5em;
}

#inbox .message {
  padding-top: 0.25em;
}

#nav {
  float: right;
  z-index: 99;
}

Example Chat Websocket, static/chatwebsocket.js.

$(document).ready(function() {
    if (!window.console) window.console = {};
    if (!window.console.log) window.console.log = function() {};

    $("#messageform").on("submit", function() {
        newMessage($(this));
        return false;
    });
    $("#messageform").on("keypress", function(e) {
        if (e.keyCode == 13) {
            newMessage($(this));
            return false;
        }
    });
    $("#message").select();
    updater.start();
});

function newMessage(form) {
    var message = form.formToDict();
    updater.socket.send(JSON.stringify(message));
    form.find("input[type=text]").val("").select();
}

jQuery.fn.formToDict = function() {
    var fields = this.serializeArray();
    var json = {}
    for (var i = 0; i < fields.length; i++) {
        json[fields[i].name] = fields[i].value;
    }
    if (json.next) delete json.next;
    return json;
};

var updater = {
    socket: null,

    start: function() {
        var url = "ws://" + location.host + "/chatsocket";
        updater.socket = new WebSocket(url);
        updater.socket.onmessage = function(event) {
            updater.showMessage(JSON.parse(event.data));
        }
    },

    showMessage: function(message) {
        var existing = $("#m" + message.id);
        if (existing.length > 0) return;
        var node = $(message.html);
        node.hide();
        $("#inbox").append(node);
        node.slideDown();
    }
};