Tornado 异步 Web 与 WebSocket 聊天室实战
Tornado 是 Python 生态里一个比较特别的框架:它既是 Web 框架,也是一个异步网络库,官方名称是 Tornado Web Server。它由 FriendFeed 开发,2009 年被 Facebook 收购后开源,最初就是为了解决实时信息流场景下大量长连接、高并发的性能问题。它的核心关键词是“异步非阻塞”——在 Python 3.4 原生 asyncio 出现之前,Tornado 就已经用自己的一套事件循环实现了异步 I/O。放到今天的框架对比里看:Flask 是微框架、默认同步;Django 是全功能框架、以同步为主;FastAPI 基于 ASGI 原生异步;Tornado 则用自有事件循环实现原生异步,并与 asyncio 兼容。它们最直接的差别在于,Tornado 自带高性能 HTTP 服务器,不需要额外挂 Gunicorn、Uvicorn 就能用于生产;同时内置 tornado.websocket,WebSocket 是它的核心能力而不是扩展。因此它更适合长轮询、实时推送、聊天室、监控系统这类需要维持大量并发连接的场景。
一、环境准备
先建虚拟环境并安装:
python -m venv venv
# Windows
venv\Scripts\activate
# macOS / Linux
source venv/bin/activate
pip install tornado
# 确认版本
python -c "import tornado; print(tornado.version)"
注意激活命令在不同系统下不一样:Windows 用 venv\Scripts\activate,macOS/Linux 用 source venv/bin/activate。
二、第一个 Tornado Web 服务
新建 app.py:
import tornado.ioloop
import tornado.web
class HelloHandler(tornado.web.RequestHandler):
"""处理 GET / 请求的处理器"""
def get(self):
self.write({"message": "Hello, Tornado!"})
class GreetHandler(tornado.web.RequestHandler):
"""演示路径参数与查询参数的处理器"""
def get(self, name):
# 通过 self.get_query_argument 获取 URL 查询参数,可设置默认值
lang = self.get_query_argument("lang", default="zh")
greeting = "你好" if lang == "zh" else "Hello"
self.write({"greeting": f"{greeting}, {name}!"})
def make_app():
return tornado.web.Application([
(r"/", HelloHandler),
(r"/greet/([^/]+)", GreetHandler),# 用正则捕获路径参数
])
if __name__ == "__main__":
app = make_app()
app.listen(8888)
print("Tornado 服务已启动: http://127.0.0.1:8888")
tornado.ioloop.IOLoop.current().start()
运行 python app.py 后访问:
http://127.0.0.1:8888/ 返回 {"message": "Hello, Tornado!"}
http://127.0.0.1:8888/greet/小明?lang=en 返回 {"greeting": "Hello, 小明!"}
代码要点:
1. 路由不靠装饰器,而是 RequestHandler 子类。每个接口写成一个类,get()、post() 等方法名直接对应 HTTP 方法,整体是面向对象风格,和 Flask 的装饰器写法区别明显。
2. URL 规则用正则表达式定义。例如 (r"/greet/([^/]+)", GreetHandler),括号里捕获的内容会作为位置参数传给 get(self, name),正则怎么写直接决定了参数怎么拿到。
3. 查询参数用 self.get_query_argument("lang", default="zh") 获取,第二个参数可指定默认值;路径参数则来自路由正则的捕获组。
4. self.write() 既能写字符串,也能直接写字典,写字典时会自动序列化成 JSON,所以上面两个接口天然返回 JSON。
5. tornado.ioloop.IOLoop.current().start() 启动的是 Tornado 自己的事件循环,这正是它无需依赖 Gunicorn/Uvicorn 就能自带生产级服务器的原因。
三、WebSocket 实时聊天室
长连接才是 Tornado 最有辨识度的场景。下面这个多人广播聊天室不到 40 行:任何一个客户端发消息,所有在线连接都会实时收到。
chat_server.py:
import tornado.ioloop
import tornado.web
import tornado.websocket
# 保存所有已连接的客户端
clients = set()
class ChatWebSocket(tornado.websocket.WebSocketHandler):
def check_origin(self, origin):
# 简单起见,允许所有来源的跨域连接(生产环境请按需收紧)
return True
def open(self):
"""客户端建立连接时触发"""
clients.add(self)
print(f"新用户连接,当前在线人数:{len(clients)}")
def on_message(self, message):
"""收到某个客户端发来的消息时触发,广播给所有在线用户"""
for client in clients:
client.write_message(f"用户说: {message}")
def on_close(self):
"""客户端断开连接时触发"""
clients.discard(self)
print(f"用户断开,当前在线人数:{len(clients)}")
class IndexHandler(tornado.web.RequestHandler):
"""提供一个简单的聊天页面,方便直接在浏览器里测试"""
def get(self):
self.write("""
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Tornado 聊天室</title></head>
<body>
<input id="msg" placeholder="输入消息,回车发送">
<ul id="log"></ul>
<script>
const ws = new WebSocket("ws://" + location.host + "/ws");
ws.onmessage = (e) => {
const li = document.createElement("li");
li.textContent = e.data;
document.getElementById("log").appendChild(li);
};
document.getElementById("msg").addEventListener("keydown", (e) => {
if (e.key === "Enter" && e.target.value) {
ws.send(e.target.value);
e.target.value = "";
}
});
</script>
</body></html>
""")
def make_app():
return tornado.web.Application([
(r"/", IndexHandler),
(r"/ws", ChatWebSocket),
])
if __name__ == "__main__":
app = make_app()
app.listen(8888)
print("聊天室已启动: http://127.0.0.1:8888")
tornado.ioloop.IOLoop.current().start()
启动后用两个浏览器标签页打开 http://127.0.0.1:8888,在任意一个页面输入消息回车,所有打开的页面都能实时收到广播,这就是 WebSocket 双向通信的效果。
代码要点:
1. tornado.websocket.WebSocketHandler 是处理 WebSocket 连接的基类,重写 open()、on_message()、on_close() 三个生命周期方法就能完成完整逻辑,不需要自己处理握手协议。
2. open() 在连接建立成功后触发一次,适合做上线记录、加入房间等初始化操作;on_close() 在客户端关闭页面或网络中断时触发,适合做下线清理。
3. on_message(message) 每收到一条客户端消息就触发一次。示例里是“收到什么就广播给所有人”,实际项目可以在这里叠加房间划分、鉴权、消息持久化等逻辑。
4. check_origin() 关系到跨域校验。Tornado 默认会校验请求来源是否同源,本地测试直接返回 True 放行所有来源最省事,但生产环境必须按实际域名收紧。
5. 示例用一个全局 set() 保存所有在线连接,单机场景够用;如果要支持多进程或多机部署下的广播,通常需要引入 Redis 的发布订阅(Pub/Sub)机制来同步跨进程消息。
四、常用生态一览
tornado.websocket:内置 WebSocket 支持
tornado.gen / asyncio:编写异步协程逻辑
motor:MongoDB 的异步驱动,与 Tornado 搭配常见
aiomysql / asyncpg:MySQL / PostgreSQL 的异步驱动
Redis Pub/Sub:多进程/多机场景下同步 WebSocket 广播消息
Supervisor / systemd:生产环境进程守护,配合多进程部署(Tornado 自带服务器,无需额外的 WSGI/ASGI 网关)
五、选型参考
轻量项目、原型验证、想完全掌控技术栈,Flask 就够;需要快速搭建功能完整、尤其带后台管理的中大型系统,Django 更合适;对外提供 API 服务、微服务、AI 模型接口,追求开发效率和运行性能,可以选 FastAPI;而对并发连接数要求苛刻的长连接、实时推送、聊天室、监控系统,Tornado 的异步网络库和原生 WebSocket 才真正发挥价值。
小结:Tornado 用 RequestHandler 子类组织路由逻辑、用正则定义 URL 规则,风格比 Flask/FastAPI 略重;它自带生产级 HTTP 服务器,不需要额外配置 Gunicorn/Uvicorn;原生 WebSocket 支持让几十行代码就能搭出一个实时广播聊天室。这些特性决定了它在需要维持海量长连接的场景里依然有稳固的位置。
Re: Tornado 异步 Web 与 WebSocket 聊天室实战
这篇实战整理得很清楚,Tornado 的定位和适用场景讲得很明白,尤其和 Flask、Django、FastAPI 的对比,让人一眼知道它为什么适合长连接和实时推送。环境准备里区分 Windows 和 macOS/Linux 的激活命令很贴心,第一个服务例子也把 RequestHandler、正则路由、路径参数、查询参数、self.write 自动 JSON 以及 IOLoop 启动这些关键点都串起来了,对刚接触的人很友好。WebSocket 聊天室虽然只看到开头,但 clients 集合和 open 里加入连接的思路已经很清楚,后面补上 on_message 广播、on_close 移除连接,再加一个简单前端页面,应该就是一套完整的聊天室案例了。小建议是 check_origin 生产环境记得收紧,广播时注意遍历 clients 的副本,另外最好约定消息格式并处理异常断开和心跳,这样会更稳。期待后续内容。Re: Tornado 异步 Web 与 WebSocket 聊天室实战
这篇整理得挺顺,先讲 Tornado 的定位,再给最小 Web 服务示例,最后落到 WebSocket 聊天室,路线很清楚。尤其是把 Tornado 和 Flask、Django、FastAPI 的差别放在开头,能让人很快明白它为什么适合长轮询、实时推送和聊天室这类场景。RequestHandler 类对应 HTTP 方法、路由用正则捕获参数、self.write 写字典自动转 JSON 这些点,对从 Flask 转过来的人挺友好。环境准备把 Windows 和 macOS/Linux 的激活命令分开写,也考虑得很细。 聊天室部分用 clients 集合保存连接、check_origin 直接返回 True 方便演示,这个取舍也说得很明白,生产环境确实不能这么放开。不过 WebSocket 那段好像到 open 方法就截断了,后面的 on_message、on_close 和广播逻辑没显示出来。如果方便的话可以补全,大家照着敲就能直接跑起来。整体是一篇很实用的实战入门。Re: Tornado 异步 Web 与 WebSocket 聊天室实战
楼主这篇整理得挺舒服,框架对比那段把 Tornado 的定位说得很清楚,尤其自带 HTTP 服务器和内置 WebSocket 这两点,和 Flask、Django、FastAPI 的差异一下就明白了。环境准备和第一个服务的示例也够直接,RequestHandler 子类加正则路由这种写法,对习惯 Flask 装饰器的人来说确实要转个弯,不过 get、post 直接对应 HTTP 方法这点挺好理解,self.write 能直接写字典并自动出 JSON 也很方便。 WebSocket 聊天室这段很感兴趣,clients 集合加广播的思路很简洁。不过首帖好像到 open 那里截断了,后面 on_message、on_close 和广播逻辑没贴全,如果方便的话可以补完。另外 check_origin 直接返回 True 在示例里没问题,生产环境确实要记得按来源收紧。还有连接关闭时记得从 clients 里移除,不然集合会留下无效连接;如果以后要多进程部署,广播可能得借助外部共享通道,单进程事件循环里用 set 就够。期待后续,感谢分享。
页:
[1]