大家好,我是Java1234_小锋老师。

Tornado 6.5 在 2025 年 5 月正式发布,到现在已经演进到 6.5.x 补丁系列。对很多人来说,这条更新里最扎眼的一句,其实就四个字:支持 3.14。老版本在 Python 3.14 上大多还能跑,但会冒 deprecation warning;想干净地用上新运行时,6.5 才是官方认可的那一档。
这篇文章不打算把 changelog 从头抄到尾。我会先把 Tornado 是什么讲清楚,再挑出 6.5 里真正会影响日常开发的几处变化,最后给几段能直接跑的例子。
先认识 Tornado
Tornado 最早出自 FriendFeed,后来跟着团队一起进了 Facebook。它既是 Web 框架,也是一套异步网络库。和当时主流的同步 WSGI 框架不同,Tornado 从第一天就押在非阻塞 I/O 上:一个进程里用事件循环把成千上万条连接同时挂着,特别适合长轮询、WebSocket、以及任何「用户连上来之后还不想马上断开」的场景。

它不是「又一个 Django」。Django、Flask 这类框架更擅长业务脚手架;Tornado 的强项是把连接本身管好。你写的还是熟悉的 RequestHandler,但底层的 IOLoop 会把 socket 读写、定时器和回调都揉进同一条事件循环。现在这条循环已经建立在 asyncio 之上,所以你完全可以在 Handler 里直接 await。
最低支持的 Python 版本,6.5 起变成了 3.9。3.8 已经告别,该换了就换。
6.5 这一版改了什么
官方把 Python 3.14 写进了支持列表。老版本在 3.14 上「能跑」和「被承认能跑」不是一回事:前者可能伴随着一串警告,后者才是你上生产该选的组合。

