SCUT01在线协作白板技术解决方案
在七牛云校園黑客馬拉松中,來自華南理工大學的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請求頭的HTTP請求之后,會調用?upgrade方法,將連接更改為websocket連接,然后給該次HTTP請求響應101狀態碼
- 至此,Websocket連接已經建立,可以使用已經建立的連接進行雙工通信
連接處理
服務端采用高性能的Go語言進行開發,github.com/gorilla/websocket開源庫已經封裝好完成了upgrade、返回101響應等方法,這里我們直接使用該庫進行開發
- 定義服務器結構體字段
- 該結構體實現ServeHTTP方法,并在方法中調用?Upgrade方法實現websocket協議的切換
白板業務下的websocket服務架構
- 將每一個白板抽象為一個Hub,所有進入該白板的Client都需要使用WebSocket進行連接到WebSocket服務器中白板對應的Hub;其數據結構定義如下
- BoardId為該Hub對應的白板ID
- Connections為該Hub中所有已經建立的WebSocket連接,key為UserId
- 當其中一個Client進行操作之后(如繪制、刪除、移動一個圖形等),Client將該操作抽象為一個?Cmd的消息,發送給WebSocket服務器
- WebSocket服務器會將來自Client的消息廣播給其他Client,其他Client會調用注冊的回調函數進行處理渲染
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)數據結構如下
- 同時Cmd也是實現撤銷/重做的OperationTracker的?狀態維護者?,可以與邏輯層統一一個命令執行接口
同步機制
- 每種工具都可能是?創建者(Creator)?或者?修改者(Modifier?),由邏輯層注冊對應onCreate和onModify回調。
- 在創建或修改的時候,構建對應?Cmd?,通過Websocket客戶端發送到服務器,服務器廣播命令到房間內其他用戶。
- 其他用戶收到Cmd時,通過白板邏輯層的 add/delete/adjustElem?ByCmd?() 等接口,使用Cmd的Payload對白板進行同步。
頻繁寫場景下的存儲架構實踐
對于白板類應用,在極大部分情況下數據的操作為更改操作(寫操作),并且頻率非常高; 應對如何應對高并發的頻繁寫入操作,成為白板技術下非常重要的問題。 Redis Buffer
如果寫入操作直接操作數據庫(如MySQL),高并發場景下,數據庫的壓力會非常大。所以我們選用分布式內存數據庫Redis進行數據的緩存,待合適的時機將數據持久化到數據庫。
Redis數據結構的選擇
Redis的數據結構包括以下五種:
下面介紹一下頁面上元素的數據結構:
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節點有一個從節點即可實現高可用的架構。
總結
以上是生活随笔為你收集整理的SCUT01在线协作白板技术解决方案的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 因为计算机丢失user32.dll,Wi
- 下一篇: spring boot 中用到的thy