Rust + WebAssembly 实现多人在线共享白板:从设计到性能验证

技术选型考虑
1. 为什么选择 Rust + WebAssembly?
Rust 是一种内存安全、高性能的系统编程语言,其编译器通过 借用检查器(Borrow Checker) 和 所有权(Ownership) 机制,在编译期消除空指针、数据竞争等常见错误。
WebAssembly(Wasm) 是一种虚拟机指令集,允许代码在浏览器中以接近原生的速度运行。Rust 通过 wasm-bindgen 工具链,可以将 Rust 代码编译为 Wasm 模块,并与 JavaScript 无缝交互。
本项目设计一个 多人在线共享白板,通过 Rust + WebAssembly 实现以下目标:
- 高性能绘图逻辑:Rust 负责图形路径计算,Wasm 提供快速执行环境;
- 低延迟通信:WebSocket 实现实时指令同步;
- 内存安全:Rust 的编译期检查确保无越界访问或悬垂指针;
- 跨平台兼容:支持所有现代浏览器,无需安装插件。

WebAssembly 入门
2. 技术选型与核心概念
2.1 WebAssembly 与 wasm-bindgen
- WebAssembly:一种二进制格式的虚拟机指令集,浏览器通过 Wasm 模块执行代码,性能接近原生 C/C++。
- wasm-bindgen:Rust 的 WebAssembly 绑定生成工具,自动处理 Rust 与 JavaScript 的类型转换(如
String↔str,Vec<u8>↔ArrayBuffer)。
wasm-bindgen 是 Rust 与 WebAssembly(WASM)生态中的核心桥梁工具,由 Rust 官方WebAssembly 工作组主导开发。它的核心目标是:让 Rust 编译成的 WebAssembly 模块能无缝与 JavaScript互操作,就像调用原生 JS 函数或使用 DOM API 一样自然。

2.2 WebSocket 通信
- WebSocket:双向实时通信协议,适合多人协作场景(如白板指令广播)。
- Tokio:Rust 的异步运行时,提供非阻塞 I/O 支持,适合高并发服务器开发。

2.3 图形渲染方案
- Canvas API:HTML5 的 2D 渲染上下文,适合简单绘图;
- WebGL:基于 OpenGL ES 的 3D 图形 API,性能更高但实现复杂;
- 选择 Canvas:兼顾实现难度与性能需求,Rust 负责路径计算,JavaScript 负责最终绘制。

