WebSocket仅定义传输帧格式,消息内容与序列化由业务层决定;JSON封装因可读性强、多语言支持好、主流框架内置send_json/receive_json而成为最轻量高效的选择。

WebSocket本身不规定消息内容格式,只负责传输帧;你传什么、怎么解析,全由业务层决定。直接用裸字符串或原始字节当然可以,但一到多类型、带元数据、要路由或做鉴权时,就会立刻失控。所以实际项目里,90%以上都得自己定义消息格式——JSON封装是最轻量且落地最快的起点。
为什么选JSON封装而不是裸字符串或纯二进制
裸字符串(比如 "online:user123")写起来快,但没法扩展字段、难加时间戳、无法嵌套结构;纯二进制(如Protobuf)性能好,但调试困难、前端要额外解码、协议变更成本高。JSON折中:人类可读、几乎所有语言原生支持、send_json() 和 receive_json() 在 FastAPI / Socket.IO / websockets 等主流库中都有现成封装,开发效率和维护性明显占优。
常见错误现象:
- 用
websocket.send("{'type':'ping'}")手动拼接 JSON 字符串 → 遇到中文或特殊字符直接解析失败 - 前端发了
{type: "chat", content: "hello"}(没引号),后端json.loads()报JSONDecodeError - 消息里混用数字和字符串 ID(如
"id": 123vs"id": "123"),导致下游类型判断混乱
标准JSON消息结构该怎么设计
一个最小可用的自定义协议结构,必须包含类型标识、有效载荷和基础元数据。参考 CustomMessage 的 Go 实现和 FastAPI 中的实践,建议固定以下字段:
-
type:字符串,必填,用于路由分发(如"auth"、"chat"、"heartbeat") -
data:任意合法 JSON 值,承载业务数据,保持空对象{}或null也合法 -
timestamp:毫秒级 Unix 时间戳(int64),服务端生成,避免客户端时间不可信 -
seq(可选):单调递增序号,用于检测丢包或乱序(尤其在非可靠子协议下) -
metadata(可选):键值对,存 trace_id、user_id、device_type 等上下文,不参与业务逻辑但利于排查
示例消息体:
{"type":"chat","data":{"to":"user456","text":"Hi there"},"timestamp":1748443800123,"seq":42,"metadata":{"trace_id":"abc123"}}
FastAPI + websockets 库中如何安全收发JSON消息
别手动 json.dumps() / json.loads() —— 大部分现代 WebSocket 库已内置 JSON 支持,用错方法反而引入编码/解码隐患。
- 发送端统一走
websocket.send_json(data_dict):自动处理 UTF-8 编码、转义、序列化,不需json.dumps() - 接收端必须用
await websocket.receive_json():它会校验 JSON 合法性并抛出WebSocketInvalidPayload异常,比手动json.loads()更安全 - 如果收到的是二进制帧(比如误配了
send_bytes),receive_json()会直接报错,此时应检查上游是否混用了send_text/send_binary - 前端对应用
socket.send(JSON.stringify(msg)),不能漏掉JSON.stringify,也不能发Object原始对象
容易被忽略的边界问题
JSON 封装看似简单,但真实环境里最容易栽在三个地方:
-
data字段里塞了不可 JSON 序列化的值(如datetime、bytes、自定义 class 实例)→ 发送前务必预处理,转成str或dict - 前端未设置
Content-Type或Accept,但其实 WebSocket 不走 HTTP 头,这个影响为零;真正要防的是跨域连接时服务端未正确响应Sec-WebSocket-Protocol(如用了json.reliable.webpubsub.azure.v1子协议却没声明) - 没有做
type白名单校验:攻击者可能发{"type":"rm -rf /","data":{}},后端必须先校验type是否在允许列表内,再进入具体处理器
协议越早约定死字段含义和类型,后期加字段、改前端、对接新客户端时就越省事。别把“先跑起来再说”当成默认选项。


