除了运行时版本,我觉得值得记住的还有这几条:
1. Python 3.14 官方支持
这是 6.5.0 的头条。3.14 自己也带了模板字符串(t-strings)、更好的 asyncio 内省、以及更成熟的 free-threading。Tornado 先把兼容性做稳,后面这些语言能力你才能放心往服务里塞。
2. 实验性支持 free-threading
从 Python 3.13 开始可以关掉 GIL。Tornado 6.5 对这种构建做了实验性支持,不过暂时没有现成的 wheel,需要源码编译。适合拿来试,还不适合当成默认部署方式。
3. 修了一个真实的安全问题
畸形的 multipart/form-data 以前会反复打警告,日志都能被打成拒绝服务。现在第一次出错就抛异常,每个请求只记一条日志,对应 CVE-2025-47287。后续 6.5.x 补丁还继续收紧了表单参数数量、multipart 解析和 Cookie 校验,升级时尽量跟到当前补丁。
4. HTTP 头更「像 RFC」
请求行里不能再夹控制字符,非法方法名改回 400 而不是 405,Header 名字和 Host 的字符集都按 RFC 收紧。重复或缺失 Host(非 HTTP/1.0)会直接报错。这些改动对正常客户端无感,对乱写协议的扫描器和老旧代理会更不客气。
5. 类型标注补了一圈
get_argument / get_cookie、HTTPHeaders、UIModule.render 这些常用接口的 typing 更准了。用 mypy 的项目,升级后往往会少掉一批「明明能跑、类型却在抱怨」的噪声。
6. 几个小而实用的修复
Linux 抽象命名空间 Unix 套接字现在可以 bind_unix_socket;WebSocket 的 ping_interval / ping_timeout 逻辑修了,超时默认等于心跳间隔,也不再允许超时大于间隔;HTTPError 新增 get_message(),老的 log_message / args 属性标了废弃。
另外,已经废弃的 TwistedResolver 被直接删掉了------RFC 8482 之后它基本解析不了主流域名。需要非阻塞 DNS 可以看已经同样废弃的 caresresolver,大多数人用默认的线程解析器就行。
一次请求是怎么走完的
流程图比文字更清楚。下面这条路径,就是 6.5 里一个普通 HTTP 请求从进门到出门的样子:
#mermaid-svg-zqYsRimS5THpXUZY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zqYsRimS5THpXUZY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zqYsRimS5THpXUZY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zqYsRimS5THpXUZY .error-icon{fill:#552222;}#mermaid-svg-zqYsRimS5THpXUZY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zqYsRimS5THpXUZY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zqYsRimS5THpXUZY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zqYsRimS5THpXUZY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zqYsRimS5THpXUZY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zqYsRimS5THpXUZY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zqYsRimS5THpXUZY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zqYsRimS5THpXUZY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zqYsRimS5THpXUZY .marker.cross{stroke:#333333;}#mermaid-svg-zqYsRimS5THpXUZY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zqYsRimS5THpXUZY p{margin:0;}#mermaid-svg-zqYsRimS5THpXUZY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zqYsRimS5THpXUZY .cluster-label text{fill:#333;}#mermaid-svg-zqYsRimS5THpXUZY .cluster-label span{color:#333;}#mermaid-svg-zqYsRimS5THpXUZY .cluster-label span p{background-color:transparent;}#mermaid-svg-zqYsRimS5THpXUZY .label text,#mermaid-svg-zqYsRimS5THpXUZY span{fill:#333;color:#333;}#mermaid-svg-zqYsRimS5THpXUZY .node rect,#mermaid-svg-zqYsRimS5THpXUZY .node circle,#mermaid-svg-zqYsRimS5THpXUZY .node ellipse,#mermaid-svg-zqYsRimS5THpXUZY .node polygon,#mermaid-svg-zqYsRimS5THpXUZY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zqYsRimS5THpXUZY .rough-node .label text,#mermaid-svg-zqYsRimS5THpXUZY .node .label text,#mermaid-svg-zqYsRimS5THpXUZY .image-shape .label,#mermaid-svg-zqYsRimS5THpXUZY .icon-shape .label{text-anchor:middle;}#mermaid-svg-zqYsRimS5THpXUZY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zqYsRimS5THpXUZY .rough-node .label,#mermaid-svg-zqYsRimS5THpXUZY .node .label,#mermaid-svg-zqYsRimS5THpXUZY .image-shape .label,#mermaid-svg-zqYsRimS5THpXUZY .icon-shape .label{text-align:center;}#mermaid-svg-zqYsRimS5THpXUZY .node.clickable{cursor:pointer;}#mermaid-svg-zqYsRimS5THpXUZY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zqYsRimS5THpXUZY .arrowheadPath{fill:#333333;}#mermaid-svg-zqYsRimS5THpXUZY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zqYsRimS5THpXUZY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zqYsRimS5THpXUZY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zqYsRimS5THpXUZY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zqYsRimS5THpXUZY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zqYsRimS5THpXUZY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zqYsRimS5THpXUZY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zqYsRimS5THpXUZY .cluster text{fill:#333;}#mermaid-svg-zqYsRimS5THpXUZY .cluster span{color:#333;}#mermaid-svg-zqYsRimS5THpXUZY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zqYsRimS5THpXUZY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zqYsRimS5THpXUZY rect.text{fill:none;stroke-width:0;}#mermaid-svg-zqYsRimS5THpXUZY .icon-shape,#mermaid-svg-zqYsRimS5THpXUZY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zqYsRimS5THpXUZY .icon-shape p,#mermaid-svg-zqYsRimS5THpXUZY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zqYsRimS5THpXUZY .icon-shape .label rect,#mermaid-svg-zqYsRimS5THpXUZY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zqYsRimS5THpXUZY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zqYsRimS5THpXUZY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zqYsRimS5THpXUZY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 非法方法 / 坏 Host / 控制字符
合法
I/O 等待
立刻返回
客户端请求
HTTPServer 读入
协议是否合法
直接 400
路由匹配 RequestHandler
prepare / HTTP 方法
业务是否 await
IOLoop 切走
去处理别的连接
写出响应
连接复用或关闭
长连接和 WebSocket 也走同一套 IOLoop,只是在「写出响应」这一步选择不断开。这就是 Tornado 能把连接数堆上去的原因:线程没有被 recv 卡住,事件循环一直在转。

