Python与SSE构建实时天气服务的极简实践
1. 实时数据推送的技术选型
在现代Web应用中,实时数据推送已成为提升用户体验的关键技术。Server-Sent Events(SSE)作为一种轻量级的服务器到客户端单向通信协议,相比WebSocket和长轮询具有独特优势:
- 协议简洁性:基于标准HTTP协议,无需额外握手过程
- 自动重连:内置断线重连机制,客户端自动恢复连接
- 文本友好:原生支持UTF-8文本数据,适合消息类应用
- 资源消耗低:服务端维护成本远低于WebSocket
# SSE响应基本格式示例
def sse_response(data):
return f"data: {json.dumps(data)}\n\n"
当我们需要构建一个实时天气查询服务时,SSE的简单性使其成为理想选择。以下是三种常见实时技术的对比:
| 特性 | SSE | WebSocket | 长轮询 |
|---|---|---|---|
| 通信方向 | 单向(服务端→客户端) | 双向 | 半双工 |
| 协议复杂度 | 低 | 高 | 中 |
| 连接开销 | 低 | 中 | 高 |
| 断线处理 | 自动重连 | 需手动实现 | 每次请求重建 |
| 适合场景 | 实时通知 | 交互应用 | 兼容性要求高 |
2. 五分钟快速搭建SSE服务框架
2.1 基础环境配置
使用Python构建SSE服务无需复杂依赖,仅需标准库即可实现核心功能。推荐使用异步框架提升并发性能:
# 创建项目目录结构
mkdir weather-sse && cd weather-sse
python -m venv .venv
source .venv/bin/activate # Linux/Mac
.venv\Scripts\activate # Windows
# 安装推荐依赖
pip install httpx fastapi uvicorn
2.2 最小化SSE服务实现
以下代码展示了使用FastAPI构建的SSE端点核心逻辑:
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
import asyncio
import json
app = FastAPI()
async def weather_stream(request: Request):
async def event_generator():
while True:
if await request.is_disconnected():
break
# 这里替换为实际的天气数据获取逻辑
weather_data = {"temp": 25, "humidity": 60}
yield f"data: {json.dumps(weather_data)}\n\n"
await asyncio.sleep(10) # 10秒更新间隔
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache"}
)
@app.get("/sse/weather")
async def weather_sse_endpoint(request: Request):
return await weather_stream(request)
启动服务命令:
uvicorn main:app --reload --port 8000
3. OpenWeather API深度集成
3.1 API接入关键步骤
-
获取API密钥:
- 访问OpenWeather官网注册账户
- 在个人仪表板创建API Key
- 免费套餐支持每分钟60次请求
-
请求参数优化:
params = { "q": "Beijing,CN", "appid": "your_api_key", "units": "metric", # 摄氏温度 "lang": "zh_cn" # 中文响应 } -
错误处理机制:
async def fetch_weather(city: str): try: async with httpx.AsyncClient(timeout=10.0) as client: response = await client.get(API_URL, params=params) response.raise_for_status() return response.json() except httpx.HTTPStatusError as e: return {"error": f"API请求失败: {e.response.status_code}"} except Exception as e: return {"error": f"网络错误: {str(e)}"}
3.2 数据格式化与增强
原始API响应包含数十个字段,需要针对前端需求进行精简:
def format_weather(raw_data: dict) -> dict:
return {
"city": raw_data.get("name"),
"temp": raw_data.get("main", {}).get("temp"),
"feels_like": raw_data.get("main", {}).get("feels_like"),
"description": raw_data["weather"][0]["description"],
"icon": f"https://openweathermap.org/img/wn/{raw_data['weather'][0]['icon']}@2x.png",
"update_time": datetime.now().isoformat()
}
4. 生产级SSE服务优化策略
4.1 性能优化要点
- 连接管理:使用
EventSource对象跟踪活跃连接 - 心跳机制:定期发送注释行保持连接活跃
async def event_generator():
while True:
yield ":heartbeat\n\n" # 每30秒发送心跳
await asyncio.sleep(30)
- 缓存策略:对相同城市的请求复用最近结果
from functools import lru_cache
@lru_cache(maxsize=100)
async def get_cached_weather(city: str):
return await fetch_weather(city)
4.2 安全增强措施
-
来源验证:
allowed_origins = ["https://yourdomain.com"] @app.middleware("http") async def check_origin(request: Request, call_next): if request.headers.get("origin") not in allowed_origins: return JSONResponse({"error": "Forbidden"}, status_code=403) return await call_next(request) -
速率限制:
from fastapi import HTTPException from slowapi import Limiter from slowapi.util import get_remote_address limiter = Limiter(key_func=get_remote_address) @app.get("/sse/weather") @limiter.limit("10/minute") async def weather_sse(request: Request): # ...
5. 客户端集成实战示例
5.1 浏览器端实现
<!DOCTYPE html>
<html>
<body>
<div id="weather"></div>
<script>
const eventSource = new EventSource('/sse/weather?city=Beijing');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
document.getElementById('weather').innerHTML = `
<h2>${data.city}天气</h2>
<img src="${data.icon}" alt="天气图标">
<p>温度: ${data.temp}°C (体感 ${data.feels_like}°C)</p>
<p>${data.description}</p>
<small>最后更新: ${new Date(data.update_time).toLocaleString()}</small>
`;
};
eventSource.onerror = () => {
console.log('连接中断,尝试重连...');
};
</script>
</body>
</html>
5.2 服务端调试技巧
使用curl测试SSE端点:
curl -N http://localhost:8000/sse/weather
监控连接状态:
@app.on_event("startup")
async def startup():
print("SSE端点已启动:/sse/weather")
print(f"文档访问:http://localhost:8000/docs")
6. 进阶功能扩展
6.1 多城市订阅支持
active_subscriptions = {}
@app.websocket("/ws/subscribe")
async def websocket_subscribe(websocket: WebSocket):
await websocket.accept()
city = await websocket.receive_text()
if city not in active_subscriptions:
active_subscriptions[city] = []
active_subscriptions[city].append(websocket)
try:
while True:
await websocket.receive_text() # 保持连接
except:
active_subscriptions[city].remove(websocket)
6.2 历史数据存储
from sqlalchemy import create_engine, Column, Integer, String, Float
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class WeatherRecord(Base):
__tablename__ = 'weather_history'
id = Column(Integer, primary_key=True)
city = Column(String)
temperature = Column(Float)
recorded_at = Column(String)
engine = create_engine('sqlite:///weather.db')
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
7. 性能监控与运维
7.1 Prometheus指标集成
from prometheus_client import Counter, Gauge
WEATHER_REQUESTS = Counter(
'weather_requests_total',
'Total weather API requests',
['city']
)
CONNECTIONS_GAUGE = Gauge(
'sse_connections_current',
'Current active SSE connections'
)
@app.get("/sse/weather")
async def weather_sse(request: Request, city: str = "Beijing"):
WEATHER_REQUESTS.labels(city=city).inc()
CONNECTIONS_GAUGE.inc()
async def generator():
try:
# ...原有逻辑...
finally:
CONNECTIONS_GAUGE.dec()
return StreamingResponse(generator())
7.2 日志结构化配置
import logging
from pythonjsonlogger import jsonlogger
logger = logging.getLogger("weather-sse")
handler = logging.StreamHandler()
formatter = jsonlogger.JsonFormatter(
'%(asctime)s %(levelname)s %(message)s %(module)s %(funcName)s'
)
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
在代码关键点添加日志记录:
logger.info("SSE连接建立", extra={"client": request.client.host})
logger.warning("API调用受限", extra={"city": city, "retry_after": 60})
452



被折叠的 条评论
为什么被折叠?



