
技术选型考虑
1. 为什么选择 Rust + WebAssembly?
Rust 是一种内存安全、高性能的系统编程语言,其编译器通过 借用检查器(Borrow Checker) 和 所有权(Ownership) 机制,在编译期消除空指针、数据竞争等常见错误。
WebAssembly(Wasm) 是一种虚拟机指令集,允许代码在浏览器中以接近原生的速度运行。Rust 通过 wasm-bindgen 工具链,可以将 Rust 代码编译为 Wasm 模块,并与 JavaScript 无缝交互。
本项目设计一个 多人在线共享白板,通过 Rust + WebAssembly 实现以下目标:
- 高性能绘图逻辑:Rust 负责图形路径计算,Wasm 提供快速执行环境;
- 低延迟通信:WebSocket 实现实时指令同步;
- 内存安全:Rust 的编译期检查确保无越界访问或悬垂指针;
- 跨平台兼容:支持所有现代浏览器,无需安装插件。
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: MessageType
data: any
timestamp: number
userId: string
roomId: string
}
// WebSocket服务类
export class WebSocketService {
private ws: WebSocket | null = null
private reconnectAttempts = 0
private maxReconnectAttempts = 5
private reconnectInterval = 3000
// 事件回调
public onMessage: ((message: WebSocketMessage) => void) | null = null
public onConnect: (() => void) | null = null
public onDisconnect: (() => void) | null = null
public 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 = true
this.reconnectAttempts = 0
this.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 = false
this.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 = null
this.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)
break
case MessageType.DRAW_ELEMENT:
case MessageType.UPDATE_ELEMENT:
case MessageType.DELETE_ELEMENT:
case MessageType.USER_CURSOR:
// 广播给房间内其他用户
this.broadcastMessage(message, clientId)
break
default:
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: boolean
data?: T
message?: string
error?: string
}
// 创建房间请求
interface CreateRoomRequest {
name: string
creator_id: string
}
// 加入房间请求
interface JoinRoomRequest {
room_id: string
user_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
// 目前使用前端生成的用户ID
const 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}`
},
// 解析房间ID
parseRoomIdFromUrl(url: string): string | null {
const match = url.match(/\/room\/([^/?]+)/)
return match ? match[1] : null
},
// 格式化错误消息
formatErrorMessage(error: any): string {
if (typeof error === 'string') return error
if (error?.message) return error.message
return '未知错误'
}
}
4. 小结
通过将核心绘图逻辑(如图形操作、撤销栈、指令序列化)用 Rust 编写,并借助 wasm-pack 和 wasm-bindgen 编译为 WebAssembly 模块,不仅实现了接近原生的执行效率,还显著提升了代码的可靠性与可维护性。

浙公网安备 33010602011771号