HTML5 WebSocket 核心
HTML5 WebSocket 定义了一个通过单个 TCP 连接运行的双向、全双工通信通道。本文讨论了其出色的性能、WebSocket 协议原理及其握手机制,并开发了一个实际的 WebSocket 应用(Team Poker)。
目录
- 引言
- 背景
- WebSocket 核心
- 实验性演示
- 浏览器支持
- WebSocket JavaScript API
- 实际开发 WebSocket - Team Poker
- 未解决的问题
- 结论
- 参考文献与资源
介绍
HTML5 WebSocket 定义了一个通过 Web 上的单个 TCP 套接字运行的双向、全双工通信通道。它在 Web 客户端和服务器之间提供了高效、低延迟、低成本的连接。基于 WebSocket,开发人员未来可以构建可扩展、实时的 Web 应用程序。下面是摘录自 IETF WebSocket 协议 页面的 WebSocket 官方定义:
WebSocket 协议使在受控环境中运行的、不受信任的代码的用户代理能够与已选择接收该代码通信的远程主机进行双向通信。所使用的安全模型是 Web 浏览器常用的基于源的安全模型。该协议由初始握手组成,然后是分层在 TCP 之上的基本消息帧。此技术的目标是为需要与服务器进行双向通信的基于浏览器的应用程序提供一种机制,而不依赖于打开多个 HTTP 连接(例如,使用 XMLHttpRequest 或 <iframe> 和长轮询)。
本文试图深入探讨 WebSocket 的基本概念、它要解决的问题,对其进行本质上的解释,观看一些实验性演示,开发一个简单的实际 WebSocket 应用程序(Team Poker),并描述 WebSocket 当前的开放问题。我真诚地希望它能做到系统性、易于理解、由浅入深,以便最终读者不仅能从高层次了解 WebSocket 是什么,还能深入理解它!您阅读本文后提出的任何想法、建议或批评都将帮助我在未来有所改进,如果您能留下评论,我将不胜感激。
背景
在传统的 Web 应用程序中,为了实现与服务器的一些实时交互,开发人员不得不采用一些棘手的技术,例如 Ajax 轮询、 Comet(又称 Ajax 推送、全双工 Ajax、HTTP 流等)。这些技术要么定期向服务器发送 HTTP 请求,要么长时间保持与服务器的 HTTP 连接,这“包含大量额外、不必要的头部数据并引入延迟”,并导致“令人震惊的高昂代价”。 websocket.org 详尽地解释了这些问题,并详细比较了 Ajax 轮询和 WebSocket 的性能。它构建了两个简单的网页,一个使用传统的 HTTP 定期与服务器通信,另一个使用 HTML5 WebSocket。在测试中,每个 HTTP 请求/响应头部大约是871 字节,而 WebSocket 连接的数据长度要短得多:连接建立后只有2 字节。随着传输次数的增加,结果将是
传统的 HTTP 请求
用例 A:1,000 个客户端每秒轮询一次:网络吞吐量为 (871 x 1,000) = 871,000 字节 = 6,968,000 比特/秒 (6.6 Mbps)
用例 B:10,000 个客户端每秒轮询一次:网络吞吐量为 (871 x 10,000) = 8,710,000 字节 = 69,680,000 比特/秒 (66 Mbps)
用例 C:100,000 个客户端每 1 秒轮询一次:网络吞吐量为 (871 x 100,000) = 87,100,000 字节 = 696,800,000 比特/秒 (665 Mbps)
HTML5 WebSocket
用例 A:1,000 个客户端每秒接收 1 条消息:网络吞吐量为 (2 x 1,000) = 2,000 字节 = 16,000 比特/秒 (0.015 Mbps)
用例 B:10,000 个客户端每秒接收 1 条消息:网络吞吐量为 (2 x 10,000) = 20,000 字节 = 160,000 比特/秒 (0.153 Kbps)
用例 C:100,000 个客户端每秒接收 1 条消息:网络吞吐量为 (2 x 100,000) = 200,000 字节 = 1,600,000 比特/秒 (1.526 Kbps)
最后是一个更直观的图表:
“HTML5 Web Sockets 可以提供 500:1 甚至 — 取决于 HTTP 头部的大小 — 1000:1 的不必要 HTTP 头部流量减少,以及 3:1 的延迟减少”。 --WebSocket.org
WebSocket 核心
创建 WebSocket 的动机是取代轮询和长轮询(Comet),并赋予 HTML5 Web 应用程序实时通信的能力。基于浏览器的 Web 应用程序可以通过 JavaScript API 发起 WebSocket 连接请求,然后通过单个 TCP 连接与服务器传输数据帧。
这是通过新的协议 — **WebSocket 协议** — 实现的,它本质上是一个独立的基于 TCP 的协议。要建立 WebSocket 连接,客户端/浏览器会构造一个带有“Upgrade: WebSocket”头部的 HTTP 请求,这表示一个协议升级请求,HTTP 服务器将解释握手密钥,并返回握手响应(详细的握手机制将在下面描述)。之后连接就建立了(可以说是客户端和服务器两端都“插上了插头”),双方可以独立且同时地传输/接收数据,不再有冗余的头部信息,并且连接在一方发送关闭信号之前不会关闭,这就是为什么 WebSocket 是双向和全双工的。此外,与 HTTP 的请求/响应范例相比,WebSocket 在 TCP 之上分层了一个帧机制,每个数据帧最少只有 2 字节。
现在是时候深入研究这个协议了,让我们从 WebSocket 版本draft-hixie-thewebsocketprotocol-76 开始,该版本目前得到了浏览器(Chrome 6+、Firefox 4+、Opera 11)和许多 WebSocket 服务器的支持(有关详细信息,请参阅下面的 浏览器/服务器支持 部分)。下面展示了一个典型的 WebSocket 请求/响应示例:
请求
GET /demo HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5
Sec-WebSocket-Key2: 12998 5 Y3 1 .P00
^n:ds[4U
响应
HTTP/1.1 101 WebSocket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Origin: http://example.com
Sec-WebSocket-Location: ws://example.com/demo
Sec-WebSocket-Protocol: sample
8jKS'y:G*Co,Wxa-
整个过程可以描述为:客户端发出一个“特殊”的 HTTP 请求,请求在“example.com”域上,路径为“/demo”的“升级”连接协议到“WebSocket”,并带有三个“握手”字段:Sec-WebSocket-Key1、Sec-WebSocket-Key2 和8 字节({^n:ds[4U})。这些字段之后是随机令牌,WebSocket 服务器将在其握手结束时使用这些令牌构造一个16 字节的安全哈希,以证明它已读取了客户端的握手。
由于 WebSocket 协议尚未最终确定,并且正在由 IETF Hypertext Bidirectional (HyBi) 工作组 进行改进和标准化,在我撰写本文时,最新的 WebSocket 版本是“draft-ietf-hybi-thewebsocketprotocol-09”,该版本由Ian Fette 于 2011 年 6 月 7 日更新。在该版本中,请求/响应头部与 76 版本相比都发生了变化,握手机制也发生了变化。Sec-WebSocket-Key1 和 Sec-WebSocket-Key2 的组合被单个 Sec-WebSocket-Key 取代,因此与 draft-hixie-thewebsocketprotocol-76 不兼容(然而,Chrome 和 Firefox Aurora 将支持它,Microsoft 互操作性策略团队也在进行实验,请参阅下面的 浏览器支持 部分了解更多详情)。
最新draft-ietf-hybi-thewebsocketprotocol-09 中的 WebSocket 请求/响应:
请求
GET /demo HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: V2ViU29ja2V0IHJvY2tzIQ==
Sec-WebSocket-Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 8
响应
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: VAuGgaNDB/reVQpGfDF8KXeZx5o=
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Key 是一个 base64 编码的随机生成的 16 字节值,在上例中是“WebSocket rocks!”。服务器读取该密钥,与一个魔术 GUID “258EAFA5-E914-47DA-95CA-C5AB0DC85B11” 连接,得到“V2ViU29ja2V0IHJvY2tzIQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算其 SHA1 哈希,得到结果“540b8681a34307fade550a467c317c297799c79a”,最后 base64 编码该哈希并将值附加到“Sec-WebSocket-Accept”头部。
我已经写了下面的 C# 代码来演示如何根据 draft-ietf-hybi-thewebsocketprotocol-09 计算 Sec-WebSocket-Accept:
public static String ComputeWebSocketHandshakeSecurityHash09(String secWebSocketKey)
{
const String MagicKEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
String secWebSocketAccept = String.Empty;
// 1. Combine the request Sec-WebSocket-Key with magic key.
String ret = secWebSocketKey + MagicKEY;
// 2. Compute the SHA1 hash
SHA1 sha = new SHA1CryptoServiceProvider();
byte[] sha1Hash = sha.ComputeHash(Encoding.UTF8.GetBytes(ret));
// 3. Base64 encode the hash
secWebSocketAccept = Convert.ToBase64String(sha1Hash);
return secWebSocketAccept;
}
单元测试代码
String secWebSocketKey = Convert.ToBase64String(Encoding.UTF8.GetBytes("WebSocket rocks!"));
Console.WriteLine("Sec-WebSocket-Key: {0}", secWebSocketKey);
String secWebSocketAccept = ComputeWebSocketHandshakeSecurityHash09(secWebSocketKey);
Console.WriteLine("Sec-WebSocket-Accept: " + secWebSocketAccept);
运行上述代码我们将看到结果
Sec-WebSocket-Key: V2ViU29ja2V0IHJvY2tzIQ== Sec-WebSocket-Accept: VAuGgaNDB/reVQpGfDF8KXeZx5o=
实验性演示
到目前为止,已经有许多基于 draft-hixie-thewebsocketprotocol-75 或 draft-hixie-thewebsocketprotocol-76 构建的实验性 WebSocket 演示。
http://rumpetroll.com/
在画布中玩一只小蝌蚪,并可以与其他小蝌蚪聊天,每个实时都能看到小蝌蚪的位置和聊天消息。
http://html5labs.interoperabilitybridges.com/prototypes/websockets/websockets/info
Microsoft HTML5 Labs 中的 WebSocket 演示
http://html5demos.com/web-socket
使用 WebSocket 的一个非常简单的“聊天室”演示。
http://kaazing.me/
通过 Kaazing 的实时消息传递网络解决方案,实时刷新股票、天气、新闻和推文。
Mr. Doob 的多人画板
在这个多人 <canvas>
绘图应用程序中,WebSocket 用于将其他用户绘制的线条的坐标传递给每个客户端,并实时更新。
等等……您还将看到 我下面开发的简单演示。
浏览器/服务器支持
WebSocket 不仅用于浏览器/服务器通信,客户端应用程序也可以使用它。但是,考虑到新兴的手机和即将到来的云,我猜测浏览器仍然将是 WebSocket 协议的主要平台。在我撰写本文时,WebSocket 协议版本 draft-ietf-hybi-thewebsocketprotocol-76 得到了 Safari 5+/Google chrome 6+(链接)、Mozilla Firefox 4+(默认禁用 链接)和 Opera 11(默认禁用 链接)的支持,IE 9/10 不支持……但对于不支持的浏览器,我们可以通过采用 web-socket-js 来使用 Flash 兼容/回退。
很棒的 Can I uses it 网站正在维护所有流行浏览器中 HTML5 新功能的兼容性。下面的截图显示了 WebSocket 的支持情况。
请注意,上面的截图讨论的是 WebSocket 版本 draft-hixie-thewebsocketprotocol-76,它并不表示 draft-ietf-hybi-thewebsocketprotocol-09 的支持情况。据我观察,浏览器支持情况总结如下:
- Webkit(Chrome/Safari)添加了一个运行时标志,使其能够切换旧/新协议版本,请参阅:
https://bugs.webkit.org/show_bug.cgi?id=60348 - Firefox Aurora 6 支持它。我没有找到官方文档,但信息来源是:
http://comments.gmane.org/gmane.os.opendarwin.webkit.devel/17074 - Microsoft 互操作性策略团队正在实验新协议,并且他们的演示可以下载(但据我所知,IE 10 不会添加 WebSocket 支持……)。
http://html5labs.interoperabilitybridges.com/prototypes/websockets/websockets/info
同时也有许多 WebSocket 服务器可用:
https://socketio.node.org.cn - 为各种传输(WebSocket、Flash WebSocket、XHR 轮询、JSONP 轮询等)提供无缝支持,由 Guillermo Rauch 开发,旨在实现实时通信。
node.ws.js - 一个简单的 WebSocket 服务器(支持 draft-hixie-thewebsocketprotocol-75 和 draft-hixie-thewebsocketprotocol-76),基于 node.websocket.js 开发。
web-socket-js - 由 Flash 提供支持的 HTML5 Web Socket 实现。
http://nugget.codeplex.com - 一个用 C# 实现的 WebSocket 服务器。
jWebSocket.org - 开源 Java WebSocket 服务器。
phpwebsocket - PHP 版 WebSocket 服务器。
WebSocket JavaScript API
W3C 定义了如下的 WebSocket 接口:
[Constructor(in DOMString url, in optional DOMString protocols)]
[Constructor(in DOMString url, in optional DOMString[] protocols)]
interface WebSocket {
readonly attribute DOMString url;
// ready state
const unsigned short CONNECTING = 0;
const unsigned short OPEN = 1;
const unsigned short CLOSING = 2;
const unsigned short CLOSED = 3;
readonly attribute unsigned short readyState;
readonly attribute unsigned long bufferedAmount;
// networking
attribute Function onopen;
attribute Function onmessage;
attribute Function onerror;
attribute Function onclose;
readonly attribute DOMString protocol;
void send(in DOMString data);
void close();
};
WebSocket implements EventTarget;
url 属性是 WebSocket 服务器的 URL,protocol 通常是“ws”(用于未加密的纯文本 WebSocket)或“wss”(用于安全的 WebSocket),send 方法在连接后向服务器发送数据,close 用于发送关闭信号。此外,还有四个重要事件:onopen、onmessage、onerror 和 onclose。我从 nettuts 借用了一张精美的图片。
- onopen:当套接字打开时,即 TCP 三次握手和 WebSocket 握手之后。
- onmessage:从 WebSocket 服务器接收到消息时。
- onerror:发生错误时触发。
- onclose:套接字关闭时。
下面的 JavaScript 代码用于设置 WebSocket 连接并检索数据:
var wsUrl = 'ws://:8888/DummyPath';
var websocket = new WebSocket(wsUrl);
websocket.onopen = function (evt) { onOpen(evt) };
websocket.onclose = function (evt) { onClose(evt) };
websocket.onmessage = function (evt) { onMessage(evt) };
websocket.onerror = function (evt) { onError(evt) };
function onOpen(evt) {
console.log("Connected to WebSocket server.");
websocket.send("HTML5 WebSocket rocks!");
}
function onClose(evt) { console.log("Disconnected"); }
function onMessage(evt) {
console.log('Retrieved data from server: ' + evt.data);
// Update UI...
}
function onError(evt) { console.log('Error occured: ' + evt.data); }
实际开发 WebSocket - Team Poker 演示
使用 Planning Poker Cards 估算用户故事的工作量是敏捷/Scrum 开发中众所周知的且广泛使用的方法。项目经理/Scrum Master 会提前准备用户故事,与利益相关者举行会议,让他们玩扑克来表示对每个故事的估算。卡片值越高,实现难度越大;相反,值越低,实现难度越小。“0”表示“无需工作”或“已完成”,“?”表示“不可能完成的任务”或“需求不明确”。
实际上有一个网站 - http://pokerplanning.com ,它做了上述工作。我的同事们和我都用过几次。然而,我们发现随着加入游戏的人数增多或经过几轮投票后,它变得越来越慢。我们确实遇到了最糟糕的结果:没有人能投票了,因为每个人的投票页面都卡住了。我强烈怀疑其主要原因是 Ajax 轮询策略,以确保每个人都能获得实时的投票状态。通过跟踪其网络活动,我猜想我是对的。
http://pokerplanning.com 中的 Ajax 轮询
我相信 HTML5 WebSocket 将解决这个问题!所以我开发了一个简单的演示(我称之为Team Poker),目前只有有限的基本功能,如下所述:
- 用户输入昵称后即可登录扑克房间。
- 当一个用户打出一张扑克牌时,所有人都收到通知。
- 当新玩家加入时,所有人都收到通知。
- 新加入的玩家可以看到当前的参与者和投票状态。
- 管理员完成一轮后,所有参与者都可以看到游戏结果。
满足要求 #1 的登录截图
满足要求 #2、#3 和 #4 的新参与者、新投票扑克的状态更新截图(请点击图片放大)
所有参与者都可以看到最终的扑克游戏结果,故事 #5。这里我添加了一个 CSS3 3D 旋转效果。投票“?”或“0”的人的扑克牌会逐渐浮起。我希望这是一个接近的设计,有助于发现团队中想法差异最大的人。截图如下,请看我开头处的视频以获得更生动的视角。
请注意,Team Poker 演示侧重于展示 WebSocket 的强大功能,而缺少诸如主持人/团队成员角色(目前简单地硬编码“Wayne”为主持人)、用户故事自定义、服务器端存储游戏状态等功能。但是,我在本文开头分享了所有源代码。此外,我在 github 上上传了源代码:https://github.com/WayneYe/TeamPoker,希望有人能将其做得更好、更具生产力,你愿意和我一起 fork 吗?亲爱的读者:)。
好了,现在是编码时间。由于所有客户端都需要获知其他客户端的变化(新玩家加入或新扑克打出),此外,新加入的玩家需要了解当前状态,我定义了两个通信契约:
- ClientMessage 表示从客户端发送的消息,包含一个 Type 属性,反映枚举类 MessageType - NewParticipaint, NewVoteInfo, ViewVoteResult,以及一个 Data 属性来存储数据。
- ServerStatus,存储当前连接的客户端 WebSocket 实例、玩家以及当前的投票状态。它们存储在三个全局数组中:[{Players}]、[{VoteValue}],在收到新的客户端消息后会广播给所有客户端。
var TeamPoker = TeamPoker || function () { };
TeamPoker.CurrentPlayerName = '';
TeamPoker.WsClient = null;
TeamPoker.VoteInfo = function (playerName, voteValue) {
this.PlayerName = playerName;
this.VoteValue = voteValue;
}
TeamPoker.voteResult = [];
TeamPoker.MessageType = {
NewParticipaint: 'NewParticipaint',
NewVoteInfo: 'NewVoteInfo',
ViewVoteResult: 'ViewVoteResult'
};
TeamPoker.ClientMessage = function (type, data) {
this.Type = type;
this.Data = data;
};
TeamPoker.ServerMsgType = {
NewParticipaint: 'NewParticipaint',
NewVoteInfo: 'NewVoteInfo',
NotifyCurrentStatus: 'NotifyCurrentStatus',
ViewVoteResult: 'ViewVoteResult'
};
TeamPoker.GameStatus = function () {
this.Players = [];
this.VotedPlayers = [];
};
在客户端,用户点击“登录”按钮后将建立 WebSocket 连接。昵称将被发送到运行在 nodejs 上的 WebSocket 服务器。核心客户端代码如下所示:
TeamPoker.connectToWsServer = function () {
// Init Web Socket connect
var WSURI = "ws://192.168.1.6:8888";
TeamPoker.WsClient = new WebSocket(WSURI);
TeamPoker.WsClient.onopen = function (evt) {
console.log('Successfully connected to WebSocket server.');
TeamPoker.joinGame();
};
TeamPoker.WsClient.onclose = function (evt) {
console.log('Connection closed.');
};
TeamPoker.WsClient.onmessage = function (evt) {
console.log('Retrived msg from server: ' + evt.data);
TeamPoker.updateGameStatus(evt.data);
};
TeamPoker.WsClient.onerror = function (evt) {
console.log('An error occured: ' + evt.data);
};
};
TeamPoker.joinGame = function () {
var joinGameMsg = new TeamPoker.ClientMessage(TeamPoker.MessageType.NewParticipaint, TeamPoker.CurrentPlayerName);
TeamPoker.WsClient.send(JSON.stringify(joinGameMsg));
}
一个重要的方法是 TeamPoker.updateGameStatus
,它负责分析来自服务器的不同类型消息并相应地更新 UI。
TeamPoker.updateGameStatus = function (data) {
var serverMsg = JSON.parse(data);
var voteBar = $('#votebar');
var playerBar = $('#participaints');
var playerSpliter = ', '
switch (serverMsg.Type) {
case TeamPoker.ServerMsgType.NewParticipaint:
// Update participants list
if (playerBar.html() == "") playerBar.html(serverMsg.Data);
else playerBar.html(playerBar.html() + playerSpliter + serverMsg.Data);
break;
case TeamPoker.ServerMsgType.NewVoteInfo:
// Update the "vote bar"
var $newVotedPoker = $('' + serverMsg.Data + '');
voteBar.append($newVotedPoker);
// Start CSS3 opacity transition
setTimeout(function () { $newVotedPoker.css('opacity', 1); }, 50);
break;
case TeamPoker.ServerMsgType.NotifyCurrentStatus:
var players = serverMsg.Data.Players;
// Update participants list and "vote bar"
playerBar.html(players.join(playerSpliter));
var votedPlayers = serverMsg.Data.VotedPlayers;
for (var i = 0; i < votedPlayers.length; i++) {
var $votedPoker = $('' + votedPlayers[i] + '');
$votedPoker.css('opacity', 1);
voteBar.append($votedPoker);
}
break;
case TeamPoker.ServerMsgType.ViewVoteResult:
$('#mask').show();
$('#voteResultWrapper').show();
var voteResultPanel = $('#voteResultPanel');
var voteStatus = serverMsg.Data;
// Traverse voted items (player name-voted value pairs), render them on result panel, pokers with value "0" or "?"
// will be bind CSS3 transitionEnd event, once 3D transition ends they will be bubbled up.
for (var key in voteStatus) {
var $votedPoker = $('' + voteStatus[key].VoteValue + '' + voteStatus[key].PlayerName + '');
if (voteStatus[key].VoteValue == '0' || voteStatus[key].VoteValue == '?')
$votedPoker.bind('webkitTransitionEnd', function () { $(this).css('margin-top', '10px'); $(this).css('color', 'red') });
voteResultPanel.append($votedPoker);
TeamPoker.voteResult.push($votedPoker);
}
function animateVotedPoker(poker) {
poker.css('opacity', 1);
poker.css('webkitTransform', 'rotateY(360deg)');
}
for (var i = 0; i < TeamPoker.voteResult.length; i++) {
// Define and execute closure so that each result be can passed-in one by one
(function (p) {
setTimeout(function () { animateVotedPoker(p); }, 100);
})(TeamPoker.voteResult[i]);
};
break;
default:
break;
}
};
在服务器端,一项重要任务是维护所有活动的客户端 WebSocket 连接,以便它可以“广播”消息给所有客户端,并移除已关闭的客户端以避免向“死”客户端发送消息。除此之外,逻辑非常简单:验证从客户端发送的消息类型,更新玩家/投票状态存储库,然后广播给所有客户端。
/*
WebSocket server based on
https://github.com/ncr/node.ws.js
Written By Wayne Ye 6/4/2011
http://wayneye.com
*/
var sys = require("sys"),
ws = require("./ws");
var wsClients = [], players = [], votedPlayers = [], voteStatus = [];
ws.createServer(function (websocket) {
websocket.addListener("connect", function (resource) {
// emitted after handshake
sys.debug("Client connected on path: " + resource);
// # Add to our list of wsClients
wsClients.push(websocket);
//sys.debug(traverseObj(websocket));
}).addListener("data", function (data) {
var clinetMsg = JSON.parse(data);
switch (clinetMsg.Type) {
case ClientMsgType.NewParticipaint:
var newPlayer = clinetMsg.Data;
sys.debug('New Participaint: ' + newPlayer);
players.push(newPlayer);
var gameStatus = new GameStatus();
gameStatus.Players = players;
gameStatus.VotedPlayers = votedPlayers;
var serverMsg = new ServerMessage(ServerMsgType.NewParticipaint, newPlayer);
broadCast(JSON.stringify(serverMsg));
// Notify the new client current game status
var notifyCurrentStatus = new ServerMessage(ServerMsgType.NotifyCurrentStatus, gameStatus);
wsClients[wsClients.length - 1].write(JSON.stringify(notifyCurrentStatus));
break;
case ClientMsgType.NewVoteInfo:
var newVoteInfo = clinetMsg.Data;
sys.debug('New VoteInfo: ' + newVoteInfo.PlayerName + ' voted ' + newVoteInfo.VoteValue);
votedPlayers.push(newVoteInfo.PlayerName);
voteStatus.push(new VoteInfo(newVoteInfo.PlayerName, newVoteInfo.VoteValue));
var notifyCurrentStatus = new ServerMessage(ServerMsgType.NewVoteInfo, newVoteInfo.PlayerName);
broadCast(JSON.stringify(notifyCurrentStatus));
break;
case ClientMsgType.ViewVoteResult:
sys.debug('Broadcast vote result to client(s)..');
var viewVoteResultMsg = new ServerMessage(ServerMsgType.ViewVoteResult, voteStatus);
broadCast(JSON.stringify(viewVoteResultMsg));
break;
default:
break;
}
}).addListener("close", function () {
// emitted when server or client closes connection
for (var i = 0; i < wsClients.length; i++) {
// # Remove from our connections list so we don't send
// # to a dead socket
if (wsClients[i] == websocket) {
sys.debug("close with client: " + websocket);
wsClients.splice(i);
break;
}
}
});
}).listen(8888);
function broadCast(msg) {
sys.debug('Broadcast server status to all wsClients: ' + msg);
for (var i = 0; i < wsClients.length; i++)
wsClients[i].write(msg);
}
var ClientMsgType = {
NewParticipaint: 'NewParticipaint',
NewVoteInfo: 'NewVoteInfo',
ViewVoteResult: 'ViewVoteResult'
};
function ClientMessage(type, data) {
this.Type = type;
this.Data = data;
};
var ServerMsgType = {
NewParticipaint: 'NewParticipaint',
NewVoteInfo: 'NewVoteInfo',
NotifyCurrentStatus: 'NotifyCurrentStatus',
ViewVoteResult: 'ViewVoteResult'
};
function ServerMessage(type, data) {
this.Type = type;
this.Data = data;
};
function VoteInfo(playerName, voteValue) {
this.PlayerName = playerName;
this.VoteValue = voteValue;
}
function GameStatus() {
this.Players = [];
this.VotedPlayers = [];
};
完整的源代码可以在 github 上找到:https://github.com/WayneYe/TeamPoker。
在浏览了代码之后,让我们看看幕后发生了什么:下面的截图是我在开发 Team Poker WebSocket 演示时截取的,它记录了整个 WebSocket 通信过程。在此图中,192.168.1.2 是 TeamPoker 页面所在的宿主机,它发起了 WebSocket 请求;192.168.1.6 是基于 nodejs 的 WebSocket 服务器,它在 Ubuntu 11.04 上运行,并暴露了 8888 端口。
WebSocket 连接后的所有数据包
WebSocket 请求和响应头部
看到了 WebSocket 的强大之处吧?
- 数据传输在一个 TCP 连接的生命周期内完成。
- 握手后没有额外的头部。您可能会注意到“length”列表示每个数据包的大小,在我的例子中平均小于 100 字节,并且它只取决于实际传输的数据大小。
在 Ajax 轮询或 Comet 中,带有头部信息的 HTTP 请求/响应不可能达到与 WebSocket 相同的性能水平。它们都创建了新的 HTTP(TCP)连接来传输数据,并且每个连接的大小相对大于 WebSocket,尤其是当头部中存储了 cookie 或长头部(如“User-Agent”、“If-Modified-Since”、“If-Match”、“X-Powered-By”等)时。
值得一提的是 TCP keep-alive 信号。我们应该尽快关闭不再需要的 WebSocket 连接,否则会浪费带宽。
未解决的问题
Adam Barth 和他的同事发现了一个 WebSocket 的安全漏洞。他指出许多路由器不识别 HTTP“Upgrade”机制。那些路由器将握手后的 WebSocket 数据包视为后续的 HTTP 数据包。结果,攻击者可以污染代理的 HTTP 缓存(您可以参考他们的 详细描述)。他们建议使用基于 CONNECT 的握手,大多数代理似乎更能理解 CONNECT 请求的语义,而不是理解 Upgrade 机制的语义。在模拟了基于 CONNECT 的握手后,他们发现无法污染代理的 HTTP 缓存。
由于安全问题,Firefox 4.0 和 Opera 11 默认禁用了 WebSocket。我们可以在 about:config 中启用它,请参阅 此处 和 此处 了解更多详情。
结论
WebSocket 是 HTML5 中的一项革命性功能。它定义了一个通过 Web 上的单个套接字运行的全双工通信通道。与 Ajax 轮询或 Comet 相比,实时数据传输从未如此简单高效,同时带宽和服务器成本相对较低。尽管它目前尚未标准化,并且存在上述安全问题,因此目前不建议在企业解决方案或数据敏感型应用程序中使用它,但开发人员应该学习它、关注它。唯一不变的是变化,WebSocket 协议的草案版本号变化很快。您可能在阅读我的文章后已经注意到了这一点。希望它很快能成为规范和标准化!
你连接了吗?如果连接了,祝你 WebSocket 愉快!
参考文献与资源
HTML5 Web Sockets: Web 可扩展性的一次飞跃
http://websocket.org/quantum.html
维基百科:WebSocket
http://en.wikipedia.org/wiki/WebSockets
WebSocket 协议
http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07
WebSocket API
http://www.w3.org/TR/websockets/
Web Applications 1.0 - Web sockets
http://www.whatwg.org/specs/web-apps/current-work/complete/network.html#network
Introducing Web Sockets
http://dev.opera.com/articles/view/introducing-web-sockets/
WebSockets - MDC Docs
https://mdn.org.cn/en/WebSockets
Stackoverflow - 学习 HTML 5 WebSockets 的好资源有哪些?
http://stackoverflow.com/questions/4262543/what-are-good-resources-for-learning-html-5-websockets
HTML Labs - WebSocket
http://html5labs.interoperabilitybridges.com/prototypes/websockets/websockets/info
立即开始使用 HTML5 WebSocket
http://net.tutsplus.com/tutorials/javascript-ajax/start-using-html5-websockets-today/
HTML 5 Web Sockets vs. Comet 和 Ajax
http://www.infoq.com/news/2008/12/websockets-vs-comet-ajax
Internet Socket
http://en.wikipedia.org/wiki/Internet_socket
实时 Web 测试 – HTML5 WebSockets 对您有效吗?
http://websocketstest.com/
最初发布于 Wayne's Geek Life (http://wayneye.com):
http://wayneye.com/Blog/HTML5-Web-Socket-In-Essence