Laravel+Node.js双后端下如何实现WebSocket服务端消息推送
问题场景
- 服务架构:Laravel 后端、Node.js 后端、前端应用
- 业务链路:前端发起请求到 Laravel → Laravel 转发请求到 Node.js → Node.js 通过 WebSocket 向指定客户端推送实时消息
- 现存问题:尝试在 Node.js 业务控制器中调用 WebSocket 实例推送消息时,导入的实例值为
undefined,推送逻辑无法执行。
现有代码
Index.js
const app = require('express')(); const http = require('http'); const WebSocket = require('ws') //initialize a simple http server const server = http.createServer(app); app.get('/', function(req, res) { res.sendFile(__dirname + '/index.html'); }); let sendNotification; //initialize the WebSocket server instance const wss = new WebSocket.Server({ server }); let socketapi = require('./socketapi') socketapi.start(wss) //start our server server.listen(process.env.PORT || 5555, () => { console.log(`Server started on port ${server.address().port} :)`); });
socketapi.js
module.exports ={ start: (wss) => { wss.on('connection', (ws) => { console.log('connected!!!'); console.log(socketIds) //connection is up, let's add a simple simple event // triggerMessage('data'); ws.id=uuidv4() ws.on('message', (message) => { console.log('received: %s', message); // ws.send(`Hello, you sent -> ${message}`); }); }); } }
问题原因
实例导入为undefined的核心原因是现有代码仅在socketapi.js中暴露了WebSocket初始化方法,没有保存初始化后的服务实例、客户端连接池,也没有对外暴露可直接调用的推送方法,其他业务文件导入时自然无法获取可用的WebSocket操作对象。此外原有代码还存在未导入uuidv4依赖、未定义socketIds变量、未做连接断开清理、未建立用户身份和连接的映射关系等问题,无法实现精准推送。
实现步骤
1. 改造socketapi.js,统一封装WebSocket能力
将连接管理、推送逻辑全部收敛到该文件,初始化后对外暴露可直接调用的推送方法,无需在业务文件中直接操作wss实例。
先安装缺失的uuid依赖:npm i uuid
替换socketapi.js代码为以下内容:
const { v4: uuidv4 } = require('uuid'); // 连接池:统一存储所有在线连接,支持按连接ID、用户ID索引 const clientMap = new Map(); let wssInstance = null; module.exports = { // 初始化WebSocket服务 start: (wss) => { wssInstance = wss; wss.on('connection', (ws) => { // 为每个连接生成临时唯一ID const connectId = uuidv4(); ws.id = connectId; clientMap.set(connectId, ws); console.log(`新客户端连接,ID:${connectId},当前在线数:${wssInstance.clients.size}`); // 处理客户端上行消息 ws.on('message', (rawMsg) => { try { const msg = JSON.parse(rawMsg.toString()); // 约定:前端连接建立后主动上报绑定的用户ID,用于精准推送 if (msg.type === 'bind' && msg.userId) { ws.userId = msg.userId; const userKey = `user_${msg.userId}`; if (!clientMap.has(userKey)) clientMap.set(userKey, []); clientMap.get(userKey).push(ws); console.log(`用户${msg.userId}绑定连接成功`); } } catch (err) { console.log(`收到非JSON格式消息:${rawMsg.toString()}`); } }); // 连接关闭时清理连接池 ws.on('close', () => { clientMap.delete(ws.id); if (ws.userId) { const userKey = `user_${ws.userId}`; const userConnections = clientMap.get(userKey) || []; const filtered = userConnections.filter(conn => conn.id !== ws.id); filtered.length ? clientMap.set(userKey, filtered) : clientMap.delete(userKey); } console.log(`客户端断开,ID:${connectId},当前在线数:${wssInstance.clients.size}`); }); // 连接异常处理 ws.on('error', (err) => { console.error(`连接${connectId}异常:`, err); ws.terminate(); }); }); }, // 给指定连接ID的客户端推送 sendToConnect: (connectId, message) => { const ws = clientMap.get(connectId); if (ws && ws.readyState === ws.OPEN) { const sendMsg = typeof message === 'string' ? message : JSON.stringify(message); ws.send(sendMsg); return true; } return false; }, // 给指定用户ID的所有在线端推送 sendToUser: (userId, message) => { const userKey = `user_${userId}`; const connections = clientMap.get(userKey) || []; if (!connections.length) return false; const sendMsg = typeof message === 'string' ? message : JSON.stringify(message); connections.forEach(ws => { if (ws.readyState === ws.OPEN) ws.send(sendMsg); }); return true; }, // 全量广播消息 broadcast: (message) => { if (!wssInstance) return false; const sendMsg = typeof message === 'string' ? message : JSON.stringify(message); wssInstance.clients.forEach(ws => { if (ws.readyState === ws.OPEN) ws.send(sendMsg); }); return true; } }
2. 前端增加连接绑定逻辑
前端建立WebSocket连接后,主动上报当前登录用户的唯一ID(和Laravel侧用户ID保持一致),建立用户和连接的映射关系:
// 前端连接代码 const ws = new WebSocket(`ws://你的Node服务地址:5555`); ws.onopen = () => { // 从本地登录态中获取当前用户ID const currentUserId = localStorage.getItem('userId'); ws.send(JSON.stringify({ type: 'bind', userId: currentUserId })); } // 监听推送消息 ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.type === 'notification') { // 处理推送通知逻辑,比如弹窗提醒 console.log('收到推送:', msg.content); } }
3. Node.js业务层调用推送方法
在处理Laravel请求的路由/控制器文件中,直接导入封装好的socketapi模块即可调用推送能力,不需要单独导入wss实例:
const express = require('express'); const router = express.Router(); // 导入封装好的WebSocket模块 const socketapi = require('./socketapi'); // 接收Laravel请求的接口 router.post('/push-notification', (req, res) => { const { targetUserId, notifyContent } = req.body; // 执行业务逻辑 // 给目标用户推送消息 const pushResult = socketapi.sendToUser(targetUserId, { type: 'notification', content: notifyContent, timestamp: Date.now() }); res.json({ code: 0, msg: '请求处理完成', pushStatus: pushResult ? '推送成功' : '目标用户不在线' }); }); module.exports = router;
记得在Index.js中挂载该路由:
// 放在server.listen之前 app.use(express.json()); // 解析post请求的json参数 app.use('/api', require('./你的路由文件名'));
4. Laravel侧触发推送
Laravel处理完自身业务逻辑后,调用Node.js暴露的推送接口即可触发消息推送:
<?php // Laravel控制器中的业务逻辑 use Illuminate\Support\Facades\Http; // 业务处理完成后,调用Node.js接口 $response = Http::post('http://Node服务内网地址:5555/api/push-notification', [ 'targetUserId' => $需要接收推送的用户ID, 'notifyContent' => '你有一条新的业务通知' ]);
生产环境建议给Laravel到Node.js的接口加签名校验,避免接口被恶意调用。
内容的提问来源于stack exchange,提问作者Bhargav Kanodiya
相关产品推荐
相关产品推荐

