在 nodejs 中 利用 websocket 实现简单的 “1对1” 消息传递
背景
简单的描述一下需求场景:应用需要进行客户端到客户端的通信,websocket 就能很好的进行这一操作,目前 网易云信的 IM 等功能也是利用 websocket 进行的。
必要性
对前端开发人员来说,目前能够提供 mock 服务的第三方工具还是比较多的,基本上,与后台开发人员约定好请求路径、请求字段和响应字段之后就能前后台独立开发了。
但 websocket 服务器与 http 服务器最大的区别就是 websocket 服务器必须得一直提供服务,否则客户端之间就无法进行通信。
为了体现前后端分离,提高开发效率的精髓。肯定是不能先把逻辑全部盲写好了之后再与后台联调的,便决定与后台约定好了接口名和数据形式之后,用 nodejs 写一个简单的 websocket 通信服务。
功能
websocket 用的比较多的应该就是传输文本(简单点说就是字符串),所以,这个字符串携带着接收方的用户标识(toId
),其他的信息(比如消息类型 type
和 消息内容 data
等),通常的做法是 JSON.stringfy()
之后转成字符串,服务端将发送方的用户标识(fromId
)、其他的信息(比如发送过来的消息类型 type
和 消息内容 data
)等信息转发给接收方,接收方接收到字符串之后再 JSON.parse()
进行下一步操作。
用下面这张图描述一下我希望 websocket 服务器能提供给客户端的功能。
客户端
websocket 提供的方法中常用的有:new
、onopen
、onclose
、onerror
、onmessage
、send
毫无疑问,对前端开发人员来说需要与业务逻辑相结合、重点关注的方法就是以下三个:
new
在连接 websocket 服务器时传递自己的 id
const ws = new WebSocket(`ws://localhost:8080/websocketServer/${id}`);
send
发送消息(字符串)给服务器,服务器再转发给目标
const msgObj = { toId: 666, type: 'hello', data: 'message......' }; const msgStr = JSON.stringfy(msgObj); ws.send(msgStr);
sendTo
由于 send
不能接收对象或者数组类型的数据,每次都得写一个临时的对象,再调用 JSON.stringfy()
方法转成字符串。
那就封装一个符合自己业务场景的方法,简化开发过程中实现发送功能的步骤。
// 在原型对象上增加的 sendTo 方法 if ( ! WebSocket.prototype.sendTo ) { WebSocket.prototype.sendTo = function(toId, type, data) { const msg = JSON.stringify({ toId, type, data }); this.send(msg); } } // 调用形式 ws.sendTo(toId, type, data);
onmessage
接收消息,通常会根据消息中 type
的不同值:
- 自己做点什么
- 给对方一个响应(调用
sendTo
方法)
ws.onmessage = function(msg) { const { fromId, type, data } = JSON.parse(msg); switch (type) { case 'message': // 自己做点什么 break; case 'hello': // 返回一个消息 this.sendTo(fromId, 'response', 'response data'); } }
最后简单的封装一下初始化方法,方便在不同的地方使用。
const wsUrl = 'ws://localhost:8080/'; const wsPath = 'socketServer/'; // 生成实例 let ws = null; const connect = async () => { ws = await connectWS(uid, messageHandle); // ... // ... // 连接成功之后做点什么 } function connectWS(uid, msgHandle) { if ( ! WebSocket.prototype.sendTo ) { WebSocket.prototype.sendTo = function(toId, type, data) { const msg = JSON.stringify({ toId, type, data }); this.send(msg); } } return new Promise((resolve, reject) => { const ws = new WebSocket(`${wsUrl}${wsPath}${uid}`); ws.onopen = () => { console.log('open'); resolve(ws); }; ws.onclose = () => { console.log('close'); }; ws.onerror = () => { console.log('error'); }; ws.onmessage = (msg) => { msgHandle(JSON.parse(msg.data)); }; }); } // 接收消息的处理函数 function messageHandle(msgData) { const { fromId, type, data } = msgData; switch (type) { case '': break; default: break; } }
服务端
常用的 nodejs-websocket
和 express-ws
两个框架,都需要自身维护一个对象来记录每个 websocket
连接是哪个用户,从而实现消息转发。
以 express-ws
为例:
记录连接
首先,服务端要记录当前连接的用户,以便转发消息的时候能够正确发送给目标。
转发消息
服务器将收到的来自于发送方消息中的 toId
值作为要转发的目标(接收方),在服务器自身维护的对象中找到接收方的这个连接,然后将发送方的标识作为 fromId
转发给接收方。
const express = require('express'); const express_ws = require('express-ws'); const app = express(); const wsObj = {}; express_ws(app); app.ws('/socketServer/:uid', (ws, req) => { const uid = req.params.uid; wsObj[uid] = ws; ws.onmessage = (msg) => { let { toId, type, data} = JSON.parse(msg.data); const fromId = uid; if (fromId != toId && wsObj[toId]) { // wsObj[toId] 表示 接收方 与服务器的那条连接 // wsObj[fromId] 表示 发送方 与服务器的那条连接 wsObj[toId].send(JSON.stringify( { fromId, type, data } )) } } }); app.listen(8080);