杰瑞科技汇

python tornado教程

Python Tornado 完整教程

Tornado 是一个由 Python 编写、开源的、异步的网络框架和 HTTP 服务器,它的核心特点是异步非阻塞 I/O,这使得它非常适合处理高并发、长连接的场景,例如实时 Web 服务、聊天室、API 服务器等。

python tornado教程-图1
(图片来源网络,侵删)

为什么选择 Tornado?

在 Flask、Django 等同步框架大行其道的今天,Tornado 的优势在于:

  1. 高性能:基于异步非阻塞 I/O 模型,可以轻松处理数以万计的并发连接,而不会因为等待 I/O(如数据库查询、网络请求)而阻塞整个程序。
  2. 原生 WebSocket 支持:Tornado 对 WebSocket 协议提供了第一方的、高质量的支持,构建实时应用非常方便。
  3. 异步友好:框架从设计之初就围绕异步编程,使得编写高并发应用逻辑更加自然。
  4. 自带的 HTTP 服务器:Tornado 包含了一个强大的、生产级的 HTTP 服务器,无需依赖其他 WSGI 服务器(如 Gunicorn、uWSGI)即可运行。

环境准备

你需要安装 Tornado,最简单的方式是使用 pip:

pip install tornado

第一个 Tornado 应用:Hello World

让我们从一个最简单的例子开始,理解 Tornado 的基本工作流程。

# hello_world.py
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
    def get(self):
        """处理 GET 请求"""
        self.write("Hello, Tornado World!")
def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),  # 路由映射:URL根路径 / 映射到 MainHandler
    ])
if __name__ == "__main__":
    app = make_app()
    app.listen(8888)  # 监听 8888 端口
    print("Server is running on http://localhost:8888")
    tornado.ioloop.IOLoop.current().start() # 启动事件循环

代码解析:

python tornado教程-图2
(图片来源网络,侵删)
  1. import:导入了 tornado.ioloop(事件循环)和 tornado.web(Web 框架核心)。
  2. MainHandler(tornado.web.RequestHandler):我们创建了一个处理类,它继承自 tornado.web.RequestHandler,这个类负责处理特定 URL 的 HTTP 请求。
  3. get(self):这是 RequestHandler 的一个方法,专门用来处理 HTTP GET 请求,当有用户访问我们注册的 URL 时,这个方法就会被调用。
  4. self.write(...):该方法将字符串内容作为 HTTP 响应体返回给客户端。
  5. make_app():这是一个工厂函数,用于创建 tornado.web.Application 实例。
  6. [(r"/", MainHandler)]:这是一个路由列表,它告诉 Tornado,当访问根路径 时,将请求交给 MainHandler 类来处理。
  7. app.listen(8888):让应用程序监听本地的 8888 端口。
  8. tornado.ioloop.IOLoop.current().start():这是 Tornado 程序的“心脏”,它启动了事件循环,让程序能够持续接收和处理请求,没有这一行,服务器启动后会立即退出。

运行你的应用:

python hello_world.py

然后在浏览器中访问 http://localhost:8888,你就能看到 "Hello, Tornado World!"。


核心概念详解

路由

路由就是 URL 模式与处理类之间的映射,它是一个元组的列表,每个元组包含一个 URL 正则表达式和一个处理类。

tornado.web.Application([
    (r"/", MainHandler),
    (r"/story/([0-9]+)", StoryHandler), # 捕获 URL 中的数字作为参数
    (r"/story/([^/]+)", StoryHandler),  # 捕获 URL 中 / 之间的所有内容
])

RequestHandler 中,可以通过 self.request.path_argsself.request.path_kwargs 获取 URL 中捕获的参数。

python tornado教程-图3
(图片来源网络,侵删)

RequestHandler

RequestHandler 是 Tornado 的核心,它封装了所有与 HTTP 请求和响应相关的操作。

  • 常用方法
    • self.get_argument(name, default=..., strip=True): 获取 GET 或 POST 请求中的参数。strip=True 会自动去除参数两端的空白字符。
    • self.get_arguments(name): 获取同名的多个参数(?foo=1&foo=2),返回一个列表。
    • self.request: 一个包含所有请求信息的对象(如 method, headers, body, remote_ip 等)。
    • self.set_status(code, reason=None): 设置 HTTP 响应状态码(如 200, 404, 500)。
    • self.set_header(name, value): 设置响应头。
    • self.add_header(name, value): 添加一个响应头。
    • self.redirect(url): 重定向客户端到另一个 URL。
    • self.render(template_name, **kwargs): 渲染一个模板文件,并返回给客户端。

模板

Tornado 使用自己的模板语言,它支持 Python 的控制流语句。

