一、背景
前段時間在知識星球中有同學讓我空閒的時候能不能分享一下 WebSocket,如果不考慮協議層的底層細節,那麼基本上一兩句話就可以說清楚:
WebSocket 是建立在傳輸層 TCP 之上,並且依賴於 HTTP 的應用層協議,它的出現主要是為了彌補 HTTP 協議中,伺服器端無法主動推送訊息到客戶端的缺陷
可是光是這麼回答,我覺著對該同學的幫助也不大,不如就付諸行動,實打實的構建一個例項
例項描述:手機可以通過掃描電腦二維碼(其實也不一定是手機控制電腦只要是端對端就可以),跟電腦建立一個關聯,然後在手機中點選方格,可以同步控制電腦上的方格
例項體驗:傳送門
二、實現思路
- 首先 PC 端先要跟伺服器端建立一個連線,連線建立之後,伺服器為連線的例項建立一個唯一的 id,並返回到客戶端。同時維護一個 Map,以連線 id 為 key 值儲存連線例項
- PC 端拿到連線 id,以 id 作為引數拼接一個控制方頁面 url,並且將 url 生成為二維碼,方便手機掃描
- 手機掃碼訪問 PC 端拼接好的 url,從 url 引數中獲取關聯方 id,向伺服器發起連線,當連線建立成功之後,向服務傳送關聯 id,伺服器收到關聯訊息,維護一個 Map 建立新例項 id 和 關聯方 id 的關聯關係
- 當手機端進行了點選方格的操作,傳送一個訊息到伺服器,伺服器找到關聯方例項,將訊息透傳到 PC 端
- PC 端根據透傳訊息做相應的動作
三、程式碼實現
1)伺服器端程式碼
結合 express 建立 WebSocket 服務
const app = express();
// 建立應用伺服器
const server = http.createServer(app);
// 啟動 HTTP 服務
server.listen(port, '0.0.0.0', function onStart(err) {
if (err) {
console.log(err);
}
console.log('啟動成功');
});
// 通過 ws 模組建立 Websocket 伺服器
const WebSocketServer = require('ws').Server;
const wss = new WebSocketServer( { server : server } );
// 連線例項 Map
process.wsMap = {}
// 連線例項關聯關係 Map
process.wsRelaMap = {}
// 連線監聽
require('./src/socket/conn.js')(wss)複製程式碼
為了方便,這裡使用了一個專門處理 WebSocket 的 node 模組 ws,前面提到過,WebSocket 要依賴於 HTTP,所以在建立 WebSocket 伺服器的時候需要傳入一個 HTTP 伺服器例項。伺服器建立成功之後,需要監聽來自客戶端的連線:
wss.on('connection', function( ws ) {
// 連線例項 id
const id = ws._ultron.id;
ws.on('message', function( data, flags ) {
const dataStr = data;
data = JSON.parse(data);
/**
* 初始連線,並且傳入了需要關聯的 id
*/
if (data.type === '1' && data.relaId) {
wsRelaMap[id] = data.relaId;
} else if (data.type === '2') { // 傳送訊息到關聯方
const rela = wsMap[wsRelaMap[id]];
if (rela) {
rela.send(dataStr);
}
}
});
// 連線關閉,從 Map 中移除,否則長期佔據記憶體
ws.on('close', function() {
console.log('stopping client');
delete wsMap[id]
});
// 保持連線例項
wsMap[id] = ws;
// 傳送 id 到客戶端
ws.send(message.buildConnectMessage(id));
});複製程式碼
根據 type 連區分訊息型別,type 為 1 為初始連線訊息,倘若傳入了關聯方 id,這建立一個關聯關係。當 type 為 2 的時候,找到該例項的關聯方,並且將訊息透傳到關聯方
2)PC 端程式碼(被控制方)
建立連線
var domain = '192.168.1.102:5001/';
var wsServer = 'ws://' + domain;
var websocket = new WebSocket(wsServer);複製程式碼
接收訊息
function onMessage (evt) {
// console.log(evt.data)
// document.getElementById('message').innerText = evt.data
var msg = JSON.parse(evt.data);
var qrcodeImg = document.getElementById('qrcodeImg');
console.log(msg);
console.log(msg.id);
// 訊息型別為1,初始化連線的時候,伺服器端返回連線 id
if (msg.type === '1') {
// 拼接控制方連線,並呼叫介面生成二維碼
qrcodeImg.src = 'http://qr.liantu.com/api.php?text=http://' + domain + 'handler.html?id=' + msg.id
} else {
// 其它型別的訊息為控制訊息,根據訊息做相應的變換
qrcodeImg.style.display = 'none';
document.getElementById('show').style.display = 'block';
if (msg.selected) {
var items = document.getElementsByClassName('item');
for (var i=0; i <items.length; i++) {
items[i].style.backgroundColor = '#ccc'
}
document.getElementById(msg.selected).style.backgroundColor = 'red'
}
}
}複製程式碼
初始連線的時候,伺服器端會返回連線例項 id(根據 type 欄位來區分訊息型別),前端根據 id 拼接控制方連結,並呼叫介面生成二維碼。對於控制訊息,解析之後,變換對應的方格顏色就可以了
3)前端控制方
連線開啟之後,從 url 獲取關聯 id,傳送到伺服器端建立關聯,並且監聽方格點選,隨時向伺服器發起控制訊息
function onOpen () {
// 獲取關聯 id
var relaId = getQueryString('id') || 1
var message = {
type: '1',
relaId: relaId
};
// 發起關聯訊息
websocket.send(JSON.stringify(message));
var conMsg = {
type: '2',
message: 'connected'
};
websocket.send(JSON.stringify(conMsg));
// 監聽點選,改變方格顏色,併發起控制訊息
var items = document.getElementsByClassName('item');
for (var i=0; i <items.length; i++) {
items[i].addEventListener('click', function (e) {
var msg = {
type: '2',
selected: this.id
};
websocket.send(JSON.stringify(msg));
for (var i=0; i <items.length; i++) {
items[i].style.backgroundColor = '#ccc';
}
this.style.backgroundColor = 'red';
});
}
}複製程式碼
四、總結
對於最終目標來說,這個例項還太過簡單,我們還可以做更加炫酷的東西,例如:鮮花從 A 手機滑動到 B 手機,只有你想不到,沒有什麼我們不可以嘗試~~
我們在菲麥前端知識星球發起了 WebSocket demos 共建計劃,誠邀您的加入,一起牛逼一起飛