你是否需要一个关于Tornado实现长轮询的实战案例

访客 全栈框架 1

本文目录导读:

  1. 目录导读
  2. 为什么需要长轮询?——实时通信的技术选型思考
  3. Tornado框架的核心优势:异步非阻塞I/O如何赋能长轮询
  4. 实战案例:用Tornado实现一个消息推送系统(含完整代码)
  5. 常见陷阱与性能优化:避免80%开发者会犯的错误
  6. 问答环节:长轮询 vs WebSocket vs SSE,何时选Tornado?
  7. 结语:Tornado长轮询的意义不止于“轮询”

Tornado长轮询实战案例:从零构建实时Web应用的最佳方案

目录导读

  1. 为什么需要长轮询?——实时通信的技术选型思考
  2. Tornado框架的核心优势:异步非阻塞I/O如何赋能长轮询
  3. 实战案例:用Tornado实现一个消息推送系统(含完整代码)
  4. 常见陷阱与性能优化:避免80%开发者会犯的错误
  5. 问答环节:长轮询 vs WebSocket vs SSE,何时选Tornado?

为什么需要长轮询?——实时通信的技术选型思考

Q:短轮询与长轮询的本质区别是什么?
A:短轮询是客户端每隔固定时间(如1秒)向服务器发送请求,即使无数据更新也会返回空响应,长轮询则是客户端发起请求后,服务器保持连接打开,直到有新数据或超时才返回,Tornado的长轮询能减少无效请求量,降低服务端负载。

关键痛点:
在电商库存监控、在线协作编辑、实时通知等场景,传统HTTP短轮询会导致带宽浪费和服务器压力激增,一个拥有10万在线的聊天系统,若用短轮询(间隔2秒),每秒需处理5万次请求,而长轮询可降至实际数据变更次数(如每秒200次)。

Tornado为何适合长轮询?

  • 基于epoll(Linux)或kqueue(macOS)的事件循环,单线程可处理上万并发连接。
  • 内置Future对象和@gen.coroutine(Tornado 5.0前)或async/await(推荐),轻松管理长连接挂起。
  • 无多线程锁竞争问题,内存占用远低于Node.js或阻塞式框架(如Django的同步模式)。

Tornado框架的核心优势:异步非阻塞I/O如何赋能长轮询

Q:Tornado处理长轮询时,工作线程数通常为1,如何应对高并发?
A:Tornado基于单线程Reactor模式,所有I/O操作(包括DB查询、HTTP请求)都通过事件循环非阻塞调度,当线程等待数据库响应时,线程不会阻塞在time.sleep(),而是切换处理其他请求,长轮询的“等待”本质上是将Future对象挂起,事件循环继续处理其他连接,数据到达后再通过回调恢复coroutine。

技术原理图解:

# 核心代码逻辑示例
class LongPollHandler(tornado.web.RequestHandler):
    async def get(self):
        # 挂起当前协程,等待新消息
        message = await self.application.message_queue.get()
        self.write({"data": message})

message_queue.get()是一个Future对象,当队列无消息时,当前协程自动挂起,Tornado事件循环接管处理其他请求,当消息被推送进队列,Future被设置结果,协程恢复执行。

优势对比表:

特性 Tornado 传统Django/Flask Node.js
并发模型 单线程协程 多线程/多进程 单线程事件循环
内存占用(10万连接) ~200MB ~800MB+ ~300MB
长轮询原生支持 内置asyncio 需额外库(如Django Channels) 原生支持但回调嵌套严重
代码可维护性 高(async/await) 低(需修改底层WSGI) 中(Promise链)

实战案例:用Tornado实现一个消息推送系统(含完整代码)

需求场景: 用户A发布公告,所有长轮询客户端即时接收,需满足:

  • 10万并发持久连接
  • 消息延迟<500ms
  • 支持消息去重(防止重复消费)

完整代码实现(可选复制但建议理解):

# server.py
import tornado.ioloop
import tornado.web
import tornado.gen
from collections import deque
import uuid
class MessageQueue:
    def __init__(self):
        self.waiters = {}  # {waiter_id: Future}
        self.messages = deque(maxlen=1000)
    def wait_for_message(self):
        # 创建一个Future并注册
        future = tornado.concurrent.Future()
        waiter_id = str(uuid.uuid4())
        self.waiters[waiter_id] = future
        # 设置超时(防止僵尸连接)
        tornado.ioloop.IOLoop.current().add_timeout(
            time.time() + 30, self._remove_waiter, waiter_id
        )
        return future, waiter_id
    def _remove_waiter(self, waiter_id):
        self.waiters.pop(waiter_id, None)
    def push_message(self, message):
        self.messages.append(message)
        # 所有等待者收到新消息
        for waiter_id, future in self.waiters.items():
            if not future.done():
                future.set_result(message)
        self.waiters.clear()  # 所有连接一次性推送
