【精选】多人共享协作画板

您所在的位置:网站首页 多人协作白板怎么做 【精选】多人共享协作画板

【精选】多人共享协作画板

2023-11-07 05:34| 来源: 网络整理| 查看: 265

前面我们介绍了一个简单的单机画板的实现,现在我们将它向多人画板进行扩展,一个很自然的想法便是将绘制过程封装成指令,然后通过网络发送出去,接收到指定的客户端,需要依照绘图指令,同步进行绘制操作。那么首先需要解决的问题是,如何发送?发送什么?

多人画板技术探究 如何发送?

这里需要解决的是所有人可以同步进行绘制,那么就需要连续不断的的接收和发送数据,所以网络协议我们选择WebSocket,我也见过使用WebRTC协议来实现的,不过这个东西我只是耳闻,从来没有使用过。选择协议的目的是为了全双工的工作,应该HTTP是半双工的协议,所以在这里就不考了。

发送什么?

我们再来思考以下,需要发送什么?这需要我们了解单机画板绘制过程中,需要哪些信息,然后将其抽取出来,在网络上进行传输。还记得嘛,我们对一个绘制路径的分析:一个moveTo方法,加上一系列连续的lineTo方法。 因此我们需要的信息是在哪一个点,使用什么颜色、什么大小的笔,沿着什么样的路径进行绘制。 所以我们就可以抽取出我们需要的信息了:

点的类型 typex坐标 xy坐标 y笔的颜色 color笔的大小 size // json对象 let data = { type: 0, // 0 表示 moveTo 1表示lineTo x: 0, y: 0, color: "#000000", size: 1 }

注:点的类型是为了区分,当前的点是执行moveTo方法,还是执行lineTo方法。

实现过程

这里我们需要一个WebSocket后端,用来分发接收到的所有绘制指令。这里其实是不限定语言的,任何语言的后端都是可以。后端的功能很简单,它只是负责对接收到的数据进行转发给所有客户端即可。主要还是前端对于绘图逻辑的控制。现在我们先不去考虑后端的实现,我们来思考一下,前端绘图的步骤:

用户按下鼠标用户移动鼠标用户松开鼠标

当用户按下鼠标时,此时画笔会移动到鼠标点击除,然后用户移动鼠标,此时会途径多个点,画笔依次绘制这些点。所以逻辑就是当用户按下鼠标时,开始执行一个moveTo方法,然后是多个lineTo方法,数据的格式按照上面定义的发送即可。那么让我们在上篇博客的基础之上,开始添加逻辑吧!

实现代码 DOCTYPE html> * { 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; } 选择画笔颜色 选择画笔大小:;1px var canvas = document.getElementById("cs");//获取画布 var context = canvas.getContext("2d"); function setLineWidth(e) { // this 指向是就是该元素本身 console.log("你点击了画笔:", e); console.log(e.value) context.lineWidth = e.value; document.getElementById("size").innerHTML = e.value + " px"; } /* 用户绘制的动作,可以分解为如下操作: 1.按下鼠标 2.移动鼠标 3.松开鼠标 它们分别对应于鼠标的onmousedown、onmousemove和onmouseup事件。 并且上述操作必然是有想后顺序的,因为人的操作必然是几个操作 集合中的一种。所以我们需要来限定以下,过滤用户的无效操作, 只对按照上诉顺序的操作进行响应。 */ let isDowned = false; // 是否按下鼠标,默认是false,如果为false,则不响应任何事件。 // 开始添加鼠标事件 canvas.onmousedown = function(e) { let x = e.clientX - canvas.offsetLeft; let y = e.clientY - canvas.offsetTop; isDowned = true; // 设置isDowned为true,可以响应鼠标移动事件 console.log("当前鼠标点击的坐标为:(", x + ", " + y + ")"); context.strokeStyle = document.getElementById("cl").value; // 设置颜色,大小已经设置完毕了 context.beginPath(); // 开始一个新的路径 context.moveTo(x, y); // 移动画笔到鼠标的点击位置 // 多人协作的逻辑 let pos = {type: 0, x: x, y: y, color: context.strokeStyle, size: context.lineWidth} client.send(JSON.stringify(pos)) } canvas.onmousemove = function(e) { if (!isDowned) { return ; } let x = e.clientX - canvas.offsetLeft; let y = e.clientY - canvas.offsetTop; console.log("当前鼠标的坐标为:(", x + ", " + y + ")"); context.lineTo(x, y); // 移动画笔绘制线条 context.stroke(); // 多人协作逻辑 let pos = {type: 1, x: x, y: y, color: context.strokeStyle, size: context.lineWidth} client.send(JSON.stringify(pos)) } canvas.onmouseup = function(e) { isDowned = false; } /* 在按下鼠标移动的过程中,如果移出了画布,则无法触发鼠标松开事件,即onmouseup。 所以需要在鼠标移出画布时,设置isDowned为false。 */ canvas.onmouseout = function(e) { isDowned = false; } function link () { client = new WebSocket("ws://192.168.0.118:30985/ws/wedraw"); //连接服务器 client.onopen = function(e){ alert('连接了'); }; client.onmessage = function (e) { let data = e.data let pos = JSON.parse(data) console.log("接受到的消息:" + data) context.strokeStyle = pos.color // 设置颜色 context.lineWidth = pos.size // 设置线宽 if (pos.type === 0) { // 如果该点是移动画笔,则移动画笔 context.beginPath() // 开始一个新的路径 context.moveTo(pos.x, pos.y) } else if (pos.type === 1) { // 如果该点是画线,就画线 context.lineTo(pos.x, pos.y); context.stroke(); // 绘制点 } else { console.log("不存在的情况,直接返回") return } } client.onclose = function(e){ alert("已经与服务器断开连接\r\n当前连接状态:" + this.readyState); }; client.onerror = function(e){ alert("WebSocket异常!"); }; } function sendMsg(position){ client.send(position); } link () // 直接建立websocket连接 测试结果