模板文件 template.html:

<html>
  <head><title>{{ title }}</title></head>
  <body>
    <h1>{{ header }}</h1>
    <ul>
      {% for item in items %}
        <li>{{ item }}</li>
      {% end %}
    </ul>
  </body>
</html>

Python 代码:

import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("template.html", 
                    title="My Page", 
                    header="Hello from Tornado Template",
                    items=["Item 1", "Item 2", "Item 3"])
# ... make_app and main 函数 ...

使用模板的步骤:

  1. 创建一个 templates 文件夹,将模板文件(如 template.html)放进去。
  2. 在创建 Application 时,指定 template_path 参数。
    def make_app():
        return tornado.web.Application([
            (r"/", MainHandler),
        ], template_path="templates") # 指定模板目录
  3. RequestHandler 中调用 self.render() 方法。

异步编程与协程

这是 Tornado 最强大的部分,当你的应用需要执行 I/O 密集型操作(如数据库查询、调用其他 API)时,不能使用同步的方式,否则会阻塞整个事件循环,导致服务器无法响应其他请求。

Tornado 使用基于 async/await 语法的协程来实现异步。

同步 vs. 异步示例

假设我们有一个非常耗时的同步函数 sync_fetch_db()

import time
import tornado.ioloop
import tornado.web
# 模拟一个耗时的同步操作
def sync_fetch_db():
    time.sleep(5) # 阻塞 5 秒
    return "Data from DB"
class SyncHandler(tornado.web.RequestHandler):
    def get(self):
        start_time = time.time()
        data = sync_fetch_db()
        self.write(f"Sync Result: {data}. Time taken: {time.time() - start_time:.2f}s")
# ... make_app ...

当你访问这个处理程序时,服务器会完全卡死 5 秒,这 5 秒内,服务器无法处理任何其他请求。

异步版本

我们用 async/await 来重写它,需要一个支持异步的客户端库,aiobotocore (AWS), aiomysql (MySQL), 或者 aiohttp (HTTP 请求),这里我们用 asyncio.sleep 来模拟异步 I/O。

import asyncio
import tornado.ioloop
import tornado.web
import tornado.httpclient # Tornado 的异步 HTTP 客户端
# 模拟一个异步操作
async def async_fetch_data():
    # 模拟网络延迟
    await asyncio.sleep(3)
    return "Data from Async API"
class AsyncHandler(tornado.web.RequestHandler):
    async def get(self):
        start_time = time.time()
        # 关键:使用 await 来等待异步操作完成
        # 这期间,事件循环可以去处理其他请求
        data = await async_fetch_data()
        self.write(f"Async Result: {data}. Time taken: {time.time() - start_time:.2f}s")
# ... make_app ...

async/await 的工作原理:

  1. async def get(self)::声明 get 方法是一个异步函数。
  2. await some_async_function():当执行到 await 时,Tornado 会暂停这个 get 方法的执行,并将控制权交还给事件循环,事件循环会去处理其他活跃的请求或连接。
  3. some_async_function 完成后,事件循环会恢复 get 方法的执行,从 await 的下一行代码继续。

最佳实践永远不要在异步处理函数中进行同步 I/O 操作,如果你必须调用一个同步的库(比如某些数据库驱动),请在一个线程池中运行它,以避免阻塞事件循环。

import concurrent.futures
# ... 在 RequestHandler 中 ...
def get(self):
    # 在线程池中运行同步函数
    with concurrent.futures.ThreadPoolExecutor() as executor:
        future = executor.submit(sync_fetch_db)
        data = future.result() # 阻塞等待线程池中的任务完成,但不会阻塞事件循环
    self.write(data)

实战案例:构建一个简单的实时聊天室

这个例子将结合 Tornado 的路由、模板、异步和 WebSocket 功能。

项目结构

chat_app/
├── templates/
│   └── index.html
├── chat.py

代码 (chat.py)

