在七牛云校园创客马拉松中,来自华南理工大学的SCUT01团队为我们带来了UI精美、体验极佳的白板作品,并获得比赛成绩二等奖。这是此在线协作白板的技术解决方案。背景在疫情背景下,在线课堂、在线会议等业务场景中出现了对在线协作白板的需求。如何实现图形绘制和实时同步是两个核心问题。本文介绍一种基于原生Canvas和Websocket通信协议的协作白板解决方案。基本技术介绍Canvas元素是HTML5新增的,一种可以使用脚本(通常是JavaScript)在其中绘制图像的HTML元素。它可以用来创建照片集、制作简单的动画,甚至可以进行实时视频处理和渲染。由API组成,除了具有基本绘图功能的2Dcontext外,它还有一个称为WebGL的3Dcontext。API参考:Canvas-WebAPI接口参考|MDN(http://mozilla.org)WebSocketWebSocket是H5中经常使用的全双工通信协议,具有以下特点一个基于单TCP连接层协议的全双工通信应用,支持服务端主动向客户端推送消息。握手阶段采用HTTP协议(101状态码,Upgrade),很好的兼容HTTP协议。它可以发送文本数据或二进制数据。WebSocket完美继承了TCPWork能力的完整双协议,也贴心地提供了解决粘包的方法。适用于大多数需要服务端和客户端(浏览器)频繁交互的场景,比如网页/小程序游戏、网页聊天室,以及一些像飞书这样的网页协同办公软件。对于白板应用的同步功能的实现,使用了Websocket来实现。协作技术下的WebSocket实践前置知识首先需要介绍一下浏览器和服务器是如何建立WebSocket连接的。TCP三次握手建立连接后,浏览器统一使用HTTP协议先进行通信。如果建立了WebSocket连接,HTTP请求中会包含一些特殊的标头。Connection:UpgradeUpgrade:WebSocketSec-WebSocket-Key:T2a6wZlAwhgQNqruZ2YUyg==\r\n服务端收到带有Connection:Upgrade请求头的HTTP请求后,会调用upgrade方法将连接改为websocket连接,然后使用101状态代码响应HTTP请求。至此,Websocket连接已经建立,您可以使用建立的连接进行双工通信和连接处理。服务器采用高性能Go语言开发。github.com/gorilla/websocket开源库已经通过升级、返回101响应等方式打包完成。这里我们直接使用这个库开发定义服务器结构体字段类型WstServerstruct{listenernet.Listenerupgrade*websocket.UpgraderonConnectHandlersOnConnectHandler}这个结构体实现了ServeHTTP方法,在方法中调用Upgrade方法实现了websocket协议func(thisServerWstServer)ServeHTTP(whttp.ResponseWriter,rhttp.Request){conn,err:=thisServer.upgrade.Upgrade(w,r,nil)iferr!=nil{log.Println("[ws升级]”,错误)返回}日志。println("[wsclientconnect]",conn.RemoteAddr())thisServer.onConnect(conn,r.URL.Path)//每个连接启动一个协程进行处理}白板业务下的websocket服务架构会连接每一个白板抽象为一个Hub,所有进入白板的客户端都需要使用WebSocket连接到WebSocket服务器中白板对应的Hub;其数据结构定义如下typeHubstruct{BoardIdstring//whiteboardidConnectionsutils.ConcurrentMap[string,UserConnection]//当前白板下的所有连接}BoardId对应于Hub中的白板IDConnections都是已建立的WebSocket连接Hub,key是UserId。当其中一个Client执行一个操作(如绘制、删除、移动图形等)时,Client将这个操作抽象成一个Cmd消息发送给WebSocketServerWebSocketServer会将Client的消息广播到otherClients,另外一个Client会调用注册的回调函数进行处理和渲染func(hub*Hub)Broadcast(objany){//遍历每一个连接,发送消息hub.Connections.Data().Range(func(key,valueany)bool{userId:=key.(string)conn:=value.(*UserConnection)err:=conn.SendJSON(obj)iferr!=nil{log.Println([Error]SendTo==============>",userId,err)returntrue}returntrue})}Websocket集群解决方案如果是单机的情况下,当websocket需要向用户推送消息时,由于用户已经与websocket服务建立了连接,消息推送才能成功。但是,如果在集群情况下,用户A向websocket发起连接请求,当有多个服务时,只能建立一个服务(以服务器A的形式为例),这些websocket服务可能会推送消息给用户A。此时服务器B和服务器C还没有建立连接。为了避免这种情况,更方便的实现同步,我们需要尽可能将同一个白板中的所有客户端都连接到同一个服务器上。这需要通过引入MQ来实现。所有websocket服务都绑定到名为locate的交换器,并从网关接收位置消息。如果白板对应的连接管理器(Hub)在本机,则将当前节点的IP、端口等信息发送给网关服务,网关会与对应的Websocket服务建立连接。如果没有找到,说明白板的Hub还没有创建,使用负载均衡等策略随机与某台Websocket服务器建立连接。Web端白板应用实现整体架构展示Web端使用React框架构建应用。整体架构分为三层:UI层、逻辑层、渲染层。UI层:处理用户交互并显示最终显示白板的Canvas。逻辑层:实现白板的核心逻辑(如undo/redo,使用ws同步白板等),与渲染层进行交互。渲染层:渲染整个白板及其元素,使用双缓冲加快渲染效率。基于原生Canvas的白板渲染方案我们将白板组成的屏幕及其包含的所有元素抽象为RenderScene,负责渲染自己的元素,渲染完成后将自身传递给UI层呈现给用户。元素状态每个元素都有两种状态:活动状态和正常状态。所谓活动状态,就是一种容易发生变化的状态(比如选中的时候,或者正在创建的时候,这时候需要脱离后台缓冲区。双面有两个Canvas画板-缓冲渲染层,其中一个作为背景缓冲区,另一个用于整个白板显示,从而提高渲染效率,渲染时先绘制背景缓冲区,再绘制活动元素。渲染过程当逻辑层调用RenderScene的render()方法时,RenderScene会先将背景缓冲区绘制到真正的画布上。如果有激活的元素,则绘制激活的元素。当逻辑层激活场景中的元素时,RenderScene会重新绘制整个背景。缓冲区,包括除活动元素调用render()进行渲染之外的所有元素。当逻辑层去激活场景中的元素时,RenderScene将活动元素绘制到后台缓冲区并调用render()进行渲染。UI层可能会收到两个事件坐标,我们根据window.devicePixelRatio进行变换,实现dpi的适配,分别转化为InteractMouseEvent和InteractTouchEvent,这两个继承自InteractEvent,分别是external的。提供统一的接口类型(type,比如down,up...)和x,y,实现事件类型统一传递到场景,然后根据画布缩放比例重新改变坐标,以及将它映射到场景画布成为SceneEvent,场景事件有两个目的地。通过逻辑层和渲染层的桥梁——工具(Tool类)的op方法操作RenderScene,通过dispatchSceneEvent方法操作被激活的元素给元素,元素反馈事件是否与自身相关(根据作用域判断,??返回一个布尔值)。同步机制的实现命令(Cmd)用于数据结构前后端之间的同步。Cmd的数据结构和Cmd的载荷(CmdPayload)如下enumCmdType{//Enumeration从末尾添加Add,//添加元素Delete,//删除元素Withdraw,//withdrawAdjust,//调整单个属性SwitchPage,//切换页面SwitchMode,//切换模式LoadPage//加载新页面}classCmd
