目录
一、为什么 Tornado 适合做实时应用?先搞懂它的 “独门绝技”
二、环境搭建:3 步搞定 Tornado 开发环境(附版本选择)
三、5 行代码跑通第一个 Tornado 应用:感受异步的 “快”
四、核心概念拆解:用 “餐厅” 模型理解 Tornado 的异步逻辑

class 卑微码农:
def __init__(self):
self.技能 = ['能读懂十年前祖传代码', '擅长用Ctrl+C/V搭建世界', '信奉"能跑就别动"的玄学']
self.发量 = 100 # 初始发量
self.咖啡因耐受度 = '极限'
def 修Bug(self, bug):
try:
# 试图用玄学解决问题
if bug.严重程度 == '离谱':
print("这一定是环境问题!")
else:
print("让我看看是谁又没写注释...哦,是我自己。")
except Exception as e:
# 如果try块都救不了,那就...
print("重启一下试试?")
self.发量 -= 1 # 每解决一个bug,头发-1
# 实例化一个我
我 = 卑微码农()
引言:异步场景的“利器”
如果你玩过在线游戏的实时聊天频道,或者用过股票行情的实时刷新功能,可能会好奇:“这些高并发、低延迟的实时应用,到底是用什么技术做的?”

今天要聊的 Tornado,就是这类场景的 “利器”。作为 Python 生态中少有的 “异步非阻塞” 原生框架,它能单线程处理成千上万的并发连接,尤其适合需要长连接、实时交互的场景。
但很多人学 Tornado 时会被 “异步”“协程” 这些概念吓退,觉得太复杂。其实不然 —— 今天这篇文章,我会用一个可运行的实时聊天应用作为案例,从基础到实战,全程用大白话解释原理,代码直接能跑,哪怕是刚接触异步编程的新手也能跟上。
一、为什么 Tornado 适合做实时应用?先搞懂它的 “独门绝技”

第一次用 Tornado 时,我最直观的感受是:它处理并发的思路和 Django、Flask 完全不一样。
我们先回忆下传统 Web 框架(比如 Flask)的工作方式:假设服务器用 10 个线程处理请求,同一时间最多只能处理 10 个请求。如果其中一个请求要等待数据库返回(比如耗时 5 秒),这个线程就会 “卡着” 不动,其他请求只能排队 —— 这就是 “同步阻塞” 模型,效率很低。
而 Tornado 的 “异步非阻塞” 模型就像一个 “聪明的服务员”:他不需要守着一桌客人点完菜再服务下一桌,而是记下单子后去招呼其他客人,等厨房做好了再回来上菜。这种模式下,一个线程就能同时处理成百上千个连接,尤其适合:
- 实时聊天(WebSocket 长连接);
- 实时数据推送(如股票行情、监控数据);
- 高并发 API 服务(如短信验证码接口)。
具体来说,Tornado 有三个核心优势:
- 原生异步支持:从底层就是为异步设计的,不用依赖额外库;
- 内置 WebSocket:完美支持长连接,不用像 Flask 那样装扩展;
- 单线程高并发:通过 IOLoop(事件循环)高效调度任务,资源占用少。
简单说:如果你的应用需要 “同时和很多用户保持对话”,选 Tornado 准没错。
二、环境搭建:3 步搞定 Tornado 开发环境(附版本选择)

