搭建简易画板(三)

342次阅读  |  发布于1年以前

前面搭建了一个单人可用的简易画板,这次我们实现一个多人协作画板。代码库地址

一 基于websocket实现的多人协作

主要用到的技术是 websocket 。因为websocket采取的方式是让所有客户端连接服务端,服务器将不同客户端发送给自己的消息进行转发或者广播。使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

在开发方面,WebSocket API 也十分简单,我们只需要实例化 WebSocket,创建连接,然后服务端和客户端就可以相互发送和响应消息。

首先,我们创建客户端代码 client.ts。


export default class websocketManager{
    static getInstance(): websocketManager {
        if (websocketManager.instance == null) {
            websocketManager.instance = new websocketManager();
        }
        return websocketManager.instance;
    }
    private static instance: any = null;
    websocket = new WebSocket('ws://localhost:3000');
    // 创建websocket链接
    createWebSocket(drawFunc) {
        this.websocket.onopen = function() {
            console.log('开始链接')
        }
        this.websocket.onerror = (err) => {
            console.log('websocket错误 ' + err)
            // 需要重连
        }
        this.websocket.onclose = (err) => {
            console.log('websocket 关闭 ' + err.reason)
        }
        this.websocket.onmessage = (event) => {
            console.log('接收服务端返回的信息')
        }
    }
    // 关闭websocket
    closeWebSocket() {
        this.websocket && this.websocket.close()
    }
}

其次,我们基于nodejs建立下服务端代码 server.ts,用的 ws 这个库。

// 导入WebSocket模块:
const serverWebSocket = require('ws');
// 实例化:
const wss = new serverWebSocket.Server({
    //端口号
    port: 3000
});
wss.on('connection', function (ws) {
    console.log('服务端连接');
    ws.on('message', function (message) {
        ws.send(message, (err) => {
            if (err) {
                console.log(`连接错误: ${err}`);
            }
        });
    })
});

运行下node ./webSocket/server.ts, 可以看到控制台和终端都响应了连接。

客户端new了一个websocket对象后,会向服务器对应端口发起一个get请求。这里绑定的是3000端口,默认情况下,websocket使用80端口。后续客户端和服务端会在这个预定的端口上进行通信。

请求报文中的 upgrade 字段 是告诉服务端需要将通信协议切换到websocket,如果服务端支持websocket协议,那么它就会将请求报文中的Sec-WebSocket-Key解析出来,然后进行相应的拼接加密编码,将最后的结果作为 Sec-WebSocket-Accept 的值返回给客户端,并将自己的通信协议切换到websocket,返回状态码101。

以上过程都是利用http通信完成的,称之为websocket协议握手。握手之后,客户端和服务端就建立了websocket连接,以后的通信走的都是websocket协议。

“Sec-WebSocket-Key”是 WebSocket 客户端发送的一个 base64 编码的密文,要求服务端必须返回一个对应加密的“Sec-WebSocket-Accept”应答,否则客户端会抛出“Error during WebSocket handshake”错误,并关闭连接。

建立连接之后,我们就可以进行数据传输了,websocket提供两种数据传输:文本数据和二进制数据。

回归到画板,我们开多个窗口来模拟下,每个链接后面我们拼一个用户id。然后一个用户在画的时候,每当笔抬起,就发送一次send请求,修改下绘制函数,这样其他用户能实时看到。服务端新增一个保存当前房间的大对象,里面是各个用户id下的绘图数据。

 // client.ts add
