日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

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

發布時間:2024/3/13 编程问答 58 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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: UpgradeUpgrade: WebSocketSec-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.Listenerupgrade *websocket.UpgraderonConnectHandlers 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 //白板idConnections *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; // 命令idpageId: string; // 操作頁面idtype: T; // 命令類型elementType: ElementType; // 命令操作元素類型o?: string; // 操作對象的idpayload: string; // 操作的 payload, 由于go無法綁定到確定類型,使用stringtime: 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的數據結構包括以下五種:

  • String:字符串類型
  • List:列表類型
  • Set:無序集合類型
  • ZSet:有序集合類型
  • 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節點有一個從節點即可實現高可用的架構。

    總結

    以上是生活随笔為你收集整理的SCUT01在线协作白板技术解决方案的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。