第10章 · Tornado 框架基础

第三个 Web 框架,主打异步、长连接、高并发。今天先把它当 Flask 用——HelloWorld、路由、模板、表单。

🎯 写两个 Tornado 程序:HelloWorld + 简易登录页

关于 Tornado

Tornado 是 FriendFeed(后被 Facebook 收购)开源的 Python Web 框架,最大特色是异步非阻塞——一台机器能扛上万的长连接,特别适合 WebSocket、聊天室、实时推送。它解决的就是著名的 C10K 问题(一台服务器同时处理 1 万个客户端)。

🌶️ Flask
小而美,靠插件扩展。同步阻塞,靠多进程扛并发。
🎸 Django
大而全,自带后台/ORM。同步阻塞,依赖 ASGI 才能异步。
🌪️ Tornado
本身就是异步框架 + 自带 Web 服务器。一台机器扛上万连接。

Tornado 自己就是一个 Web 服务器,不需要 Apache / Nginx 也能直接跑。今天写两个最小程序,先认识它的"长相"——下节课再讲异步。

1 安装

装 Tornado

SHELL
# 国内用清华源更快
$ pip3 install tornado -i https://pypi.tuna.tsinghua.edu.cn/simple

# 验证装好了
$ python3 -c "import tornado; print(tornado.version)"
6.4.1
💡 课堂目录约定
第8章我们把项目放 /home/django/,本章统一放 /home/tornado/,方便区分:
mkdir -p /home/tornado && cd /home/tornado
2 第一个程序

HelloWorld:6 步看清 Tornado 长什么样

💬 Tornado 三个核心角色
  • RequestHandler — 处理一个请求的""。每来一个请求,Tornado 实例化一个,调用对应的 get() / post() 方法。
  • Application — 路由表的容器:URL 模式 → Handler 类。
  • IOLoop — 事件循环。它就是那个"扛上万连接"的引擎。

新建 /home/tornado/hello.py

PYTHON /home/tornado/hello.py
# ============ Tornado 最小程序 ============
import tornado.ioloop      # 事件循环
import tornado.web         # Web 框架核心


# 1. 写一个 Handler——继承 RequestHandler,定义 get 方法
class MainHandler(tornado.web.RequestHandler):
    def get(self):
        # self.write 给浏览器写响应内容(可以是字符串、字典)
        self.write("Hello, Tornado!")


# 2. 创建 Application:参数是路由表
def make_app():
    return tornado.web.Application([
        # 元组 (URL 正则, Handler 类)
        (r"/", MainHandler),
    ])


# 3. 启动入口
if __name__ == "__main__":
    app = make_app()
    app.listen(8888)             # 监听 8888 端口
    print("服务器已启动:http://0.0.0.0:8888")
    tornado.ioloop.IOLoop.current().start()   # ★ 启动事件循环

跑起来

SHELL
$ cd /home/tornado
$ python3 hello.py
服务器已启动:http://0.0.0.0:8888

浏览器访问 http://服务器IP:8888/,看到 Hello, Tornado! 就成了。

⚠️ Tornado 不会自动重载
改了代码之后必须手动重启(Ctrl+C 停掉,再 python3 hello.py)。Django/Flask 默认开发模式会自动重启,Tornado 默认不会,要在 app.listen 之后写 autoreload.start() 才有。课堂就手动重启好了。

路由怎么写

路由就是 Application 第一个参数那个列表。元组 = (URL 正则, Handler 类),可以挂多条:

PYTHON
tornado.web.Application([
    (r"/",        MainHandler),       # 首页
    (r"/about",   AboutHandler),      # 关于页
    (r"/user/(\d+)", UserHandler),    # 带参数:/user/123
])
💡 URL 里的捕获组
正则里的 (\d+) 这种捕获组会作为参数传给 handler 的方法。比如 /user/(\d+) 命中 /user/123 时,会调用 handler.get('123')——多了一个字符串参数。
3 HTTP 方法

区分 GET 和 POST:登录例子

💬 一个 Handler 同时挂多个方法
Handler 类里写 def get(self):——浏览器 GET 请求时被调用;
def post(self):——浏览器 POST 请求时被调用。
同一个 URL,根据请求方法分发到不同方法

读取参数的几种方式

写法用来读什么
self.get_argument('name')同时支持 URL 查询参数 和 POST 表单字段
self.get_argument('name', 默认值)没传时不会报错,用默认值
self.request.method判断当前是 GET 还是 POST

新建 /home/tornado/login.py

PYTHON /home/tornado/login.py
import tornado.ioloop
import tornado.web


