在 HTML5 Canvas、WebSocket、JQuery 和 ASP.NET 中找乐子。最终成果:网页上的实时白板!






4.97/5 (91投票s)
玩转一些最前沿的技术,开发了一个网页上的实时绘图白板,允许多人协作,并且每个人都能实时看到相同的视图,无需刷新页面。
引言
团队讨论的最佳沟通媒介是什么?
当然是经典的白板,只需一块粉笔和一个擦子。
是的,确实如此。如果你们团队的几位成员需要讨论某件事,并且你们都能在同一个地方碰面,那么白板就是表达和分享想法的最佳方式。但现在是业务外包的时代,当你需要与客户或合作伙伴讨论问题时,你可能身处华盛顿,他身处苏黎世,而你身处达卡。在这种情况下,没有白板可以帮助你们在远程进行讨论,对吧?
好吧,以前确实是这样。现在不一样了。我开发了一个非常简单的网页白板,可以使用鼠标绘图,或者使用键盘输入文字来表达你的观点给其他团队成员,无论他们身在何处。只要他们都参与到同一个白板会话中,他们就能在你完成后立即看到你在白板上画的或写的内容,而且他们无需刷新页面。当你画了东西在白板上,或者在聊天窗口写了字,所有其他人都会立即看到。同样,当其他参与者在白板上画图或在讨论窗口写字时,你也能立即看到他们的即时反馈。是的,我们谈论的是一个网页上的实时白板!
想象一下,白板可以用来演示古埃及金字塔的概念!
以下是埃及国王法老如何使用白板向他的建筑师福凯纳演示金字塔的概念。
福凯纳和法老都使用他们的 HTML5 兼容浏览器登录白板,并互相打招呼(假设在这个 Web 2.0 时代,他们不再遵循冗长的君臣礼节)。
福凯纳的白板
法老的白板
法老说:“我想建一座金字塔!”
图:白板演示
福凯纳说:“陛下,金字塔会是这样的吗?”
法老说:“金字塔是这样的”
福凯纳说:“嗯……如果陛下允许,从上面看,它会是这样的吗?”
法老说:“是的, exactly”
你看,通过白板演示一个想法是多么容易?一点点的绘画,加上最少的交谈,就帮助理解了这个想法。图片确实能胜过千言万语!
运行白板应用程序
请按照以下步骤运行白板应用程序:
- 下载 *whiteBoard.zip*(下载链接在本文章顶部)并将其解压到方便的位置。
- 在 IIS 中创建一个 ASP.NET 4.0 网站(例如,
http://whiteboard
),将其指向解压文件夹中的 Web 应用程序文件夹“whiteboard”。 - 在 hosts 文件中添加一个条目,使网站名称“whiteboard”指向本地 IP。
whiteboard 127.0.0.1
- 使用以下 HTML5 支持的浏览器之一,通过浏览 URL
http://whiteboard/whiteboard.aspx
来访问白板应用程序,请使用两个不同的浏览器窗口。- IE9
- Google Chrome
- Safari 5.1
- 通过在文本框中输入您的名字并按“OK”来识别自己。完成!
您可以使用白板执行以下操作:
- 通过拖动鼠标在白板上绘图(如果通过点击白板下方的铅笔图标选择了铅笔模式,该模式默认选中)。
- 通过点击铅笔图标,选择橡皮擦模式(此时显示橡皮擦图标),然后在白板上拖动鼠标来擦除白板上的绘图。
- 通过点击白板下方的“Clear white board”链接来清除整个白板。
- 使用对话窗口发送/接收消息给/从其他参与者。
请按照以下步骤来观看白板的实际运行效果:确保您已安装至少两个 HTML5 支持的浏览器。(我安装了最新版的 Google Chrome 和 Safari 5.1)。
- 同时使用两个浏览器浏览 *WhiteBoard.aspx*,并使用两个不同的用户名登录白板(无需注册)。
- 使用每个浏览器中的对话/聊天窗口向其他用户发送消息。您会立即在对方的对话窗口中看到消息出现。
- 在一个浏览器窗口中,拖动鼠标在白板上绘图。绘制完成后,打开另一个浏览器窗口。请注意,您刚刚在第一个浏览器白板上绘制的内容会自动绘制到第二个浏览器白板上(无需任何页面刷新)。对第二个浏览器白板也执行相同的操作,您将看到相同的绘图自动显示在第一个浏览器白板上。
- 点击铅笔图标(位于白板正下方)切换到橡皮擦模式,并通过在白板上拖动鼠标来擦除部分绘图。请注意,同一部分的绘图也会自动从其他浏览器白板上擦除。
- 点击白板下方的“Clear board”链接来清除白板。请注意,其他浏览器窗口中的白板也会自动清除。
技术(以及一点点编程)使其成为可能
HTML5 是核心技术,它使得在网页上绘图(Canvas
对象)并将绘图信息或消息广播给其他参与者(通过 WebSocket
)成为可能。除了 HTML5 技术,还使用了一些 JQuery 进行快速的客户端编程,并且使用了一个 WebSocket
服务器(SuperWebSocket
服务器的一个稍作修改的版本,感谢其作者)来实现 WebSocket
的服务器端功能。别忘了,ASP.NET Web 应用程序被用来托管整个功能,这也是我最喜欢的。 :)
WebSocket
您有没有想过 Gmail 中的聊天功能是如何实现的?
您可能已经知道,通常情况下,Web 服务器在没有浏览器发起的 HttpRequest
的情况下是无法向浏览器发送响应的。那么,聊天消息是如何到达您的聊天窗口的呢?这些消息是间隔很短的时间从服务器轮询过来的吗?还是服务器应用程序以某种特定机制将消息广播给您?
轮询可能不是一个好的选择,原因如下:
- 以固定间隔进行轮询会给服务器带来巨大且持续的负载,因为每个用户会话都会在很短的时间间隔内发起频繁的轮询请求。
- 对话将不会显得自然和实时,因为即使您的聊天伙伴可能已经发送了消息,下一条消息也要等到下一次轮询才会到达。所以,您的伙伴也不会及时收到您的回复。
因此,它必须是一个服务器应用程序,通过某种特定机制将消息广播给连接的参与者(浏览器)。有趣的是,通过一些非常巧妙的编程可以实现这一点,这种技术通常被称为 HTTP Push 或 Comet。
不幸的是,实现一个稳定且万无一失的 HTTP Push 或 Comet 引擎并不容易。存在防火墙和代理服务器相关的问题,并且 JavaScript 无法通过 Sockets 进行通信。此外,HTTP 连接是有限的(在某些浏览器中每个域只有两个连接),并且必须保持一个连接开放以接收来自服务器的广播消息,这对于处理来说并不容易。
所以,需要一些新的、简单的方法来帮助以一种简单的方式实现服务器广播的需求。感谢 HTML 5。它提供了 WebSocket
,这是一种运行在 TCP 之上的新协议,支持全双工通信(与 HTTP 不同,HTTP 是一种单工协议),并且使得发送或接收来自 WebSocket
服务器的消息变得极其容易。WebSocket
API 正在由 W3C 标准化。
不幸的是,目前还没有一个功能齐全的 WebSocket
服务器,因此,我不得不依赖一个基于开源贡献 SuperWebSocket
(http://superwebsocket.codeplex.com/)构建的 WebSocket
服务器来构建这个白板。
以下是基本的 WebSocket
功能:
//Initialize WebSocket
function connect()
{
try
{
var socket;
var host = "ws://:8000/WebSocket/Default.aspx";
var socket = new WebSocket(host);
message('Socket Status: '+socket.readyState);
socket.onopen = function(){
message('Socket Status: '+socket.readyState+' (open)');
}
socket.onmessage = function(msg){
message('Received: '+msg.data);
}
socket.onclose = function(){
message('Socket Status: '+socket.readyState+' (Closed)');
}
}
catch(exception)
{
message('Error'+exception);
}
}
//Send Message to WebSocket Server
function send(message)
{
try
{
socket.send(message);
}
catch(exception)
{
message('Error:' + exception);
}
}
HTML5 Canvas
HTML5 canvas
元素可以被 JavaScript 用来在网页上绘制图形。Canvas 是一个矩形区域,您可以控制它的每一个像素。
您可以通过使用其 JavaScript API 在 Canvas 上绘制不同的形状(例如 Path
、Box
、Circle
等)。
以下是 HTML Canvas
的基本用法:
<!DOCTYPE HTML>
<html>
<body>
<canvas id="myCanvas" width="200" height="100" style="border: 1px solid #c3c3c3;">
Your browser does not support the canvas element.
</canvas>
<script type="text/javascript">
var c = document.getElementById("myCanvas");
var cxt = c.getContext("2d");
//Go to x=20, y=20 position
cxt.moveTo(20, 20);
//Draw a line from point1 (x=20,y=20) to point2 (x=150,y=50)
cxt.lineTo(150, 50);
//Renders the actual line
cxt.stroke();
</script>
</body>
</html>
它在浏览器中渲染以下输出:
很不错,而且很简单,对吧?
使用鼠标绘制连续线条
白板上的绘图需要使用 Canvas 绘制连续且不规则的线条。因此,当用户在 canvas
上拖动鼠标时,必须通过绘制鼠标轨迹上的小线段来绘制连续的线条。基本上,必须使用以下逻辑(利用 JQuery 的帮助):
$("#drawing-canvus").mousedown(function () {
startDraw = start;
context.beginPath();
});
$("#drawing-canvus").mousemove(function (e) {
context.lineTo((e.clientX - position.left), (e.clientY - position.top));
context.stroke();
});
$("#drawing-canvus").mouseup(function () {
startDraw = false;
});
请查看 *\scripts\jqdraw.js* 来了解绘图功能的实现方式。
将绘图信息发送到 WebSocket 服务器
当用户在白板上绘图时,绘图会同时发生在所有当前连接到同一个白板会话的浏览器中。此操作按以下步骤顺序进行:
- 当用户拖动鼠标绘图时,鼠标轨迹上的点的坐标 (x,y) 会被收集到一个 JavaScript 变量中,一旦用户释放鼠标,坐标信息就会被发送到套接字服务器。
- 套接字服务器收集坐标信息,并将相同的坐标信息广播给已连接的浏览器。
- 浏览器端的脚本从套接字服务器收集消息(坐标),并在各自的白板(
canvas
)上绘制这些点。
逻辑体现在以下代码中:
$("#drawing-canvus").mousedown(function () {
startDraw = true;
//Start building the co-ordinate string
coOordinates = "" + isInk;
context.beginPath();
});
$("#drawing-canvus").mousemove(function (e) {
if (e.target.getAttribute("id") == "drawing-canvus") {
if (startDraw) {
context.lineTo((e.clientX - position.left), (e.clientY - position.top));
//Add points to the co-ordinate string as long as user drags the mouse
coOordinates += "#" + (e.clientX - position.left) + "," +
(e.clientY - position.top);
context.stroke();
}
});
$("#drawing-canvus").mouseup(function () {
startDraw = false;
$("#drawing-canvus").trigger("drawmultiple", coOordinates );
});
克服 WebSocket 的一个限制
当用户在 Canvas 上拖动鼠标时,鼠标轨迹坐标会产生如下输出:
1#274,162#277,164#280,167#285,171#286,173#288,173#290,175#292,176#298,176#305,183#309,189
#315,197#317,201#319,201#323,206#328,211#331,214#332,215#334,216#338,223#342,229#346,233
#351,238#354,241#354,244#357,248#359,252#362,256#363,257#365,258#367,261#372,266#375,268
#377,268#380,274#383,277#386,281#386,282#386,283#388,284#390,284#390,285#392,287#393,289
#394,290#396,293#397,296#399,299#402,307#403,307#404,310#406,313#406,314#406,318#407,323
#407,325#408,326#408,327#410,331#412,336#414,345#416,350#418,358#419,360#420,366#422,374
#422,376#423,380#424,382#424,384#424,385#424,386#424,388#422,391#420,395#419,397#418,397
#417,399#416,399#415,401#413,401#410,403#407,406#404,411#397,415#395,419#390,420#386,421
#383,422#382,423#381,423#381,424#379,425#369,425#361,428#357,429#350,431#345,434#334,435
#325,436#318,436#305,436#296,432#289,431#284,430#279,429#272,427#263,422#258,420#247,416
#235,410#228,404#221,400#213,395#207,391#199,384#189,377#183,371#180,369#175,366#174,365
#170,358#166,348#160,342#157,334#154,327#154,320#154,315#155,306#153,298#152,290#153,273
#156,261#161,244#161,243#163,238#164,233#167,231#169,222#171,214#173,211#175,209#178,199
#179,198#185,191#187,185#189,183#189,182#190,180#191,180
第一个数字(上面 string
中的 1
)表示当前操作模式,可以是 0
或 1
。0
表示橡皮擦模式,1
表示铅笔模式。
后面的数字(例如,274,162
),以哈希(#)分隔,表示绘图上的坐标点。
当用户绘制一条长线或绘制速度很慢时,坐标消息通常会变得很大,在这种情况下,白板会因为 WebSocket
协议的大小限制而无法将坐标消息发送到 Socket
服务器(我不确定 WebSocket
消息的确切大小限制,但肯定存在大小限制)。结果是,如果用户绘制了可能导致生成长坐标消息的内容,其他连接浏览器的白板将无法更新用户在一个特定浏览器白板上绘制的内容。
为了解决这个问题,坐标消息在内部被分割成块,每个块被发送到 websocket 服务器,然后服务器将每个坐标消息块广播给每个连接的浏览器,以在各自的 canvas
上绘制每个绘图片段,从而呈现整体绘图。
实现其他一些辅助功能
在白板正下方,有一些控件允许用户在铅笔和橡皮擦模式之间切换,并清除整个白板。白板还会显示用户的活动,当用户执行某些操作时(在白板上绘图或在对话/聊天窗口中输入文字),这些活动会同时显示在所有连接到同一会话的浏览器中。
擦除绘图
就像真实的白板一样,用户可以通过选择橡皮擦模式来擦除白板上的绘图或部分绘图。默认情况下,选择的是铅笔模式(当前选择的模式通过铅笔和橡皮擦图标显示),因此当用户在白板上拖动鼠标时,会绘制一些内容。如果用户想擦除自己绘图的某一部分,他/她需要选择橡皮擦,这就像点击铅笔图标切换到橡皮擦模式一样简单。一旦选择了橡皮擦模式,在白板上拖动就会擦除鼠标指针轨迹上的那部分绘图,并且,所有参与同一个白板会话的浏览器都会从它们的白板上擦除相同的部分绘图。
擦除使用了与绘制内容相同的技术,只是当选择橡皮擦模式时,绘图颜色会切换为白色(而在选择铅笔模式时,默认设置为黑色)。
清空白板
用户可以通过点击“Clear Board”链接来清空整个白板。点击此链接会执行以下脚本来清空整个白板。
function clearCanvas() {
canvas=document.getElementById("drawing-canvus");
c=canvas.getContext("2d");
c.clearRect(0,0,canvas.width,canvas.height);
}
与绘图和擦除一样,当用户在一个特定的浏览器窗口中清空白板时,所有参与同一会话的浏览器窗口中的白板都会被同时清空。
显示用户活动信息
每当用户在白板上绘图,或者在对话/聊天窗口输入文字时,一条信息消息会显示在白板正下方。显示以下消息:
“User is drawing something...”
“User is erasing something...”
“User cleared the white board ”
与上述功能类似,用户活动信息会同时显示在所有浏览器中。因此,当 User1
绘图时,以下消息会显示在参与同一会话的所有浏览器中:
“User1 is drawing something...”
发送和接收聊天消息
与 Gmail 网页聊天一样,同一会话中的用户可以使用位于白板旁边的对话窗口发送或接收文本消息。当用户通过对话窗口发送消息时,消息会广播给所有参与用户,他们会立即同时在各自的对话窗口中看到该消息。
用户在白板上的任何操作,都会立即反映到所有浏览器的白板上。这是通过以下逻辑实现的:
- 无论用户做什么,都会构建一个包含必要信息的相应 JSON
string
,并将 JSON 消息发送到WebSocket
服务器。 WebSocket
服务器接收 JSON 消息并将其广播给所有已连接的浏览器。- 浏览器接收 JSON 消息,解析以检索数据,并在白板页面上渲染相应的输出。
以下方法用于构建不同活动的 JSON 消息格式:
function getJsonMessage(action, message) {
var jsonMessage = '{"Action":"' + action + '","Message":"' + message + '"}';
return jsonMessage;
}
对于每个不同的用户活动,以下是如何将相应的 JSON 消息发送到 WebSocket
服务器:
function sendPixelMessage(message) {
var jsonMessage = getJsonMessage("Pixel", message);
ws.send(jsonMessage);
}
function sendChatMessage(textArea) {
var message = $(textArea).val();
if (message == "") return false;
$(textArea).val("");
var jsonMessage = getJsonMessage("Chat", message);
ws.send(jsonMessage);
}
function sendInfoMessage(message) {
var jsonMessage = getJsonMessage("Info", message);
ws.send(jsonMessage);
}
function sendClearMessage() {
var jsonMessage = getJsonMessage("Clear", "");
ws.send(jsonMessage);
}
为新用户显示最新的白板状态
当新用户加入会话时,他们应该能够看到白板的最新状态。可能存在这种情况:一个用户稍后加入白板,在这种情况下,他们应该看到已在白板上绘制或擦除的内容,以及其他参与者之间已交换的聊天消息。这是通过以下逻辑实现的:
- 在广播消息给已连接浏览器之前,所有 JSON 消息(从浏览器通过
WebSocket
发送)在SocketServer
中都会存储在一个全局变量中。这通过WebSocket
服务器中的以下方法实现:void socketServer_CommandHandler(WebSocketSession session, WebSocketCommandInfo commandInfo) { lock (m_SessionSyncRoot) { string messageType = GetTypeFromMessage(commandInfo.Data); string message = buildJSONMessage (GetDataFromMessage(commandInfo.Data), messageType, session); List<string> messages = ApplicationData.Data as List<string>; if (messages == null) { messages = new List<string>(); ApplicationData.Data = messages; } messages.Add(message); ApplicationData.Data = messages; SendToAll(message); } }
- 当新用户加入白板时,通过在白板上渲染绘图并向对话窗口显示用户活动(聊天消息),来向他们展示白板的最新情况。
这通过 WhiteBoard.aspx 中的以下代码段实现:
function LoadPreviousActivity() { <% if(SuperWebSocket.ApplicationData.Data != null) { List<string> history = SuperWebSocket.ApplicationData.Data as List<string>; if (history != null) { foreach (string activity in history) { %> AppendActivity('<%=activity %>'); <% } } } %> }
WebSocket
服务器的一个限制是,与 ASP.NET 不同,它没有像ApplicationState
或SessionState
这样的状态管理系统。因此,有两种方法可以解决这个问题。 - 在发送消息到
WebSocket
时,通过 Ajax 将同一消息发送到 ASP.NET,并将消息存储在 Application State 或 Cache 中,稍后从 ASP.NET 中检索这些消息。 - 在
WebSocket
服务器实现一些自定义存储机制,并将消息存储在那里。我为了简单起见,选择了后一种方法。我使用了一个public
类的static
属性(SuperWebSocket.ApplicationData.Data
)来存储所有用户消息,然后解析所有这些消息,以便向稍后加入白板会话的用户显示白板的最新状态。
注意:这里实现的服务器端存储机制并不是最好的方法,它只是为了演示目的。实际上,需要开发一个功能齐全的服务器端存储机制,我期望 WebSocket
服务器迟早会实现这个重要功能。
摘要
那么,可能会被问到的一个显而易见的问题是:“这个白板有趣吗?或者说它是一个可以用于实际生产系统的真实应用吗?”
我认为,WebSocket
仍处于早期阶段,而且坦白说,并非所有浏览器都支持 WebSocket
(或 Html5)。所以,是的,这确实是一个有趣的实现,但它展示了 WebSocket
的能力和潜力,并预示着它光明的未来。
HTML5 棒极了(白板也是如此)!
历史
- 2011年3月12日:初始发布