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

多人画板技术探究

如何发送?

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

发送什么?

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

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

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

实现过程

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

  1. 用户按下鼠标
  2. 用户移动鼠标
  3. 用户松开鼠标

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

实现代码

<!DOCTYPE html>
<html><head><meta charset="utf-8"><title></title><style type="text/css">* {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;}</style></head><body><div id="seclect"><div class="rg" id="secc"><p>选择画笔颜色</p><input type="color" id="cl"/></div><div class="rg" id="secw"><p>选择画笔大小:&nbsp;<span id="size">1px</span></p><input type="range" onchange="setLineWidth(this)" value="1" min="1" max="10"/></div></div><div id="cas"><canvas id="cs" width="800" height="600"></canvas></div><script type="text/javascript">                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;}</script><script>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.datalet 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连接</script></body></html>

测试结果

总结

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

附 后端代码

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

代码结构图

message_push.go

package mainimport ("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 wsimport ("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]*ClientclientCount          uintLock                 sync.MutexRegister, UnRegister chan *ClientBroadCastMessage     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 <- clog.Printf("client [%s] disconnect", c.Id)if err := c.Conn.Close(); err != nil {log.Printf("client [%s] disconnect err: %s", c.Id, err)}}()for {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 <- &BroadCastMessageData{Id: c.Id, Message: message}}
}// 向所有客户发送广播数据
func (m *Manager) WriteToAll() {for {select {case data, ok := <-m.BroadCastMessage:if !ok {log.Println("没有取到广播数据。")}for _, client := range m.ClientMap {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:log.Printf("client [%s] connect", client.Id)log.Printf("register client [%s]", client.Id)manager.Lock.Lock()manager.ClientMap[client.Id] = clientmanager.clientCount += 1manager.Lock.Unlock()// 注销case client := <-manager.UnRegister:log.Printf("unregister client [%s]", client.Id)manager.Lock.Lock()if _, ok := manager.ClientMap[client.Id]; ok {delete(manager.ClientMap, client.Id)manager.clientCount -= 1}manager.Lock.Unlock()}}
}// 注册
func (manager *Manager) RegisterClient(client *Client) {manager.Register <- client
}// 注销
func (manager *Manager) UnRegisterClient(client *Client) {manager.UnRegister <- client
}// 当前连接个数
func (manager *Manager) LenClient() uint {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 domainCheckOrigin: func(r *http.Request) bool {return true},// 处理 Sec-WebSocket-Protocol HeaderSubprotocols: []string{ctx.GetHeader("Sec-WebSocket-Protocol")},}// 生成uuid,作为sessionidid := strings.ToUpper(strings.Join(strings.Split(uuid.NewV4().String(), "-"), ""))// 设置http头部,添加sessionidheq := 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()        // 将数据写入所有客户端
}

多人共享协作画板——多人画板相关推荐

  1. 多人共享协作画板——单机画板

    最近由于工作需要,接触到了多人共享协作画板这个东西.在这个过程中,学习到了不少前端的知识,现在正好抽时间来对这一段时间学习的知识做一个总结.我都计划是分几篇博客,对共享画板这一块我理解的知识做一个总结 ...

  2. 知识管理系统中的在线编辑,让共享协作更简便

    编者按:共享协作越来越被企业重视,那么如何实现企业内部共享协作呢?在线编辑是关键,本文分析了共享协作的作用,并进一步介绍了知识管理中的在线编辑功能. 关键词: 在线编辑,在线预览,资料分享,全文检索, ...

  3. 一步一步搭建客服系统 (7) 多人共享的电子白板、画板

    多人共享.同时操作的电子白板,让不同的参入者以不同的颜色来画画:可以保存当前room的内容,以让后来者可以直接加载所有内容. 在github上找到一个用html5 canvas实现的一个电子白板的例子 ...

  4. 如何实现Excel多人共享与协作

    1.写在前面的话 本人从事信息化工作多年,对Excel等电子表格的多人共享与协作接触较早,帮助客户实施的方案也较多,因此有些体会和认识.正好看到网上这方面的讨论较多,但都不完整,我就进一步做了专题调研 ...

  5. 企业多人共享文档工具用哪一个团队协作软件?

    企业销售部都面临一个共同的问题:客户.很多企业客户资源是共享的,但究竟成员能与哪个客户签单要看各自的业务能力.为了共享客户资源,很多企业采用的方法是将客户的信息整理在文档中并打印出来,人手一份. 每签 ...

  6. 网页版Rstudio︱RStudio Server多人在线协作开发

    网页版Rstudio︱RStudio Server多人在线协作开发 想了解一下RStudio Server,太给力的应用,可以说成是代码分布式运行,可以节省时间,放大空间. RStudio是一个非常优 ...

  7. 多人共享的待办事项app有哪些

    在如今快节奏的生活中,一个高效的待办事项app是我们必不可少的工具.然而,对于一个团队来说,多人共享的功能则是一个非常重要的需求.因此,一个待办APP可以多人共用,并且能适时同步数据和发出提醒,会对一 ...

  8. 【CIKM 2020】基于多视图协作学习的人岗匹配研究

    点击上方,选择星标或置顶,每天给你送干货! 阅读大概需要16分钟 跟随小博主,每天进步一丢丢 来自:RUC AI BOX 近日,第29届国际计算机学会信息与知识管理大会(CIKM 2020)在线上召开 ...

  9. 升级 | ONES Wiki 多人实时协作,万物皆可编辑

    近日,ONES 完成了对知名文档工具为知笔记的全资收购,为知笔记将多人协同.区块编辑等核心编辑器能力赋予 ONES Wiki,带来新一代的文档协作与知识管理体验. 全新升级的 ONES Wiki 帮助 ...

  10. CIKM 2020 | 基于多视图协作学习的人岗匹配研究

    论文简介 论文:Learning to Match Jobs with Resumes from Sparse Interaction Data using Multi-View Co-Teachin ...

最新文章

  1. vcenter用到java吗_Vijava 学习笔记之 VCenter连接
  2. 有关缅甸语学习的一些网站
  3. Winform中设置ZedGraph的坐标轴的标题和刻度不显示十次幂
  4. python 列表比较不同物质的吸热能力_python列表里面根据一定的条件挑选元素
  5. 2018 疯狂微服务之死
  6. APMServ5.2.6 升级php5.2 到 5.3版本及Memcache升级!
  7. Linux 命令(87)—— tail 命令
  8. 王者荣耀英雄铭文;出装
  9. Vue3动态路由与路由守卫
  10. vue引入jsmind(右键菜单)
  11. 前端学习-吃豆子游戏设计
  12. 《道德经》「人法地 地法天 天法道 道法自然」
  13. 尚硅谷-Promise
  14. 2.4 货币转换 B
  15. heka study
  16. BCT4157/4157B是一种高带宽、快速单刀双掷(SPDT)CMOS开关
  17. 大陆打电话到香港要怎么打?那发信息呢?
  18. Java自学1(哭唧唧又重头开始学了)
  19. 劳务派遣有五险一金吗?
  20. Kotlin 笔记 纯属娱乐萌新大佬绕道

热门文章

  1. 西门子plc和c语言交换数据,西门子PLC字节交换指令及实例
  2. 虚拟机win 7 上安装VWware Tools提示升级系统到SP1
  3. 简单集成华为PUSH
  4. 【管理度量网络安全风险】丨上海道宁为您带来强大的Tenable漏洞及风险管理解决方案
  5. 萤石云视频监控电脑版 v2.6.11.0官方版
  6. python安装不了whl文件_python安装.whl文件失败
  7. 三星note10显示无法连接服务器,三星Note10+ 体验后遇到的小麻烦
  8. 斐讯k1刷入Breed以及openwrt的教程
  9. Codesys和基恩士扫码枪Ethernet/IP通信
  10. android按钮图标大小设置,调整浮动操作button(fab)的图标大小