开始写代码前,先把环境搭好。Tornado 对 Python 版本有要求(至少 3.5+,推荐 3.7+),这里以 “Windows 10+Python 3.9” 为例,macOS 和 Linux 操作基本一致。
步骤 1:确认 Python 版本
打开 cmd(Windows)或终端(macOS/Linux),输入python --version,确保显示 “Python 3.7.x” 及以上。如果版本太低,去官网下载最新版(https://www.python.org/),安装时勾选 “Add Python to PATH”。
步骤 2:创建虚拟环境(避免依赖冲突)
和其他框架一样,建议用虚拟环境隔离项目依赖:
- 新建项目文件夹(比如
D:\tornado_chat); - 打开 cmd,cd 到该文件夹:
cd D:\tornado_chat; - 创建虚拟环境:
python -m venv venv; - 激活虚拟环境:
- Windows:
venv\Scripts\activate(激活后命令行前有(venv)); - macOS/Linux:
source venv/bin/activate。
- Windows:
步骤 3:安装 Tornado
Tornado 的稳定版本是 6.x(截止 2024 年),直接用 pip 安装:pip install tornado==6.3.3。
验证安装:输入python -c "import tornado; print(tornado.version)",显示 “6.3.3” 就说明成功了。
小提醒:如果后续代码中用到async/await(Python 3.5 + 的异步语法),确保 Python 版本不低于 3.5,否则会报错。
三、5 行代码跑通第一个 Tornado 应用:感受异步的 “快”

先从最简单的 “Hello World” 开始,感受 Tornado 的基本用法和异步特性。
步骤 1:写一个基础服务器
在tornado_chat文件夹下新建app.py,内容如下:
import tornado.ioloop
import tornado.web
# 定义处理请求的Handler(类似Flask的视图函数)
class MainHandler(tornado.web.RequestHandler):
def get(self): # 处理GET请求
self.write("Hello Tornado! 这是我的第一个异步应用~")
# 路由配置:URL路径对应哪个Handler
def make_app():
return tornado.web.Application([
(r"/", MainHandler), # 访问根路径时,由MainHandler处理
])
if __name__ == "__main__":
app = make_app()
app.listen(8888) # 监听8888端口
print("服务器启动,访问 http://127.0.0.1:8888")
tornado.ioloop.IOLoop.current().start() # 启动事件循环
步骤 2:启动服务器,测试效果
在 cmd 中运行python app.py,看到 “服务器启动,访问 http://127.0.0.1:8888” 后,打开浏览器访问该地址,会显示 “Hello Tornado! 这是我的第一个异步应用~”。
关键区别:和 Flask 不同,Tornado 启动后不会自动重启(修改代码需要手动重启),但你可以在启动时加autoreload=True参数实现热重载:
# 修改make_app和启动部分
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
], autoreload=True) # 开发时开启自动重载
if __name__ == "__main__":
app = make_app()
app.listen(8888)
print("服务器启动,访问 http://127.0.0.1:8888")
tornado.ioloop.IOLoop.current().start()
四、核心概念拆解:用 “餐厅” 模型理解 Tornado 的异步逻辑

很多人学 Tornado 卡在用 “同步思维” 理解 “异步逻辑”。我们用 “餐厅运营” 来类比,彻底搞懂它的工作原理:
- IOLoop(事件循环):相当于餐厅的 “前台总调度”,负责接收所有客人(请求),并安排服务员处理;
- Handler(处理器):相当于 “服务员”,负责处理具体的客人需求(比如点单、上菜);
- 异步操作(如网络请求、数据库查询):相当于 “厨房做菜”,服务员不用盯着,做好了会通知前台;
- 协程(coroutine):相当于 “服务员的工作流程”,遇到需要等待的任务(比如等菜),先去服务其他客人,等通知再回来。
举个具体例子:
- 同步模式:1 个服务员服务 1 桌客人,点完菜就站在厨房门口等,期间其他客人没人管;
- 异步模式:1 个服务员同时服务 10 桌客人,点完菜就记下来交给前台,然后去服务下一桌,厨房做好后前台通知服务员来上菜。
Tornado 的 “快”,本质就是通过 IOLoop 高效调度任务,让处理器(服务员)从不 “闲着”,把等待的时间用在处理其他请求上。
五、实战:开发实时聊天应用(从单聊到群聊,附完整代码)

