本文目录导读:
- 目录导读
- 为什么需要长轮询?——实时通信的技术选型思考
- Tornado框架的核心优势:异步非阻塞I/O如何赋能长轮询
- 实战案例:用Tornado实现一个消息推送系统(含完整代码)
- 常见陷阱与性能优化:避免80%开发者会犯的错误
- 问答环节:长轮询 vs WebSocket vs SSE,何时选Tornado?
- 结语:Tornado长轮询的意义不止于“轮询”
Tornado长轮询实战案例:从零构建实时Web应用的最佳方案
目录导读
- 为什么需要长轮询?——实时通信的技术选型思考
- Tornado框架的核心优势:异步非阻塞I/O如何赋能长轮询
- 实战案例:用Tornado实现一个消息推送系统(含完整代码)
- 常见陷阱与性能优化:避免80%开发者会犯的错误
- 问答环节:长轮询 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.sleep或asyncio.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长轮询就是你的最佳拍档。
标签: 长轮询