Tornado is widely used in Zhihu. When you open the webpage version of Zhihu with Chrome and carefully observe the requests in the Network with developer tools, you will find a special request with status code 101. It is to use the browser websocket technology and back-end server to establish a long connection to receive the server to actively push the notification message. Tornado Server is used on this backend server. Tornado server can provide websocket service, long connection service, HTTP short link service, UDP service, etc. Tornado is open source by Facebook and is widely used on the back end of IReader.

This article will guide readers to learn how to use Tornado as a basic Web server step by step.

Hello, World

import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

def make_app(a):
    return tornado.web.Application([
        (r"/", MainHandler),
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()
Copy the code

Python hello.py, open a browser, visit http://localhost:8888/, and see the normal output of Hello, World.

A typical Tornado Web server usually consists of four components.

  1. Ioloop instance, which is the global Tornado event loop, is the engine core of the server, in the exampletornado.ioloop.IOLoop.current()Is the default Tornado Ioloop instance.
  2. App instance, which represents a completed back-end app that attaches to a server socket port to provide services externally. An ioloop instance can have more than one app instance. In the example, there is only one app instance. In practice, more than one app instance is allowed, but it is almost never used.
  3. Handler class, which stands for business logic, and when we do server-side development we write a bunch of handlers to service client requests.
  4. Routing table, which links the specified URL rules with handlers to form a routing mapping table. When the request arrives, the routing mapping table is queried based on the requested access URL to find the corresponding business handler.

The relationship between the four components is that one ioloop contains multiple apps (managing multiple service ports), one app contains one routing table, and one routing table contains multiple handlers. Ioloop is the engine core of the service. It is the engine responsible for receiving and responding to client requests, driving the operation of business handlers, and executing scheduled tasks inside the server.

When a request comes in, ioloop reads the request and unpackets it into an HTTP request object, finds the routing table of the corresponding app on the socket, queries the handler attached to the routing table using the URL of the request object, and then executes the handler. The Handler method typically returns an object, and ioloop is responsible for serializing the object as an HTTP response object and sending it to the client.

The same ioloop instance runs in a single-threaded environment.

Factorial service

Let’s write a normal Web server that will provide the factorial service. That’s helping us calculate n factorial. The value of the. The server will provide a cache of factorials, and the ones that have been calculated will be stored so that they don’t have to be recalculated next time. The nice thing about using Python is that we don’t have to worry about overrunning factorials, and Python integers can be infinitely large.

# fact.py
import tornado.ioloop
import tornado.web


class FactorialService(object):  Define a factorial service object

    def __init__(self):
        self.cache = {}   Use a dictionary to record the factorials that have been calculated

    def calc(self, n):
        if n in self.cache:  # if there is a direct return
            return self.cache[n]
        s = 1
        for i in range(1, n):
            s *= i
        self.cache[n] = s  # cache it
        return s


class FactorialHandler(tornado.web.RequestHandler):

    service = FactorialService()  # new creates the factorial service object

    def get(self):
        n = int(self.get_argument("n"))  Get the url parameter value
        self.write(str(self.service.calc(n)))  # Use the factorial service


def make_app(a):
    return tornado.web.Application([
        (r"/fact", FactorialHandler),  # register route
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()
Copy the code

Execute python fact. Py, open a browser and type in http://localhost:8888/fact? N = 50, we can see the browser output 608281864034267560872252163321295376887552831379210240000000000, if we don’t provide n parameters, visit http://localhost:8888/fact, You can see that the browser outputs 400: Bad Request, telling you that the Request is in error, i.e. one parameter is missing.

Using Redis

In the example above, the cache is stored in local memory. If a new port and its factorial service are accessed through this new port, it needs to be recalculated for each n because local memory cannot be shared across processes and machines.

So in this example, we will use Redis to cache the results so that we can completely avoid double-counting. And instead of returning plain text, we’re going to return json, and we’re going to add the name of the field to the response and we’re going to calculate it from the cache or actually calculate it. In addition, we provide a default argument. If the client does not provide n, it defaults to n=1.

import json
import redis
import tornado.ioloop
import tornado.web


class FactorialService(object):

    def __init__(self):
        self.cache = redis.StrictRedis("localhost".6379)  # cache changed to Redis
        self.key = "factorials"

    def calc(self, n):
        s = self.cache.hget(self.key, str(n))  Save the result in a hash structure
        if s:
            return int(s), True
        s = 1
        for i in range(1, n):
            s *= i
        self.cache.hset(self.key, str(n), str(s))  # save the result
        return s, False


class FactorialHandler(tornado.web.RequestHandler):

    service = FactorialService()

    def get(self):
        n = int(self.get_argument("n") or 1)  Parameter default value
        fact, cached = self.service.calc(n)
        result = {
            "n": n,
            "fact": fact,
            "cached": cached
        }
        self.set_header("Content-Type"."application/json; charset=UTF-8")
        self.write(json.dumps(result))


def make_app(a):
    return tornado.web.Application([
        (r"/fact", FactorialHandler),
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()
Copy the code

When we visit http://localhost:8888/fact? again N = 50, can see the following browser output {” cached “: false,” fact “: 608281864034267560872252163321295376887552831379210240000000000,” n “: 50}, refresh again, the browser output {” cached “: true,” fact “: 608281864034267560872252163321295376887552831379210240000000000,” n “: 50}, you can see that the cached field is programmed with false from true, indicating that the cache has indeed saved the computed results. Restart the process, access the connection again, and observe the browser output. You can see that cached is still true. The cached result is no longer stored in local memory.

PI calculation services

And then we’re going to add another service, we’re going to calculate PI, and there are a lot of different formulas for calculating PI, and we’re going to use the simplest one.

We provide a parameter n in the service as the precision index of PI. The larger n is, the more accurate the calculation of PI will be. Meanwhile, we also cache the calculation results in Redis server to avoid repeated calculation.

# pi.py
import json
import math
import redis
import tornado.ioloop
import tornado.web


class FactorialService(object):

    def __init__(self, cache):
        self.cache = cache
        self.key = "factorials"

    def calc(self, n):
        s = self.cache.hget(self.key, str(n))
        if s:
            return int(s), True
        s = 1
        for i in range(1, n):
            s *= i
        self.cache.hset(self.key, str(n), str(s))
        return s, False


class PiService(object):

    def __init__(self, cache):
        self.cache = cache
        self.key = "pis"

    def calc(self, n):
        s = self.cache.hget(self.key, str(n))
        if s:
            return float(s), True
        s = 0.0
        for i inRange (n) : s + = 1.0 / (2 * I + 1)/(2 * I + 1) s = math.h SQRT (s * 8) self. Cache. Hset (self. Key, STR (n), STR (s))return s, False


class FactorialHandler(tornado.web.RequestHandler):

    def initialize(self, factorial):
        self.factorial = factorial

    def get(self):
        n = int(self.get_argument("n") or 1)
        fact, cached = self.factorial.calc(n)
        result = {
            "n": n,
            "fact": fact,
            "cached": cached
        }
        self.set_header("Content-Type"."application/json; charset=UTF-8")
        self.write(json.dumps(result))


class PiHandler(tornado.web.RequestHandler):

    def initialize(self, pi):
        self.pi = pi

    def get(self):
        n = int(self.get_argument("n") or 1)
        pi, cached = self.pi.calc(n)
        result = {
            "n": n,
            "pi": pi,
            "cached": cached
        }
        self.set_header("Content-Type"."application/json; charset=UTF-8")
        self.write(json.dumps(result))


def make_app():
    cache = redis.StrictRedis("localhost", 6379)
    factorial = FactorialService(cache)
    pi = PiService(cache)
    return tornado.web.Application([
        (r"/fact", FactorialHandler, {"factorial": factorial}),
        (r"/pi", PiHandler, {"pi": pi}),
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()
Copy the code

Since both handlers need redis, we pull out the redis and pass it in as a parameter. In addition, the Handler can pass parameters through the initialize function. When registering a route, it can pass any parameters by providing a dictionary. The dictionary key must match the parameter name. Let’s run Python pi.py, open a browser and go to http://localhost:8888/pi? N =200, you can see the browser output {“cached”: false, “PI “: 3.1412743276, “n”: 1000}, which is very close to PI.

Read more advanced Python articles and follow the public account “Code hole”