Node.js中SQL查询报错时如何通过Socket向客户端发送消息
实现方案
你现有代码存在两个基础问题,需要先修正再完成消息推送:
- 把Express的POST路由直接写在Socket.IO的
connect回调内是错误写法,HTTP请求和Socket连接是独立链路,这种写法无法直接获取发起请求对应的Socket实例 - 提前在SQL执行前调用
res.send(),后续SQL执行完成后再次调用res.send()会触发「响应已发送」的报错,所有响应需要等SQL执行完成后统一返回
方案1:保留现有HTTP接口,通过SocketID匹配对应连接
该方案不需要改动你现有接口的整体逻辑,只需要补全SocketID的传递和匹配逻辑即可。
客户端改造:发起
/addDiel请求时,把当前连接的socket.id作为参数一起传给服务端,示例(以axios为例):// 你原来的接口请求参数里新增socketId字段 axios.post('/addDiel', { MenoDielu: xxx, DruhDielu: xxx, ProjektName: xxx, socketId: socket.id // 传入当前Socket连接的唯一标识 })你现有客户端监听
receiveDUPmessage的代码不需要改动,保持原样即可。服务端改造:把POST路由挪到Express路由的正确位置(不要写在
io.on('connect')回调里),在ER_DUP_ENTRY错误分支通过传入的socketId找到对应连接推送消息,修正后的代码示例:// Socket连接逻辑单独维护 io.on('connect', (socket) => { console.log("User connected: " + socket.id); // 注意:原代码disconnet拼写错误,修正为disconnect socket.on('disconnect', () => { console.log("disconnected: " + socket.id); }) }) // POST路由放到Express路由配置段 app.post('/addDiel', (req, res) => { pool.getConnection((err, connection) => { if(err) { console.error(err); // 不要直接throw err打崩进程,返回错误响应 return res.status(500).send({code: 500, msg: '数据库连接失败'}); } console.log(`Pripojene ako ID ${connection.threadId}`) const params = req.body; const ProjektNameDB = params.ProjektName.split('.').join("_"); connection.query('INSERT INTO `Skener_db`.`?` (`MenoDielu`, `DruhDielu`, `DatumCas`) VALUES (?, ?, NOW())', [ProjektNameDB, params.MenoDielu, params.DruhDielu],(err, rows)=> { connection.release() if(!err) { console.log(`Hodnota ${params.MenoDielu} bola pridana.`); // SQL执行成功再返回响应,删除你之前提前写的res.send return res.send({ code: 200, MenoDielures: params.MenoDielu, DruhDielures: params.DruhDielu, ProjektNameres: params.ProjektName, msg: '添加成功' }) } else { console.log(err); if (err.code === 'ER_DUP_ENTRY') { // 通过传入的socketId获取对应连接实例 const targetSocket = io.sockets.sockets.get(params.socketId); if (targetSocket) { // 触发客户端监听的事件,推送错误消息 targetSocket.emit('receiveDUPmessage', 'Diel už bol oskenovaný. Oskenuj ďaľší.'); } return res.send({ code: 400, alertMessage: 'Diel už bol oskenovaný. Oskenuj ďaľší.' }) } return res.status(500).send({code:500, msg: '数据库执行错误'}) } }) }) })
方案2:改用Socket自定义事件实现添加逻辑(更简单)
如果不需要严格保留HTTP接口,可以直接把添加数据的逻辑改成Socket自定义事件,事件回调里可以直接拿到当前连接的socket实例,不需要额外传递、匹配socketId,代码更简洁:
- 服务端逻辑:
io.on('connect', (socket) => { console.log("User connected: " + socket.id); socket.on('disconnect', () => { console.log("disconnected: " + socket.id); }) // 直接监听客户端发起的addDiel事件 socket.on('addDiel', (params) => { pool.getConnection((err, connection) => { if(err) { console.error(err); return socket.emit('addDielRes', {code:500, msg:'数据库连接失败'}); } console.log(`Pripojene ako ID ${connection.threadId}`) const ProjektNameDB = params.ProjektName.split('.').join("_"); connection.query('INSERT INTO `Skener_db`.`?` (`MenoDielu`, `DruhDielu`, `DatumCas`) VALUES (?, ?, NOW())', [ProjektNameDB, params.MenoDielu, params.DruhDielu],(err, rows)=> { connection.release(); if(!err) { console.log(`Hodnota ${params.MenoDielu} bola pridana.`); socket.emit('addDielRes', {code:200, msg:'添加成功', data: params}); } else { console.log(err); if(err.code === 'ER_DUP_ENTRY') { // 直接用当前socket实例推送消息,不需要额外查找 socket.emit('receiveDUPmessage', 'Diel už bol oskenovaný. Oskenuj ďaľší.'); } socket.emit('addDielRes', {code:500, msg:'数据库执行错误'}); } }) }) }) }) - 客户端改造:把原来的POST请求改成Socket事件发送即可:
原有的// 替换原来的axios.post请求 socket.emit('addDiel', { MenoDielu: xxx, DruhDielu: xxx, ProjektName: xxx }) // 如果需要接收添加结果,可以监听addDielRes事件 socket.on('addDielRes', (res) => { console.log('添加结果', res); })receiveDUPmessage监听逻辑不需要改动。
注意事项
- 用户刷新页面后socket.id会重新生成,每次发请求/事件时取最新的socket.id即可,不需要手动清理失效连接,Socket.IO会自动回收断开的实例
- 数据库报错时不要直接
throw err,会直接终止Node进程,必须捕获后返回错误响应/事件 - 方案1如果是多实例部署(多个Node进程负载均衡),需要配置Socket.IO的redis适配器,否则会找不到对应socket实例;单实例部署无此问题
内容的提问来源于stack exchange,提问作者Kristian Masar
相关产品推荐
相关产品推荐

