SCUT01在线协作白板技术解决方案

news2024/11/24 8:39:45

在七牛云校园黑客马拉松中,来自华南理工大学的SCUT01团队,为我们带来了UI精美、体验优秀的白板作品,在大赛中获得二等奖的好成绩。以下是这款在线协作白板的技术解决方案。

背景

疫情背景下,线上课堂、线上会议等业务背景下都有着在线协作白板的需求。如何实现图形的绘制和实时同步,这是核心的两个问题。本文介绍一种基于原生Canvas和Websocket通信协议的协作白板解决方案。

基础技术介绍

Canvas

元素是HTML5新增的,一个可以使用脚本( 通常为JavaScript )在其中绘制图像的HTML元素。它可以用来制作照片集制作简单的动画,甚至可以进行实时视频处理和渲染。 由API构成,除了具备基本绘图能力的 2D上下文 , 还具备一个名为WebGL的 3D上下文 。

API参考:Canvas - Web API 接口参考 | MDN (http://mozilla.org)

WebSocket

WebSocket是在H5中常被使用的全双工通信协议,它有以下特点

  • 建立在单个TCP连接上的全双工通信应用层协议,支持服务端主动向客户端推送消息
  • 握手阶段采用HTTP协议 (101状态码,Upgrade),与HTTP协议良好兼容
  • 既可以发送文本数据,也可以发送二进制数据

WebSocket完美继承了 TCP 协议的全双工能力,并且还贴心的提供了解决粘包的方案。

它适用于需要服务器和客户端(浏览器)频繁交互的大部分场景,比如网页/小程序游戏,网页聊天室,以及一些类似飞书这样的网页协同办公软件。

对于白板应用的同步功能实现,就使用了Websocket进行实现。

协作技术下WebSocket实践

前置知识

首先需要介绍一下浏览器与服务器是如何建立WebSocket连接的。

  • 浏览器在 TCP 三次握手建立连接之后,都统一使用 HTTP 协议先进行一次通信
  • 如果 建立 WebSocket 连接 ,就会在 HTTP 请求里带上一些特殊的header 头
Connection: Upgrade
 Upgrade: WebSocket
 Sec-WebSocket-Key: T2a6wZlAwhgQNqruZ2YUyg==\r\n
  • 服务器收到带有 Connection: Upgrade请求头的HTTP请求之后,会调用 upgrade方法,将连接更改为websocket连接,然后给该次HTTP请求响应101状态码
  • 至此,Websocket连接已经建立,可以使用已经建立的连接进行双工通信

连接处理

服务端采用高性能的Go语言进行开发,github.com/gorilla/websocket开源库已经封装好完成了upgrade、返回101响应等方法,这里我们直接使用该库进行开发

  • 定义服务器结构体字段
type WstServer struct {
   listener          net.Listener
   upgrade           *websocket.Upgrader
   onConnectHandlers OnConnectHandler
}
  • 该结构体实现ServeHTTP方法,并在方法中调用 Upgrade方法实现websocket协议的切换
func (thisServer *WstServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   conn, err := thisServer.upgrade.Upgrade(w, r, nil)
   if err != nil {
      log.Println("[ws upgrade]", err)
      return
   }
   log.Println("[ws client connect]", conn.RemoteAddr())
   thisServer.onConnect(conn, r.URL.Path) //每个连接开启协程进行处理
}

白板业务下的websocket服务架构

  • 将每一个白板抽象为一个Hub,所有进入该白板的Client都需要使用WebSocket进行连接到WebSocket服务器中白板对应的Hub;其数据结构定义如下
type Hub struct {
   BoardId     string                                        //白板id
   Connections *utils.ConcurrentMap[string, *UserConnection] //当前白板下所有的连接
}
  • BoardId为该Hub对应的白板ID
  • Connections为该Hub中所有已经建立的WebSocket连接,key为UserId
  • 当其中一个Client进行操作之后(如绘制、删除、移动一个图形等),Client将该操作抽象为一个 Cmd的消息,发送给WebSocket服务器
  • WebSocket服务器会将来自Client的消息广播给其他Client,其他Client会调用注册的回调函数进行处理渲染
func (hub *Hub) Broadcast(obj any) {
   //遍历每一个连接,发送消息
   hub.Connections.Data().Range(func(key, value any) bool {
      userId := key.(string)
      conn := value.(*UserConnection)
      err := conn.SendJSON(obj)
      if err != nil {
         log.Println("[Error] Send To ===============> ", userId, err)
         return true
      }
      return true
   })
}

Websocket集群解决方案

如果在单机情况下,当websocket需要给用户推送消息时,由于用户已经与websocket服务建立连接,消息推送能够成功。

但如果在集群情况下,用户甲向websocket发起连接请求,有多台服务时,只能与一台服务建立连接(以服务器A为例),而这些websocket服务都是有可能会给用户甲推送消息,这时候的服务器B和服务器C并没有建立连接。

为避免这种情况,以及更方便实现同步,我们需要尽可能让同一个白板内的所有Client连接到同一台服务器上。

这需要引入MQ来实现。所有的websocket服务都绑定到一个名称为locate的exchange中并接收来自网关的定位消息。如果对应白板的连接管理(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层可能接收到两种事件,来自桌面端的鼠标事件MouseEvent和移动端的触摸事件TouchEvent

  • 我们根据window.devicePixelRatio对事件坐标进行变换,从而实现dpi的适配
  • 将其分别转化成InteractMouseEvent和 InteractTouchEvent ,两者都继承自InteractEvent,分别对外提供统一的接口type(类型,比如down,up...) 和 x, y,从而实现事件类型的统一
  • 传递到场景时,再根据画布缩放比例 scale ,再次进行坐标变化,将其映射到场景画布中成为SceneEvent,场景事件的去向有两个。
    • 通过逻辑层与渲染层的 桥梁 ——工具(Tool类)的op方法 操作RenderScene ,对激活元素进行操作
    • 通过dispatchSceneEvent方法传递给元素,由元素反馈该事件是否与 自己相关 (通过范围判断,返回布尔值)。

同步机制的实现

数据结构

  • 前后端之间使用命令(Cmd)进行同步,Cmd和Cmd的载荷(CmdPayload)数据结构如下
enum CmdType { //枚举从最后开始添加
    Add, // 添加元素
    Delete, // 删除元素
    Withdraw, // 撤回
    Adjust, //调整单个属性
    SwitchPage,  //切换页面
    SwitchMode, // 切换模式
    LoadPage // 加载新页面
}

class Cmd<T extends CmdType> extends SerializableData {
    id: string; // 命令id
    pageId: string; // 操作页面id
    type: T; // 命令类型
    elementType: ElementType; // 命令操作元素类型
    o?: string; // 操作对象的id
    payload: string;  // 操作的 payload, 由于go无法绑定到确定类型,使用string
    time: number; // 操作的时间戳
    boardId: string; // 操作所属的白板
    creator: string; // 操作创建人的userId
}

type CmdPayloads = {
    [CmdType.Add]: ElementBase, //需要增加的元素
    [CmdType.Delete]: null //需要删除的元素
    [CmdType.Withdraw]: Cmd<CmdType> //需要撤销的操作
    [CmdType.Adjust]: Record<string, [any, any]> //p键值为操作的属性,[0]:before, [1]:after
    [CmdType.SwitchPage]: {from: string, to: string} //从from页面切换到to页面
    [CmdType.SwitchMode]: number //新的mode
    [CmdType.LoadPage]: null
}
  • 同时Cmd也是实现撤销/重做的OperationTracker的 状态维护者 ,可以与逻辑层统一一个命令执行接口
export class WhiteBoardApp implements IWebsocket, ToolReactor {

    /* ... */
    public cmdTracker:OperationTracker<Cmd<any>>;
    /* ... */   
   
}

同步机制

  • 每种工具都可能是 创建者(Creator) 或者 修改者(Modifier ),由逻辑层注册对应onCreate和onModify回调。
  • 在创建或修改的时候,构建对应 Cmd ,通过Websocket客户端发送到服务器,服务器广播命令到房间内其他用户。
  • 其他用户收到Cmd时,通过白板逻辑层的 add/delete/adjustElem ByCmd () 等接口,使用Cmd的Payload对白板进行同步。

频繁写场景下的存储架构实践

对于白板类应用,在极大部分情况下数据的操作为更改操作(写操作),并且频率非常高; 应对如何应对高并发的频繁写入操作,成为白板技术下非常重要的问题。 Redis Buffer

如果写入操作直接操作数据库(如MySQL),高并发场景下,数据库的压力会非常大。所以我们选用分布式内存数据库Redis进行数据的缓存,待合适的时机将数据持久化到数据库。

Redis数据结构的选择

Redis的数据结构包括以下五种:

  1. String:字符串类型
  2. List:列表类型
  3. Set:无序集合类型
  4. ZSet:有序集合类型
  5. Hash:哈希表类型

下面介绍一下页面上元素的数据结构:

class ElementBase extends SerializableData {
    public id:string;
    public type:ElementType;
    public x:number; // 左上角点的x坐标
    public y:number;
    public width:number = 0;
    public height:number = 0;
    public angle:number = 0; // 弧度制
    public strokeColor:string = "#ff5656"; // 十六进制整数
    ...
 }

要存储这样一个含有许多属性的对象在Redis中,一般有以下两种方案:

  • 方案一:将整个对象序列化为一个JSON字符串,使用Redis的简单String,进行存储;
    • 优点:实现简单
    • 缺点:如果每次修改只会更改其中某少量属性(如移动只会更改有元素x,y属性),但是采用简单字符串的方式每次都需要重新序列化整个对象,再进行覆盖存储,效率比较低(主要从网络传输的网络包大小考虑)
  • 方案二:将对象存储于Hash结构中,field存储对象的属性名,value存储属性值
  • 优点:可以实现对该对象的某个或多个属性的精准控制
  • 缺点:实现起来复杂

在我们的应用场景下,只更改单个或少数属性的场景较多,所以我们选用Hash结构进行存储 同时,如果我们要知道一个页面内所有的所有的元素的集合,如果采用元素的key值内拼接页面id的方式,必须使用Scan进行全局键的遍历。为了避免全局,选用一个Set结构用于存储一个页面内所有元素的id Redis Pipeline操作

在白板业务场景下,无法避免需要执行多个Redis命令的场景(如读取整个页面上的所有的元素数据的hash结构) 管道(pipeline)可以一次性发送多条命令给服务端,服务端依次处理完完毕后,通过一条响应一次性将结果返回,pipeline 通过减少客户端与 redis 的通信次数来实现降低往返延时时间,而且 Pipeline 实现的原理是队列,而队列的原理是时先进先出,这样就保证数据的顺序性。

使用pipeline可以批量执行Redis命令,非常有效地提高系统吞吐量 Redis集群方案

在整个系统中,需要缓存页面上大量的元素数据,应用的拓展性受到Redis存储容量的限制,并且单节点Redis可用性较低。所以有必要在架构中引入集群方案。 Redis 集群提供了一种运行 Redis 的方式,其中数据在多个 Redis 节点间自动分区。Redis 集群还在分区期间提供一定程度的可用性,即在实际情况下能够在某些节点发生故障或无法通信时继续运行。

Redis集群有以下特点:

  • 每一个master节点都有其对应的一个或多个slave节点,他们之间为主从关系,会进行主从复制
  • 每增加一个key会通过一定哈希算法分配到某一个master节点,理论上可以实现存储能力的扩展

在白板应用中一般读取的场景相对较少,所有每一个master节点有一个从节点即可实现高可用的架构。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/100366.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PCL 点云配准衡量指标

0. 简介 PCL作为目前最为强大的点云库&#xff0c;内部存在有大量集成好的算法。而对于数据量大、非同源、含大量噪声且部分重叠的激光点云与影像重建点云&#xff0c;其稀疏程度、噪声程度等不同&#xff0c;非重叠区域的面积很大。真实场景的点云尤其是影像重建点云噪声较多…

用javascript分类刷leetcode23.并查集(图文视频讲解)

并查集&#xff08;union & find&#xff09;&#xff1a;用于处理一些元素的合并和查询问题 Find&#xff1a;确定元素属于哪一个子集&#xff0c;他可以被用来确定两个元素是否属于同一个子集&#xff0c;加入路径压缩&#xff0c;复杂度近乎O(1) Union&#xff1a;将两…

如何在 ESXi 7.x 上安装 Windows Server 2019

安装 Windows Server 2019 连接 ESXi 7.0上传 Windows Server 2019 镜像创建虚拟机安装 Windows Server 2019在本文中,我们将逐步在 ESXi 7.x 上安装 Windows Server 2019。 连接 ESXi 7.0 使用 VMware 主机或 vSphere Web 客户端连接到 VMware vSphere Hypervisor 7.0(ESX…

Unity-ROS与话题与服务(二)

0. 简介 对于ROS而言&#xff0c;其最常用的就是Topic话题以及Service两个了。之前我们在了解Unity Robotics Hub时候就了解到基本的Unity和ROS的通讯&#xff0c;下面我们来详细介绍一下Unity与ROS的话题与服务。 ROS和Unity之间的通信是通过Unity的“ROS-TCP-Connector”软…

Linux学习07-vim程序编辑器

1 vi与vim vi 是老式的文书处理器&#xff0c;不过功能已经很齐全了&#xff0c;但是还是有可以进步的地方。 vim 则可以说是程序开发者的一项很好用的工具&#xff0c;就连 vim 的官方网站 &#xff08;http://www.vim.org&#xff09; 自己也说 vim 是一个“程序开发工具”而…

服务器内存CPU负载监控

1&#xff0c;系统平均负载(Load average)&#xff1a; 系统平均负载被定义为在特定时间间隔内运行队列中的平均进程树&#xff0c;一般来说只要每个CPU的当前活动进程数不大于3那么系统的性能就是良好的&#xff0c;如果每个CPU的任务数大于5&#xff0c;那么就表示这台机器的…

SpringBoot+MDC实现链路调用日志

1.首先介绍什么是MDC MDC&#xff08;Mapped Diagnostic Context&#xff0c;映射调试上下文&#xff09;是 log4j 、logback及log4j2 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的哈希表&#xff0c;可以往其中添加键值对。MDC 中包含的…

毕业设计 STM32自行车智能无线防盗报警器 -物联网 单片机 嵌入式

0 前言 &#x1f525; 这两年开始毕业设计和毕业答辩的要求和难度不断提升&#xff0c;传统的毕设题目缺少创新和亮点&#xff0c;往往达不到毕业答辩的要求&#xff0c;这两年不断有学弟学妹告诉学长自己做的项目系统达不到老师的要求。 为了大家能够顺利以及最少的精力通过…

【C++进阶】C++11新特性下篇(万字详解)

&#x1f387;C学习历程&#xff1a;入门 博客主页&#xff1a;一起去看日落吗持续分享博主的C学习历程博主的能力有限&#xff0c;出现错误希望大家不吝赐教分享给大家一句我很喜欢的话&#xff1a; 也许你现在做的事情&#xff0c;暂时看不到成果&#xff0c;但不要忘记&…

京东前端高频vue面试题(边面边更)

Redux 和 Vuex 有什么区别&#xff0c;它们的共同思想 &#xff08;1&#xff09;Redux 和 Vuex区别 Vuex改进了Redux中的Action和Reducer函数&#xff0c;以mutations变化函数取代Reducer&#xff0c;无需switch&#xff0c;只需在对应的mutation函数里改变state值即可Vuex由…

【树莓派】擦灰重启行动

高中时候看大佬各种秀项目&#xff0c;于是乎兴致冲冲买了一块树莓派4B&#xff0c;400r&#xff0c;当时没想到光是开机&#xff0c;就折腾了两个星期~后来不出意外它在房间的角落很安逸地吃灰&#xff0c;但是&#xff0c;后来&#xff0c;我误打误撞学了CS&#xff0c;再误打…

Okhttp源码分析实践(五)【实践环节:Okhttp的基本框架搭建请求实现】

http的基础知识、okhttp的框架基本源码,我们通过之前课程都已学习总结过,接下来,就是关键的实践课程。 各位coder,需要紧跟小编脚步,要开始加速飙车了。 1.基本框架的搭建实现 既然不知道如何入手,我们不妨就以okhttp的基本使用代码为例,作为入手点,去开始编程实现。…

机器学习理论介绍

前言 图灵奖获得者、著名数据库专家James Gray 博士观察并总结人类自古以来&#xff0c;在科学研究上&#xff0c;先后历经了实验、理论、计算和数据四种范式。 科学研究第一种范式&#xff1a;实验 在古代&#xff0c;人们的认知水平较低&#xff0c;对事物的认识很大程度上…

React redux使用

1.redux是什么 redux是一个专门用于状态管理的JS库&#xff08;不是react插件库&#xff09; 它可以用在react,angular,vue等项目中&#xff0c;但基本与react配合使用 作用&#xff1a;集中式管理react应用中多个组件共享的状态 2.为什么要使用redux 某个组件的状态&#…

Windows命令行到底有多强大?

程序员宝藏库&#xff1a;https://gitee.com/sharetech_lee/CS-Books-Store DevWeekly收集整理每周优质开发者内容&#xff0c;包括开源项目、资源工具、技术文章等方面。 每周五定期发布&#xff0c;同步更新到 知乎&#xff1a;Jackpop。 欢迎大家投稿&#xff0c;提交issu…

高可用系列文章之三 - NGINX 高可用实施方案

前文链接 高可用系列文章之一 - 概述 - 东风微鸣技术博客 (ewhisper.cn)高可用系列文章之二 - 传统分层架构技术方案 - 东风微鸣技术博客 (ewhisper.cn) 四 NGINX 高可用实施方案 高可用的实施, 主要步骤概述如下: NGINX 的安装及基础配置负载均衡层高可用: NGINX Keepali…

在BSV上运行深度神经网络

我们已经实现了一个用于手写数字分类的深度神经网络。已经训练好的模型完全在链上运行。它使用手写数字的 MNIST 数据集进行离线训练。该模型采用 28x28 灰度像素的图像并输出 0 到 9 的数字。 深度神经网络简介 人工神经网络是受生物神经网络启发而构建的。网络通过接触大量带…

[附源码]计算机毕业设计Python的小说阅读系统(程序+源码+LW文档)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等…

智云通CRM:如何正确的提出报价?(一)

智云通CRM认为完成销售包括三个步骤&#xff1a;提出报价&#xff0c;解决最后问题&#xff0c;讨论下一步方案。 第一步是提出报价&#xff0c;首先我们讨论如何将提出报价。 在与客户讨论费用问题时&#xff0c;我们应当向客户提出两个不同报价&#xff0c;一个销售方案对应…