【203篇系列】041 Websocket Server

搞机器人迟早得搞这个(以前只是做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是更好的选择。

相关推荐
Sheffield2 天前
Docker的跨主机服务与其对应的优缺点
linux·网络协议·docker
YuMiao6 天前
gstatic连接问题导致Google Gemini / Studio页面乱码或图标缺失问题
服务器·网络协议
Jony_9 天前
高可用移动网络连接
网络协议
chilix9 天前
Linux 跨网段路由转发配置
网络协议
DianSan_ERP11 天前
电商API接口全链路监控:构建坚不可摧的线上运维防线
大数据·运维·网络·人工智能·git·servlet
呉師傅11 天前
火狐浏览器报错配置文件缺失如何解决#操作技巧#
运维·网络·windows·电脑
gihigo199811 天前
基于TCP协议实现视频采集与通信
网络协议·tcp/ip·音视频
2501_9462055211 天前
晶圆机器人双臂怎么选型?适配2-12寸晶圆的末端效应器有哪些?
服务器·网络·机器人
linux kernel11 天前
第七部分:高级IO
服务器·网络
数字护盾(和中)11 天前
BAS+ATT&CK:企业主动防御的黄金组合
服务器·网络·数据库