我们来开发一个功能完整的实时聊天应用,包含:
- 支持多用户同时连接;
- 发送消息后所有人实时收到;
- 显示在线用户列表;
- 简单的用户认证(输入昵称登录)。
这个案例能完美体现 Tornado 处理 WebSocket 长连接的优势,全程分阶段实现,每一步都能看到效果。
阶段 1:实现基础 WebSocket 连接
WebSocket 是一种允许客户端和服务器 “持续对话” 的协议(不同于 HTTP 的 “一次请求一次响应”),Tornado 内置了对 WebSocket 的支持,不用额外装扩展。
步骤 1:编写 WebSocket 处理器
修改app.py,添加 WebSocket 相关代码:
import tornado.ioloop
import tornado.web
import tornado.websocket
from tornado.options import define, options, parse_command_line
# 存储所有连接的客户端(WebSocket对象)
connected_clients = set()
# HTTP请求处理器:返回聊天页面
class ChatIndexHandler(tornado.web.RequestHandler):
def get(self):
# 渲染聊天页面(后面会创建这个HTML文件)
self.render("chat.html")
# WebSocket处理器:处理实时消息
class ChatWebSocketHandler(tornado.websocket.WebSocketHandler):
def open(self):
"""新客户端连接时调用"""
print("新客户端连接")
connected_clients.add(self) # 把当前连接加入集合
def on_message(self, message):
"""收到客户端消息时调用"""
print(f"收到消息:{message}")
# 把消息广播给所有连接的客户端
for client in connected_clients:
client.write_message(message) # 发送消息给客户端
def on_close(self):
"""客户端断开连接时调用"""
print("客户端断开连接")
connected_clients.remove(self) # 从集合中移除
def check_origin(self, origin):
"""允许跨域请求(开发时方便测试,生产环境需限制)"""
return True
def make_app():
return tornado.web.Application([
(r"/", ChatIndexHandler), # 聊天页面
(r"/ws", ChatWebSocketHandler), # WebSocket连接地址
], autoreload=True, template_path="templates") # 指定模板文件夹
if __name__ == "__main__":
parse_command_line()
app = make_app()
app.listen(8888)
print("聊天服务器启动,访问 http://127.0.0.1:8888")
tornado.ioloop.IOLoop.current().start()
代码说明:
connected_clients:用集合存储所有在线的 WebSocket 连接,方便广播消息;ChatIndexHandler:处理 HTTP 请求,返回聊天页面;ChatWebSocketHandler:继承自WebSocketHandler,重写三个核心方法:open:客户端连接时触发(比如记录连接);on_message:收到消息时触发(这里实现广播,发给所有在线用户);on_close:客户端断开时触发(移除连接);
check_origin:开发时允许跨域,否则本地测试可能连不上。
步骤 2:创建聊天页面模板
在项目根目录下创建templates文件夹,然后新建chat.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Tornado实时聊天</title>
<style>
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
#messages {
border: 1px solid #ccc;
height: 400px;
overflow-y: auto;
margin-bottom: 10px;
padding: 10px;
}
.message {
margin: 5px 0;
padding: 8px;
background: #f0f0f0;
border-radius: 4px;
}
#messageInput {
width: 70%;
padding: 8px;
}
#sendBtn {
width: 25%;
padding: 8px;
background: #4CAF50;
color: white;
border: none;
cursor: pointer;
}
</style>
</head>
<body>
<div class="container">
<h1>实时聊天</h1>
<div id="messages"></div>
<input type="text" id="messageInput" placeholder="输入消息...">
<button id="sendBtn">发送</button>
</div>
<script>
// 连接WebSocket服务器(注意协议是ws,不是http)
const ws = new WebSocket('ws://' + window.location.host + '/ws');
// 收到服务器消息时显示
ws.onmessage = function(event) {
const messagesDiv = document.getElementById('messages');
const messageDiv = document.createElement('div');
messageDiv.className = 'message';
messageDiv.textContent = event.data;
messagesDiv.appendChild(messageDiv);
// 滚动到最新消息
messagesDiv.scrollTop = messagesDiv.scrollHeight;
};
// 发送消息
document.getElementById('sendBtn').addEventListener('click', function() {
const input = document.getElementById('messageInput');
const message = input.value.trim();
if (message) {
ws.send(message); // 发送到服务器
input.value = ''; // 清空输入框
}
});
// 按回车发送消息
document.getElementById('messageInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
document.getElementById('sendBtn').click();
}
});
</script>
</body>
</html>
页面逻辑:
- 用 JavaScript 的
WebSocket对象连接服务器的/ws地址; - 输入消息后,点击按钮或按回车发送到服务器;
- 收到服务器广播的消息时,显示在消息列表中。
步骤 3:测试基础聊天功能
启动服务器python app.py,打开两个浏览器窗口(或隐私模式)访问http://127.0.0.1:8888,在一个窗口发送消息,另一个窗口会实时收到 —— 基础的群聊功能就实现了!
阶段 2:添加用户认证(昵称登录)
现在所有消息都是匿名的,我们给应用加个简单的登录页面,让用户输入昵称后才能进入聊天。
步骤 1:修改处理器,添加登录逻辑
更新app.py,新增登录相关的 Handler 和用户信息存储:
import tornado.ioloop
import tornado.web
import tornado.websocket
from tornado.options import define, options, parse_command_line
# 存储客户端连接和对应的用户昵称:{websocket对象: 昵称}
clients = {} # 替代之前的connected_clients
# 登录页面处理器
class LoginHandler(tornado.web.RequestHandler):
def get(self):
self.render("login.html") # 显示登录页
def post(self):
# 获取表单提交的昵称
nickname = self.get_argument("nickname", "").strip()
if nickname and len(nickname) <= 10: # 简单验证:不为空且长度<=10
# 记录用户昵称到cookie(有效期1天)
self.set_cookie("nickname", nickname, expires_days=1)
self.redirect("/chat") # 跳转到聊天页
else:
self.render("login.html", error="昵称不能为空且长度不超过10字")
# 聊天页面处理器(需要登录才能访问)
class ChatIndexHandler(tornado.web.RequestHandler):
def get(self):
nickname = self.get_cookie("nickname")
if not nickname:
self.redirect("/") # 没登录就跳转到登录页
self.render("chat.html", nickname=nickname)
# WebSocket处理器(关联用户昵称)
class ChatWebSocketHandler(tornado.websocket.WebSocketHandler):
def open(self):
# 从cookie获取用户昵称
nickname = self.get_cookie("nickname")
if not nickname:
self.close(code=1008, reason="未登录") # 没登录就关闭连接
return
# 存储连接和昵称
clients[self] = nickname
# 广播“xxx加入聊天”
self.broadcast(f"系统消息:{nickname}加入了聊天")
def on_message(self, message):
nickname = clients.get(self)
if nickname and message:
# 发送带昵称的消息
self.broadcast(f"{nickname}: {message}")
def on_close(self):
if self in clients:
nickname = clients[self]
del clients[self] # 移除连接
self.broadcast(f"系统消息:{nickname}离开了聊天")
def broadcast(self, message):
"""广播消息给所有在线用户"""
for client in clients:
try:
client.write_message(message)
except:
# 处理可能的连接错误
print("发送消息失败,可能客户端已断开")
def check_origin(self, origin):
return True
def make_app():
return tornado.web.Application([
(r"/", LoginHandler), # 登录页
(r"/chat", ChatIndexHandler), # 聊天页
(r"/ws", ChatWebSocketHandler), # WebSocket连接
], autoreload=True, template_path="templates")
if __name__ == "__main__":
parse_command_line()
app = make_app()
app.listen(8888)
print("聊天服务器启动,访问 http://127.0.0.1:8888")
tornado.ioloop.IOLoop.current().start()
核心变化:
- 用
clients字典关联 WebSocket 连接和用户昵称,替代之前的集合; - 新增
LoginHandler:处理登录请求,验证昵称并写入 cookie; ChatIndexHandler:检查 cookie,未登录则跳转到登录页;- WebSocket 处理器中,消息会带上用户昵称,用户加入 / 离开时广播系统消息。
步骤 2:创建登录页面模板
在templates文件夹下新建login.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录 - Tornado聊天</title>
<style>
.login-box {
max-width: 300px;
margin: 100px auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 5px;
}
.error {
color: red;
margin-bottom: 10px;
}
input[type="text"] {
width: 100%;
padding: 8px;
margin: 10px 0;
box-sizing: border-box;
}
button {
width: 100%;
padding: 8px;
background: #4CAF50;
color: white;
border: none;
cursor: pointer;
}
</style>
</head>
<body>
<div class="login-box">
<h2>请输入昵称登录</h2>
{% if error %}
<div class="error">{{ error }}</div>
{% endif %}
<form method="post">
<input type="text" name="nickname" placeholder="你的昵称" required>
<button type="submit">进入聊天</button>
</form>
</div>
</body>
</html>
步骤 3:修改聊天页面,显示用户信息
更新chat.html,添加在线用户列表(简化版):
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Tornado实时聊天</title>
<style>
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
display: flex;
gap: 20px;
}
.chat-area {
flex: 3;
}
.user-list {
flex: 1;
border: 1px solid #ccc;
padding: 10px;
}
#messages {
border: 1px solid #ccc;
height: 400px;
overflow-y: auto;
margin-bottom: 10px;
padding: 10px;
}
.message {
margin: 5px 0;
padding: 8px;
border-radius: 4px;
}
.system {
background: #e3f2fd;
color: #0d47a1;
}
.user {
background: #f0f0f0;
}
#messageInput {
width: 70%;
padding: 8px;
}
#sendBtn {
width: 25%;
padding: 8px;
background: #4CAF50;
color: white;
border: none;
cursor: pointer;
}
</style>
</head>
<body>
<div class="container">
<div class="chat-area">
<h1>实时聊天</h1>
<div id="messages"></div>
<input type="text" id="messageInput" placeholder="输入消息...">
<button id="sendBtn">发送</button>
</div>
<div class="user-list">
<h3>在线用户</h3>
<ul id="users"></ul>
</div>
</div>
<script>
const nickname = "{{ nickname }}"; // 从模板获取当前用户昵称
const ws = new WebSocket('ws://' + window.location.host + '/ws');
// 收到消息时处理
ws.onmessage = function(event) {
const message = event.data;
const messagesDiv = document.getElementById('messages');
const messageDiv = document.createElement('div');
// 区分系统消息和用户消息(加不同样式)
if (message.startsWith("系统消息:")) {
messageDiv.className = 'message system';
// 系统消息可能是用户加入/离开,需要刷新在线列表
updateUserList();
} else {
messageDiv.className = 'message user';
}
messageDiv.textContent = message;
messagesDiv.appendChild(messageDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
};
// 发送消息
document.getElementById('sendBtn').addEventListener('click', sendMessage);
document.getElementById('messageInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') sendMessage();
});
function sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value.trim();
if (message) {
ws.send(message);
input.value = '';
}
}
// 简单模拟更新在线用户列表(实际项目需要服务器推送用户列表)
function updateUserList() {
// 这里只是示例,真实场景中应该由服务器发送在线用户数据
const usersList = document.getElementById('users');
usersList.innerHTML = ''; // 清空现有列表
// 临时逻辑:假设当前用户一定在线(实际需从服务器获取)
const li = document.createElement('li');
li.textContent = nickname;
usersList.appendChild(li);
}
</script>
</body>
</html>
页面变化:
- 分左右两栏:左侧聊天区,右侧在线用户列表;
- 系统消息和用户消息用不同样式区分;
- 用户加入 / 离开时触发
updateUserList(简化版,实际需服务器推送完整列表)。
步骤 4:测试登录和聊天功能
启动服务器后,访问http://127.0.0.1:8888,会先显示登录页,输入昵称后进入聊天。用两个不同的昵称登录(比如 “张三” 和 “李四”),发送消息会显示昵称,加入 / 离开时会有系统提示,基本的带用户身份的群聊就实现了。
阶段 3:优化体验(在线用户实时更新 + 消息时间戳)
现在的用户列表只是模拟的,我们让服务器实时推送在线用户列表,并给每条消息加上时间戳,让体验更完善。
步骤 1:修改 WebSocket 处理器,推送用户列表
更新app.py的ChatWebSocketHandler:
import time # 新增:用于时间戳
class ChatWebSocketHandler(tornado.websocket.WebSocketHandler):
def open(self):
nickname = self.get_cookie("nickname")
if not nickname:
self.close(code=1008, reason="未登录")
return
clients[self] = nickname
# 广播加入消息
self.broadcast({
"type": "system",
"message": f"{nickname}加入了聊天",
"time": self.get_timestamp()
})
# 推送最新在线用户列表
self.send_user_list()
def on_message(self, message):
nickname = clients.get(self)
if nickname and message:
# 发送带类型、时间戳的用户消息
self.broadcast({
"type": "user",
"nickname": nickname,
"message": message,
"time": self.get_timestamp()
})
def on_close(self):
if self in clients:
nickname = clients[self]
del clients[self]
self.broadcast({
"type": "system",
"message": f"{nickname}离开了聊天",
"time": self.get_timestamp()
})
self.send_user_list() # 离开后更新用户列表
def broadcast(self, data):
"""广播JSON格式的数据(支持更多信息)"""
import json # 用JSON序列化数据
message = json.dumps(data)
for client in clients:
try:
client.write_message(message)
except:
print("发送消息失败")
def send_user_list(self):
"""推送在线用户列表给所有客户端"""
import json
user_list = list(clients.values()) # 提取所有昵称
data = {
"type": "user_list",
"users": user_list
}
message = json.dumps(data)
for client in clients:
try:
client.write_message(message)
except:
print("发送用户列表失败")
def get_timestamp(self):
"""获取格式化的时间戳(如:2024-05-20 15:30:22)"""
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
# check_origin不变
def check_origin(self, origin):
return True
核心优化:
- 消息用 JSON 格式发送,包含
type(系统消息 / 用户消息 / 用户列表)、time(时间戳)等字段; - 新增
send_user_list方法:当有用户加入 / 离开时,向所有客户端推送最新在线用户列表; get_timestamp方法:生成人类可读的时间戳,附在每条消息上。
步骤 2:更新聊天页面,处理 JSON 消息
修改chat.html的 JavaScript 部分,解析 JSON 格式的消息:
<!-- 省略样式部分,保持不变 -->
<script>
const nickname = "{{ nickname }}";
const ws = new WebSocket('ws://' + window.location.host + '/ws');
ws.onmessage = function(event) {
// 解析JSON格式的消息
const data = JSON.parse(event.data);
switch(data.type) {
case "system":
case "user":
// 显示系统消息或用户消息
showMessage(data);
break;
case "user_list":
// 更新在线用户列表
updateUserList(data.users);
break;
}
};
// 显示消息
function showMessage(data) {
const messagesDiv = document.getElementById('messages');
const messageDiv = document.createElement('div');
messageDiv.className = `message ${data.type}`;
// 格式化消息:[时间] 内容(用户消息显示昵称)
let content = `[${data.time}] `;
if (data.type === "user") {
content += `${data.nickname}: ${data.message}`;
} else {
content += data.message;
}
messageDiv.textContent = content;
messagesDiv.appendChild(messageDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
// 更新在线用户列表
function updateUserList(users) {
const usersList = document.getElementById('users');
usersList.innerHTML = '';
users.forEach(user => {
const li = document.createElement('li');
// 当前用户的昵称标红
if (user === nickname) {
li.style.color = 'red';
li.textContent = `${user}(你)`;
} else {
li.textContent = user;
}
usersList.appendChild(li);
});
}
// 发送消息函数不变
function sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value.trim();
if (message) {
ws.send(message);
input.value = '';
}
}
document.getElementById('sendBtn').addEventListener('click', sendMessage);
document.getElementById('messageInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') sendMessage();
});
</script>
现在测试:
- 新用户加入,所有人能看到带时间戳的系统消息,在线列表实时更新;
- 发送消息会显示 “[时间] 昵称:内容”;
- 自己的昵称在列表中会标红显示 “(你)”,体验更友好。
六、Tornado 异步进阶:用协程处理耗时操作