class LongPollHandler(tornado.web.RequestHandler):
    async def get(self):
        # 步骤1:获取Future对象
        future, waiter_id = self.application.mq.wait_for_message()
        # 步骤2:挂起连接
        try:
            message = await future
        except tornado.util.TimeoutError:
            # 30秒超时返回空
            self.write({"status": "timeout"})
            return
        # 步骤3:返回消息
        self.write({"status": "ok", "data": message})
    def on_finish(self):
        # 清理已断开的waiter(但实际由_mremove_waiter处理)
        pass
class PushHandler(tornado.web.RequestHandler):
    def post(self):
        message = self.get_argument("msg")
        self.application.mq.push_message(message)
        self.write({"status": "pushed", "count": len(self.application.mq.waiters)})
def make_app():
    app = tornado.web.Application([
        (r"/poll", LongPollHandler),
        (r"/push", PushHandler),
    ])
    app.mq = MessageQueue()
    return app
if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

客户端示例(JavaScript):

function longPoll() {
    fetch('/poll')
        .then(res => res.json())
        .then(data => {
            if (data.status === 'ok') {
                console.log('新消息:', data.data);
                // 更新UI...
            }
            longPoll(); // 立即发起下一次轮询
        })
        .catch(() => {
            setTimeout(longPoll, 5000); // 断线重试
        });
}
longPoll();

常见陷阱与性能优化:避免80%开发者会犯的错误

陷阱1:忘记设置超时机制
没有超时的长轮询会导致客户端断网后,服务端永久占用的Future对象泄露,解决方案如上文的add_timeout

陷阱2:使用time.sleep(30)代替await
time.sleep会阻塞整个事件循环,导致所有连接卡死,必须使用tornado.gen.sleepasyncio.sleep

陷阱3:消息重复消费
当客户端在消息推送前断开连接,下一次重连时可能漏掉消息,解决方案:

  • 服务端维护last_message_id,客户端传递该ID,服务端从历史队列补发。
  • 或者使用Redis有序集合存储消息ID+时间戳。

性能优化清单: | 优化项 | 具体做法 | 预期效果 | |--------|----------|----------| | 使用HTTP/2 | 支持多路复用,减少TCP握手 | 连接建立时间减少60% | | 压缩返回数据 | gzip压缩JSON | 带宽降低80% | | 避免全局GIL限制 | 将CPU密集型操作(如加密)移出协程 | 延迟降低50% | | 连接池管理 | 对DB/Redis连接使用asyncio.Queue | 连接复用率提升90% |


问答环节:长轮询 vs WebSocket vs SSE,何时选Tornado?

Q1:为什么不直接用WebSocket?
A:WebSocket需要客户端支持且需升级到ws://协议,在某些企业防火墙或代理环境下会被拦截,长轮询仅依赖标准HTTP GET/POST,兼容性极优(支持IE8+),若项目需兼容老旧系统或受限网络,长轮询是稳妥选择。

Q2:与Server-Sent Events(SSE)相比呢?
A:SSE是服务器单向推送,客户端通过EventSource API接入,但SSE限制连接数为6(浏览器),且无法自定义请求头(如认证Token),Tornado长轮询可携带任何HTTP头,适合需要身份校验的API。

Q3:高并发下长轮询的瓶颈在哪?
A:主要瓶颈在内存:每个挂起的Future占用约2KB内存,10万连接需200MB,可通过以下方式缓解:

  • 使用tornado.platform.asyncio.AsyncIOMainLoop(Tornado 6.0+默认asyncio)
  • 超时将未响应的Future及时GC
  • 消息广播时使用for waiter in list(self.waiters.values()):避免迭代中修改字典

最终建议:

  • 需要双向实时通信 → WebSocket(如游戏、语音)
  • 只需服务器推送给浏览器 → SSE(如股票行情)
  • 需兼容老浏览器、穿透防火墙、或已有HTTP RESTful架构 → Tornado长轮询

Tornado长轮询的意义不止于“轮询”

实战案例证明,Tornado的长轮询解决了一个核心矛盾:在HTTP无状态协议上实现有状态的准实时通信,它不是最新技术,但却是最稳定、兼容性最好的方案之一,当你下次面对“是否需要长轮询”的疑问时,答案很明确——如果你的用户需要使用IE浏览器、通过公司代理、或仅需偶发消息推送,那么Tornado长轮询就是你的最佳拍档。

标签: 长轮询

抱歉,评论功能暂时关闭!