RSS协议诞生之初主要面向博客与新闻聚合,其基础模型是客户端按固定间隔拉取Feed文件。这种轮询方式在内容更新频繁的场景下显得迟钝,也让源站承受大量无效请求。要让RSS具备实时更新能力,核心思路是把被动拉取改为主动推送,或者借助中间代理降低轮询成本。

为什么原生RSS做不到真正实时
标准RSS文档只是一个静态或动态生成的XML文件,例如<rss>根节点下包含<channel>与若干<item>。订阅器每隔十分钟、半小时甚至更久才请求一次,期间发布的新文章不会被立刻感知。如果缩短间隔,源站带宽与CPU开销会线性上升,小型博客根本无力承担。
另一个隐藏问题是缓存。许多CDN或代理服务器会缓存Feed响应,即使源站已经更新,边缘节点也可能返回旧文档。轮询频率再高,只要命中缓存就无法拿到新内容。因此单纯靠客户端频繁请求并不能称为实时,必须引入事件驱动的通知机制。
基于PubSubHubbub的实时推送原理
PubSubHubbub(后来演进为WebSub)是目前最成熟的RSS实时方案。它引入hub这个中间角色:源站声明自己支持的hub地址,订阅者不直接抓源站,而是向hub发起订阅。当源站发布新内容时,主动给hub发送一个轻量ping,hub随后向所有订阅者的回调地址推送更新报文。
这种结构下,源站只需在内容变更时发一次通知,由hub负责一对多分发,避免了成千上万个客户端同时轮询。订阅端收到的是包含新增条目的完整或增量Feed,解析逻辑与传统RSS完全一致,无需重写整个系统。
源站如何通知hub
以WordPress或自建发布系统为例,文章保存后执行一段HTTP POST即可。下面是用Python实现的简单ping逻辑:
import requests
hub_url = "https://pubsubhubbub.appspot.com/"
topic_url = "https://blog.ippipp.com/rss.xml"
def ping_hub():
data = {
"hub.mode": "publish",
"hub.url": topic_url
}
resp = requests.post(hub_url, data=data)
if resp.status_code == 204:
print("hub已收到发布通知")
else:
print("通知失败", resp.status_code)
ping_hub()
代码中hub.mode设为publish,告诉hub对应topic有更新。hub验证topic确由其接管后,会抓取最新Feed并推送给订阅者。注意topic_url必须是订阅者当初登记的同一个地址,否则hub拒绝分发。
订阅端的回调处理
订阅者第一次向hub订阅时,hub会发来验证请求,需要在回调接口中原样返回hub.challenge。验证通过后,后续更新以POST形式到达。以下Node.js示例展示了订阅与接收:
const express = require('express');
const app = express();
app.use(express.text({ type: '*/*' }));
// 订阅时hub发来GET验证
app.get('/callback', (req, res) => {
const challenge = req.query['hub.challenge'];
res.send(challenge);
});
// 更新推送到达
app.post('/callback', (req, res) => {
const feed = req.body;
console.log('收到新Feed:', feed);
// 此处解析XML并存入数据库
res.sendStatus(200);
});
app.listen(3000, () => console.log('回调服务已启动'));
回调接口必须公网可访问,且能区分GET验证和POST数据。很多开发者忽略HTTPS证书有效性,导致hub推送被拒绝,这是接入阶段最常见的坑。
不依赖公共hub的轻量方案
如果内容源和订阅系统同属一个内部网络,引入公共hub反而增加外部依赖。此时可以用Webhook直接串联两个系统:发布平台在保存内容后,向订阅端暴露的Webhook地址发送JSON或XML片段。
这种方式延迟最低,但不具备标准协议互通性。下面是一段PHP接收Webhook并写入RSS缓冲区的代码:
<?php
$raw = file_get_contents('php://input');
$data = json_decode($raw, true);
if (!$data || !isset($data['title'])) {
http_response_code(400);
exit('无效数据');
}
$item = "<item><title>" . htmlspecialchars($data['title']) . "</title>";
$item .= "<link>" . htmlspecialchars($data['url']) . "</link></item>";
file_put_contents('/var/rss/recent.xml', $item, FILE_APPEND);
echo "ok";
?>
该脚本把接收到的标题与链接追加到最近更新文件,聚合器可直接读取。由于跳过了hub中转,内网环境下延迟通常小于一秒。缺点是每接入一个新订阅方都要改发布端配置,扩展性弱于标准hub。
轮询优化的折中策略
并非所有场景都能部署推送。对于不支持hub的旧源,可采用条件轮询:利用HTTP的ETag与Last-Modified头,仅当内容真正变化时才下载全部Body。这样能把无效流量压缩到极小。
| 方案 | 延迟 | 源站压力 | 互通性 |
|---|---|---|---|
| 固定间隔轮询 | 分钟级 | 高 | 好 |
| 条件轮询 | 取决于间隔 | 低 | 好 |
| PubSubHubbub | 秒级 | 极低 | 标准协议 |
| 私有Webhook | 亚秒级 | 低 | 差 |
从上表可以看出,条件轮询适合无法改造的第三方源,而标准hub在开放生态中综合表现最好。私有Webhook仅推荐封闭系统使用。
实践中的注意事项
部署实时RSS时,订阅端一定要做好幂等处理。hub或Webhook都可能因网络重试发送重复推送,若直接追加条目会出现重复文章。建议以文章唯一ID或链接做去重校验。
另外,部分公共hub对topic抓取频率有限制,源站更新后hub未必立刻来抓,而是等待其自有节奏。若业务要求极致实时,应自建hub或选择私有Webhook。无论哪种方式,都应在发布链路中加上失败重试与告警,避免通知丢失导致订阅端静默停滞。