尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

基于SpringBoot+Vue+WebSocket的多人实时协作绘画平台设计

基于SpringBoot+Vue+WebSocket的多人实时协作绘画平台设计 简介这是一套面向Web全栈开发者与计算机专业学生的实时协作应用实战源码聚焦多人在线绘画场景解决创意团队远程协同绘图的技术落地问题。资源共40个文件含18个Java后端逻辑文件SpringBoot构建服务与WebSocket通信管理、6个前端核心文件Vue组件、路由及状态管理以及JSON配置、XML配置、Markdown文档、MP4演示视频等辅助材料压缩包大小为16.92MB。已有369人学习下载适合具备基础Java和Vue开发能力的学习者深入理解实时通信架构设计。资源包含完整前后端工程结构、可运行的WebSocket广播机制实现、用户操作同步逻辑、配套架构设计文档及功能演示视频便于快速部署、调试与二次开发。 多人实时协作绘画这个东西听起来挺有吸引力但真正动手做的时候坑比想象中多得多。我最早接触这个需求是在一个远程白板工具的项目里当时团队分散在几个城市开会讲方案光靠语音说不清楚就想做一个能大家一起画、一起标注的在线白板。后来这个方向越做越深从单房间到多房间从纯画笔到图形、图片、文字混合编辑技术栈也慢慢收敛到了SpringBoot Vue WebSocket这套组合上。这篇文章不聊虚的就把这套多人实时协作绘画平台的核心设计思路、WebSocket通信细节、画布同步机制、以及我实际开发中踩过的那些坑一次性讲清楚。不管你是刚学Vue的新手还是已经在SpringBoot上写过几个接口的开发者只要对实时协作类应用感兴趣这篇文章都值得花十分钟看完。1. 项目定位与整体设计思路1.1 为什么选这套技术组合做实时协作先说说技术选型。很多人一听到“实时协作”第一反应是WebSocket这个没错但选SpringBoot和Vue其实是基于一个很现实的考量这套组合的生态最成熟踩坑资料最多招人也好招。后端用SpringBoot核心原因有几个。第一它内置了对WebSocket的支持不管是基于原生注解ServerEndpoint还是Spring封装好的WebSocketHandler都能快速集成不需要额外引入Netty这种重型框架。第二SpringBoot的自动配置和starter机制让项目搭建成本低到可以忽略一个空项目从初始化到能跑起来几分钟的事。第三对于绘画这种高频、短连接式的消息交互SpringBoot默认的Tomcat容器完全扛得住只有到了几千人同时在线的规模才需要考虑换Netty。前端选Vue主要看中的是它的响应式数据流和组件化开发方式。绘画白板本质上是一个状态极其复杂的组件画布数据、工具状态、成员光标位置、聊天消息所有这些都需要实时响应。Vue的双向绑定和computed计算属性能很自然地把“服务端推送的消息”映射成“界面上的状态变化”代码写起来很直观。至于Vue 2还是Vue 3说实话如果是新项目直接上Vue 3加Composition API组合式函数在管理WebSocket连接状态时比Options API舒服太多了。1.2 系统架构与消息流转设计整个系统的架构其实不复杂核心就是“一个中心双向通道”。中心是后端服务它负责管理所有的绘画房间、房间内的成员列表、以及消息的广播转发。双向通道是WebSocket连接每个客户端连接上服务端之后服务端维护一个连接池某个客户端绘制了一笔这个消息会通过WebSocket发送到服务端服务端根据这个消息携带的房间号找到这个房间内所有的其他客户端连接然后把消息广播出去。这里有一个很多人容易搞混的点WebSocket是点对点的全双工通信但“多人协作”需要的是“一对多”的广播能力。这个广播逻辑不是浏览器自带的而是要在服务端自己实现的。所以在设计后端的时候重中之重就是把这个“房间”的概念建模清楚。我当时抽象了三个核心实体Room房间一个绘画空间包含房间ID、房间名称、创建时间、成员列表、画布历史数据。Member成员加入房间的参与者一个成员对应一个WebSocket连接包含用户ID、昵称、光标颜色、连接会话ID。Message消息在客户端和服务端之间传输的通信单元包含消息类型、房间ID、发送者信息、消息内容。消息流转的路径是这样的成员A在画布上画了一笔前端把这一笔的路径坐标、颜色、粗细、工具类型封装成一个JSON消息通过WebSocket发送到服务端。服务端解析这个消息根据房间ID找到房间里所有其他的成员连接逐个调用session.sendMessage()把消息推过去。成员B的客户端收到消息后解析内容调用Canvas的绘图API在本地画布上画出同一笔。这个过程听起来很直接但实际做起来性能、并发、可靠性、消息顺序这些问题都会冒出来后面我一个个讲。2. 核心模块拆解与关键技术点2.1 前端画布交互层怎么设计画布是用户直接操作的部分它的设计质量直接决定产品的体验。我这里用的方案是HTML5 Canvas加Vue封装没有引入Konva、Fabric这类重量级绘图库。原因有两点一是协作绘画的核心功能是自由画笔Canvas原生API完全够用没必要为了用库而用库二是引入第三方库之后它封装好的图形对象模型和WebSocket消息之间的转换反而增加了一层不必要的复杂度。Canvas交互层的核心是一个drawingBoard组件这个组件内部维护了几个关键状态工具类型画笔、橡皮擦、直线、矩形、圆形。画笔配置颜色、粗细、透明度。当前绘制状态是否正在绘制中、当前这一笔的路径点数组。画布历史用于撤销/重做的历史栈。鼠标事件的处理是整个交互层最核心的部分。mousedown时开始记录路径起点mousemove时不断把新的坐标点push进路径数组同时调用Canvas的lineTo方法画线mouseup时结束这一笔把完整的路径数据封装成消息发送到服务端。这里有一个性能优化的细节如果每移动一像素就发送一条消息那WebSocket的压力会非常大。正确的做法是节流。我在mousemove里加了简单的节流逻辑每30毫秒最多只发送一次坐标点这样既能保证绘画的流畅度又能把消息量控制在一个合理的范围。实际测试下来一条包含几十个坐标点的完整路径消息大小大概是2到3KB一个成员连续画一分钟产生的消息总量也就在几百KB级别完全在可控范围内。2.2 WebSocket消息协议设计整个协作系统的核心是消息协议这个协议设计得不好后面所有功能都会写得很痛苦。我采用的是一种轻量级的JSON文本协议每个消息统一包一层外层是消息头和消息体。消息头的结构是这样的{ type: draw, roomId: room_001, memberId: user_123, timestamp: 1698765432100, data: {} }type消息类型枚举值包括join加入房间、leave离开房间、draw绘制、cursor光标移动、undo撤销、redo重做、clear清空画布、chat聊天消息。roomId房间标识服务端根据这个字段做广播路由。memberId会员标识用于前端展示“这个笔迹是谁画的”。timestamp时间戳用于消息排序和延迟统计。data具体的业务数据不同消息类型有不同的结构。绘制消息的data结构大概长这样{ tool: brush, color: #FF0000, size: 4, points: [ {x: 120, y: 240}, {x: 121, y: 241}, {x: 125, y: 245} ] }这里有个设计细节值得参考坐标点数组只记录关键点不记录每一个像素。因为Canvas绘制曲线时本身就会做插值处理点太密反而浪费带宽和存储。我实际测下来鼠标正常移动速度下每30毫秒采样一个点画出来的曲线已经很平滑了。关于消息协议我要强调一点协议一定要做版本号预留。虽然现在看起来消息类型就那几种但随着业务扩展加新类型是必然的。如果你在协议外层预留了version字段后面升级的时候就可以做兼容处理不至于老客户端收到新消息直接解析报错。2.3 服务端房间管理与连接管理机制后端服务的核心是一个DrawingRoomManager组件它是连接管理和消息路由的中枢。我用ConcurrentHashMap来维护房间和连接的对应关系key是房间IDvalue是一个封装了房间所有成员的Room对象。每个Room对象内部又维护了一个ConcurrentHashMapkey是成员IDvalue是该成员对应的WebSocketSession对象。这里有几个并发安全的关键点需要特别注意为什么用ConcurrentHashMap而不用HashMap因为WebSocket的onMessage回调是多线程并发触发的多个客户端同时发消息Tomcat的线程池会分配不同的线程来处理如果使用普通的HashMap在高并发下扩容会出现CPU占用飙升的问题。ConcurrentHashMap内部用了分段锁机制读操作几乎无锁写操作锁粒度小性能和安全都能兼顾。为什么连接对象和成员ID都要存因为消息路由的时候需要双向查找。根据成员ID找到连接是给指定用户发消息根据连接找到成员ID是从消息里解析发送者信息之后拼接广播消息体需要用到。当一个WebSocket连接建立时前端会先发送一个join消息带上房间ID和昵称。服务端收到后执行这个逻辑检查房间是否存在不存在则新建。把成员加入房间的成员列表。给房间里其他成员广播一条memberJoin消息告诉其他人有新成员加入。把当前画布的历史操作记录返回给新成员让他的画布能同步到最新状态。第三步和第四步的顺序不能反。如果先拉历史记录再广播加入消息可能会出现新成员刚把历史记录画完又收到一条其他成员在“加入消息”之后画的笔迹两边时间线就错位了。3. 实操实现与核心代码解析3.1 SpringBoot后端WebSocket接入的两种姿势SpringBoot集成WebSocket常见的两种方式一种是使用JSR-356标准的ServerEndpoint注解另一种是使用Spring框架封装好的TextWebSocketHandler。两种我都在项目里用过说说我的感受。ServerEndpoint方式写起来更轻量只要在类上标注注解配置一个配置类把ServerEndpointExporter注册进Spring容器就能直接用了。它的优点是写法直观一个类里用OnOpen、OnMessage、OnClose、OnError注解标注生命周期方法即可。缺点是它和Spring MVC的整合度不高在端点类里注入Service很别扭需要通过静态方法获取ApplicationContext来绕代码显得不干净。Spring封装的TextWebSocketHandler方式则需要实现一个类重写afterConnectionEstablished、handleTextMessage、afterConnectionClosed、handleTransportError这几个方法。这个方式的优点是它是Spring容器管理的Bean可以正常使用Autowired依赖注入和业务层整合很顺滑。我的项目最终选了TextWebSocketHandler方式。核心配置代码大概这样Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Resource private DrawingWebSocketHandler drawingWebSocketHandler; Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(drawingWebSocketHandler, /ws/drawing) .setAllowedOrigins(*); } }这里有一个跨域的小坑如果你的前端和后端不在同一个域WebSocket握手阶段的Origin校验会失败。setAllowedOrigins(*)是开发阶段的妥协方案生产环境建议配置成具体的域名白名单否则会带来安全隐患。DrawingWebSocketHandler的核心逻辑在一个handleTextMessage方法里它的处理流程是解析JSON消息根据消息类型分派到不同的业务处理方法。Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { JSONObject payload JSON.parseObject(message.getPayload()); String type payload.getString(type); String roomId payload.getString(roomId); String memberId payload.getString(memberId); switch (type) { case join: handleJoin(session, roomId, memberId); break; case draw: case cursor: case undo: case redo: case clear: handleBroadcastMessage(roomId, memberId, message.getPayload()); break; case chat: handleChatMessage(roomId, memberId, payload); break; default: // 未知消息类型记录日志 } }handleBroadcastMessage的实现就是遍历Room里的所有成员Session排除发送者自己通常前端自己画的笔迹已经实时显示在本地画布上了不需要再回放一遍逐个发送消息。这里还需要一个异常捕获某个成员的连接如果已经失效了sendMessage会抛出异常这时候要把这个连接从房间成员列表里移除避免影响后续广播。3.2 前端Vue组件里如何维护WebSocket生命周期前端WebSocket的管理如果直接写在组件的mounted里会在组件销毁时造成连接泄漏。我在项目里把所有连接管理逻辑抽成了一个独立的模块用单例模式维护一个全局的WebSocket连接。核心的逻辑是// socket.js class DrawingSocket { constructor() { this.ws null; this.roomId null; this.memberId null; this.reconnectAttempts 0; this.maxReconnectAttempts 5; this.messageHandlers new Map(); } connect(roomId, memberId) { this.roomId roomId; this.memberId memberId; const protocol window.location.protocol https: ? wss : ws; const wsUrl ${protocol}://${window.location.host}/ws/drawing; this.ws new WebSocket(wsUrl); this.ws.onopen () { // 连接建立后发送join消息 this.send({ type: join, roomId: this.roomId, memberId: this.memberId, data: { nickname: this.nickname } }); }; this.ws.onmessage (event) { const message JSON.parse(event.data); this.dispatch(message); }; this.ws.onclose () { // 断线重连逻辑 this.handleReconnect(); }; } send(message) { if (this.ws this.ws.readyState WebSocket.OPEN) { this.ws.send(JSON.stringify(message)); } } }这个模块有几个设计要点消息分发机制前端组件通过on(type, handler)方法注册消息处理器类似于事件总线。这样画布组件只关心draw、clear、undo消息聊天组件只关心chat消息各组件之间互不干扰。断线重连策略WebSocket断开的场景很多网络抖动、服务端重启、防火墙超时等。合理的重连策略是指数退避。第一次重连等待1秒第二次2秒第三次4秒最多等待30秒同时设置最大重连次数避免无限重试消耗资源。心跳机制WebSocket连接如果长时间没有数据交互中间的网络设备可能会回收连接。解决方案是前端每30秒发送一个ping消息服务端收到后回复pong。如果连续几次都没有收到pong前端就主动断开重连。3.3 Nginx代理WebSocket配置这里必须讲一下Nginx的WebSocket代理配置因为前后端分离部署的场景下WebSocket请求默认不会走代理配置导致前端永远连不上后端。普通HTTP请求的Nginx代理不涉及协议升级但WebSocket握手需要Upgrade头Nginx默认会丢掉这个头。所以必须在location块里显式配置location /ws/ { proxy_pass http://backend-server:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 3600s; proxy_send_timeout 3600s; }这里proxy_read_timeout默认是60秒如果服务端和客户端之间有超过60秒没有数据传输Nginx就会主动断开连接。我之前就因为没配这个超时时间导致绘画过程中画着画着连接就断了排查了好久才发现是这个问题。另外如果线上用了HTTPS前端连接WebSocket时要用wss://协议而不是ws://否则浏览器会直接拦截混合内容。Nginx那边的SSL配置和普通HTTPS站点没有区别只是location /ws/块里需要加上SSL相关配置。4. 性能优化与并发冲突处理4.1 多人同时绘制时的消息风暴问题当房间里的人数增多消息量会呈指数级上升。举个例子假设每个人每秒画30个像素点每个点都发一条消息10个人同时画服务端每秒要处理300条消息虽然这个量对Tomcat来说不算什么但如果前端不节流每个点一条消息消息量就会变得很大。所以我在前端做了两个优化点位合并和定时批量发送。点位合并是把一小段时间内的点位缓存到数组里每100毫秒或者攒够20个点合并成一条消息发送。定时批量发送则是把发送频率限制在每100毫秒最多一条。这两个优化叠加后实际消息量能降到原来的三分之一左右但画面呈现的流畅度几乎没有差别。服务端这边的优化主要是在广播消息的时候做批量发送。Tomcat的WebSocketSession支持并发发送但如果直接在for循环里挨个调用sendMessage当某个连接的网络状况不好时sendMessage方法会阻塞拖慢整个循环。解决办法是使用异步发送private void broadcastToRoom(String roomId, String message, String excludeMemberId) { Room room roomManager.getRoom(roomId); if (room null) return; TextMessage textMessage new TextMessage(message); for (Member member : room.getMembers().values()) { if (member.getMemberId().equals(excludeMemberId)) { continue; } WebSocketSession session member.getSession(); if (session ! null session.isOpen()) { synchronized (session) { try { session.sendMessage(textMessage); } catch (IOException e) { // 发送失败可能是连接断了 roomManager.removeMember(roomId, member.getMemberId()); } } } } }这里用了synchronized块来保证同一个Session的发送操作线程安全。Spring的WebSocketSession并不保证sendMessage是线程安全的如果同一个Session被多个线程并发发送会出现消息内容交错甚至报错的问题。4.2 画布历史记录与持久化策略多人协作绘画还有一个很现实的问题如果有人中途加入怎么拿到之前画的内容如果服务端重启了画布内容还能恢复吗第一个问题的答案在产品设计阶段就要想清楚。轻量方案是房间内保存操作记录列表新人加入时把历史操作记录全部发送给他。这个方案实现简单但缺点也明显操作记录会越积越多当积累了几千条记录后新人加入时的数据量会很可观。我当时采用了这个方案但做了优化。服务端不保存每一个像素点的操作而是保存“完整的笔迹数据”。也就是说当一笔画完之后服务端就把这一笔的完整路径持久化到一个内存队列里。新成员加入时把队列里的所有笔迹数据一次性发给他前端收到后按顺序重放。这里要注意重放的过程不能直接生成消息发完就完了前端的重放是在本地Canvas上同步绘制速度比消息流转快得多。几千条笔迹记录重放耗时不会超过两秒。对于服务端重启的数据恢复问题我用了最简单的方式定期把操作记录序列化后存到本地文件或者数据库服务端启动时再加载回来。这个方案不高级但足够可靠。考虑到协作绘画本身是一个低频率持久化的场景没必要引入Redis或者消息队列来做持久化。4.3 橡皮擦与撤销重做的数据同步橡皮擦和撤销重做是多人协作里最容易出bug的两个功能因为它们都涉及“删除”操作而删除在分布式场景下天然比新增复杂。橡皮擦的实现我采用的方案不是真正删除画布上的像素而是在画笔图层上叠加一层白色墨迹用globalCompositeOperation destination-out来做到真正的像素擦除。但这里有个问题如果每个人的画布都是独立维护的成员A擦了某个位置成员B的本地画布上其实并没有收到擦除通知。所以橡皮擦本质上也是一种“绘制”操作只不过工具类型是eraser服务端广播的消息内容和画笔一样是坐标点数组和粗细。前端收到后以同样的方式在本地画布上用destination-out进行擦除。撤销重做在多人协作场景下是一个非常复杂的话题因为撤销的语义在多人场景下会变得模糊A撤销了自己刚才的一笔是只撤销A自己的操作还是撤销整个画布上最后一步操作这里我采用的是最简单的语义只允许撤销自己的操作。实现方式是在后端保存每个成员的操作记录栈当成员A发来撤销请求时服务端找到A的最后一次操作记录把它从房间的历史记录里标记为“已撤销”然后广播一条撤销消息所有成员客户端都执行对应的撤销逻辑。这个方法不是完美的比如多人几乎同时画完一笔撤销的时候顺序可能会比较混乱但对于一个教学场景或者小团队协作场景这个语义已经足够清晰了。4.4 前端Canvas性能与内存管理Canvas在长时间绘制之后如果不做清理内存占用会越来越大浏览器开始卡顿。我优化了几个地方第一点关闭Canvas的平滑缩放。如果画布固定尺寸不需要响应式缩放可以设置canvas.width为固定值配合CSS做缩放显示再加上ctx.imageSmoothingEnabled false能省下一部分渲染开销。第二点控制重绘范围。每次只有收到新的绘制消息时才重绘这一个笔迹相关的区域而不是每次都清空整个画布重画。这在画布内容很多的时候性能差距非常明显。第三点定期清理远端历史记录。当本地维护的操作记录超过一定数量比如2000条就把最老的记录从内存里移除只保留画面效果。否则Vue的响应式系统会一直追踪这些不断增长的数组引用造成内存泄漏和渲染性能下降。5. 常见问题排查与避坑指南5.1 开发部署中典型的连接与配置问题我在开发和部署过程中遇到过很多连接相关的问题这些问题在本地环境不容易出现一上测试环境或者生产环境就暴露了。整理几个最有代表性的。问题一WebSocket连接握手失败报403这个通常是setAllowedOrigins配置得太严格导致的。浏览器在发起WebSocket连接时会带上Origin头如果这个域名不在允许列表里握手就会失败。解决办法是在registerWebSocketHandlers方法里配置允许的来源或者在前端做代理转发让前后端同源。问题二连接偶尔断开没有任何报错日志这种情况十有八九是Nginx的超时配置问题。如果proxy_read_timeout设置得太短默认60秒Nginx会在空闲一段时间后主动断开连接。解决办法是配置长超时时间同时增加心跳机制让连接保持活跃状态。问题三关闭浏览器标签页后服务端还是有残留连接这是因为浏览器突然关闭时WebSocket的关闭帧可能发送不出去服务端感知不到连接断开。解决办法有两个层面服务端加心跳检测定期检查连接的空闲状态并清理前端在beforeunload事件里发送一个leave消息然后主动关闭连接。问题四多人同时画布时画面出现明显延迟这个问题的根源可能是消息量过大也可能是消息处理路径上有多余的环节。我之前遇到过一次是因为服务端广播消息时走了MQ多了一层无谓的转发。后来把MQ去掉直接把消息从Tomcat线程池推给WebSocketSession延迟从几百毫秒降到了十几毫秒。5.2 调试WebSocket消息的工具与方法WebSocket的调试比普通HTTP接口要麻烦一些因为没有现成的URL可以在浏览器里直接访问。我常用的调试工具和技巧浏览器DevTools的Network面板。Chrome DevTools的Network面板里有一个WS标签可以查看WebSocket的每条收发消息还能直接看到握手请求的请求头和响应头。调试消息格式、确认消息有没有发送成功用这个最方便。服务端日志打点。我在消息处理的入口和出口都打了日志记录消息类型、房间ID、成员ID、消息大小、耗时。定位问题的时候对比入口日志和出口日志的时间差就能判断是服务端处理慢了还是网络传输慢了。用Postman模拟WebSocket客户端。Postman支持WebSocket请求可以手动连接、发消息、收消息。调试服务端逻辑的时候我经常先不开前端直接用Postman去连接WebSocket手动发各种格式的消息验证服务端的消息解析和广播逻辑是否正确。前端断点调试。Vue项目里可以在WebSocket的onmessage回调里打断点查看收到的原始数据。这个在调试数据格式不匹配的问题时特别有用。5.3 几个容易忽略的细节最后分享几个项目里容易忽略但很影响体验的细节。消息确认机制的重要性。WebSocket发送消息是“发完即忘”的不保证对端一定收到。在协作绘画这个场景下一条绘制消息丢了画面就会出现一个缺口这个问题很难排查。解决方案是在前端加一个简单的确认机制服务端收到消息后返回一个ack消息前端如果发送后超过一定时间没收到ack就重发。实际操作中我发现这个机制对普通绘画场景不是刚需但对clear清空画布和undo撤销这类“状态变更”消息确认机制很有必要。前端消息处理要幂等。因为有了重发机制前端收到重复消息的概率就增加了。处理消息时要设计成幂等的比如清空画布操作执行两次和一次效果一样不会出现状态错乱。房间人数上限的控制。一个房间的成员数不能无限增加否则服务端的广播压力会线性上涨。我在服务端做了限制默认一个房间最多20人。超出后新成员会收到一条roomFull消息客户端弹出提示。这个数值可以根据服务器的带宽和CPU配置调整。6. 从能用到好用协作体验提升的进阶技巧6.1 光标实时同步与在线状态展示基础版的协作绘画只要有画笔同步就能用了但“能用”和“好用”之间差了好几个细节。第一个值得加的功能是实时光标同步。当成员A移动鼠标时把鼠标的坐标点通过WebSocket广播给房间里的其他成员其他成员在自己的画布上在对应坐标绘制一个带有成员昵称和颜色标识的小光标。这个功能看似简单但对协作感的提升是质变——你能够看到其他人正在画什么位置就不用频繁用语言沟通“你画到哪里了”。实现上光标消息的data结构很简单只有x和y坐标{ type: cursor, roomId: room_001, memberId: user_123, data: { x: 520, y: 310 } }光标移动消息的发送频率比画笔消息要高很多但不需要做像素级同步。前端收到光标消息后用一个绝对定位的元素跟随鼠标移动即可。值得注意的是光标位置消息也应该做节流我实际测试下来100毫秒发一次够用了再高的频率纯属浪费带宽。6.2 成员管理和发言权限的落地多人协作平台不可避免地要考虑成员管理的权限问题。绘画场景里最基础的需求是“谁可以画谁只能看”。我做的方案是房间创建者默认是管理员管理员可以在成员列表里把某个成员设为“只读”只读成员依然能看到其他人的绘制过程但画笔消息在前端会被拦截不允许发送。权限控制的实现要放在服务端而不是前端。前端开关只是一个体验优化真正要拦截非法绘制请求需要在服务端校验每个绘制消息的发送者是否具有绘画权限。我之前只在前端做了控制结果有人绕过前端直接往WebSocket发消息照样能画。后来在服务端加了一个PermissionChecker组件每次收到draw消息时先查一下该成员的权限没有权限的直接丢弃。6.3 白板导出与离线回放白板画完之后通常需要把成果保存下来。我做了两种形式的导出一种是导出图片。用Canvas的toDataURL(image/png)方法把当前画布内容转成图片然后通过a标签下载。这个实现很简单注意点在于画布尺寸如果超过了浏览器的Canvas上限需要分段导出再拼接。另一种是导出操作记录。把房间内的所有操作记录序列化成JSON文件保存到本地。以后想看这段绘画过程只要把这个JSON文件导入回系统前端就能重放整个绘画过程就像看视频回放一样。这个功能在教学中特别有用学生可以回看老师画图的每一步操作。6.4 多房间隔离与画布数据隔离最后一个不得不提的是多房间隔离问题。如果服务端只做单房间管理代码会很简单但真实使用场景一定需要多个房间同时存在。我设计了一个RoomManager内部维护房间的创建、销毁、查询接口每个房间都是完全隔离的消息不会互相串扰。房间销毁是一个容易被忽略的点。当房间里最后一个成员离开时应该自动把房间从RoomManager里移除并清理掉它的历史记录缓存否则长时间运行后内存里会堆积大量废弃房间的数据。我加了一个定时任务每小时扫描一次把超过一定时间没有活跃成员的空房间清理掉。关于房间隔离还有一个安全细节服务端在收到消息时一定要校验roomId在请求的URL或Token中是否合法比如是否有权限加入这个房间。如果忽略这个校验任何知道房间ID的人都可以伪造消息加入任意房间造成数据泄露。最后想说的一点做这个多人实时协作绘画平台技术上最核心的难点就是WebSocket的可靠性和消息同步的序列一致性。WebSocket本身的API并不复杂复杂的是连接生命周期管理、断线重连、消息去重、以及多人场景下的操作冲突处理。我踩过最深的坑是连接断开后没有及时从服务端的连接池里移除失效连接导致广播消息时一直在给无效连接发数据日志刷屏服务端CPU飙升。后来排查了很久发现是客户端浏览器关闭标签页时服务端没有触发close事件。从那之后我开始在服务端加心跳检测定期清理无效连接这个问题才算彻底解决。如果你正准备上手类似的项目我建议你先做一个最小可行版本一个房间、两个人、一支画笔。先把WebSocket打通把画笔消息从A传到B再逐步加上多人管理、房间隔离、历史回放这些进阶功能。不要太早引入复杂的架构和中间件这个项目本身的技术复杂度Tomcat加SpringBoot完全足以支撑真正的复杂度都在业务逻辑和细节处理上。本文还有配套的精品资源点击获取
返回列表