import tornado.ioloop
import tornado.web
import tornado.websocket
import uuid
import json
# 用于存储所有连接的客户端
clients = set()
class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("index.html")
class ChatWebSocketHandler(tornado.websocket.WebSocketHandler):
    def open(self):
        """当新的 WebSocket 连接建立时调用"""
        print("New client connected")
        # 为每个连接生成一个唯一的 ID
        self.client_id = str(uuid.uuid4())
        clients.add(self)
        # 通知所有客户端有新用户加入
        self.broadcast_message({
            "type": "system",
            "message": f"User {self.client_id[:8]} joined."
        })
    def on_message(self, message):
        """当收到客户端消息时调用"""
        print(f"Received message: {message}")
        try:
            data = json.loads(message)
            # 广播消息给所有客户端
            self.broadcast_message({
                "type": "user",
                "user_id": self.client_id[:8],
                "message": data['message']
            })
        except json.JSONDecodeError:
            pass
    def on_close(self):
        """当 WebSocket 连接关闭时调用"""
        print("Client disconnected")
        clients.discard(self)
        # 通知所有用户有用户离开
        self.broadcast_message({
            "type": "system",
            "message": f"User {self.client_id[:8]} left."
        })
    def broadcast_message(self, message):
        """向所有连接的客户端广播消息"""
        for client in clients:
            try:
                client.write_message(json.dumps(message))
            except:
                # 如果客户端已经断开,则从集合中移除
                clients.discard(client)
def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
        (r"/ws", ChatWebSocketHandler),
    ], template_path="templates")
if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    print("Chat server is running on ws://localhost:8888")
    tornado.ioloop.IOLoop.current().start()

模板 (templates/index.html)

<!DOCTYPE html>
<html>
<head>Tornado Chat</title>
    <style>
        body { font-family: sans-serif; }
        #messages { border: 1px solid #ccc; height: 300px; overflow-y: scroll; padding: 10px; margin-bottom: 10px; }
        #message-input { width: 80%; padding: 5px; }
        #send-button { padding: 5px 10px; }
        .system { color: #888; font-style: italic; }
    </style>
</head>
<body>
    <h1>Tornado Chat Room</h1>
    <div id="messages"></div>
    <input type="text" id="message-input" placeholder="Type a message...">
    <button id="send-button">Send</button>
    <script>
        const messagesDiv = document.getElementById('messages');
        const messageInput = document.getElementById('message-input');
        const sendButton = document.getElementById('send-button');
        // 连接到 WebSocket 服务器
        const ws = new WebSocket("ws://localhost:8888/ws");
        ws.onopen = function(event) {
            console.log("Connected to chat server.");
        };
        ws.onmessage = function(event) {
            const data = JSON.parse(event.data);
            const messageElement = document.createElement('div');
            if (data.type === 'system') {
                messageElement.className = 'system';
                messageElement.textContent = data.message;
            } else {
                messageElement.textContent = `<${data.user_id}>: ${data.message}`;
            }
            messagesDiv.appendChild(messageElement);
            messagesDiv.scrollTop = messagesDiv.scrollHeight; // 自动滚动到底部
        };
        ws.onclose = function(event) {
            console.log("Disconnected from chat server.");
        };
        function sendMessage() {
            const message = messageInput.value.trim();
            if (message) {
                ws.send(JSON.stringify({ message: message }));
                messageInput.value = '';
            }
        }
        sendButton.onclick = sendMessage;
        messageInput.addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {
                sendMessage();
            }
        });
    </script>
</body>
</html>

运行聊天室:

  1. 确保文件结构正确。
  2. chat_app 目录下运行 python chat.py
  3. 在浏览器中打开两个或多个标签页,访问 http://localhost:8888
  4. 你就可以看到实时聊天效果了!

进阶主题

  • 认证与授权:Tornado 提供了 tornado.web.authenticated 装饰器,在处理类上使用它,如果用户未登录,Tornado 会自动重定向到配置的 login_url,你需要自己实现 get_current_user() 方法,通常从 cookie 或 session 中获取用户信息。
  • 静态文件:在 Application 中配置 static_path 参数,Tornado 就可以提供 CSS、JS、图片等静态文件。
    tornado.web.Application([
        # ...
    ], static_path="static")
  • 部署:Tornado 自带的服务器虽然强大,但在生产环境中,通常会使用 Nginx 或 Apache 作为反向代理,Nginx 负责处理静态文件、HTTPS 终止和负载均衡,并将动态请求转发给 Tornado 应用,这可以大大提高性能和安全性。
  • 测试:可以使用 tornado.testing 模块中的 AsyncHTTPTestCase 来编写异步的单元测试,方便地测试你的 Web 应用。

Tornado 是一个功能强大且性能卓越的 Python Web 框架,尤其适合构建需要处理大量并发连接的实时应用。

  • 入门:从 RequestHandler、路由和模板开始,理解其同步式的 API。
  • 进阶:深入理解异步编程,熟练使用 async/await,避免在异步代码中执行同步 I/O。
  • 实战:通过构建 WebSocket 应用(如聊天室)来巩固对异步和长连接的理解。
  • 生产:学习使用 Nginx 进行反向代理部署,并关注认证、日志等生产环境要素。

希望这份教程能帮助你快速上手 Tornado!

分享:
扫描分享到社交APP
上一篇
下一篇