WebSocket 通信指南
一、WebSocket 基础概念
1.1 什么是 WebSocket
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,于 2011 年被 IETF 定为标准 RFC 6455。
| 特性 | WebSocket | HTTP |
|---|---|---|
| 连接方式 | 持久连接 | 短连接(请求 - 响应) |
| 通信方向 | 双向(全双工) | 单向(客户端发起) |
| 数据开销 | 小(头部仅 2-14 字节) | 大(每次请求带完整头部) |
| 实时性 | 高(服务端可主动推送) | 低(需轮询) |
| 使用场景 | 实时聊天、推送、协作编辑 | 常规数据请求 |
1.2 连接建立流程
客户端 服务端
| |
|--- HTTP GET (Upgrade) ------>| 1. 握手请求
| Upgrade: websocket |
| Connection: Upgrade |
| Sec-WebSocket-Key: ... |
| |
|<-- HTTP 101 Switching --------| 2. 协议升级
| WebSocket Protocol |
| |
|==== WebSocket 连接建立 =======|
| |
|<== 双向数据传输 =============>| 3. 全双工通信
| |
|--- close 帧 ---------------->| 4. 连接关闭
1.3 readyState 状态
| 值 | 常量 | 说明 |
|---|---|---|
| 0 | CONNECTING | 连接中 |
| 1 | OPEN | 已连接,可以通信 |
| 2 | CLOSING | 关闭中 |
| 3 | CLOSED | 已关闭 |
二、项目 WebSocket 架构
2.1 整体架构
┌─────────────────┐ WebSocket ┌─────────────────┐ WebSocket ┌─────────────────┐
│ 浏览器客户端 │ ◄────────────────► │ Node.js 中转 │ ◄────────────────► │ 后端服务 │
│ (Vue 组件) │ ws://localhost │ (wsServer.js) │ 自定义配置 │ (业务服务) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ initWebsocket(url) │ │
│ url 格式:/path?service¶ms │ │
│ │ │
│ ◄─────────────────────────────────────────────────────────────────────────► │
│ 消息转发 │
2.2 连接 URL 格式
/ws?service-name¶m1=value1¶m2=value2
│ │ │
│ │ └─ 可选参数
│ └─ 服务标识(用于路由到不同后端服务)
└─ WebSocket 路径
三、maintenance-center 前端通信流程
3.1 连接初始化
// 在 Vue 组件的 created 生命周期中调用
created() {
this.connectWebsokcet();
},
methods: {
connectWebsokcet() {
// 构建 WebSocket URL
const url = `ws://localhost:8080/ws?maintenance-center&vin=${this.vin}`;
this.initWebsocket(url);
}
}
3.2 消息发送与接收流程图
┌─────────────────────────────────────────────────────────────────────┐
│ Vue 组件 (使用 mixin) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ connectWebsokcet() │
│ │ │
│ ▼ │
│ initWebsocket(url) │
│ │ │
│ ├─► 关闭旧连接 (如有) │
│ │ - this.websocket.close() │
│ │ │
│ └─► 创建新连接 │
│ - new WebSocket(url) │
│ │ │
│ ├── onopen ──► start() 启动心跳 (每 5 秒发送 ping) │
│ │ │
│ ├── onmessage ──► 收到消息 │
│ │ │ │
│ │ ├ │
│ │ └─► handleMessageArrived(data) 处理业务逻辑 │
│ │ │
│ ├── onerror ──► reconnect() 断线重连 │
│ │ │
│ └── onclose ──► 检查 destroyedFlag │
│ │ │
│ ├─► true: 组件已销毁,不再重连 │
│ │ │
│ └─► false: reconnect() 断线重连 │
│ │
└─────────────────────────────────────────────────────────────────────┘
3.3 心跳机制
// 每 5 秒发送一次 ping
start() {
this.heartTime = setInterval(() => {
if (this.websocket.readyState === WebSocket.OPEN) {
this.websocket.send("ping");
}
}, 5000);
}
心跳触发时机:
- 连接成功时(onopen)
- 收到消息时(onmessage)- 收到消息后
- 页面从后台切回前台时(visibilitychange)
降频监控: 检测浏览器是否对定时器进行降频(通常发生在页面不可见时)
3.4 重连机制
连接失败/断开
│
▼
reconnect() 检查 lockReconnect
│
├─► true: 正在重连中,直接返回
│
└─► false: 设置 lockReconnect = true
│
▼
延迟 1 秒后执行
│
▼
connectWebsokcet() 重新连接
│
▼
lockReconnect = false (释放锁)
3.5 页面可见性监听
// 监听页面可见性变化
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
// 用户切回 tab
if (!this.websocket ||
this.websocket.readyState !== WebSocket.OPEN) {
this.connectWebsokcet(); // 尝试重连
}
}
});
解决的问题:
- 浏览器切到后台后,JS 定时器可能被降频
- WebSocket 连接可能因超时被服务端关闭
- 切回前台时自动检测并重连
3.6 组件销毁清理
destroyed() {
clearInterval(this.heartTime); // 清除心跳定时器
this.destroyedFlag = true; // 设置销毁标记
this.websocket.close(); // 关闭连接
this.websocket = null;
// 移除 visibilitychange 监听
document.removeEventListener('visibilitychange', this.visibilityHandler);
}
四、Node.js wsServer 中转服务流程
4.1 架构角色
wsServer.js 作为WebSocket 中转代理,连接浏览器客户端和后端服务。
4.2 连接建立流程
wsServer.js - webSocketServer(httpServer)
│
▼
创建 WebSocket 服务器 (基于 ws 库)
│
▼
监听 'connection' 事件
│
├── conn: 浏览器端 WebSocket 连接
│
└── req: HTTP 请求对象 (包含 URL 参数)
│
▼
解析 URL: /path?service¶ms
│
├── url[0]: /path
├── url[1]: service (服务标识)
└── url[2]: params (可选参数)
│
▼
从配置获取后端服务地址: conf.websocket[service]
│
▼
创建到后端服务的 WebSocket 连接
clientFront[req.url] = new WebSocket(
`ws://${conf.websocket[service]}${path}?${params}`
)
4.3 消息转发流程
┌──────────────────────────────────────────────────────────────┐
│ wsServer.js │
├──────────────────────────────────────────────────────────────┤
│ │
│ 浏览器端 (wsClient) 服务端 (clientFront) │
│ │ │ │
│ │ on('message') │ │
│ │ (预留转发逻辑) │ │
│ │ │ │
│ │ │ on('message') │
│ │ │ │ │
│ │ │ ▼ │
│ │◄─────────────────────────────────┤ 转发消息 │
│ │ wsClient.send(e.data) │ │
│ │ │ │
│ │ │ │
│ │ on('close') │ on('close') │
│ │ │ │ │ │
│ │ ▼ │ ▼ │
│ │ 清理连接 │ 停止心跳 │
│ │ delete wsClient[req.url] │ stopHeartbeat() │
│ │ delete clientFront[req.url] │ │
│ │ │ │
└──────────────────────────────────────────────────────────────┘
4.4 心跳机制
// 为每个服务端连接独立维护心跳
const heartbeatTimers = new Map();
// 启动心跳 (连接打开时触发)
clientFront[item].onopen = function () {
startHeartbeat(clientFront[item]);
};
// 每 5 秒发送 ping
const startHeartbeat = (ws) => {
const timer = setInterval(() => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send("ping");
}
}, 5000);
heartbeatTimers.set(ws, timer);
};
// 停止心跳 (连接关闭时触发)
clientFront[item].onclose = function () {
stopHeartbeat(clientFront[item]);
};
五、完整通信链路示例
5.1 消息流转
1. 后端服务推送消息
↓
2. clientFront[req.url] 收到消息 (wsServer.js)
↓
3. wsServer 转发给 wsClient[req.url]
↓
4. 浏览器 WebSocket.onmessage 触发 (lpWebsocket.js)
↓
5. websocketOnmessage() 处理
↓
6. JSON.parse(e.data) 解析
↓
7. handleMessageArrived() 业务处理
5.2 当前实现的限制
| 方向 | 状态 | 说明 |
|---|---|---|
| 后端 → 前端 | ✅ 已实现 | wsServer 收到服务端消息后转发给浏览器 |
| 前端 → 后端 | ❌ 未实现 | wsClient.on('message') 中转发逻辑为空 |
如需实现前端到后端的消息发送,需在 wsServer.js 第 75-77 行添加:
wsClient[req.url].on("message", (mess) => {
clientFront[req.url] && clientFront[req.url].send(mess);
});
六、关键配置项
6.1 config/index.js 中的 WebSocket 配置
// 示例配置结构
{
websocket: {
'maintenance-center': 'backend-host:port',
'other-service': 'backend-host:port'
}
}
6.2 心跳间隔
| 位置 | 配置 | 值 |
|---|---|---|
| lpWebsocket.js | setInterval | 5000ms |
| wsServer.js | setInterval | 5000ms |
6.3 重连延迟
| 位置 | 配置 | 值 |
|---|---|---|
| lpWebsocket.js | setTimeout | 1000ms |
WebSocket 连接流程说明
七、WebSocket连接案例
本项目采用 WebSocket 实现前端与后台的实时通信,主要应用于以下场景:
- 故障诊断 (
remoteWs):实时接收车辆控制器诊断响应数据
1.前端 WebSocket 建立流程
1.1 连接初始化
前端通过 Mixin 方式封装 WebSocket 通用逻辑,主要文件:
lpWebsocket.js(主 mixin)GlobalFault.vue(独立实现)
// 在组件的 created 生命周期中调用
created() {
this.connectWebsokcet();
}
// 构建 WebSocket URL
connectWebsokcet() {
const wsUrl = `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${apiPaths.API}/${this.$store.state.account.name}/${this.vin}?remoteWs`;
this.initWebsocket(wsUrl);
}
1.2 URL 参数说明
| 参数 | 格式 | 示例 |
|---|---|---|
| remoteWs | /{username}/{vin}?remoteWs | /admin/VIN123?remoteWs |
| faultServer | /{username}?faultServer | /admin?faultServer |
| upgradeServer | /{username}?upgradeServer | /admin?upgradeServer |
| updownpowerServer | /{username}?updownpowerServer | /admin?updownpowerServer |
1.3 连接建立步骤
initWebsocket(url) {
// 1. 关闭已存在的连接
if (this.websocket) {
this.destroyedFlag = true;
this.websocket.close();
this.websocket = null;
}
// 2. 创建新的 WebSocket 连接
this.websocket = new WebSocket(url);
// 3. 绑定事件处理函数
this.websocket.onopen = this.websocketOnopen;
this.websocket.onerror = this.websocketOnerror;
this.websocket.onclose = this.websocketOnClose;
this.websocket.onmessage = this.websocketOnmessage;
}
1.4 事件处理
| 事件 | 处理函数 | 功能说明 |
|---|---|---|
onopen | websocketOnopen | 连接成功,启动心跳定时器 |
onerror | websocketOnerror | 连接错误,触发重连 |
onclose | websocketOnClose | 连接关闭,判断是否需要重连 |
onmessage | websocketOnmessage | 收到消息,解析并处理 |
1.5 心跳机制
start() {
this.heartTime = setInterval(() => {
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
this.websocket.send("ping"); // 发送心跳
}
}, 5000); // 每 5 秒发送一次
}
1.6 断线重连机制
reconnect() {
if (this.lockReconnect) return; // 防止重复连接
this.lockReconnect = true;
setTimeout(() => {
this.connectWebsokcet(); // 1 秒后重连
this.lockReconnect = false;
}, 1000);
}
1.7 页面可见性监听
// 监听页面可见性变化,用户切回时检查连接
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
this.connectWebsokcet(); // 连接已断开,尝试重连
}
}
});
1.8 资源清理
destroyed() {
clearInterval(this.heartTime); // 清除心跳定时器
this.destroyedFlag = true; // 标记已销毁
this.websocket.close(); // 关闭连接
this.websocket = null;
this.reconnect = null;
// 移除 visibilitychange 监听
if (this.visibilityHandler) {
document.removeEventListener('visibilitychange', this.visibilityHandler);
}
}
2.后端 WebSocket 服务架构
2.1 服务端配置文件
文件: web-server/index.js
WebSocket 服务器地址配置(以生产环境为例):
websocket: {
remoteWs: "car:28088/carmonitor/remotews", // 远程诊断
}
2.2 服务端启动流程
文件: web-server/app.js
const http = require("http");
const webSocketServer = require("./websocket/wsServer");
const app = express();
const server = http.createServer(app);
// 初始化 WebSocket 服务
webSocketServer(server);
server.listen(config.port, "0.0.0.0", () => {
logger.info("零云平台启动成功!");
});
2.3 WebSocket 服务端实现
文件: websocket/wsServer.js
const WServer = require("ws").Server;
const Websocket = require("ws");
// 为每个 WebSocket 连接独立存储心跳定时器,避免多个连接互相干扰
const heartbeatTimers = new Map();
// 启动心跳:为每个 ws 连接单独维护定时器
const startHeartbeat = (ws) => {
// 先停止该连接可能存在的旧心跳
stopHeartbeat(ws);
const timer = setInterval(() => {
// 只在连接打开时发送 ping
if (ws && ws.readyState === Websocket.OPEN) {
ws.send("ping");
}
}, 5000);
// 将定时器与 ws 连接绑定存储
heartbeatTimers.set(ws, timer);
};
// 停止心跳:清理指定连接的定时器
const stopHeartbeat = (ws) => {
const timer = heartbeatTimers.get(ws);
if (timer) {
clearInterval(timer);
heartbeatTimers.delete(ws);
}
};
function webSocketServer(server) {
const wsServer = new WServer({ server });
wsServer.on("connection", (conn, req) => {
// 1. 解析请求 URL 和参数
let url = req.url.split("?");
// 2. 保存浏览器端连接
wsClient[req.url] = conn;
// 3. 创建与后端服务的 WebSocket 连接
clientFront[req.url] = new Websocket(
`ws://${conf.websocket[url[1]]}${url[0]}`
);
// 4. 监听浏览器消息
wsClient[req.url].on("message", (mess) => {
// 转发给后端服务
clientFront[req.url].send(mess);
});
// 5. 监听后端服务消息
clientFront[item].onmessage = function (e) {
// 转发给浏览器
wsClient[item].send(e.data);
};
// 6. 连接成功后启动心跳(中转 → 后台)
clientFront[item].onopen = function () {
startHeartbeat(clientFront[item]); // 每个连接独立的心跳定时器
};
// 7. 连接关闭时清理心跳
clientFront[item].onclose = function () {
stopHeartbeat(clientFront[item]); // 只清理当前连接的定时器
};
// 8. 连接关闭处理
wsClient[req.url].on("close", () => {
delete clientFront[req.url];
delete wsClient[req.url];
});
});
}
关键改进:
- 使用
Map为每个 WebSocket 连接独立存储心跳定时器 - 避免了原来
intervalTime全局变量导致多个连接互相覆盖的问题 - 连接关闭时精确清理对应定时器,不影响其他连接
3.数据流向图
┌─────────────────────────────────────────────────────────────────────────┐
│ WebSocket 数据流向 │
└─────────────────────────────────────────────────────────────────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐
│ 浏览器客户端 │ <----> │ Node.js 中转 │ <----> │ 后端业务服务 │
│ (Vue 组件) │ ws:// │ (wsServer) │ ws:// │ (carmonitor/ota 等) │
└──────────────┘ └──────────────┘ └──────────────────────┘
│ │ │
│ 1. 发起连接 │ │
│ /{user}/{vin}?type │ │
├----------------------->│ │
│ │ 2. 建立后端连接 │
│ ├------------------------->│
│ │ │
│ │ 3. 转发心跳/请求 │
│ <--------------------- │ <----------------------- │
│ 4. 返回响应数据 │ │
│ │ │
双向心跳机制:
┌─────────────────────────────────────────────────────────────────┐
│ 前端 --ping(5s)--> Node.js 中转 --ping(5s)--> 后台服务 │
│ <--pong-- <--pong-- │
│ │
│ 为什么需要两层心跳? │
│ - 前端→中转:由 mixin 的 start() 负责,维持浏览器与 Node.js 的连接 │
│ - 中转→后台:由 intervalPing() 负责,维持 Node.js 与后台服务的连接│
│ │
│ 如果缺少中转→后台的心跳: │
│ 后台服务会因超时主动断开连接,即使前端心跳正常 │
└─────────────────────────────────────────────────────────────────┘
数据格式示例:
┌─────────────────────────────────────────────────────────────────┐
│ 请求:{ "functionID": "123", "command": "10", ... } │
│ 响应:{ "vin": "XXX", "faultName": "XXX", "data": {...} } │
└─────────────────────────────────────────────────────────────────┘
4.典型使用场景
4.1 远程故障诊断
文件: remote/index.vue
<script>
import lpWebsocket from "mix/lpWebsocket";
export default {
mixins: [lpWebsocket],
created() {
this.connectWebsokcet(); // 建立 WebSocket 连接
},
methods: {
connectWebsokcet() {
if (this.vin) {
const wsUrl = `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${apiPaths.API}/${this.$store.state.account.name}/${this.vin}?remoteWs`;
console.log('[WebSocket] 开始连接:', wsUrl, 'vin:', this.vin);
this.initWebsocket(wsUrl);
} else {
console.warn('[WebSocket] vin 为空,跳过连接');
}
},
// 发送自定义诊断指令 websocket服务接受到消息后,推送信息过来
handleSend() {
RemoteTroubleService.getCustomDid({
vin: this.vin,
functionID: this.functionID,
command: this.command,
ecu: this.ecu,
timeOut: this.timeOut,
});
},
// 处理收到的消息
handleMessageArrived(message) {
// 将消息传递给子组件处理
}
}
}
</script>
5、关键配置项总结
5.1 前端配置
| 配置项 | 默认值 | 说明 |
|---|---|---|
| 心跳间隔 | 5000ms | 每 5 秒发送一次 ping |
| 重连延迟 | 1000ms | 断开后 1 秒尝试重连 |
| 消息限制 | 100 条 | 实时故障列表最大保留 100 条 |
5.2 后端配置
| 环境 | 配置文件 | WebSocket 服务地址 |
|---|---|---|
| local | config/index.js | 10.192.8.169:28088/car/remotews |
| dev | config/index.js | car:28088/monitor/remotews |
| prod | config/index.js | car:28088/monitor/remotews |
6、注意事项
- 连接复用: 同一 URL 的连接会复用,避免重复创建
- 重连锁: 使用
lockReconnect防止并发重连 - 销毁标记:
destroyedFlag用于标识页面是否已退出,防止无效重连 - 消息解析: 注意消息可能需要双重
JSON.parse()(如 GlobalFault.vue) - 环境差异: 不同环境的 WebSocket 服务地址不同,需正确配置