在这里插入图片描述

总结

我们已经实现了通过网络来进行绘制图形的功能了,是不是很有趣呢?但是这样就结束了吗?问题显然是不可能这么简单的,在下一篇博客,我将介绍一个严重的问题和一个悲伤的故事。

附 后端代码

注:这个后端代码严格来说不是我写的,因为我是刚接触go的后端开发人员。这个代码是我参考网上的一个代码修改的,删除了很多我需要的功能,只保留这个广播分发的功能了。而且,你也可以不使用它。自己使用SpringBoot框架写一个WebSocket后端,只要满足功能就行了。

代码结构图 在这里插入图片描述

message_push.go

package main import ( "fmt" "net/http" "ws/ws" "github.com/gin-gonic/gin" ) func main() { go ws.WebsocketManager.Start() // 启动websocket管理器的协程,它的主要功能是注册和注销用户。 // 设置调试模式或者发布模式必须是第一步! gin.SetMode(gin.ReleaseMode) r := gin.Default() // 注册中间件 r.Use(MiddleWare()) // 这个中间件注册在后面就无法起作用了,必须在前面调用。 r.GET("/", func(c *gin.Context) { c.String(http.StatusOK, "Welcome to here!") }) wsGroup := r.Group("/ws") { wsGroup.GET("/wedraw", ws.WebsocketManager.WsClient) // 每一个访问都会调用该路由对应的方法 } bindAddress := ":30985" r.Run(bindAddress) } func MiddleWare() gin.HandlerFunc { return func(ctx *gin.Context) { fmt.Println("调用中间件,请求访问路径为:", ctx.Request.RequestURI) } }

ws.go

package ws import ( "log" "net/http" "strings" "sync" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" uuid "github.com/satori/uuid" ) // Manager 所有 websocket 信息 type Manager struct { ClientMap map[string]*Client clientCount uint Lock sync.Mutex Register, UnRegister chan *Client BroadCastMessage chan *BroadCastMessageData } // Client 单个 websocket 信息 type Client struct { Lock sync.Mutex // 加一把锁 Id string // 用户标识 Conn *websocket.Conn // 用户连接 } // 广播发送数据信息 type BroadCastMessageData struct { Id string // 消息的标识符,标识指定用户 Message []byte } // 读信息,从 websocket 连接直接读取数据 func (c *Client) Read(manager *Manager) { defer func() { WebsocketManager.UnRegister messageType, message, err := c.Conn.ReadMessage() if err != nil || messageType == websocket.CloseMessage { break } log.Printf("client [%s] receive message: %s", c.Id, string(message)) // 向广播消息写入数据 manager.BroadCastMessage for { select { case data, ok := sender, flag := m.ClientMap[data.Id] // 绘图数据不会发给自己,如果这里是将绘图数据写给客户端,应该跳过正在绘图的人 if sender.Id == client.Id { continue } if !flag { log.Println("用户不存在") // 这里应该是存在的,先判断一下 } client.Lock.Lock() client.Conn.WriteMessage(websocket.TextMessage, data.Message) client.Lock.Unlock() } log.Println("广播数据:", data.Message) } } } // 启动 websocket 管理器 func (manager *Manager) Start() { log.Printf("websocket manage start") for { select { // 注册 case client := manager.Register return manager.clientCount } // 获取 wsManager 管理器信息 func (manager *Manager) Info() map[string]interface{} { managerInfo := make(map[string]interface{}) managerInfo["clientLen"] = manager.LenClient() managerInfo["chanRegisterLen"] = len(manager.Register) managerInfo["chanUnregisterLen"] = len(manager.UnRegister) managerInfo["chanBroadCastMessageLen"] = len(manager.BroadCastMessage) return managerInfo } // 初始化 wsManager 管理器 var WebsocketManager = Manager{ ClientMap: make(map[string]*Client), Register: make(chan *Client, 128), UnRegister: make(chan *Client, 128), BroadCastMessage: make(chan *BroadCastMessageData, 128), clientCount: 0, } // gin 处理 websocket handler func (manager *Manager) WsClient(ctx *gin.Context) { // 参数为 ctx *gin.Context 的即为 gin的路由绑定函数 upGrader := websocket.Upgrader{ // cross origin domain CheckOrigin: func(r *http.Request) bool { return true }, // 处理 Sec-WebSocket-Protocol Header Subprotocols: []string{ctx.GetHeader("Sec-WebSocket-Protocol")}, } // 生成uuid,作为sessionid id := strings.ToUpper(strings.Join(strings.Split(uuid.NewV4().String(), "-"), "")) // 设置http头部,添加sessionid heq := make(http.Header) heq.Set("sessionid", id) // 建立一个websocket的连接 conn, err := upGrader.Upgrade(ctx.Writer, ctx.Request, heq) if err != nil { log.Printf("websocket connect error: %s", id) return } // 创建一个client对象(包装websocket连接) client := &Client{ Id: id, Conn: conn, } manager.RegisterClient(client) // 将client对象添加到管理器中 go client.Read(manager) // 从一个客户端读取数据 go manager.WriteToAll() // 将数据写入所有客户端 }


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3