基於 Node、WebSocket 的手機控制電腦例項

小蟲巨蟹發表於2017-09-16

一、背景

前段時間在知識星球中有同學讓我空閒的時候能不能分享一下 WebSocket,如果不考慮協議層的底層細節,那麼基本上一兩句話就可以說清楚:

WebSocket 是建立在傳輸層 TCP 之上,並且依賴於 HTTP 的應用層協議,它的出現主要是為了彌補 HTTP 協議中,伺服器端無法主動推送訊息到客戶端的缺陷

可是光是這麼回答,我覺著對該同學的幫助也不大,不如就付諸行動,實打實的構建一個例項

例項效果
例項效果

例項描述:手機可以通過掃描電腦二維碼(其實也不一定是手機控制電腦只要是端對端就可以),跟電腦建立一個關聯,然後在手機中點選方格,可以同步控制電腦上的方格
例項體驗:傳送門

二、實現思路

用例圖
用例圖

  1. 首先 PC 端先要跟伺服器端建立一個連線,連線建立之後,伺服器為連線的例項建立一個唯一的 id,並返回到客戶端。同時維護一個 Map,以連線 id 為 key 值儲存連線例項
  2. PC 端拿到連線 id,以 id 作為引數拼接一個控制方頁面 url,並且將 url 生成為二維碼,方便手機掃描
  3. 手機掃碼訪問 PC 端拼接好的 url,從 url 引數中獲取關聯方 id,向伺服器發起連線,當連線建立成功之後,向服務傳送關聯 id,伺服器收到關聯訊息,維護一個 Map 建立新例項 id 和 關聯方 id 的關聯關係
  4. 當手機端進行了點選方格的操作,傳送一個訊息到伺服器,伺服器找到關聯方例項,將訊息透傳到 PC 端
  5. 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 共建計劃,誠邀您的加入,一起牛逼一起飛

菲麥前端,一個讓知識深入原理的星球
菲麥前端,一個讓知識深入原理的星球

相關文章