上手写几段代码
先装当前 6.5 系列:
bash
pip install "tornado>=6.5,<7"
建议在 Python 3.14 的干净虚拟环境里装,避免和系统包搅在一起。
一个最小的异步服务
python
import asyncio
import tornado
class HelloHandler(tornado.web.RequestHandler):
"""首页:演示原生 async Handler。"""
async def get(self):
# 假装去查库或调下游,IOLoop 不会被堵住
await asyncio.sleep(0.05)
self.write({
"message": "hello from Tornado 6.5",
"python": "3.14",
})
def make_app():
return tornado.web.Application(
[(r"/", HelloHandler)],
debug=True,
)
async def main():
app = make_app()
app.listen(8888)
print("listening on http://127.0.0.1:8888")
await asyncio.Event().wait()
if __name__ == "__main__":
asyncio.run(main())
跑起来之后访问 /,你会拿到一段 JSON。真正有价值的是 async def get:睡眠期间事件循环可以去伺候别的连接,这是 Tornado 一直以来的写法,6.5 把它在 3.14 上跑顺了。
WebSocket:心跳参数别再写反
6.5 修过 ping 相关的几个坑,也明确了:ping_timeout 不能大于 ping_interval,默认两者相等。 下面这个聊天室骨架可以直接当起点。
python
import asyncio
import tornado
from tornado.websocket import WebSocketHandler
clients = set()
class ChatHandler(WebSocketHandler):
"""简单广播聊天,依赖服务端心跳发现死连接。"""
def open(self):
clients.add(self)
self.write_message("welcome")
def on_message(self, message):
for client in list(clients):
if client is not self:
client.write_message(message)
def on_close(self):
clients.discard(self)
def make_app():
return tornado.web.Application(
[(r"/chat", ChatHandler)],
websocket_ping_interval=30,
websocket_ping_timeout=30, # 不要大于 interval
)
async def main():
make_app().listen(8888)
await asyncio.Event().wait()
if __name__ == "__main__":
asyncio.run(main())
客户端那边,websocket_connect 的 callback 参数已经废弃,7.0 会删。新代码请用协程,或者继续用 on_message_callback。
python
from tornado.websocket import websocket_connect
async def watch():
conn = await websocket_connect("ws://127.0.0.1:8888/chat")
await conn.write_message("ping from client")
print(await conn.read_message())
抛 HTTPError 时,用 get_message()
以前有人直接读 HTTPError.log_message 和 args。6.5 把这两处标了废弃,并补上了拼好的完整文案:
python
from tornado.web import HTTPError, RequestHandler
class OrderHandler(RequestHandler):
"""演示 6.5 新增的 get_message。"""
async def get(self, order_id):
if not order_id.isdigit():
err = HTTPError(400, "非法订单号: %s", order_id)
# 拿到已经完成占位符替换的消息
self.set_status(400)
self.write({"error": err.get_message()})
return
self.write({"order_id": order_id})
顺手一提:日志里的 % 不再会在没有额外参数时被写成 %%,和标准库 logging.LogRecord 对齐了。
Linux 抽象命名空间套接字
不需要落盘路径、也不用担心残留 sock 文件时,可以用 Linux 的抽象命名空间。6.5 给 bind_unix_socket 补上了这个能力:
python
import asyncio
import tornado
from tornado.netutil import bind_unix_socket
from tornado.httpserver import HTTPServer
class PingHandler(tornado.web.RequestHandler):
async def get(self):
self.write("pong")
async def main():
app = tornado.web.Application([(r"/ping", PingHandler)])
server = HTTPServer(app)
# 名字以 \0 开头,表示抽象命名空间
socket = bind_unix_socket("\0tornado65-demo")
server.add_socket(socket)
await asyncio.Event().wait()
if __name__ == "__main__":
asyncio.run(main())
这个接口只在 Linux 上有意义,macOS / Windows 请继续用 TCP 或普通 Unix 路径。
升级时别踩这些坑
- Python 至少 3.9,推荐直接上 3.14。3.8 已经不在支持范围里。
- 别再用
obs-fold折行头 ,也不要用单独的\r当头部分隔符,这两样 7.0 会拿掉。 websocket_connect(..., callback=...)尽快改掉。HTTPError.log_message/args换成get_message()。- 代码里如果还引用
TwistedResolver,升级会直接 ImportError。 - WebSocket 心跳:
ping_timeout≤ping_interval。 - 生产环境请跟到 6.5 的最新补丁,不要停在 6.5.0。安全类修复是按小版本往上叠的。
如果你维护的是网关、文件上传或者会接收乱七八糟客户端的服务,HTTP 头和 multipart 变严之后,偶尔会多看到 400。这通常不是回归,是以前被默默容忍的坏请求终于被拦下来了。
写在最后
Tornado 6.5 不是一次「把 API 翻新一遍」的大改。它更像一次把地基浇实:Python 3.14 能名正言顺地跑,类型更准,协议更干净,安全边界更清楚。对已经在用 Tornado 的项目,这是一次该尽快跟进的小版本;对还在 3.10 / 3.11 上观望的人,它也给出了一个明确信号------这条框架还在跟语言一起往前走。
想试的话,新建一个 3.14 虚拟环境,pip install "tornado>=6.5,<7",把上面的 Hello 例子跑通,基本就能判断自己的项目能不能迁。真正花时间的,往往不是 API,而是那些曾经依赖「宽松 HTTP 解析」的边角客户端。