搞机器人迟早得搞这个(以前只是做api,一直没有精力看sse和ws),正好有空就开搞。
WebSocket 服务器如何工作?
一句话:WebSocket 连接始于一次带有特殊头部的 HTTP 请求,通过秘钥+魔数+SHA-1完成协议升级握手,之后双方维持在内存中的长连接,所有数据以帧格式传输,实现真正的双向实时通信。
WS的核心就是将短连接转为长连接,在长期的双向通信下,不仅可以让客户端传递消息给服务端,也可以让服务端传递新的变化给客户端。
- HTTP短连接模式:App每隔1秒问服务器"股价变了没?",99%的回答都是"没变"
- WebSocket长连接模式:连接一次,服务器只在股价真正变动时主动告诉App
前者浪费流量和服务器资源,后者高效实时------这就是你总结的"核心"在实际中的体现。
WS主要就是连接管理和收发消息两种模式。不过收发有 text、bytes、json和frame(帧)等集中模式,基本也应对了主要的场景。
WS的应用
基于这个特性,WS用于做"实时、双向"的应用
- 实时聊天应用:如在线客服、群聊。
- 实时数据推送:如股票行情、体育比分、服务器监控面板。
- 多人在线游戏:用于同步玩家位置和动作。
- 协同编辑:如在线文档,实时看到他人的编辑内容。
Python技术栈实现WS
📊 Python WebSocket方案对比
| 方案 | 一句话定位 | 代码量 | 性能 | 上手难度 | 适合场景 |
|---|---|---|---|---|---|
| websockets | "纯Python标准派" | 少 | 基准 | ⭐ 极简 | 学习、原型、标准场景 |
| FastAPI | "现代Web整合派" | 少 | 基准 | ⭐⭐ 简单 | 新项目、REST+WS统一 |
| Tornado | "框架自带派" | 中 | 基准 | ⭐⭐ 简单 | 已有Tornado、定时推送 |
| picows | "极致低延迟派" | 多 | RR: 0.034ms | ⭐⭐⭐⭐ 较难 | 请求-响应模式(聊天、游戏) |
| websocket-rs | "Rust加持性能派" | 中 | Pipeline: 2.96ms | ⭐⭐ 简单 | 高并发批处理、流式数据 |
我对于FastAPI和Tornado比较熟,目前阶段看起来也是够用的。
| 你的情况 | 选哪个 |
|---|---|
| 刚学WebSocket,想快速跑通 | websockets |
| 写一个新项目,要有HTTP也有WS | FastAPI |
| 需要服务器每秒定时推数据 | Tornado |
| 追求极致低延迟(<0.1ms) | picows(聊天/游戏) |
| 高并发批处理、流式数据 | websocket-rs Async(12倍优势) |
| 想要性能提升,又不想改代码 | websocket-rs开monkeypatch模式 |
原理性实验
ws1_basic.py
使用50行代码,构建一个简单的ws服务,服务就是将用户传入的消息原路返回。
python
"""
WebSocket基础版本 - Echo服务器
最简单的WebSocket示例:服务器原样返回客户端发送的消息
"""
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
import uvicorn
app = FastAPI(title="WebSocket基础示例")
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
"""
WebSocket端点
功能:
1. 接受客户端连接
2. 接收客户端消息
3. 原样返回消息(echo)
"""
# 等待客户端连接
await websocket.accept()
print(f"[连接] 客户端 {websocket.client} 已连接")
try:
# 持续监听消息
while True:
# 接收消息(这里会阻塞,直到收到消息)
data = await websocket.receive_text()
print(f"[接收] 收到消息: {data}")
# 把消息发回去(echo)
response = f"服务器回复: {data}"
await websocket.send_text(response)
print(f"[发送] 已回复: {response}")
except WebSocketDisconnect as e:
# 客户端主动断开连接
print(f"[正常关闭] 客户端 {websocket.client} 断开 (code={e.code})")
except Exception as e:
# 其他异常
print(f"[错误] 连接异常: {e}")
finally:
print(f"[断开] 客户端 {websocket.client} 已断开")
if __name__ == "__main__":
# 启动服务器
print("=" * 50)
print("WebSocket Echo服务器启动中...")
print("地址: ws://localhost:21000/ws")
print("=" * 50)
uvicorn.run(app, host="0.0.0.0", port=21000)
脚本测试,一个是同步方法,一个是异步方法,和httpx的方式差不多。个人觉得尽量是同步多线程方式,简单且更稳一些;异步的总是感觉不那么稳,而且比较麻烦。
同步测试
python
(同步,用 websocket-client)
from websocket import create_connection
ws = create_connection("ws://localhost:21000/ws")
ws.send("hello")
print(ws.recv())
ws.close()
服务器回复: hello
(异步,用 websockets)
import asyncio
import websockets
async def test():
async with websockets.connect("ws://localhost:21000/ws") as ws:
await ws.send("hello")
print(await ws.recv())
await test()
服务器回复: hello
原理性实验完成了一个简单的原理性实验,可以证明在使用上,在一个整个连接会话中,客户端与服务器是可以保持一个长时间的双向连接。
进阶实验
推广到一个更通用的场景,ws需要管理来自不同用户的连接,而且之前已经固定了jwt的规范和函数,所以现在可以基于jwt来区分不同用户的连接。
需求:
- 1 ws可以区分来自不同用户的连接
- 2 ws给用户进行精确的推送
1 连接管理
实验先创建一个全局变量(connected_users),存放已连接用户。
python
# ============== 全局状态 ==============
# 用户连接管理: user_id -> WebSocket
connected_users: Dict[str, WebSocket] = {}
# 用户会话历史: user_id -> List[消息]
user_sessions: Dict[str, List[Dict]] = {}
...
# 3. 接受连接并注册用户
await websocket.accept()
connected_users[user_id] = websocket
user_connect_time[user_id] = datetime.now()
print(f"[连接] 用户 {user_id} 已连接,当前在线: {len(connected_users)}人")
查看已连接的客户数
python
{'service': 'WebSocket Token版服务端',
'status': 'running',
'online_users': 2,
'ws_endpoint': 'ws://localhost:21000/ws?token=YOUR_TOKEN'}
2 连接保持
ws本质上还是TCP连接,需要定期保活,否则连接会中断。
2.1 客户端响应服务端
服务端
python
服务端可以尝试接受,然后超时发起一个心跳
# 4. 消息循环(带服务端心跳)
while True:
try:
# 使用 receive() 配合超时,实现服务端心跳
message = await asyncio.wait_for(websocket.receive(), timeout=30.0)
if message["type"] == "websocket.receive":
data = message.get("text", "")
elif message["type"] == "websocket.disconnect":
break
else:
continue
except asyncio.TimeoutError:
# 超时,发送服务端心跳
try:
await websocket.send_text(json.dumps({"type": "heartbeat", "from": "server"}))
print(f"[心跳] 发送心跳给用户 {user_id}")
continue
except:
print(f"[心跳失败] 用户 {user_id} 可能已断开")
break
客户端启动一个后台进程接收数据和心跳,这样连接不会断掉。ws.recv() 起到了类似ping/pong的作用。
python
import threading
def receive_loop(ws):
"""后台持续接收消息"""
while True:
try:
msg = ws.recv()
data = json.loads(msg)
if data.get("type") == "heartbeat":
print("💓 收到服务端心跳")
else:
print(f"📨 {data}")
except:
break
threading.Thread(target=receive_loop, args=(ws,), daemon=True).start()
2.2 服务端响应客户端
客户端定期发出心跳消息,服务端响应
bash
// 最常见的做法:自己在应用层实现
// 客户端
setInterval(() => {
ws.send(JSON.stringify({type: 'heartbeat'})); // 普通消息
}, 30000);
// 服务端
ws.on('message', (data) => {
const msg = JSON.parse(data);
if (msg.type === 'heartbeat') {
ws.send(JSON.stringify({type: 'heartbeat_ack'})); // 普通回复
}
});
3 定向推送
维持长连接的一个目的是当服务端需要时可以定向的向用户推送消息。
python
@app.post("/push")
async def push_message(user_id: str, message: str):
"""
向指定用户推送消息
Args:
user_id: 目标用户ID
message: 要推送的消息内容
"""
if user_id not in connected_users:
return {"success": False, "error": f"用户 {user_id} 不在线"}
ws = connected_users[user_id]
try:
await ws.send_text(json.dumps({
"type": "push",
"content": message,
"timestamp": datetime.now().isoformat()
}))
print(f"[推送] 向用户 {user_id} 推送消息: {message}")
return {"success": True, "user_id": user_id, "message": message}
except Exception as e:
return {"success": False, "error": str(e)}
import httpx
# 向 user01 推送消息
resp = httpx.post("http://localhost:21000/push?user_id=user01&message=这是给user01的私信")
print(resp.json())
# 向 user02 推送消息
resp = httpx.post("http://localhost:21000/push?user_id=user02&message=这是给user02的私信")
print(resp.json())
用户在线时
python
{'success': True, 'user_id': 'user02', 'message': '这是给user02的私信'}
当用户下线时,结果是这样
python
{'success': False, 'error': '用户 user01 不在线'}
总结
本篇完成了ws的理论,基础实验和进阶实验,应该可以试着用到我的bot里。之所以选ws,主要还是因为openclaw也这么用,然后这块未来延展会更有意义。
严格来说,ws是为了开发「基于实时事件驱动的系统,持续运行的交互式智能体操作系统」。
这里有个概念:任务型Agent和AgentOS?
如果 30 分钟内没有用户输入,它是否仍然主动运行?
- 任务型:否
- Agent OS:是
所以,如果是任务型Agent,也许不用ws,而是http streaming。目前主流的大模型问答应该都是用SSE的方式,而openclaw则是用ws的方式。所以我想,虽然目前openclaw更像是任务型的,但是未来应该是有想成为Agent OS型的。
| 项目 | SSE | WS |
|---|---|---|
| 服务端实现 | 非常简单 | 稍复杂 |
| 前端实现 | 简单 | 中等 |
| 状态管理 | 依赖 HTTP session | 连接即 session |
| 错误恢复 | 自动重连简单 | 需要手动处理 |
我觉得Agent OS才是我真正想做的,所以我觉得用WS是更好的选择。