上一篇博客我们已经介绍了一个多人画板的实现过程,并且在最后添加了一个演示的GIF图片,证明它是可以正常工作的。我也是这样认为了,直到多个人一起测试的时候,出现了严重的bug。下面让我带领你去探索这个问题吧。
多人画板的bug及分析
多人画板的bug复现
由于前期我都是一个人写一个人进行测试,完全觉得没有问题,不是我喜欢一个人测试,是因为我就一个人——形单影只(唉,这是一件伤心事!)。所以我在这个画板上绘制,然后跑到另一个画板上进行绘制,从来没有出现过问题,我就认为这是正常的了。可是后来才发现,当多个人同时绘制时会出现严重的问题。为了复现这个问题,我找了我的同学来参与,以下为复现结果。
复现bug的步骤:
红色的是我,黑色的是我同学,一开始是我先画,然后他开始画,最后我们一起画。可以可见整个画板上出现了非常奇特的现象,这是一个明显的bug了。多人协作共享画板在这里来看是无法实现了。
bug分析
这种严重的bug是我所始料未及的,我甚至想出了一个特别的笨办法,每次只能一个人进行绘制。这就类似腾讯课堂的举手功能,想要绘制的人举手,然后老师来选择(这里是系统来选择)。但是这也是一个难点,怎么每次只让一个人开始画,看起来也并不是可以简单实现的功能。
而且对于上面这个问题,我还没有进行分析呢!我们来试着分析一下,看看能不能解决这个问题!
多人协作问题分析
一个用户时,画一笔的步骤是:一个moveTo,然后跟上一系列的lineTo。所以整个绘制过程可以看作是每次添加一个完整的一笔的过程,但是由于多个用户同时绘制,打破了每一笔的界限,因此导致了严重的问题。因为上诉绘制一笔可以成立的原因是,每个人都可以完整占据一笔的绘制过程。但是现在来看,多个用户一起绘制是不行,因为客户端接收到了其它人的绘制和自己的绘制数据产生了冲突。
举一个非专业的例子(忽略一点点细节):
一群人手握笔玩笔仙游戏,当前一个人正在纸张的左下角画线,但是另一个人忽然想在纸张的右上角画线,因为笔在纸上不能离开,所以他是直接把笔从左上角拉到右上角。所以,每个人会对笔进行争抢,你们会看到非常奇怪的现象。
我们来举一个例子说明:
moveTo 从某点到某点(直接移动,没有痕迹)
lineTo 从某点到某点的连线(会有痕迹,就是画的轨迹)
我们来模拟两个用户(毕竟找一个人太麻烦了):
用户1:
moveTo(20, 20)
lineTo(20, 100)
lineTo(100, 100)
lineTo(100, 20)
用户2:
moveTo(150, 150)
lineTo(150, 250)
lineTo(250, 250)
lineTo(150, 150)
分别顺序执行这两个操作,模拟两个用户依次进行绘制(先后顺序):
我们可以看出来,此时的绘制是正确的。然后我们来调换上述操作的步骤,不是打乱绘制顺序(如果一笔内部的点的顺序错乱了,那么就一定是错误的),而且两个用户的操作进行交替,例如第一个用户的第一笔,然后第二个用户的第二笔,以这种方式进行交替,但是每一笔内部的顺序还是有序的。
用户1:用户2:
moveTo(20, 20)
lineTo(20, 100)
moveTo(150, 150)
lineTo(100, 100)
lineTo(150, 250)
lineTo(100, 20)
lineTo(250, 250)
lineTo(150, 150)
多用户参与以后,绘制的canvas,可以看出来发生了非常严重的错误。这里实际上的原因是moveTo丢失的问题。(因为多个lineTo无法区分属于谁的那一笔了,这里我称之为moveTo丢失)每一笔的内部顺序不变,但是两笔的坐标是交叉的(模拟多人协作的过程),就可以看到这种非常严重的问题。在网络通信中暂时不用考虑消息的顺序问题(默认为有序),多个人同时绘制会出现这种严重的错误问题,从而导致白板协作的失败。
分析代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<style type="text/css">
* {
margin: 0;
padding: 0;
}
.rg{
float: left;
width: 400px;
height: 100px;
text-align: center;
border: 1px black solid;
margin-left:-1px ;
}
#cas{
width: 800px;
height: 600px;
border: #000000 1px solid;
}
p{
margin: 5px 0 5px 0;
}
</style>
</head>
<body>
<div id="seclect">
<div class="rg" id="secc">
<p>选择画笔颜色</p>
<input type="color" id="cl"/>
</div>
<div class="rg" id="secw">
<p>选择画笔大小: <span id="size">1px</span></p>
<input type="range" onchange="setLineWidth(this)" value="1" min="1" max="10"/>
</div>
</div>
<div id="cas">
<canvas id="cs" width="800" height="600"></canvas>
</div>
<script type="text/javascript">
var canvas = document.getElementById("cs");//获取画布
var context = canvas.getContext("2d");
// 有序绘制
/*
poss = [
{"id": 1, "type":0,"x":20,"y":20,"color":"#000000","size":5}, // 1_0
{"id": 1, "type":1,"x":20,"y":100,"color":"#000000","size":5}, // 1_1
{"id": 1, "type":1,"x":100,"y":100,"color":"#000000","size":5}, // 1_2
{"id": 1, "type":1,"x":100,"y":20,"color":"#000000","size":5}, // 1_3
{"id": 2, "type":0,"x":150,"y":150,"color":"#f20707","size":20}, // 2_0
{"id": 2, "type":1,"x":150,"y":250,"color":"#f20707","size":20}, // 2_1
{"id": 2, "type":1,"x":250,"y":250,"color":"#f20707","size":20}, // 2_2
{"id": 2, "type":1,"x":250,"y":150,"color":"#f20707","size":20}, // 2_3
]
*/
// 交替绘制
poss = [
{"id": 1, "type":0,"x":20,"y":20,"color":"#000000","size":5}, // 1_0
{"id": 1, "type":1,"x":20,"y":100,"color":"#000000","size":5}, // 1_1
{"id": 2, "type":0,"x":150,"y":150,"color":"#f20707","size":20}, // 2_0
{"id": 1, "type":1,"x":100,"y":100,"color":"#000000","size":5}, // 1_2
{"id": 2, "type":1,"x":150,"y":250,"color":"#f20707","size":20}, // 2_1
{"id": 1, "type":1,"x":100,"y":20,"color":"#000000","size":5}, // 1_3
{"id": 2, "type":1,"x":250,"y":250,"color":"#f20707","size":20}, // 2_2
{"id": 2, "type":1,"x":250,"y":150,"color":"#f20707","size":20}, // 2_3
]
poss.forEach(pos => {
context.strokeStyle = pos.color // 设置颜色
context.lineWidth = pos.size // 设置线宽
if (pos.type === 0) { // 如果该点是移动画笔,则移动画笔
console.log("移动画笔");
context.beginPath() ; // 开始一个新的路径
context.moveTo(pos.x, pos.y);
context.stroke(); // 绘制点
} else if (pos.type === 1) { // 如果该点是画线,就画线
console.log("正在画线");
context.lineTo(pos.x, pos.y);
context.stroke(); // 绘制点
} else {
console.log("不存在的情况,直接返回")
}
})
</script>
</body>
</html>
多人协作问题解决探究
多个用户时,lineTo会造成冲突,导致非常奇怪的错误,因为没有moveTo来正确的切分它们了。但是这里可以考虑多次使用moveTo来确保一笔可以完整画出来。继续接上面的笔仙的例子:左下角的人记住了自己当前正在画的点,右上角的人直接把笔移动过去,然后左下角想要绘制,移动到他原来的那一点,继续绘制。每个人想要进行绘制,就移动到他原来的那一点,这样就把一个完整的绘制过程打乱,但是依然保持每一笔的内部顺序。
也就是说,每个点进行绘制时,需要记住它上一个点在哪里?当还是属于这一笔的点进行绘制时,它就先跳到它的上一个点,然后再进行绘制。所以我们需要来区分每个用户的数据,这里我们需要给原来的数据结构,额外添加一个id作为标识,用来区分它属于的那一笔或者说那一条路径。
解决代码
var canvas = document.getElementById("cs");//获取画布
var context = canvas.getContext("2d");
const map = new Map();
poss = [
{"id": 1, "type":0,"x":20,"y":20,"color":"#000000","size":5}, // 1_0
{"id": 1, "type":1,"x":20,"y":100,"color":"#000000","size":5}, // 1_1
{"id": 2, "type":0,"x":150,"y":150,"color":"#f20707","size":20}, // 2_0
{"id": 1, "type":1,"x":100,"y":100,"color":"#000000","size":5}, // 1_2
{"id": 2, "type":1,"x":150,"y":250,"color":"#f20707","size":20}, // 2_1
{"id": 1, "type":1,"x":100,"y":20,"color":"#000000","size":5}, // 1_3
{"id": 2, "type":1,"x":250,"y":250,"color":"#f20707","size":20}, // 2_2
{"id": 2, "type":1,"x":250,"y":150,"color":"#f20707","size":20}, // 2_3
]
console.log("数组的长度:" + poss.length)
poss.forEach(pos => {
context.strokeStyle = pos.color // 设置颜色
context.lineWidth = pos.size // 设置线宽
if (pos.type === 0) { // 如果该点是移动画笔,则移动画笔
console.log("移动画笔");
context.beginPath() // 开始一个新的路径
context.moveTo(pos.x, pos.y)
context.stroke(); // 绘制点
map.set(pos.id, pos) // 记住当前点的位置
} else if (pos.type === 1) { // 如果该点是画线,就画线
// 对于画线,不再是直接画线,而是要区分它是谁的那一笔
let mpos = map.get(pos.id)
// 开始一个新路径
context.beginPath();
context.lineWidth = mpos.size;
context.strokeStyle = mpos.color;
// 然后移动到它上一点的位置
context.moveTo(mpos.x, mpos.y)
console.log("正在画线")
context.lineTo(pos.x, pos.y)
// 记录当前点的位置,覆盖原来的记录,这里默认所有的点都是moveTo了,不需要修改type了
map.set(pos.id, pos)
context.stroke(); // 绘制点
} else {
console.log("不存在的情况,直接返回")
}
})
运行结果:
不过,canvas似乎丢失了某些细节性的部分,但是它证明了这种解决问题的可能性是存在的。
总结
这里经过分析我们探索出了这个问题的解决之道。这个分析过程是很有趣的,所以不能简单的因为困难就直接放弃,先去进行一个细致的分析,也许会有不一样的发现呢。下一篇博客我们继续探索之旅,将在这里得出的解决方法,应用到多人白板绘制上。
附
我这里提一点,如果大家学习过操作系统的话,一定知道最开始的计算机一次只能运行一个程序或者演进到后来的单道批处理系统。但是当同时运行多个程序时,这就会出现问题。我们知道程序本质上就是一系列指令的集合,只允许一个程序或者一次运行每一个程序,其实各个程序并不会产生冲突。但是,当多个程序在运行时,该如何区分这个指令是属于谁呢?计算机真的是同时运行多个程序吗?你还记得PCB(进程控制块)吗?
学习过操作系统的肯定都知道,计算机是不能同时运行多个程序的(单核处理器),假设我们现在打开了A、B、C三个程序。作为终端用户可能会认为我现在正在同时运行三个程序,但是实际上是CPU运行一会A,再运行一会B,然后再运行一会C。它们其实是轮流运行的,但是给你的感觉却是同时运行的! 再回到我们今天这个问题的解决方法,是不是很类似呢?大学科班的这些基础知识,虽然不能直接在工作中进行运用,但是它潜移默化的改变你的思考方法,显得更那么就计算机了。如果学习过计算机网络,也可以考虑一下,当你同时下载多个文件时,它们在网络上只是一股连续的比特流,为什么每一个文件都能被很好的区分开?