Tornado 6.5,全面支持 Python 3.14

大家好,我是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_cookieHTTPHeadersUIModule.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_connectcallback 参数已经废弃,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_messageargs。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_timeoutping_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 解析」的边角客户端。

相关推荐
jyOverQ1 小时前
MySQL 事务到底是什么?ACID 四大特性与隔离级别怎么理解?
数据库·mysql
IT毕设梦工厂1 小时前
计算机毕业设计选题推荐:基于大数据的印度上市公司财务指标数据可视化分析|毕业设计选题|计算机毕设|选题推荐|毕设指导|项目定制|源码|高质量项目
大数据·hadoop·python·信息可视化·spark·课程设计·大数据毕设项目
SEO_juper1 小时前
你的服务器正在被 AI 爬虫“白嫖“带宽:2026 用日志把 Googlebot 和 AI 洪流分开算账(附脚本)
运维·人工智能·爬虫·python·chatgpt·seo
浩风祭月1 小时前
GPT Image 2.5 Sunburst还是Flare?模型选型、Python接入与成本避坑
python·aigc·openai·图像生成·gpt image 2.5
SelectDB1 小时前
Doris 直查 Paimon 索引:快手湖上向量检索的共建与落地
大数据·数据库·数据分析
风123456789~1 小时前
【Oracle专栏】全局 && 本地复合索引 实验
数据库·oracle
1314lay_10071 小时前
通过Sql Server创建EXCEL文件:master..xp_cmdshell
数据库·excel
泡干脆面就番茄2 小时前
03_LBPH人脸识别算法原理与实战
python·opencv
Yanjun2i2 小时前
Agent学习记录四:多工具时如何处理
人工智能·python·学习·agent