websocket服务

4 阅读4分钟

WebSocket 通信指南

一、WebSocket 基础概念

1.1 什么是 WebSocket

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,于 2011 年被 IETF 定为标准 RFC 6455。

特性WebSocketHTTP
连接方式持久连接短连接(请求 - 响应)
通信方向双向(全双工)单向(客户端发起)
数据开销小(头部仅 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 状态

常量说明
0CONNECTING连接中
1OPEN已连接,可以通信
2CLOSING关闭中
3CLOSED已关闭

二、项目 WebSocket 架构

2.1 整体架构

┌─────────────────┐     WebSocket      ┌─────────────────┐     WebSocket      ┌─────────────────┐
│   浏览器客户端    │ ◄────────────────► │   Node.js 中转   │ ◄────────────────► │   后端服务       │
│  (Vue 组件)      │   ws://localhost   │   (wsServer.js)  │   自定义配置       │   (业务服务)     │
└─────────────────┘                    └─────────────────┘                    └─────────────────┘
       │                                      │                                      │
       │  initWebsocket(url)                  │                                      │
       │  url 格式:/path?service&params      │                                      │
       │                                      │                                      │
       │ ◄─────────────────────────────────────────────────────────────────────────► │
       │                           消息转发                                         │

2.2 连接 URL 格式

/ws?service-name&param1=value1&param2=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);
}

心跳触发时机:

  1. 连接成功时(onopen)
  2. 收到消息时(onmessage)- 收到消息后
  3. 页面从后台切回前台时(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&params
          │
          ├── 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.jssetInterval5000ms
wsServer.jssetInterval5000ms

6.3 重连延迟

位置配置
lpWebsocket.jssetTimeout1000ms

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 事件处理
事件处理函数功能说明
onopenwebsocketOnopen连接成功,启动心跳定时器
onerrorwebsocketOnerror连接错误,触发重连
onclosewebsocketOnClose连接关闭,判断是否需要重连
onmessagewebsocketOnmessage收到消息,解析并处理
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 服务地址
localconfig/index.js10.192.8.169:28088/car/remotews
devconfig/index.jscar:28088/monitor/remotews
prodconfig/index.jscar:28088/monitor/remotews

6、注意事项

  1. 连接复用: 同一 URL 的连接会复用,避免重复创建
  2. 重连锁: 使用 lockReconnect 防止并发重连
  3. 销毁标记: destroyedFlag 用于标识页面是否已退出,防止无效重连
  4. 消息解析: 注意消息可能需要双重 JSON.parse()(如 GlobalFault.vue)
  5. 环境差异: 不同环境的 WebSocket 服务地址不同,需正确配置