背景
簡單的描述一下需求場景:應用需要進行客戶端到客戶端的通信,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);