异步服务器框架Tornado

827 阅读3分钟

tornado的基本组成与实践

  1. web服务器组成。 一个tornado web服务器通常由四大组件组成。

    • ioloop实例,它是全局的tornado事件循环,是服务器的引擎核心。
    • app实例,它代表一个完成的后端app,它会挂接一个服务端套接字端口对外提供服务。可以有多个,但是一般只使用一个。
    • handler类,它代表业务逻辑,我们进行服务端开发时就是编写多个handler用来服务客户端请求。
    • 路由表,它将指定的url规则和handler挂接起来,形成一个路由映射表。当请求到来时,根据请求的访问url查询路由映射表来找到相应的业务handler。
  2. 组件之间的关系

    • 一个ioloop包含多个app(管理多个服务端口),一个app包含一个路由表,一个路由表包含多个handler。ioloop是服务的引擎核心,它是发动机,负责接收和响应客户端请求,负责驱动业务handler的运行,负责服务器内部定时任务的执行。
  3. 示例

    当一个请求到来时,ioloop读取这个请求解包成一个http请求对象,找到该套接字上对应app的路由表,通过请求对象的url查询路由表中挂接的handler,然后执行handler。handler方法执行后一般会返回一个对象,ioloop负责将对象包装成http响应对象序列化发送给客户端。

同一个ioloop实例运行在一个单线程环境下。

实例

下面实现一个只有两个API的服务。

import tornado.ioloop
import tornado.web
import asyncio


class FactorialService(object):  # 定义一个阶乘服务对象

    def __init__(self):
        self.cache = {}  # 用字典记录已经计算过的阶乘

    def calc(self, n):
        if n in self.cache:  # 如果有直接返回
            return self.cache[n]
        s = 1
        for i in range(1, n):
            s *= i
        self.cache[n] = s  # 缓存起来
        return s


class FactorialHandler(tornado.web.RequestHandler):
    service = FactorialService()  # new出阶乘服务对象

    async def get(self):
        n = int(self.get_argument("n"))  # 获取url的参数值
        print("get n = {}".format(n))
        await asyncio.sleep(5)
        self.write(str(self.service.calc(n)))  # 使用阶乘服务
        print("FactorialHandler end")


class SayHelloHanlder(tornado.web.RequestHandler):
    async def get(self):
        name = self.get_argument("name")  # 获取url的参数值
        print("SayHelloHanlder to {}".format(name))
        await asyncio.sleep(1)
        self.write("hello {}".format(name))
        print("SayHelloHanlder end")


def make_app():
    return tornado.web.Application([
        (r"/fact", FactorialHandler),  # 注册路由
        (r"/say/hello", SayHelloHanlder),
    ])


if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()
  1. 请求两个不同的接口

分别起两个终端,分别输入下面两行。

curl http://localhost:8888/say/hello?name=lil
curl http://localhost:8888/fact?n=20

之后可以看到运行代码的终端打印出下面的运行结果:

get n = 20
SayHelloHanlder to lily
SayHelloHanlder end
FactorialHandler end

从结果可以看出,/fact先被请求,但是结束得反而晚,而say/hello虽然开始得晚,却比/fact结束得早。这正说明了在单个线程中,这两个函数并发执行了。

  1. 请求同一个接口。

首先添加一个RandomRSleepHandler,然后在路由表中注册一个新的路由项。

class RandomRSleepHandler(tornado.web.RequestHandler):
    async def get(self):
        name = self.get_argument("name")
        seconds = random.randint(60, 120)  # 随机产生睡眠的时间
        print("RandomRSleepHandler %s sleep %s seconds" % (name, seconds))
        await asyncio.sleep(seconds)
        self.write("I am RandomRSleepHandler")
        print("RandomRSleepHandler awake")

添加路由项。

def make_app():
    return tornado.web.Application([
        .... 
        (r"/random/sleep", RandomRSleepHandler),
    ])

分别起两个终端,分别输入下面两行。

http://127.0.0.1:8888/random/sleep?name=lily
http://127.0.0.1:8888/random/sleep?name=huhu

服务器输出如下。

RandomRSleepHandler lily sleep 76 seconds
RandomRSleepHandler huhu sleep 73 seconds
RandomRSleepHandler huhu awake
RandomRSleepHandler lily awake

从结果可以看出,用户huhu先发出请求,但是他获得响应的时间反而比后发出请求的用户lily晚。服务器在收到huhu的请求之后,通过await asyncio.sleep(seconds)请求阻塞住了,但是,服务器进程并没有就此阻塞,而是去处理了另一个用户的请求。这正说明了在单个线程中,两个用户请求并发执行了。

而其中的await asyncio.sleep(seconds)可以看作是具体业务逻辑的模拟。

参考文献

  1. 知乎后端主力框架tornado入门体验:juejin.cn/post/684490…
  2. 官方文档:www.tornadoweb.org/en/stable/i…
  3. 深入理解tornado之底层ioloop实现:segmentfault.com/a/119000000…