阅读 2653

基于 Node、WebSocket 的手机控制电脑实例

一、背景

前段时间在知识星球中有同学让我空闲的时候能不能分享一下 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 共建计划,诚邀您的加入,一起牛逼一起飞

菲麦前端,一个让知识深入原理的星球
菲麦前端,一个让知识深入原理的星球