Tornado 的核心优势是异步处理,如果你的应用中有耗时操作(比如数据库查询、网络请求),一定要用协程(async/await)避免阻塞 IOLoop。
示例:用协程处理模拟的耗时任务
在app.py中新增一个演示异步处理的 Handler:
import asyncio # 用于模拟异步耗时操作
class AsyncDemoHandler(tornado.web.RequestHandler):
async def get(self):
"""异步处理示例:同时执行两个耗时任务"""
self.write("开始处理任务...<br>")
# 用asyncio.gather同时运行两个异步任务(并行执行)
result1, result2 = await asyncio.gather(
self.async_task(1, 2), # 任务1:耗时2秒
self.async_task(2, 3) # 任务2:耗时3秒
)
self.write(f"任务1结果:{result1}<br>")
self.write(f"任务2结果:{result2}<br>")
self.write("所有任务处理完成!")
async def async_task(self, task_id, seconds):
"""模拟异步耗时任务:等待指定秒数后返回结果"""
await asyncio.sleep(seconds) # 异步等待(不阻塞IOLoop)
return f"任务{task_id}完成(耗时{seconds}秒)"
# 在make_app的路由中添加
def make_app():
return tornado.web.Application([
(r"/", LoginHandler),
(r"/chat", ChatIndexHandler),
(r"/ws", ChatWebSocketHandler),
(r"/async-demo", AsyncDemoHandler), # 新增异步演示路由
], autoreload=True, template_path="templates")
访问http://127.0.0.1:8888/async-demo,会看到:
- 页面先显示 “开始处理任务...”;
- 等待 3 秒(而不是 2+3=5 秒)后,同时显示两个任务的结果 —— 因为两个任务是并行执行的。
关键区别:如果用同步代码(time.sleep),两个任务会串行执行(总耗时 5 秒),期间 IOLoop 被阻塞,其他请求无法处理;而用asyncio.sleep(异步等待),IOLoop 在等待时可以处理其他请求,效率极大提升。
七、部署与优化:让你的聊天应用抗住高并发
开发完成后,需要考虑部署和优化,让应用能在生产环境稳定运行。
部署注意事项:
- 关闭自动重载:生产环境中
autoreload=True会消耗额外资源,必须关闭; - 设置适当的进程数:Tornado 是单线程的,可以启动多个进程(通常等于 CPU 核心数)充分利用多核:
# 生产环境启动方式(替换main函数)
if __name__ == "__main__":
import tornado.httpserver
app = make_app()
server = tornado.httpserver.HTTPServer(app)
server.bind(8888) # 绑定端口
server.start(4) # 启动4个进程(根据CPU核心数调整)
print("生产环境服务器启动,端口8888")
tornado.ioloop.IOLoop.current().start()
- 使用 Nginx 作为反向代理:处理静态文件、负载均衡、SSL 终止(HTTPS),配置示例:
server {
listen 80;
server_name yourdomain.com; # 你的域名
location / {
proxy_pass http://127.0.0.1:8888;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; # 支持WebSocket
proxy_set_header Host $host;
}
# 静态文件由Nginx直接处理(如果有)
location /static/ {
alias /path/to/your/static/files/;
}
}
高并发优化技巧:
- 限制单 IP 连接数:防止恶意攻击占用过多连接:
class ChatWebSocketHandler(tornado.websocket.WebSocketHandler):
def open(self):
client_ip = self.request.remote_ip
# 简单限制:同一IP最多10个连接
ip_connections = [c for c in clients if c.request.remote_ip == client_ip]
if len(ip_connections) > 10:
self.close(code=1008, reason="连接数过多")
return
# 其他逻辑...
- 消息队列缓冲:当并发消息量大时,用队列异步处理广播,避免阻塞:
from tornado.queues import Queue
message_queue = Queue()
async def message_worker():
"""异步消费消息队列,处理广播"""
while True:
message = await message_queue.get()
for client in clients:
try:
client.write_message(message)
except:
pass
message_queue.task_done()
# 在服务器启动时启动工作线程
if __name__ == "__main__":
# ... 其他代码
tornado.ioloop.IOLoop.current().spawn_callback(message_worker)
tornado.ioloop.IOLoop.current().start()
- 定期清理无效连接:对长时间无活动的连接进行清理,释放资源。
八、常见问题与避坑指南(新手必看)
-
WebSocket 连接失败,报 403 错误原因:跨域限制。解决方法:重写
check_origin方法,生产环境中指定允许的域名(不要直接返回True)。 -
异步函数没加
await,导致不执行比如调用async_task(1, 2)时忘记加await,函数会变成普通的协程对象,不会实际执行。记住:调用异步函数必须加await。 -
使用同步库(如
requests)导致阻塞Tornado 的异步是 “非阻塞” 的,但如果在协程中调用同步阻塞的库(如requests.get),会阻塞整个 IOLoop。解决方法:用异步库替代,比如用aiohttp替代requests。 -
开发时修改代码不生效原因:没开启
autoreload=True,或修改了IOLoop相关代码(自动重载对这类代码支持不好)。解决方法:手动重启服务器。 -
部署后 WebSocket 连接频繁断开可能是 Nginx 配置中缺少 WebSocket 支持,确保添加了
proxy_set_header Upgrade $http_upgrade;和proxy_set_header Connection "upgrade";。
九、学习资源推荐:从入门到精通 Tornado
想深入学习 Tornado,这些资源亲测有用:
- 官方文档:https://www.tornadoweb.org/ (最权威,示例简洁,必看);
- 《Tornado Web 框架实战》:李辉著,适合新手入门,案例实用;
- Tornado 源码:Tornado 的源码简洁易懂,尤其是
ioloop.py和websocket.py,读源码能加深对异步的理解; - 实战项目:GitHub 搜索 “tornado chat”,看别人的聊天应用实现,学习代码组织和优化技巧。
总结:Tornado 的适用场景与学习路径
回顾整个开发过程,我们从基础的 WebSocket 连接,到带用户认证、实时列表的聊天应用,再到异步协程的优化,一步步体验了 Tornado 处理实时场景的优势。
Tornado 不是 “万能框架”,它的强项在高并发实时应用,如果你的项目是简单的 CRUD(增删改查)网站,用 Django 或 Flask 可能更高效;但如果需要处理大量长连接、实时数据交互,Tornado 会是更好的选择。
对于新手,我的学习建议是:
- 先掌握基础用法(Handler、路由、模板),跑通一个简单的 HTTP 应用;
- 学习 WebSocket,实现一个简单的实时功能(如聊天、计数器);
- 理解
IOLoop和协程的工作原理,用async/await处理异步任务; - 最后学习部署和优化,理解生产环境的注意事项。
Tornado 的异步编程思维需要一点时间适应,但一旦掌握,你会发现处理高并发场景变得非常轻松。就像我们开发的聊天应用,即使同时有上千人在线,也能保持流畅的实时交互 —— 这就是异步非阻塞的魅力。
祝你在 Tornado 的学习路上,写出既高效又稳定的实时应用!
675

被折叠的 条评论
为什么被折叠?