3. 客户端实现(Rust + WebAssembly)
3.1 核心结构体定义
#[derive(Serialize, Deserialize)]
pub enum DrawingCommand {Line { from: (f64, f64), to: (f64, f64), color: String },Clear,
}#[wasm_bindgen]
pub struct Whiteboard {commands: Vec<DrawingCommand>,ws: web_sys::WebSocket,
}
- DrawingCommand:定义白板操作类型(如画线、清屏);
- Whiteboard:封装 WebSocket 通信与命令管理逻辑。
3.2 鼠标事件绑定
#[wasm_bindgen]
impl Whiteboard {pub fn new(url: &str) -> Result<Whiteboard, JsValue> {let ws = web_sys::WebSocket::new(url)?;Ok(Whiteboard {commands: Vec::new(),ws,})}pub fn on_mouse_move(&mut self, x: f64, y: f64) {// 记录路径并发送指令let cmd = DrawingCommand::Line {from: (self.last_x, self.last_y),to: (x, y),color: "#000000".to_string(),};self.send_command(cmd);}
}
3.3 构建 WebAssembly 模块
wasm-pack build --target web
# 输出目录:pkg/whiteboard_client.js + whiteboard_client_bg.wasm
4. 前端集成(JavaScript + HTML)
4.1 加载 Wasm 模块
<script type="module">import init from './pkg/whiteboard_client.js';let instance;async function initWasm() {instance = await init();const whiteboard = new instance.Whiteboard("ws://localhost:8080");canvas.addEventListener('mousemove', (e) => {whiteboard.on_mouse_move(e.offsetX, e.offsetY);});whiteboard.add_command = (cmd) => {instance.whiteboard.add_command(cmd);redraw();};}function redraw() {// 通过 Canvas 渲染所有 commands// 此处省略具体绘制逻辑}
</script>
5. 服务器端实现(Rust + Tokio)
5.1 WebSocket 广播服务器
use tokio::net::TcpListener;
use tokio_tungstenite::{accept_async, WebSocketStream};
use futures::StreamExt;#[tokio::main]
async fn main() {let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();println!("WebSocket server running on ws://localhost:8080");while let Ok((stream, _)) = listener.accept().await {tokio::spawn(handle_connection(stream));}
}async fn handle_connection(stream: tokio::net::TcpStream) {let ws_stream = accept_async(stream).await.unwrap();let (mut write, mut read) = ws_stream.split();let (tx, rx) = tokio::sync::broadcast::channel(100);read.for_each(|msg| async {if let Ok(msg) = msg {if let Ok(text) = msg.to_text() {if let Ok(cmd) = serde_json::from_str::<DrawingCommand>(text) {tx.send(serde_json::to_string(&cmd).unwrap()).unwrap();}}}}).await;let mut rx = rx.subscribe();while let Ok(cmd) = rx.recv().await {write.send(tokio_tungstenite::tungstenite::Message::Text(cmd)).await.unwrap();}
}
5. 参考文献
- Rust 官方文档:https://doc.rust-lang.org
- WebAssembly 官方指南:https://webassembly.org
- wasm-bindgen GitHub:https://github.com/rustwasm/wasm-bindgen
- Tokio 异步运行时:https://tokio.rs
- WebSocket 协议规范:https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
- Canvas 与 WebGL 对比:https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API

设计说明
1. 项目概述
1.1 产品愿景
开发一个高性能、低延迟的多人在线共享白板应用,支持实时协作绘图,为远程教育、团队协作、在线会议等场景提供专业的绘图工具。
1.2 核心价值
- 高性能绘图:基于Rust + WebAssembly的图形渲染引擎
- 实时协作:毫秒级延迟的多用户同步
- 安全可靠:内存安全的底层架构
- 跨平台兼容:无需安装插件的Web应用
2. 功能需求

2.1 核心功能模块
2.1.1 绘图工具
-
基础绘图工具
- 铅笔/画笔工具
- 直线/曲线工具
- 矩形/圆形工具
- 文字输入工具
- 橡皮擦工具
-
高级绘图功能
- 图形选择与编辑
- 图层管理
- 撤销/重做操作
- 图形缩放与旋转
2.1.2 多人协作
-
实时同步机制
- WebSocket实时通信
- 操作指令同步
- 冲突解决策略
- 用户状态显示
-
用户管理
- 用户身份标识
- 权限控制(创建者/参与者)
- 用户列表显示
- 用户光标追踪
2.1.3 白板管理
-
白板操作
- 新建/保存白板
- 导入/导出功能
- 白板模板库
- 历史版本管理
-
画布设置
- 背景颜色/网格设置
- 画布尺寸调整
- 缩放与平移
- 标尺与参考线
3. 项目效果

//websocket.rs
use actix_web::{web, HttpRequest, HttpResponse};
use actix_web_actors::ws;
use uuid::Uuid;
use chrono::Utc;use crate::services::room::RoomService;// 导入必要的actix模块
use actix::prelude::*;pub struct WebSocketConnection {pub user_id: Uuid,pub room_id: String,pub room_service: web::Data<RoomService>,
}impl actix::Actor for WebSocketConnection {type Context = ws::WebsocketContext<Self>;fn started(&mut self, ctx: &mut Self::Context) {log::info!("WebSocket连接建立: 用户 {} 加入房间 {}", self.user_id, self.room_id);// 通知房间内其他用户有新用户加入let message = serde_json::json!({"type": "user_joined","user_id": self.user_id.to_string(),"timestamp": Utc::now().timestamp_millis()});// 暂时直接发送回客户端(测试用)ctx.text(message.to_string());}fn stopped(&mut self, _ctx: &mut Self::Context) {log::info!("WebSocket连接关闭: 用户 {} 离开房间 {}", self.user_id, self.room_id);}
}impl actix::StreamHandler<Result<ws::Message, ws::ProtocolError>> for WebSocketConnection {fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {match msg {Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),Ok(ws::Message::Text(text)) => {log::debug!("收到WebSocket消息: {}", text);// 处理绘图操作消息match serde_json::from_str::<serde_json::Value>(&text) {Ok(data) => {if let Some(msg_type) = data.get("type").and_then(|t| t.as_str()) {match msg_type {"drawing_operation" => {// 处理绘图操作self.handle_drawing_operation(&data, ctx);}"cursor_move" => {// 处理光标移动self.handle_cursor_move(&data, ctx);}"chat_message" => {// 处理聊天消息self.handle_chat_message(&data, ctx);}_ => {log::warn!("未知的消息类型: {}", msg_type);}}}}Err(e) => {log::error!("消息解析失败: {}", e);}}}Ok(ws::Message::Close(reason)) => {ctx.close(reason);ctx.stop();}_ => {}}}
}impl WebSocketConnection {fn handle_drawing_operation(&mut self, data: &serde_json::Value, ctx: &mut ws::WebsocketContext<Self>) {// 广播绘图操作给房间内其他用户let broadcast_msg = serde_json::json!({"type": "drawing_operation","data": data,"user_id": self.user_id.to_string(),"timestamp": Utc::now().timestamp_millis()});// 暂时直接发送回客户端(测试用)ctx.text(broadcast_msg.to_string());}fn handle_cursor_move(&mut self, data: &serde_json::Value, ctx: &mut ws::WebsocketContext<Self>) {// 广播光标移动给房间内其他用户let broadcast_msg = serde_json::json!({"type": "cursor_move","data": data,"user_id": self.user_id.to_string(),"timestamp": Utc::now().timestamp_millis()});// 暂时直接发送回客户端(测试用)ctx.text(broadcast_msg.to_string());}fn handle_chat_message(&mut self, data: &serde_json::Value, ctx: &mut ws::WebsocketContext<Self>) {// 广播聊天消息给房间内其他用户let broadcast_msg = serde_json::json!({"type": "chat_message","data": data,"user_id": self.user_id.to_string(),"timestamp": Utc::now().timestamp_millis()});// 暂时直接发送回客户端(测试用)ctx.text(broadcast_msg.to_string());}
}pub async fn websocket_handler(req: HttpRequest,stream: web::Payload,room_service: web::Data<RoomService>,path: web::Path<(String, String)>,
) -> Result<HttpResponse, actix_web::Error> {let (room_id, user_id) = path.into_inner();let user_uuid = Uuid::parse_str(&user_id).map_err(|_| {log::error!("无效的用户ID: {}", user_id);actix_web::error::ErrorBadRequest("无效的用户ID")})?;// 验证用户和房间if room_service.get_room(&room_id).await.is_none() {return Err(actix_web::error::ErrorNotFound("房间不存在"));}let ws = WebSocketConnection {user_id: user_uuid,room_id,room_service,};let resp = ws::start(ws, &req, stream)?;Ok(resp)
}pub fn config(cfg: &mut web::ServiceConfig) {cfg.service(web::scope("/ws").route("/{room_id}/{user_id}", web::get().to(websocket_handler)),);
}
//api.rs
use actix_web::{web, HttpResponse, Result};
use serde::{Deserialize, Serialize};
use uuid::Uuid;use crate::services::{room::RoomService, user::UserService};// 健康检查路由
pub async fn health_check() -> Result<HttpResponse> {Ok(HttpResponse::Ok().json(serde_json::json!({"status": "ok","service": "墨契白板服务器","version": "1.0.0"})))
}#[derive(Debug, Serialize, Deserialize)]
pub struct CreateRoomRequest {pub name: String,pub creator_id: Uuid,
}#[derive(Debug, Serialize, Deserialize)]
pub struct JoinRoomRequest {pub room_id: String,pub user_id: Uuid,
}#[derive(Debug, Serialize, Deserialize)]
pub struct UserInfo {pub id: Uuid,pub name: String,pub color: String,
}pub async fn create_room(room_service: web::Data<RoomService>,req: web::Json<CreateRoomRequest>,
) -> Result<HttpResponse> {let room_id = room_service.create_room(req.name.clone(), req.creator_id).await.map_err(|e| {log::error!("创建房间失败: {}", e);actix_web::error::ErrorInternalServerError("创建房间失败")})?;Ok(HttpResponse::Ok().json(serde_json::json!({"success": true,"room_id": room_id,"message": "房间创建成功"})))
}pub async fn join_room(room_service: web::Data<RoomService>,user_service: web::Data<UserService>,req: web::Json<JoinRoomRequest>,
) -> Result<HttpResponse> {let user = user_service.get_user(req.user_id).await.ok_or_else(|| {log::error!("用户不存在: {}", req.user_id);actix_web::error::ErrorBadRequest("用户不存在")})?;room_service.join_room(&req.room_id, user).await.map_err(|e| {log::error!("加入房间失败: {}", e);actix_web::error::ErrorBadRequest("加入房间失败")})?;Ok(HttpResponse::Ok().json(serde_json::json!({"success": true,"message": "加入房间成功"})))
}pub async fn get_room_info(room_service: web::Data<RoomService>,path: web::Path<String>,
) -> Result<HttpResponse> {let room_id = path.into_inner();let room = room_service.get_room(&room_id).await.ok_or_else(|| {log::error!("房间不存在: {}", room_id);actix_web::error::ErrorNotFound("房间不存在")})?;Ok(HttpResponse::Ok().json(room))
}pub async fn get_room_users(room_service: web::Data<RoomService>,path: web::Path<String>,
) -> Result<HttpResponse> {let room_id = path.into_inner();let users = room_service.get_room_users(&room_id).await.ok_or_else(|| {log::error!("房间不存在: {}", room_id);actix_web::error::ErrorNotFound("房间不存在")})?;Ok(HttpResponse::Ok().json(users))
}pub fn config(cfg: &mut web::ServiceConfig) {cfg.route("/", web::get().to(health_check)).service(web::scope("/api").route("/rooms", web::post().to(create_room)).route("/rooms/join", web::post().to(join_room)).route("/rooms/{room_id}", web::get().to(get_room_info)).route("/rooms/{room_id}/users", web::get().to(get_room_users)),);
}
import { ref } from 'vue'
import type { DrawingElement, Point } from '@/types/drawing'
import type { User } from '@/types/user'// WebSocket消息类型
export enum MessageType {JOIN_ROOM = 'join_room',LEAVE_ROOM = 'leave_room',DRAW_ELEMENT = 'draw_element',UPDATE_ELEMENT = 'update_element',DELETE_ELEMENT = 'delete_element',USER_CURSOR = 'user_cursor',USER_JOINED = 'user_joined',USER_LEFT = 'user_left'
}// WebSocket消息接口
export interface WebSocketMessage {type: MessageTypedata: anytimestamp: numberuserId: stringroomId: string
}// WebSocket服务类
export class WebSocketService {private ws: WebSocket | null = nullprivate reconnectAttempts = 0private maxReconnectAttempts = 5private reconnectInterval = 3000// 事件回调public onMessage: ((message: WebSocketMessage) => void) | null = nullpublic onConnect: (() => void) | null = nullpublic onDisconnect: (() => void) | null = nullpublic onError: ((error: Event) => void) | null = null// 连接状态public isConnected = ref(false)// 连接到WebSocket服务器connect(serverUrl: string, roomId: string, userId: string): Promise<void> {return new Promise((resolve, reject) => {try {// 确保URL格式正确const url = serverUrl.startsWith('ws://') || serverUrl.startsWith('wss://') ? serverUrl : `ws://${serverUrl}`this.ws = new WebSocket(`${url}?roomId=${roomId}&userId=${userId}`)this.ws.onopen = () => {console.log('WebSocket连接成功')this.isConnected.value = truethis.reconnectAttempts = 0this.onConnect?.()resolve()}this.ws.onmessage = (event) => {try {const message: WebSocketMessage = JSON.parse(event.data)this.onMessage?.(message)} catch (error) {console.error('解析WebSocket消息失败:', error)}}this.ws.onclose = (event) => {console.log('WebSocket连接关闭:', event.code, event.reason)this.isConnected.value = falsethis.onDisconnect?.()// 自动重连if (this.reconnectAttempts < this.maxReconnectAttempts) {setTimeout(() => {this.reconnectAttempts++console.log(`尝试重新连接 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`)this.connect(serverUrl, roomId, userId)}, this.reconnectInterval)}}this.ws.onerror = (error) => {console.error('WebSocket连接错误:', error)this.onError?.(error)reject(error)}} catch (error) {reject(error)}})}// 断开连接disconnect(): void {if (this.ws) {this.ws.close()this.ws = nullthis.isConnected.value = false}}// 发送消息sendMessage(message: Omit<WebSocketMessage, 'timestamp'>): void {if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {console.warn('WebSocket未连接,无法发送消息')return}const fullMessage: WebSocketMessage = {...message,timestamp: Date.now()}this.ws.send(JSON.stringify(fullMessage))}// 发送绘图元素sendDrawingElement(element: DrawingElement, roomId: string, userId: string): void {this.sendMessage({type: MessageType.DRAW_ELEMENT,data: element,userId,roomId})}// 发送元素更新sendElementUpdate(elementId: string, updates: Partial<DrawingElement>, roomId: string, userId: string): void {this.sendMessage({type: MessageType.UPDATE_ELEMENT,data: { elementId, updates },userId,roomId})}// 发送元素删除sendElementDelete(elementId: string, roomId: string, userId: string): void {this.sendMessage({type: MessageType.DELETE_ELEMENT,data: { elementId },userId,roomId})}// 发送用户光标位置sendUserCursor(position: Point, roomId: string, userId: string): void {this.sendMessage({type: MessageType.USER_CURSOR,data: { position },userId,roomId})}// 发送用户加入房间sendUserJoin(user: User, roomId: string): void {this.sendMessage({type: MessageType.USER_JOINED,data: { user },userId: user.id,roomId})}// 发送用户离开房间sendUserLeave(userId: string, roomId: string): void {this.sendMessage({type: MessageType.USER_LEFT,data: { userId },userId,roomId})}
}// 创建WebSocket服务实例
export const webSocketService = new WebSocketService()// 模拟WebSocket服务器(开发环境使用)
export class MockWebSocketServer {private clients: Map<string, any> = new Map()// 模拟接收消息并广播handleMessage(message: WebSocketMessage, clientId: string): void {// 模拟服务器处理逻辑switch (message.type) {case MessageType.JOIN_ROOM:this.broadcastMessage({type: MessageType.USER_JOINED,data: { user: message.data },userId: message.userId,roomId: message.roomId,timestamp: Date.now()}, clientId)breakcase MessageType.DRAW_ELEMENT:case MessageType.UPDATE_ELEMENT:case MessageType.DELETE_ELEMENT:case MessageType.USER_CURSOR:// 广播给房间内其他用户this.broadcastMessage(message, clientId)breakdefault:console.log('未知消息类型:', message.type)}}// 模拟广播消息private broadcastMessage(message: WebSocketMessage, excludeClientId?: string): void {// 在实际项目中,这里应该只广播给同一房间的用户this.clients.forEach((client, clientId) => {if (clientId !== excludeClientId && client.roomId === message.roomId) {// 模拟网络延迟setTimeout(() => {if (typeof client.onMessage === 'function') {client.onMessage({ data: JSON.stringify(message) })}}, Math.random() * 100 + 50) // 50-150ms延迟}})}// 添加客户端addClient(clientId: string, client: any): void {this.clients.set(clientId, client)}// 移除客户端removeClient(clientId: string): void {this.clients.delete(clientId)}
}// 创建模拟服务器实例
export const mockWebSocketServer = new MockWebSocketServer()
import type { User, Room } from '@/types/user'// API基础配置
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8084'// API响应接口
interface ApiResponse<T = any> {success: booleandata?: Tmessage?: stringerror?: string
}// 创建房间请求
interface CreateRoomRequest {name: stringcreator_id: string
}// 加入房间请求
interface JoinRoomRequest {room_id: stringuser_id: string
}// API服务类
export class ApiService {// 通用请求方法private async request<T>(endpoint: string,options: RequestInit = {}): Promise<ApiResponse<T>> {try {const url = `${API_BASE_URL}${endpoint}`const response = await fetch(url, {headers: {'Content-Type': 'application/json',...options.headers,},...options,})if (!response.ok) {throw new Error(`HTTP ${response.status}: ${response.statusText}`)}const data = await response.json()return { success: true, data }} catch (error) {console.error('API请求失败:', error)const errorMessage = error instanceof Error ? error.message : '未知错误'// 在控制台输出错误信息,不在UI中显示console.error(`API请求失败: ${errorMessage}`)return { success: false, error: errorMessage }}}// 健康检查async healthCheck(): Promise<ApiResponse<{ status: string; service: string; version: string }>> {return this.request('/')}// 创建房间async createRoom(roomData: CreateRoomRequest): Promise<ApiResponse<{ room_id: string; message: string }>> {return this.request('/api/rooms', {method: 'POST',body: JSON.stringify(roomData),})}// 加入房间async joinRoom(joinData: JoinRoomRequest): Promise<ApiResponse<{ message: string }>> {return this.request('/api/rooms/join', {method: 'POST',body: JSON.stringify(joinData),})}// 获取房间信息async getRoomInfo(roomId: string): Promise<ApiResponse<Room>> {return this.request(`/api/rooms/${roomId}`)}// 获取房间用户列表async getRoomUsers(roomId: string): Promise<ApiResponse<User[]>> {return this.request(`/api/rooms/${roomId}/users`)}// 创建用户async createUser(userData: Partial<User>): Promise<ApiResponse<User>> {// 这里可以扩展为调用后端用户创建API// 目前使用前端生成的用户IDconst user: User = {id: userData.id || `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,name: userData.name || '匿名用户',color: userData.color || '#3498db',isOnline: true,lastActive: Date.now(),}return { success: true, data: user }}// 验证房间是否存在async validateRoom(roomId: string): Promise<boolean> {const response = await this.getRoomInfo(roomId)return response.success && response.data !== undefined}
}// 创建API服务实例
export const apiService = new ApiService()// API工具函数
export const apiUtils = {// 生成房间链接generateRoomLink(roomId: string): string {return `${window.location.origin}/room/${roomId}`},// 解析房间IDparseRoomIdFromUrl(url: string): string | null {const match = url.match(/\/room\/([^/?]+)/)return match ? match[1] : null},// 格式化错误消息formatErrorMessage(error: any): string {if (typeof error === 'string') return errorif (error?.message) return error.messagereturn '未知错误'}
}
4. 小结
通过将核心绘图逻辑(如图形操作、撤销栈、指令序列化)用 Rust 编写,并借助 wasm-pack 和 wasm-bindgen 编译为 WebAssembly 模块,不仅实现了接近原生的执行效率,还显著提升了代码的可靠性与可维护性。