class LoginHandler(tornado.web.RequestHandler):

    # 浏览器访问 /login 时(默认是 GET),显示登录表单
    def get(self):
        # 直接写 HTML 字符串。注意:表单提交回 /login 自身,method=post
        self.write("""
            <h2>登录</h2>
            <form method='post'>
                用户名:<input name='username'><br><br>
                密  码:<input name='password' type='password'><br><br>
                <button>登录</button>
            </form>
        """)

    # 表单提交时(POST),处理用户提交的数据
    def post(self):
        # get_argument 自动从 POST 表单取值(也支持 ?xxx 的 URL 参数)
        username = self.get_argument("username")
        password = self.get_argument("password")

        if username == "admin" and password == "123456":
            self.write(f"<h2>✅ 欢迎,{username}</h2>")
        else:
            # 用 redirect 跳转回登录页
            self.write("<h2>❌ 用户名或密码错误</h2><a href='/login'>返回</a>")


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


if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    print("启动:http://0.0.0.0:8888/login")
    tornado.ioloop.IOLoop.current().start()

python3 login.py,浏览器开 http://IP:8888/login——填表 → 提交 → 看到欢迎或报错。

❌ 易错:xsrf 报错
Tornado 默认没开 XSRF 防护(这一点跟 Django 不同),不用写 csrf_token。但如果在 Application 里加了 xsrf_cookies=True,POST 表单就必须加 {% module xsrf_form_html() %},否则 403。课堂演示先不开。
4 模板

用模板代替字符串拼 HTML

💬 为什么要模板
把 HTML 全塞进 Python 字符串里——丑、难维护、不能复用。模板把 HTML 单独放到文件里,里面留占位符,Python 把数据塞进去。Tornado 自带模板引擎,语法和 Django/Flask 类似。

3 条模板语法(够用了)

语法作用
{{ name }}填入变量值
{% for x in items %} ... {% end %}循环
{% if x %} ... {% end %}条件判断
⚠️ Tornado 用 {% end %}
Django 用 {% endfor %}{% endif %}Tornado 统一用 {% end %}——别混了。

把 HTML 抽出来

项目结构:

/home/tornado/ ├── app.py └── templates/ └── login.html # ★ 新建

新建 /home/tornado/templates/login.html

