4. WebSocket实时通信机制
在 ERP 系统中,库存变动、订单状态更新、生产报工等场景要求数据在 秒级甚至毫秒级 内从服务器推送到客户端。传统的 HTTP 轮询(Polling)存在延迟高、带宽浪费、服务器压力大等缺陷。WebSocket 协议通过建立全双工、长连接通道,从根本上解决了 ERP 实时数据推送的难题。
4.1 为什么 ERP 需要 WebSocket
对比传统 HTTP 请求与 WebSocket 在 ERP 典型场景下的差异:
| 对比维度 | HTTP 短轮询 | HTTP 长轮询 | WebSocket |
|---|---|---|---|
| 连接方式 | 每次请求新建 TCP 连接 | 保持连接直到超时或数据返回 | 一次握手,持久连接 |
| 数据推送方向 | 仅客户端请求后响应 | 模拟推送,仍需客户端发起 | 服务器主动推送 |
| 实时性 | 取决于轮询间隔(通常 3-10s) | 取决于超时设置(通常 1-5s) | 毫秒级 |
| 头部开销 | 每次请求约 800-1000 字节 | 每次请求约 800-1000 字节 | 连接建立后仅 2-6 字节帧头 |
| ERP 典型场景 | 非实时报表查询 | 低频率消息提醒 | 库存变动、订单状态、设备告警 |
关键结论: 在 ERP 高频实时场景下,WebSocket 相比 HTTP 轮询可降低 80% 以上的网络带宽消耗,并将端到端延迟从秒级降至 100ms 以内。
4.2 WebSocket 握手流程详解
WebSocket 连接从 HTTP 升级请求开始,经过三次握手后建立全双工通道。以下是标准握手流程的五个核心步骤:
- 客户端发起 HTTP 升级请求: 客户端发送一个带有
Upgrade: websocket和Connection: Upgrade头部的 HTTP GET 请求,同时包含Sec-WebSocket-Key(一个随机 Base64 编码的 16 字节值)。 - 服务器验证并响应: 服务器接收请求后,将
Sec-WebSocket-Key与固定 GUID(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)拼接,进行 SHA-1 哈希后再 Base64 编码,生成Sec-WebSocket-Accept值。服务器返回 HTTP 101 Switching Protocols 状态码。 - 连接协议升级完成: 客户端验证
Sec-WebSocket-Accept值正确后,TCP 连接从 HTTP 协议切换为 WebSocket 协议。 - 全双工数据帧传输: 双方可以随时发送数据帧,帧类型包括文本帧、二进制帧、Ping/Pong 心跳帧、关闭帧。
- 连接关闭: 任意一方发送关闭帧(Opcode 0x08),对方回复关闭帧确认,TCP 连接释放。
ERP 开发提示: 握手阶段必须验证
Origin 头部,防止跨站 WebSocket 劫持攻击。生产环境中建议使用 WSS(WebSocket Secure)协议,即 WebSocket over TLS,端口 443。
4.3 ERP 实时数据推送场景
WebSocket 在 ERP 中的核心价值体现在以下三个高频场景:
| 场景 | 推送内容 | 典型延迟要求 | 推送频率 |
|---|---|---|---|
| 库存变动 | 物料入库/出库数量、库位变更、批次锁定 | < 500ms | 高(每次扫码/过账触发) |
| 订单状态更新 | 销售订单审核、生产工单开工/完工、采购订单到货 | < 1s | 中(业务节点流转触发) |
| 设备告警与工单 | 设备停机、温度超限、质检不合格、紧急维修派单 | < 200ms | 低但突发(异常事件驱动) |
架构避坑: 不要将所有 ERP 数据都通过 WebSocket 推送。高频但非实时的数据(如日志归档、批量报表)仍应使用 HTTP 或消息队列异步处理。WebSocket 连接数受限于服务器内存和文件描述符,单机建议不超过 10 万并发连接。
4.4 数据帧结构与心跳机制
WebSocket 数据帧最小结构包含以下字段:
- FIN(1 bit): 标记是否为消息的最后一个帧。
- Opcode(4 bits): 0x1 文本帧、0x2 二进制帧、0x8 关闭帧、0x9 Ping、0xA Pong。
- Mask(1 bit): 客户端发送给服务器的帧必须为 1(掩码处理),服务器到客户端为 0。
- Payload Length(7/16/64 bits): 数据长度,根据大小使用不同扩展长度。
- Masking Key(32 bits): 仅当 Mask=1 时存在,用于数据混淆。
- Payload Data: 实际传输的应用数据。
ERP 客户端与服务端之间通常每 30-60 秒 发送一次 Ping 帧,服务端回复 Pong 帧,用于检测连接存活状态。如果连续 3 次未收到 Pong,客户端应主动重连。
4.5 客户端连接示例(JavaScript)
// ERP WebSocket 客户端连接与重连逻辑
class ERPWebSocket {
constructor(url, options = {}) {
this.url = url;
this.reconnectInterval = options.reconnectInterval || 3000;
this.maxRetries = options.maxRetries || 10;
this.retryCount = 0;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('ERP WebSocket 连接已建立');
this.retryCount = 0;
// 发送身份认证消息
this.send({ type: 'auth', : localStorage.getItem('erp_') });
};
this.ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
switch (msg.type) {
case 'inventory_change':
this.handleInventoryChange(msg.payload);
break;
case 'order_status':
this.handleOrderStatus(msg.payload);
break;
case 'pong':
// 心跳回复,无需处理
break;
default:
console.warn('未知消息类型:', msg.type);
}
};
this.ws.onclose = (event) => {
console.warn('WebSocket 连接关闭, code:', event.code);
if (this.retryCount < this.maxRetries) {
setTimeout(() => {
this.retryCount++;
this.connect();
}, this.reconnectInterval);
}
};
this.ws.onerror = (error) => {
console.error('WebSocket 错误:', error);
};
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
handleInventoryChange(payload) {
// 更新前端库存看板
console.log('库存变动:', payload.sku, payload.newQty);
}
handleOrderStatus(payload) {
// 更新订单列表状态
console.log('订单状态:', payload.orderId, payload.status);
}
}
// 实例化连接
const erpWs = new ERPWebSocket('wss://erp.example.com/ws');
4.6 服务端推送示例(Node.js + ws 库)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
// 存储所有已认证的客户端连接
const clients = new Map();
wss.on('connection', (ws, req) => {
console.log('新客户端连接, IP:', req.socket.remoteAddress);
ws.on('message', (message) => {
try {
const data = JSON.parse(message);
if (data.type === 'auth') {
// 验证 后绑定用户会话
const userId = verifyToken(data.);
clients.set(userId, ws);
ws.userId = userId;
ws.send(JSON.stringify({ type: 'auth_ok' }));
}
} catch (e) {
ws.send(JSON.stringify({ type: 'error', message: '无效消息格式' }));
}
});
ws.on('close', () => {
if (ws.userId) {
clients.delete(ws.userId);
}
});
// 发送心跳 Ping
const heartbeat = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping();
} else {
clearInterval(heartbeat);
}
}, 30000);
});
// ERP 业务系统调用推送接口
function pushInventoryChange(sku, newQty, warehouseId) {
const message = {
type: 'inventory_change',
payload: { sku, newQty, warehouseId, timestamp: Date.now() }
};
// 广播给所有相关客户端(可按仓库过滤)
clients.forEach((ws) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
}
});
}
function pushOrderStatus(orderId, status) {
const message = {
type: 'order_status',
payload: { orderId, status, timestamp: Date.now() }
};
// 推送给特定订单负责人
const ownerWs = clients.get(getOrderOwner(orderId));
if (ownerWs && ownerWs.readyState === WebSocket.OPEN) {
ownerWs.send(JSON.stringify(message));
}
}
4.7 连接调试与常见问题
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 握手失败(HTTP 400/403) | Sec-WebSocket-Key 格式错误、Origin 被拒绝、缺少 Upgrade 头 | 使用浏览器开发者工具 Network 面板检查请求头;服务端日志查看拒绝原因 |
| 连接频繁断开 | 心跳超时、Nginx 代理超时配置过短、防火墙空闲超时 | 调整心跳间隔至 20-30s;检查代理层 proxy_read_timeout 配置 |
| 消息丢失或乱序 | 未使用消息序列号、服务端多进程广播未同步 | 在消息体中增加 seq 字段;使用 Redis Pub/Sub 同步多节点推送 |
| WSS 连接证书错误 | 自签名证书、证书域名不匹配、中间证书缺失 | 使用受信任 CA 证书;检查证书链完整性 |
调试工具推荐: 使用 Chrome DevTools 的 Network 面板筛选 WS 协议,可以实时查看 WebSocket 帧内容。服务端可使用
wscat 命令行工具模拟客户端连接进行快速验证。