this.websocket.onmessage = (event) => {
    drawFunc(JSON.parse(event.data))
}
sendMessage(value) {
    this.websocket.send(`${JSON.stringify(value)}`)
}
// pointer.ts add
function drawAll(userPathData) {
    let canvasDom: any = document.getElementById('drawCanvas');
    let curCtx = canvasDom!.getContext('2d');
    let rect = canvasDom!.getBoundingClientRect();
    curCtx.clearRect(rect.x, rect.y, rect.width, rect.height);
    for (const key in userPathData) {
        if (Object.prototype.hasOwnProperty.call(userPathData, key)) {
            const pathData = userPathData[key];
            pathData.map(item => {
                if (Object.prototype.toString.call(item) === '[object Array]') {
                item.map(info => draw(info, curCtx))
                } else {
                flowDraw(item, curCtx)
                }
            })
        }
    }
}
// 撤销函数更改
function undo() {
    pathData.pop();
    websocketManager.getInstance().sendMessage(pathData)
    // let canvasDom: any = document.getElementById('drawCanvas');
    // let curCtx = canvasDom!.getContext('2d');
    // let rect = canvasDom!.getBoundingClientRect();
    // curCtx.clearRect(rect.x, rect.y, rect.width, rect.height);
    // pathData.map(item => {
    // if (Object.prototype.toString.call(item) === '[object Array]') {
    // item.map(info => draw(info, curCtx))
    // } else {
    // flowDraw(item, curCtx)
    // }
    // })
}
// 监听鼠标放开函数中取消绘制函数
function handleMouseMove(event) {
    if (mouseButtonDown && !config.flowType) {
        let singleData = {beginX: lastPt.x, beginY: lastPt.y, lastX: event.pageX, lastY: event.pageY, strokeStyle: config.strokeStyle, lineWidth: config.lineWidth, drawType: config.drawType, flowType: config.flowType};
        singlePathData.push(singleData)
        // draw(singleData)
        lastPt = {
            x: event.pageX,
            y: event.pageY
        }
    }
    if (mouseButtonDown && config.flowType) {
        let flowPathData = {beginX: flowLastPt.x, beginY: flowLastPt.y, lastX: event.pageX, lastY: event.pageY, strokeStyle: config.strokeStyle, lineWidth: config.lineWidth, drawType: config.drawType, flowType: config.flowType};
        tempDomDraw(flowPathData)
    }
}
// useEffect函数中增加websocket逻辑
useEffect(() => {
    ...
    websocketManager.getInstance().createWebSocket(drawAll);
    return () => {
        websocketManager.getInstance().closeWebSocket()
    }
}, [])
// server.ts add
let allData = {};
wss.on('connection', function (ws) {
    console.log('服务端连接');
    ws.on('message', function (message) {
        const {userID, pathData} = JSON.parse(message);
        allData[userID] = pathData;
        ws.send(JSON.stringify(allData), (err) => {
            if (err) {
                console.log(`连接错误: ${err}`);
            }
        });
    })
});

二、基于share实现的文件共享

Navigator.share() 方法通过调用本机的共享机制。是 Web Share API 的一部分。不过这是一个实验中的功能,浏览器兼容性不是很好。语法很简单。

/*
data可用选项包括:
url: 要共享的 URL( USVString )
text: 要共享的文本( USVString )
title: 要共享的标题( USVString)
files: 要共享的文件(“FrozenArray”)
*/
const data = {
    title: document.title,
    text: '简易画板分享链接',
    url: document.location.href
}
const sharePromise = window.navigator.share(data);

接下来我们共享下图片。share API 的files参数需要接收 file 格式的数组,其次生成file的构造函数方法,它接收的是UTF-8 格式的编码(一种针对Unicode的可变长度字符编码)格式的数组。但canvas.toDataURL("image/png") 生成的是Data URL,由四个部分组成:前缀 (data:)、指示数据类型的 MIME 类型、Base64编码标记以及数据本身。所以我们需要将base64编码的数据转化成UTF-8再转化成file。

data URL 也是 URL,网上有base64转化成UTF-8用的是decodeURI。但亲测这个方法会报错,原因是canvas生成的data URI相对来说很长,又经过了base64的编码,一些换行符、制表符、空格的格式化会有问题。

 /* file 构造函数
`bits: 一个包含ArrayBuffer,ArrayBufferView,Blob,或者 DOMString 对象的 Array
*/
let file = new window.File([bits], filename[, options]);
// 生成canvas图片地址
let imgUrl:any = canvas.toDataURL("image/png");
// data:image/png;base64 字符需要单独提出来
var arr = imgUrl.split(',');
// 拿到图片格式 image/png
var mime = arr[0].match(/:(.*?);/)[1];
var suffix = mime.split('/')[1];
// 解析后面的文件流
var bstr = atob(arr[1]);
var n = bstr.length;
// 初始化Uint8Array数组
var u8arr = new Uint8Array(n);
while(n--) {
    // 对应位置放上相应字符的unicode编码
    u8arr[n] = bstr.charCodeAt(n);
}

我们把得到的file文件读取成路径形式,和咱们一开始的toDataURL得到的路径是一样的。

var reader = new FileReader();
reader.readAsDataURL(imgFile);
reader.onload = function() {
    console.log(this.result == imgUrl) // true
}
var reader = new FileReader();
reader.readAsDataURL(imgFile);
reader.onload = function() {
    console.log(this.result == imgUrl) // true
}

参考资料

MDN websocket

ws

MDN share

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8