HTML templates/login.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8"><title>登录</title>
    <style>
        body { font-family: sans-serif;
               max-width: 360px; margin: 80px auto; }
        input { width: 100%; padding: 8px; margin: 5px 0;
                box-sizing: border-box; }
        button { width: 100%; padding: 10px;
                 background: #3B6B9A; color: #fff;
                 border: none; cursor: pointer; }
        .err { color: #B83B2E; }
    </style>
</head>
<body>
    <h2>🌪️ Tornado 登录</h2>

    <form method="post">
        <input name="username" placeholder="用户名">
        <input name="password" type="password" placeholder="密码">
        <button>登录</button>
    </form>

    <!-- {% if error %} 在 error 非空时显示一行红字 -->
    {% if error %}
        <p class="err">❌ {{ error }}</p>
    {% end %}

    <p style="color:#888">课堂账号:admin / 123456</p>
</body>
</html>

用 self.render 渲染模板

新建 /home/tornado/app.py

PYTHON /home/tornado/app.py
import os
import tornado.ioloop
import tornado.web


class LoginHandler(tornado.web.RequestHandler):

    def get(self):
        # self.render 的两个参数:模板文件名、传给模板的数据(key=value)
        # 这里 error="" 表示页面初次进入时没有错误信息
        self.render("login.html", error="")

    def post(self):
        username = self.get_argument("username")
        password = self.get_argument("password")

        if username == "admin" and password == "123456":
            # 登录成功:另开一个简单页面
            self.write(f"<h2>✅ 欢迎,{username}!</h2>")
        else:
            # 失败:仍渲染登录页,但这次带上错误信息
            self.render("login.html", error="用户名或密码错误")


def make_app():
    return tornado.web.Application(
        [(r"/login", LoginHandler)],
        # ★ 关键设置:告诉 Tornado 模板放哪个目录
        template_path=os.path.join(os.path.dirname(__file__), "templates"),
    )


if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    print("启动:http://0.0.0.0:8888/login")
    tornado.ioloop.IOLoop.current().start()

🎉 验收

停掉之前的进程(Ctrl+C),跑 python3 app.py

访问 http://IP:8888/login,输错密码 → 页面变红字 ❌;输对 admin/123456 → 看到欢迎页 ✅

🎯 模板三步法
templates/ 目录放 .html
② Application 里设 template_path=...
③ Handler 里 self.render('xxx.html', a=1, b=2)

📋 本节小结

三个框架对比

主题FlaskDjangoTornado
路由写法@app.route('/')path('', view)(r"/", Handler)
视图形式函数函数 / 类类(继承 RequestHandler)
取参数request.args.getrequest.GET.getself.get_argument
渲染模板render_template()render(req, ...)self.render()
循环结束符{% endfor %}{% endfor %}{% end %}
异步天然支持需要 ASGI

记住这套"三件套"启动模板

  1. Handler:定义 class XxxHandler(tornado.web.RequestHandler) + get/post 方法
  2. Application:列路由表 [(r"/path", XxxHandler), ...]
  3. 启动app.listen(端口) + IOLoop.current().start()

课堂练习

两组共 12 题:先 Tornado 概念,再三框架对比。

第一组

Tornado 概念基础(6 题)

把 Tornado 的核心组件和写法记住。

Q1单选

Tornado 处理一个请求时,最先被调用的是?

AApplication 的 __init__
B对应 Handler 的 getpost 方法
CIOLoop 的 add_callback
DPython 的 main
解析:每来一个请求,Application 实例化对应的 Handler 类,然后根据 HTTP 方法(GET/POST/...)调用对应的方法。
Q2单选

Handler 里读取 GET 参数和 POST 表单字段,统一用?

Arequest.args.get
Bself.params
Cself.get_argument('name')
Dself.read('name')
解析:Tornado 的 get_argument 同时处理 URL 查询参数和 POST 表单字段——比 Flask 还简单一点。
Q3单选

下面哪一行不能省,否则 Tornado 启动后立刻退出?

Aapp.listen(8888)
Bapp = make_app()
Ctornado.ioloop.IOLoop.current().start()
Dprint("启动")
解析:IOLoop 是事件循环,start() 是阻塞的——不写它,主程序立刻结束。Application 和 listen 只是"挂好招牌",IOLoop 才是"开门迎客"。
Q4单选

Tornado 模板里,循环的结束标签是?

A{% endfor %}
B{% end %}
C{% /for %}
D</for>
解析:Tornado 的特色——所有逻辑块(for/if/while/...)都用 {% end %} 收尾,不区分具体是结束什么。
Q5判断

Tornado 默认开发模式下,改了代码会自动重启服务器。

解析:Tornado 默认不会。Django/Flask 默认会,但 Tornado 要主动调用 autoreload 才行。课堂里手动重启就行。
Q6判断

用模板时,必须在 Application 中设置 template_path,否则 self.render 找不到模板文件。

解析:Tornado 不知道你的模板放哪——必须明确告诉它 template_path=os.path.join(...)
第二组

三框架对比 + 异步入门(6 题)

理解 Tornado 跟 Flask/Django 的差异,以及异步是什么。

Q1单选

"C10K 问题"指的是?

A1000 行代码的 C 程序
B10000 元的服务器
C一台服务器同时处理 1 万个客户端连接的难题
D10K 字节的内存限制
解析:Concurrent 10K——单机扛 10000 并发连接。Tornado 就是为这个场景设计的。
Q2单选

下面对"阻塞"的描述哪个对?

A程序运行得很慢
B程序在等某操作完成期间,自身被挂起,没法做其他事
C程序崩溃了
D需要管理员密码才能继续
解析:阻塞 = 卡在那里。比如 time.sleep(5)、读大文件、查慢 SQL——这 5 秒钟里别的请求都要排队等。Tornado 的非阻塞就是要避免这种排队。
Q3单选

如果 URL 写成 r"/user/(\d+)",访问 /user/42 时 Handler 的 get 方法签名应该是?

Adef get(self):
Bdef get(self, request):
Cdef get(self, user_id):
Ddef get(user_id):
解析:正则里有几个 (...) 捕获组,方法就要相应多几个参数。这里捕到 "42" 字符串。
Q4单选

self.writeself.render 的区别?

A没区别
Bwrite 直接输出字符串/字典;render 加载模板文件并填入数据
Crender 是写日志的
Dwrite 是只读的
解析:简单响应用 write,要展示页面用 render——render 内部其实也是用 write 把渲染后的字符串发出去。
Q5单选

同一个写"博客文章列表",三个框架最相似的是?

A路由写法完全一样
B视图都用类
C都需要 admin 后台
D核心思路一样:路由→视图→模板渲染
解析:Web 框架的核心套路都是这三件事,差别只在写法。学了 Flask 和 Django 之后再看 Tornado,应该感觉"似曾相识"。
Q6判断

Tornado 自带 Web 服务器,部署上线时不需要 Nginx/Apache。

解析:Tornado 是HTTP 服务器 + Web 框架二合一,理论上能直接对外服务。但实际上线时通常前面挂 Nginx 做反向代理(处理 HTTPS、静态文件、负载均衡),让 Tornado 专心处理动态请求——这是常见架构。