HTML5 WebSocket 权威指南(一)
一、HTML5 WebSocket 简介
这本书是为任何想学习如何构建实时 web 应用的人准备的。你可能会对自己说,“我已经这样做了!”或者问“那到底是什么意思?”让我们澄清一下:这本书将向您展示如何使用一种革命性的新的、被广泛支持的开放式行业标准技术 webSocket 来构建真正实时的 Web 应用,这种技术可以在您的客户端应用和远程服务器之间通过 Web 实现全双工、双向通信——无需插件!
还在迷茫?几年前我们也是,在我们开始使用 HTML5 WebSocket 之前。在本指南中,我们将解释您需要了解的 WebSocket 知识,以及为什么您现在应该考虑使用 WebSocket。我们将向您展示如何在您的 web 应用中实现 WebSocket 客户端,创建您自己的 WebSocket 服务器,将 WebSocket 与 XMPP 和 STOMP 等高级协议一起使用,保护您的客户端和服务器之间的流量,以及部署您的基于 WebSocket 的应用。最后,我们将解释为什么你现在应该考虑使用 WebSocket。
HTML5 是什么?
首先,让我们检查“HTML5 WebSocket”的“HTML5”部分。如果你已经是 HTML5 的专家,已经阅读过,比如说, Pro HTML5 Programming ,并且已经在开发非常现代和响应迅速的 web 应用,那么请随意跳过这一部分,继续阅读。但是,如果你是 HTML5 的新手,这里有一个快速介绍。
HTML 最初是为互联网上静态的、基于文本的文档共享而设计的。随着时间的推移,由于 web 用户和设计者希望在他们的 HTML 文档中有更多的交互性,他们开始通过添加表单功能和早期的“门户”类型功能来增强这些文档。现在,这些静态文档集合,或者网站,更像是基于富客户机/服务器桌面应用的 web 应用。这些网络应用几乎可以在任何设备上使用:笔记本电脑、智能手机、平板电脑——应有尽有。
HTML5 是设计来使这些富 web 应用的开发更容易、更自然、更符合逻辑,开发者可以设计和构建一次,然后部署到任何地方。HTML5 也使得网络应用更加有用,因为它不再需要插件。有了 HTML5,你现在可以使用像<header>这样的语义标记语言来代替<div class="header">.多媒体也更容易编码,通过使用像<audio>和<video>这样的标签来引入和分配适当的媒体类型。此外,由于具有语义,HTML5 更容易访问,因为屏幕阅读器可以更容易地读取它的标签。
HTML5 是一个总括术语,涵盖了 web 技术中发生的大量改进和变化,包括从您在网页上使用的标记到 CSS3 样式、离线和存储、多媒体、连接性等等。图 1-1 显示了不同的 HTML5 特性区域。
图 1-1 。HTML5 功能区(W3C,2011)
有很多资源深入研究 HTML5 的这些领域。在本书中,我们关注连接性领域,即 WebSocket API 和协议。让我们来看看 HTML5 连接的历史。
HTML5 连接性
HTML5 的连接领域包括 WebSocket、服务器发送事件和跨文档消息传递等技术。这些 API 包含在 HTML5 规范中,有助于简化浏览器限制阻止 web 应用开发人员创建他们想要的丰富行为或 web 应用开发变得过于复杂的一些领域。HTML5 简化的一个例子是跨文档消息传递。
在 HTML5 之前,由于安全原因,浏览器窗口和框架之间的通信受到限制。然而,随着 web 应用开始将来自不同网站的内容和应用集合在一起,这些应用之间的相互通信变得很有必要。为了解决这个问题,标准团体和主要浏览器厂商同意支持跨文档消息传递,这使得跨浏览器窗口、选项卡和 iFrames 的跨来源通信变得安全。跨文档消息传递将 postMessage API 定义为发送和接收消息的标准方式。有许多使用来自不同主机和域的内容的用例,例如地图、聊天和社交网络,以便在 web 浏览器内部进行通信。跨文档消息传递提供了 JavaScript 上下文之间的异步消息传递。
跨文档消息传递的 HTML5 规范还通过引入由方案、主机和端口定义的源的概念来澄清和细化域安全性。基本上,当且仅当两个 URIs 具有相同的方案、主机和端口时,它们才被认为来自相同的来源。原点值中不考虑路径。
以下示例显示了不匹配的方案、主机和端口(以及不同的来源):
https://www.example.com and http://www.example.comhttp://www.example.com and http://example.comhttp://example.com:8080 and http://example.com:8081
下面的例子是同源的 URL:http://www.example.com/page1.html和http://www.example.com/page2.html。
跨文档消息传递通过允许消息在不同来源之间交换,克服了同源限制。当您发送邮件时,发件人会指定收件人的来源,当您收到邮件时,发件人的来源会包含在邮件中。消息的来源是由浏览器提供的,不能被欺骗。在接收方,您可以决定处理哪些消息,忽略哪些消息。您还可以保留一个“白名单”,只处理来自来源可信的文档的消息。
*跨文档消息传递是一个很好的例子,说明 HTML5 规范用一个非常强大的 API 简化了 web 应用之间的通信。但是,它的重点仅限于跨窗口、选项卡和 iFrames 的通信。它没有解决在协议通信中变得势不可挡的复杂性,这就把我们带到了 WebSocket。
HTML5 规范的主要作者伊恩·希克森在 HTML5 规范的通信部分增加了我们现在所说的 WebSocket。WebSocket 最初名为 TCPConnection ,现在已经演变成了自己独立的规范。虽然 WebSocket 现在不属于 HTML5 的范畴,但它对于在现代(基于 HTML5 的)web 应用中实现实时连接非常重要。WebSocket 也经常被讨论为 HTML5 的连接领域的一部分。那么,为什么 WebSocket 在今天的 Web 中有意义呢?让我们首先来看看协议通信非常重要的较老的 HTTP 架构。
旧 HTTP 架构概述
为了理解 WebSocket 的重要性,让我们先来看一下旧的架构,特别是那些使用 HTTP 的架构。
HTTP 101 (或者说,HTTP/1.0 和 HTTP/1.1)
在旧的架构中,连接是由 HTTP/1.0 和 HTTP/1.1 处理的。HTTP 是客户端/服务器模型中的请求-响应协议,其中客户端(通常是 web 浏览器)向服务器提交 HTTP 请求,服务器使用请求的资源(如 HTML 页面)以及关于页面的附加信息进行响应。HTTP 也是为获取文档而设计的;HTTP/1.0 足以满足来自服务器的单个文档请求。然而,随着 Web 的发展超出了简单的文档共享,并开始包含更多的交互性,连接性需要改进,以实现浏览器请求和服务器响应之间更快的响应时间。
在 HTTP/1.0 中,对服务器的每个请求都要为建立一个单独的连接,至少可以说,这样做的扩展性不好。HTTP 的下一个版本 HTTP/1.1 增加了可重用的连接。随着可重用连接的引入,浏览器可以初始化到 web 服务器的连接来检索 HTML 页面,然后重用相同的连接来检索图像、脚本等资源。HTTP/1.1 通过减少从客户端到服务器的连接数量,减少了请求之间的延迟。
HTTP 是无状态的,这意味着它将每个请求视为唯一和独立的。无状态协议有一些优点:例如,服务器不需要保存关于会话的信息,因此不需要存储这些数据。然而,这也意味着为每个 HTTP 请求和响应发送关于请求的冗余信息。
让我们看一个从客户机到服务器的 HTTP/1.1 请求的例子。清单 1-1 显示了一个包含几个 HTTP 头的完整的 HTTP 请求。
*清单 1-1 。??【HTTP/1.1】客户端到服务器的请求头 *
GET /PollingStock/PollingStock HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Referer:http://localhost:8080/PollingStock/
Cookie: showInheritedConstant=false; showInheritedProtectedConst
ant=false; showInheritedProperty=false; showInheritedProtectedPr
operty=false; showInheritedMethod=false; showInheritedProtectedM
ethod=false; showInheritedEvent=false; showInheritedStyle=false;
showInheritedEffect=false;
清单 1-2 显示了一个从服务器到客户端的 HTTP/1.1 响应的例子。
***清单 1-2 。*HTTP/1.1 Response Headers 从服务器到客户端
HTTP/1.x 200 OK
X-Powered-By: Servlet/2.5
Server: Sun Java System Application Server 9.1_02
Content-Type: text/html;charset=UTF-8
Content-Length: 321
Date: Wed, 06 Dec 2012 00:32:46 GMT
在清单 1-1 和 1-2 中,总开销是 871 字节的单独头信息(也就是说,没有实际数据)。这两个例子只显示了请求的头部信息,这些信息通过网络在每个方向上传输:从客户机到服务器,以及从服务器到客户机,而不管服务器是否有实际的数据或信息要传递给客户机。
对于 HTTP/1.0 和 HTTP/1.1,效率低下的主要原因如下:
- HTTP 是为文档共享而设计的,而不是我们在桌面和现在的网络上已经习惯的丰富的交互式应用
- 客户机和服务器之间的交互越多,HTTP 协议在客户机和服务器之间通信所需的信息量就越大
从本质上来说,HTTP 也是半双工,意思是流量每次单向流动:客户端向服务器发送请求(单向);然后,服务器响应请求(单向)。半双工是非常低效的。想象一次电话交谈,每次你想交流时,你必须按一个按钮,陈述你的信息,然后按另一个按钮来完成它。与此同时,你的对话伙伴必须耐心地等待你结束,按下按钮,然后最终以同样的方式回应。听起来熟悉吗?我们小时候在小范围内使用这种交流方式,我们的军队一直在使用这种方式:这是一种对讲机。虽然对讲机肯定有好处和很大的用途,但它们并不总是最有效的沟通方式。
多年来,工程师们一直在用各种众所周知的方法解决这个问题:轮询、长轮询和 HTTP 流。
绕远路:HTTP 轮询、长轮询和流
通常,当浏览器访问一个网页时,一个 HTTP 请求被发送到承载该网页的服务器。web 服务器确认该请求,并将响应发送回 web 浏览器。在许多情况下,返回的信息,如股票价格、新闻、交通模式、医疗设备读数和天气信息,在浏览器呈现页面时可能已经过时。如果您的用户需要获得最新的实时信息,他们可以不断地手动刷新页面,但这显然是不切实际的,也不是一个特别好的解决方案。
当前提供实时 web 应用的尝试主要围绕一种称为轮询 的技术,以模拟其他服务器端推送技术,其中最流行的是 Comet ,它基本上延迟了 HTTP 响应的完成,以将消息传递给客户端。
轮询是一种定时的同步调用,客户端向服务器发出请求,以查看是否有任何可用的信息。这些请求是定期提出的;无论是否有信息,客户端都会收到响应。具体来说,如果有可用的信息,服务器会发送它。如果没有可用信息,服务器将返回否定响应,客户端将关闭连接。
如果您知道消息传递的确切时间间隔,轮询是一个很好的解决方案,因为只有当您知道服务器上有可用的信息时,您才能同步客户端来发送请求。然而,实时数据通常是不可预测的,不必要的请求和多余的连接是不可避免的。因此,在低消息速率的情况下,您可能会不必要地打开和关闭许多连接。
长轮询 是另一种流行的通信方式,客户端向服务器请求信息,并在设定的时间段内打开连接。如果服务器没有任何信息,它会保持请求打开,直到它有客户端的信息,或者直到它到达指定的超时结束。此时,客户端向服务器重新请求信息。长轮询也被称为 Comet,我们前面提到过,或者反向 AJAX。Comet 延迟 HTTP 响应的完成,直到服务器有东西要发送给客户机,这种技术通常被称为挂起-获取或挂起-发送。重要的是要明白,当您的消息量很大时,长轮询并不能提供比传统轮询更好的性能,因为客户端必须不断地重新连接到服务器以获取新信息,导致网络行为等同于快速轮询。长轮询的另一个问题是缺乏标准实现。
使用流 ,客户端发送一个请求,服务器发送并维护一个开放响应,该响应不断更新并保持开放(无限期地或在设定的时间段内)。每当消息准备好传递时,服务器都会更新响应。虽然流听起来像是适应不可预测的消息传递的一个很好的解决方案,但是服务器从不发出完成 HTTP 响应的信号,因此连接一直保持打开。在这种情况下,代理和防火墙可能会缓冲响应,从而增加消息传递的延迟。因此,在存在防火墙或代理的网络上,许多流式传输尝试都是脆弱的。
这些方法提供了几乎实时的通信,但是它们也涉及 HTTP 请求和响应头,其中包含大量额外的和不必要的头数据和延迟。此外,在每种情况下,客户端必须等待请求返回,然后才能启动后续请求,因此大大增加了延迟。
图 1-2 显示了这些连接在网络上的半双工性质,集成到一个架构中,在你的内部网中,你有通过 TCP 的全双工连接。
图 1-2 。网络上的半双工;后端 TCP 上的全双工
WebSocket 简介
那么,这会给我们带来什么?为了消除这些问题,HTML5 规范的连接部分包含了 WebSocket。WebSocket 是一种自然的全双工、双向、单路连接。使用 WebSocket,您的 HTTP 请求变成了打开 WebSocket 连接的单个请求(WebSocket 或 TLS(传输层安全性,以前称为 SSL)上的 WebSocket),并重用从客户端到服务器以及从服务器到客户端的相同连接。
WebSocket 减少了延迟,因为一旦 WebSocket 连接建立,服务器就可以在消息可用时发送消息。例如,与轮询不同,WebSocket 只发出一个请求。服务器不需要等待来自客户端的请求。类似地,客户端可以随时向服务器发送消息。这个请求大大减少了轮询的延迟,轮询每隔一段时间发送一个请求,而不管消息是否可用。
图 1-3 比较了一个样本轮询场景和一个 WebSocket 场景。
图 1-3 。轮询 vs WebSocket
本质上,WebSocket 符合语义和简化的 HTML5 范式。它不仅消除了对复杂工作区和延迟的需求,还简化了体系结构。让我们更深入地探究一下原因。
为什么需要 WebSocket?
现在我们已经探索了 WebSocket 的历史,让我们看看为什么你应该使用 WebSocket。
WebSocket 讲的是性能
WebSocket 使得实时通信更加高效。
您总是可以使用 HTTP 上的轮询(有时甚至是流)来接收 HTTP 上的通知。然而,WebSocket 节省了带宽、CPU 功率和延迟。
WebSocket 是性能上的创新。
WebSocket 讲的是简洁
WebSocket 使得客户机和服务器之间通过 Web 的通信更加简单。
那些已经经历过在 WebSocket 之前的体系结构中建立实时通信的痛苦的人知道,通过 HTTP 进行实时通知的技术过于复杂。跨无状态请求维护会话状态增加了复杂性。跨源 AJAX 很复杂,用 AJAX 处理有序请求需要特别考虑,用 AJAX 通信也很复杂。每次试图将 HTTP 扩展到非设计用例中都会增加软件的复杂性。
WebSocket 使您能够大大简化实时应用中面向连接的通信。
WebSocket 大约是标准
WebSocket 是一个底层网络协议,它使您能够在其上构建其他标准协议。
许多 web 应用本质上是单一的。大多数 AJAX 应用通常由紧密耦合的客户端和服务器组件组成。因为 WebSocket 自然支持高级应用协议的概念,所以您可以更加灵活地独立发展客户端和服务器。支持这些高级协议支持模块化,并鼓励可重用组件的开发。例如,您可以使用相同的 XMPP over WebSocket 客户端登录不同的聊天服务器,因为所有的 XMPP 服务器都理解相同的标准协议。
WebSocket 是互操作 web 应用的创新。
WebSocket 大约是html 5
WebSocket 是为 HTML5 应用提供高级功能的努力的一部分,以便与其他平台竞争。
每个操作系统都需要联网功能。应用打开套接字并与其他主机通信的能力是每个主要平台都提供的核心特性。从许多方面来说,HTML5 是使 web 浏览器成为类似于操作系统的全功能应用平台的趋势。像套接字这样的低级网络 API 无法与 Web 的原始安全模型或 API 设计风格相适应。WebSocket 为 HTML5 应用提供 TCP 风格的网络,而不会破坏浏览器的安全性和它有一个现代的 API。
WebSocket 是 HTML5 平台的一个关键组件,对于开发者来说是一个非常强大的工具。
你需要 WebSocket!
简单来说,你需要 WebSocket 来构建世界级的 web 应用。WebSocket 解决了使 HTTP 不适合实时通信的主要缺陷。WebSocket 支持的异步双向通信模式是对 Internet 上传输层协议所提供的一般灵活性的回归。
想想使用 WebSocket 并在应用中构建真正的实时功能的所有好方法,如聊天、协作文档编辑、大型多人在线(MMO)游戏、股票交易应用等等。我们将在本书的后面看一下具体的应用。
WebSocket 和 RFC 6455
WebSocket 是一个协议,但也有一个 WebSocket API,它使您的应用能够控制 WebSocket 协议并响应服务器触发的事件。API 由 W3C(万维网联盟)开发,协议由 IETF(互联网工程任务组)开发。现代浏览器现在支持 WebSocket API,它包括使用全双工、双向 WebSocket 连接所需的方法和属性。API 使您能够执行必要的操作,如打开和关闭连接、发送和接收消息,以及侦听服务器触发的事件。第二章更详细地描述了 API,并举例说明了如何使用 API。
WebSocket 协议支持客户端和远程服务器之间通过 Web 进行全双工通信,并支持二进制数据和文本字符串的传输。该协议由一个开始握手和随后的基本消息组帧组成,位于 TCP 之上。第三章更详细地描述了该协议,并向您展示了如何创建自己的 WebSocket 服务器。
WebSocket 的世界
WebSocket API 和协议有一个蓬勃发展的社区,这反映在各种 WebSocket 服务器选项、开发人员社区和目前正在使用的无数现实生活中的 WebSocket 应用上。
WebSocket 选项
现在有各种各样的 WebSocket 服务器实现,比如 Apache mod_pywebsocket、Jetty、Socket。IO,以及 Kaazing 的 WebSocket 网关。
HTML5 WebSocket 的权威指南的想法源于分享我们多年来在 Kaazing 使用 WebSocket 和相关技术的知识、经验和观点的愿望。Kaazing 五年来一直在构建一个企业 WebSocket 网关服务器及其客户端库。
WebSocket 社区:它活了!
我们已经列出了一些使用 WebSocket 的理由,并将探索如何自己实现 WebSocket 的真实、适用的例子。除了各种可用的 WebSocket 服务器,WebSocket 社区也在蓬勃发展,特别是在 HTML5 游戏、企业消息和在线聊天方面。每天都有更多的会议和编码会议,不仅致力于 HTML5 的特定领域,还致力于实时通信方法,尤其是 WebSocket。即使是构建广泛使用的企业消息服务的公司也在将 WebSocket 集成到他们的系统中。因为 WebSocket 是基于标准的,所以很容易增强您现有的架构,标准化和扩展您的实现,以及构建以前不可能或难以构建的新服务。
围绕 WebSocket 的兴奋也反映在 GitHub 这样的在线社区中,在那里每天都有更多与 WebSocket 相关的服务器、应用和项目被创建。其他蓬勃发展的在线社区是http://www.websocket.org,它托管了一个 WebSocket 服务器,我们将在后续章节中以此为例,还有http://webplatform.org和http://html5rocks.com,它们是开放的社区,鼓励共享所有与 HTML5 相关的信息,包括 WebSocket。
注更多 WebSocket 服务器在附录 b 中列出
WebSocket 的应用
在写这本书的时候,WebSocket 正被广泛应用。以前的“实时”通信技术(如 AJAX)可以实现一些应用,但是它们已经显著提高了性能。外汇和股票报价应用也受益于 WebSocket 提供的减少的带宽和全双工连接。我们将在第三章中了解如何检查 WebSocket 流量。
随着浏览器应用部署的增加,HTML5 游戏开发也出现了热潮。WebSocket 是网络游戏的天然选择,因为游戏玩法和游戏交互对响应能力的依赖令人难以置信。使用 WebSocket 的 HTML5 游戏的一些示例是流行的在线赌博应用、通过 WebSocket 与 WebGL 集成的游戏控制器应用以及游戏中的在线聊天。还有一些非常令人兴奋的大型多人在线(MMO)游戏,广泛应用于各种移动和桌面设备的浏览器中。
相关技术
您可能会惊讶地发现,还有其他技术可以与 WebSocket 结合使用,或者作为 web socket 的替代技术。以下是一些其他新兴的网络通信技术。
服务器发送的事件
当您的架构需要双向、全双工通信时,WebSocket 是一个不错的选择。但是,如果您的服务主要是向其客户端广播或推送信息,并且不需要任何交互性(例如新闻提要、天气预报等),那么使用服务器发送事件(SSE)提供的 EventSource API 是一个不错的选择。SSE 是 HTML5 规范的一部分,它整合了一些 Comet 技术。可以将 SSE 用作 HTTP 轮询、长时间轮询和流式传输的通用互操作语法。使用 SSE,您可以获得自动重新连接、事件 id 等等。
注意虽然 WebSocket 和 SSE 连接都是以 HTTP 请求开始的,但是你看到的性能优势和它们的能力可能会有很大的不同。例如,SSE 不能将流数据从客户端向上游发送到服务器,并且只支持文本数据。
SPDY
SPDY(发音为“speedy”)是 Google 正在开发的一种网络协议,并且受到越来越多浏览器的支持,包括 Google Chrome、Opera 和 Mozilla Firefox。本质上,SPDY 通过压缩 HTTP 头和多路复用来增强 HTTP 以提高 HTTP 请求的性能。其主要目的是提高 web 页面的性能。虽然 WebSocket 专注于优化 web 应用前端和服务器之间的通信,但 SPDY 也优化了交付应用内容和静态页面。HTTP 和 WebSocket 的区别是架构上的,而不是增量的。SPDY 是 HTTP 的修订版,所以它共享相同的架构风格和语义。它修复了 HTTP 的许多非固有问题,增加了多路复用、工作流水线和其他有用的增强。WebSocket 消除了请求-响应风格的通信,支持实时交互和替代的架构模式。
WebSocket 和 SPDY 是互补的;您将能够将您的 SPDY 增强的 HTTP 连接升级到 WebSocket,从而在 SPDY 上使用 WebSocket,并从两个世界的优点中获益。
网络实时通讯
Web 实时通信 (WebRTC )是增强现代 web 浏览器通信能力的又一努力。WebRTC 是用于 Web 的对等技术。浏览器可以直接通信,不需要通过服务器传输所有数据。WebRTC 包括 API,让浏览器能够实时地相互通信。在写这本书的时候,WebRTC 仍然是万维网联盟(W3C)的草案格式,可以在http://www.w3.org/TR/webrtc/找到。
WebRTC 的第一个应用是实时语音和视频聊天。对于媒体应用来说,WebRTC 已经是一项引人注目的新技术,网上有许多可用的示例应用,使您能够通过 Web 上的视频和音频来测试这一点。
WebRTC 稍后将添加数据通道。为了一致性,这些数据通道计划使用与 WebSocket 类似的 API。此外,如果您的应用使用流媒体和其他数据,您可以同时使用 WebSocket 和 WebRTC。
摘要
在这一章中,我们向你介绍了 HTML5 和 WebSocket,并了解了一点 HTTP 的历史,它把我们带到了 WebSocket。我们希望到现在为止,您和我们一样兴奋地学习更多关于 WebSocket 的知识,进入代码,并梦想您能够用它做所有美好的事情。
在随后的章节中,我们将更深入地研究 WebSocket API 和协议,解释如何将 WebSocket 与标准的、更高级别的应用协议一起使用,讨论 WebSocket 的安全方面,并描述企业级的特性和部署。*
二、WebSocket API
本章向您介绍 WebSocket 应用编程接口 (API),您可以使用它来控制 WebSocket 协议和创建 WebSocket 应用。在本章中,我们将研究 WebSocket API 的构建块,包括它的事件、方法和属性。为了学习如何使用 API,我们编写了一个简单的客户端应用,连接到一个现有的、公开可用的服务器(http://websocket.org),它允许我们通过 WebSocket 发送和接收消息。通过使用现有的服务器,我们可以专注于学习使您能够创建 WebSocket 应用的易于使用的 API。我们还将逐步解释如何使用 WebSocket API 来支持使用二进制数据的 HTML5 媒体。最后,我们讨论浏览器支持和连接。
本章重点介绍 WebSocket 的客户端应用,它使您能够将 WebSocket 协议扩展到您的 web 应用。后续章节将描述 WebSocket 协议本身,以及在您的环境中使用 WebSocket。
WebSocket API 概述
正如我们在第一章中提到的,WebSocket 由网络协议和 API 组成,使您能够在客户端应用和服务器之间建立 WebSocket 连接。我们将在第三章中更详细地讨论这个协议,但是让我们先来看看 API。
WebSocket API 是一个使应用能够使用 WebSocket 协议的接口。通过在应用中使用 API,您可以控制一个全双工通信通道,应用可以通过该通道发送和接收消息。WebSocket 界面非常简单易用。要连接到远程主机,只需创建一个新的 WebSocket 对象实例,并为新对象提供一个 URL,该 URL 表示您希望连接的端点。
在客户端和服务器之间的初始握手期间,通过相同的底层 TCP 连接,通过从 HTTP 协议升级到 WebSocket 协议来建立 WebSocket 连接。一旦建立,WebSocket 消息可以在 WebSocket 接口定义的方法之间来回发送。在应用代码中,然后使用异步事件侦听器来处理连接生命周期的每个阶段。
WebSocket API 是纯粹的(也是真正的)事件驱动的。一旦建立了全双工连接,当服务器有数据要发送到客户端时,或者如果您关心的资源改变了它们的状态,它会自动发送数据或通知。有了事件驱动的 API,您不需要向服务器轮询目标资源的最新状态;相反,客户端只是监听想要的通知和更改。
我们将在后续章节谈到更高层协议时看到使用 WebSocket API 的不同例子,比如 STOMP和 XMPP 。但是现在,让我们仔细看看 API。
WebSocket API 入门
WebSocket API 使您能够通过 Web 在客户端应用和服务器端进程之间建立全双工双向通信。WebSocket 接口指定了客户端可用的方法以及客户端如何与网络交互。
首先,通过调用 WebSocket 构造函数,创建一个 WebSocket 连接。构造函数返回一个 WebSocket 对象实例。您可以监听该对象上的事件。这些事件告诉您连接何时打开、消息何时到达、连接何时关闭以及何时发生错误。您可以与 WebSocket 实例交互来发送消息或关闭连接。随后的章节将探讨 WebSocket API 的每一个方面。
WebSocket 构造函数
要建立到服务器的 WebSocket 连接,可以使用 WebSocket 接口通过指向表示要连接的端点的 URL 来实例化 WebSocket 对象。WebSocket 协议定义了两种 URI 方案,ws和wss,分别用于客户端和服务器之间的未加密和加密流量。ws (WebSocket) 方案类似于 HTTP URI 方案。wss (WebSocket Secure) URI 方案代表了一个基于传输层安全 (TLS,也称为 SSL)的 WebSocket 连接,并使用与 HTTPS 保护 HTTP 连接相同的安全机制。
注我们将在第七章中深入讨论 WebSocket 安全性。
WebSocket 构造函数采用一个必需的参数URL(您要连接的 URL)和一个可选参数protocols(服务器必须在其响应中包含的单个协议名或协议名数组,以建立连接)。可以在protocols参数中使用的协议示例有 XMPP(可扩展消息和存在协议)、SOAP(简单对象访问协议)或自定义协议。
清单 2-1 展示了 WebSocket 构造函数中的一个必需参数,它必须是一个以ws://或wss://方案开头的全限定 URL。在本例中,完全限定的 URL 是ws:// www.websocket.org。如果 URL 中有语法错误,构造函数将抛出异常。
清单 2-1 。 样本 WebSocket 构造函数
// Create new WebSocket connection
var ws = new WebSocket("[ws://www.websocket.org](http://www.websocket.org)");
当连接到 WebSocket 服务器时,可以选择使用第二个参数来列出应用支持的协议,即协议协商。
为了确保客户端和服务器发送和接收它们都理解的消息,它们必须使用相同的协议。WebSocket 构造函数使您能够定义客户端可以用来与服务器通信的一个或多个协议。服务器依次选择要使用的协议;在客户端和服务器之间只能使用一种协议。这些协议在 WebSocket 协议上使用。正如你将在第三章到第六章中了解到的,WebSocket 的一大好处是能够在 WebSocket 上对广泛使用的协议进行分层,这让你可以做一些伟大的事情,比如将传统的桌面应用带到网络上。
注意WebSocket 协议(RFC 6455)指的是可以作为“子协议”、与 web socket 一起使用的协议,即使它们是更高级的、完全形成的协议。在本书中,为了避免混淆,我们通常将可以与 WebSocket 一起使用的协议简称为“协议”。
在我们走得太远之前,让我们回到 API 中的 WebSocket 构造函数。在最初的 WebSocket 连接握手过程中,你会在第三章中了解到更多,客户端发送一个带有协议名的Sec-WebSocket-Protocol头。服务器选择零个或一个协议,并以与客户端请求的名称相同的Sec-WebSocket-Protocol报头进行响应;否则,它会关闭连接。
协议协商对于确定给定的 WebSocket 服务器支持哪个协议或协议版本非常有用。应用可能支持多种协议,并使用协议协商来选择特定服务器使用哪种协议。清单 2-2 显示了支持一个假想协议“myProtocol”的 WebSocket 构造函数:
清单 2-2 。 支持协议的示例 WebSocket 构造函数
// Connecting to the server with one protocol called myProtocol
var ws = new WebSocket("ws://echo.websocket.org", "myProtocol");
注意在清单 2-2 中,假设的协议“myProtocol”是一个定义明确的,甚至可能是注册的和标准化的,客户端应用和服务器都能理解的协议名称。
WebSocket 构造函数还可以包含一组客户机支持的协议名称,这让服务器决定使用哪一个。清单 2-3 显示了一个样本 WebSocket 构造函数,它有一个它支持的协议列表,用数组表示:
清单 2-3 。 支持协议的 WebSocket 构造器示例
// Connecting to the server with multiple protocol choices
var echoSocket = new WebSocket("ws://echo.websocket.org", [ "com.kaazing.echo", "example.imaginary.protocol"])
echoSocket.onopen = function(e) {
// Check the protocol chosen by the server
console.log( echoSocket.protocol);
}
在清单 2-3 中,因为 ws://echo.websocket.org 的 WebSocket 服务器只理解com.kaazing.echo协议而不理解example.imaginary.protocol,所以当 WebSocket open事件触发时,服务器选择 com.kaazing.echo 协议。使用数组可以让您的应用灵活地对不同的服务器使用不同的协议。
我们将在下一章深入讨论 WebSocket 协议,但本质上,有三种类型的协议可以用 protocols 参数来表示:
- 已注册协议:已根据 RFC 6455(web socket 协议)正式注册的标准协议,并已向 IANA(Internet Assigned Numbers Authority,互联网号码分配机构)正式注册的协议。注册协议的一个例子是微软的 SOAP over WebSocket 协议。更多信息见
http://www.iana.org/assignments/websocket/websocket.xml。 - 开放协议 : 广泛使用的标准化协议,如 XMPP 和 STOMP,这些协议尚未注册为官方标准协议。我们将在随后的章节中研究如何在 WebSocket 中使用这些类型的协议。
- 自定义协议: 您已经编写并希望与 WebSocket 一起使用的协议。
在这一章中,我们将重点介绍如何使用 WebSocket API,就像您使用自己的自定义协议一样,并在后面的章节中研究如何使用开放协议。让我们分别看一下事件、对象和方法,并将它们放在一个工作示例中。
WebSocket 事件
WebSocket API 完全是事件驱动的。您的应用代码侦听 WebSocket 对象上的事件,以便处理传入的数据和连接状态的变化。WebSocket 协议也是事件驱动的。您的客户端应用不需要向服务器轮询更新的数据。服务器发送消息和事件时,它们将异步到达。
WebSocket 编程遵循异步编程模型,这意味着只要 WebSocket 连接是打开的,您的应用就只是侦听事件。您的客户端不需要主动轮询服务器来获取更多信息。要开始监听事件,只需向 WebSocket 对象添加回调函数。或者,可以使用addEventListener() DOM 方法将事件监听器添加到 WebSocket 对象中。
WebSocket 对象调度四个不同的事件:
- 打开
- 消息
- 错误
- 关闭
与所有 web APIs 一样,您可以使用on<eventname>处理程序属性以及addEventListener();方法来监听这些事件。
WebSocket 事件:打开
一旦服务器响应 WebSocket 连接请求,就会触发open 事件并建立连接。对open事件的相应回调称为onopen 。
清单 2-4 展示了当 WebSocket 连接建立后如何处理事件。
清单 2-4 。 示例打开事件处理程序
// Event handler for the WebSocket connection opening
ws.onopen = function(e) {
console.log("Connection open...");
};
当 open 事件触发时,协议握手已经完成,WebSocket 准备好发送和接收数据。如果您的应用接收到一个 open 事件,您可以确定 WebSocket 服务器成功地处理了连接请求,并同意与您的应用进行通信。
WebSocket 事件:消息
WebSocket 消息包含来自服务器的数据。您可能也听说过 WebSocket 框架,它包含 WebSocket 消息。我们将在第三章中更深入地讨论消息和帧的概念。为了理解消息如何与 API 一起工作,WebSocket API 只公开完整的消息,而不公开 WebSocket 框架。收到消息时触发message事件。消息事件对应的回调称为onmessage 。
清单 2-5 显示了一个接收文本消息并显示消息内容的消息处理器。
清单 2-5 。 短信事件处理程序示例
// Event handler for receiving text messages
ws.onmessage = function(e) {
if(typeof e.data === "string"){
console.log("String message received", e, e.data);
} else {
console.log("Other message received", e, e.data);
}
};
除了文本,WebSocket 消息还可以处理二进制数据,它们被处理为 Blob 消息,如清单 2-6 所示,或者被处理为 ArrayBuffer 消息,如清单 2-7 所示。因为 WebSocket 消息二进制数据类型的应用设置会影响传入的二进制消息,所以在读取数据之前,您必须决定要在客户端上为传入的二进制数据使用的类型。
***清单 2-6 。*Blob 消息的示例消息事件处理程序
// Set binaryType to blob (Blob is the default.)
ws.binaryType = "blob";
// Event handler for receiving Blob messages
ws.onmessage = function(e) {
if(e.data instanceof Blob){
console.log("Blob message received", e.data);
var blob = new Blob(e.data);
}
};
清单 2-7 显示了一个检查和处理 ArrayBuffer 消息的消息处理器。
***清单 2-7 。***array buffer 消息的示例消息事件处理程序
// Set binaryType to ArrayBuffer messages
ws.binaryType = "arraybuffer";
// Event handler for receiving ArrayBuffer messages
ws.onmessage = function(e) {
if(e.data instanceof ArrayBuffer){
console.log("ArrayBuffer Message Received", + e.data);
// e.data is an ArrayBuffer. Create a byte view of that object.
var a = new Uint8Array(e.data);
}
};
WebSocket 事件:错误
为了响应意外失败,触发了error事件。对error事件的相应回调称为onerror 。错误还会导致 WebSocket 连接关闭。如果您收到一个错误事件,您可以期待一个关闭事件紧随其后。close 事件中的代码和原因有时可以告诉您是什么导致了错误。error event handler是调用服务器的重新连接逻辑和处理来自 WebSocket 对象的异常的好地方。清单 2-8 显示了一个如何监听error事件的例子。
清单 2-8 。 样本错误事件处理程序
// Event handler for errors in the WebSocket object
ws.onerror = function(e) {
console.log("WebSocket Error: " , e);
//Custom function for handling errors
handleErrors(e);
};
WebSocket 事件:关闭
WebSocket 连接关闭时会触发close事件。对close事件的相应回调被称为onclose。一旦连接关闭,客户端和服务器就不能再接收或发送消息。
注意web socket 规范还定义了可用于保持活动、心跳、网络状态探测、延迟检测等的
ping和pong帧,但是 WebSocket API 目前并没有公开这些特性。尽管浏览器收到了一个ping帧,但它不会在相应的 WebSocket 上触发一个可见的ping事件。相反,浏览器会自动用一个pong框架来响应。然而,浏览器发起的 ping在一段时间后未被pong应答也可能触发连接close事件。第八章详细介绍了 WebSocket pings 和 pongs。
当您调用close()方法并终止与服务器的连接时,您也触发了onclose事件处理程序,如清单 2-9 所示。
清单 2-9 。 样本关闭事件处理程序
// Event handler for closed connections
ws.onclose = function(e) {
console.log("Connection closed", e);
};
WebSocket close事件在连接关闭时被触发,这可能是由于多种原因,如连接失败或 WebSocket 关闭握手成功。WebSocket 对象属性readyState反映了连接的状态(2 表示关闭,3 表示关闭)。
close事件有三个有用的属性可以用于错误处理和恢复 : wasClean、code和error。wasClean属性 是一个布尔值,指示连接是否被干净地关闭。如果 WebSocket 响应来自服务器的关闭帧而关闭,则属性为true。如果连接由于其他原因关闭(例如,因为底层 TCP 连接关闭),则wasClean属性为false。code 和 reason 属性指示从服务器传送的结束握手的状态。这些属性与WebSocket.close()方法中给出的代码和原因参数是对称的,我们将在本章后面详细描述。在第三章中,我们将在讨论 WebSocket 协议时讨论结束代码及其含义。
注关于 WebSocket 事件的更多细节,参见
http://www.w3.org/TR/websockets/的 WebSocket API 规范。
WebSocket 方法
WebSocket 对象有两种方法:send()和close()。
WebSocket 方法:send()
一旦使用 WebSocket 在客户机和服务器之间建立了全双工的双向连接,就可以在连接打开时调用send()方法(也就是说,在调用onopen监听器之后和调用onclose监听器之前)。您使用send()方法将消息从您的客户机发送到服务器。发送一条或多条消息后,您可以保持连接打开,或者调用close()方法来终止连接。
清单 2-10 是一个如何向服务器发送文本消息的例子。
清单 2-10 。 通过 WebSocket 发送短信
// Send a text message
ws.send("Hello WebSocket!");
send()方法在连接打开时传输数据。如果连接不可用或关闭,它将引发一个关于无效连接状态的异常。当开始使用 WebSocket API 时,人们犯的一个常见错误是试图在连接打开之前发送消息,如清单 2-11 所示。
清单 2-11 。 在打开连接之前尝试发送消息
// Open a connection and try to send a message. (This will not work!)
var ws = new WebSocket("ws://echo.websocket.org")
ws.send("Initial data");
清单 2-11 不会工作,因为连接还没有打开。相反,你应该在新构建的 WebSocket 上发送第一条消息之前等待open事件,如清单 2-12 所示。
清单 2-12 。?? 发送消息前等待打开事件
// Wait until the open event before calling send().
var ws = new WebSocket("ws://echo.websocket.org")
ws.onopen = function(e) {
ws.send("Initial data");
}
如果您想发送消息来响应另一个事件,您可以检查 WebSocket readyState属性并选择仅在套接字打开时发送数据,如清单 2-13 所示。
清单 2-13 。 检查打开的 WebSocket 的 readyState 属性
// Handle outgoing data. Send on a WebSocket if that socket is open.
function myEventHandler(data) {
if (ws.readyState === WebSocket.OPEN) {
// The socket is open, so it is ok to send the data.
ws.send(data);
} else {
// Do something else in this case.
//Possibly ignore the data or enqueue it.
}
}
除了文本(字符串)消息之外,WebSocket API 还允许您发送二进制数据,这对于实现二进制协议尤其有用。这种二进制协议可以是典型地位于 TCP 之上的标准互联网协议,其中有效载荷可以是 Blob 或 ArrayBuffer。清单 2-14 是一个如何通过 WebSocket 发送二进制消息的例子。
第六章展示了一个如何通过 WebSocket 发送二进制数据的例子。
清单 2-14 。 通过 WebSocket 发送二进制消息
// Send a Blob
var blob = new Blob("blob contents");
ws.send(blob);
// Send an ArrayBuffer
var a = new Uint8Array([8,6,7,5,3,0,9]);
ws.send(a.buffer);
Blob 对象在与 JavaScript 文件 API 结合用于发送和接收文件时特别有用,这些文件主要是多媒体文件、图像、视频和音频。本章末尾的示例代码将 WebSocket API 与 File API 结合使用,读取文件的内容,并将其作为 WebSocket 消息发送。
WebSocket 方法: close()
要关闭 WebSocket 连接或终止连接尝试,请使用close()方法。如果连接已经关闭,则该方法不执行任何操作。调用close()后,就不能在关闭的 WebSocket 上再发送任何数据了。清单 2-15 显示了一个close()方法的例子:
清单 2-15 。 调用close()方法
// Close the WebSocket connection
ws.close();
您可以选择向close()方法传递两个参数:code(一个数字状态代码)和reason(一个文本字符串)。传递这些参数会将有关客户端关闭连接的原因的信息传递给服务器。我们将在第三章中更详细地讨论状态代码和原因,当我们讨论 WebSocket 关闭握手时。清单 2-16 显示了一个用参数调用close()方法的例子。
清单 2-16 。 调用close()方法有原因
// Close the WebSocket connection because the session has ended successfully
ws.close(1000, "Closing normally");
清单 2-16 使用代码 1000,正如代码中所述,这意味着连接正常关闭。
WebSocket 对象属性
有几个 WebSocket 对象属性可以用来提供关于 WebSocket 对象的更多信息:readyState、bufferedAmount和协议。
WebSocket 对象属性:readyState
WebSocket 对象通过只读属性readyState报告连接的状态,您在前面的章节中已经了解了一些。该属性根据连接状态自动更改,并提供有关 WebSocket 连接的有用信息。
表 2-1 描述了四个不同的值,可以将readyState属性设置为这些值来描述连接状态。
表 2-1 。就绪状态属性,值和状态描述
| 属性常数 | 价值 | 状态 |
|---|---|---|
| WebSocket。连接 | Zero | 连接正在进行中,但尚未建立。 |
| websxmlsocket . open | one | 连接已经建立。消息可以在客户端和服务器之间流动。 |
| WebSocket。结束的 | Two | 连接正在通过结束握手。 |
| WebSocket。关闭的 | three | 连接已关闭或无法打开。 |
(万维网联盟,2012 年)
正如 WebSocket API 所描述的,当 WebSocket 对象第一次被创建时,它的readyState是0,表示套接字正在连接。了解 WebSocket 连接的当前状态可以帮助您调试应用,例如,确保在尝试开始向服务器发送请求之前已经打开了 WebSocket 连接。这些信息也有助于理解您的连接的生命周期。
WebSocket 对象属性:bufferedAmount
设计应用时,您可能希望检查为传输到服务器而缓冲的数据量,特别是当客户端应用向服务器传输大量数据时。尽管调用send()是即时的,但通过互联网传输数据却不是。浏览器将代表您的客户端应用缓冲传出的数据,因此您可以随时调用send(),使用您喜欢的数据。但是,如果您想知道数据流出网络的速度有多快,WebSocket 对象可以告诉您缓冲区的大小。您可以使用bufferedAmount属性来检查已经排队但尚未传输到服务器的字节数。此属性中报告的值不包括协议引起的帧开销或操作系统或网络硬件完成的缓冲。
清单 2-17 展示了一个如何使用bufferedAmount属性每秒发送更新的例子;如果网络无法处理该速率,它会相应地进行调整。
清单 2-17 。 bufferedAmount 示例
// 10k max buffer size.
var THRESHOLD = 10240;
// Create a New WebSocket connection
var ws = new WebSocket("ws://echo.websocket.org/updates");
// Listen for the opening event
ws.onopen = function () {
// Attempt to send update every second.
setInterval( function() {
// Send only if the buffer is not full
if (ws.bufferedAmount < THRESHOLD) {
ws.send(getApplicationState());
}
}, 1000);
};
使用bufferedAmount属性有助于限制应用向服务器发送数据的速率,从而避免网络饱和。
专业提示在尝试关闭连接之前,您可能希望检查 WebSocket 对象的
bufferedAmount属性,以确定是否有任何数据尚未从应用传输。
WebSocket 对象属性:协议
在我们之前关于 WebSocket 构造函数的讨论中,我们提到了protocol参数,它让服务器知道客户端理解并可以通过 WebSocket 使用哪个协议。WebSocket 对象protocol属性提供了另一条关于 WebSocket 实例的有用信息。客户端和服务器之间的协议协商结果在 WebSocket 对象上是可见的。protocol属性包含 WebSocket 服务器在开始握手时选择的协议名称。换句话说,protocol属性告诉您特定的 WebSocket 使用哪种协议。在开始握手完成之前,protocol属性是一个空字符串,如果服务器没有选择客户端提供的协议之一,它就仍然是一个空字符串。
将所有这些放在一起
既然我们已经走过了 WebSocket 构造函数、事件、属性和方法,那么让我们把我们所学到的关于 WebSocket API 的东西放在一起。这里,我们创建一个客户机应用,通过 Web 与远程服务器通信,并使用 WebSocket 交换数据。我们的示例 JavaScript 客户机使用托管在ws://echo.websocket.org的“Echo”服务器,它接收并返回您发送给服务器的任何消息。使用 Echo 服务器对于纯客户端测试非常有用,特别是对于理解 WebSocket API 如何与服务器交互。
首先,我们创建连接,然后在网页上显示由我们的代码触发的事件,这些事件来自服务器。该页面将显示有关客户端连接到服务器、向服务器发送消息和从服务器接收消息,然后从服务器断开连接的信息。
清单 2-18 显示了一个与服务器通信和消息传递的完整例子。
清单 2-18 。 使用 WebSocket API 完成客户端应用
<!DOCTYPE html>
<title>WebSocket Echo Client</title>
<h2>Websocket Echo Client</h2>
<div id="output"></div>
<script>
// Initialize WebSocket connection and event handlers
function setup() {
output = document.getElementById("output");
ws = new WebSocket("ws://echo.websocket.org/echo");
// Listen for the connection open event then call the sendMessage function
ws.onopen = function(e) {
log("Connected");
sendMessage("Hello WebSocket!")
}
// Listen for the close connection event
ws.onclose = function(e) {
log("Disconnected: " + e.reason);
}
// Listen for connection errors
ws.onerror = function(e) {
log("Error ");
}
// Listen for new messages arriving at the client
ws.onmessage = function(e) {
log("Message received: " + e.data);
// Close the socket once one message has arrived.
ws.close();
}
}
// Send a message on the WebSocket.
function sendMessage(msg){
ws.send(msg);
log("Message sent");
}
// Display logging information in the document.
function log(s) {
var p = document.createElement("p");
p.style.wordWrap = "break-word";
p.textContent = s;
output.appendChild(p);
// Also log information on the javascript console
console.log(s);
}
// Start running the example.
setup();
</script>
运行网页后,输出应类似于以下内容:
WebSocket Sample Client
Connected
Message sent
Message received: Hello WebSocket!
Disconnected
如果你看到这个输出,恭喜你!您已经成功地创建并执行了第一个示例 WebSocket 客户端应用。如果这个例子不起作用,你需要调查它失败的原因。您可以在浏览器的 JavaScript 控制台中找到有用的信息。虽然可能性越来越小,但您的浏览器可能不支持 WebSocket。虽然每个主流浏览器的最新版本都包含对 WebSocket API 和协议的支持,但仍有一些旧浏览器不支持这种支持。下一节将向您展示如何确保您的浏览器支持 WebSocket。
检查 WebSocket 支持
因为(令人惊讶的)并非所有的 web 浏览器都支持 WebSocket,所以在代码中包含一种确定浏览器支持的方法是一个好的实践,如果可能的话,提供一个后备。大多数现代浏览器都支持 WebSocket,但是根据用户的不同,您可能希望使用这些技术中的一种来覆盖您的基础。
注 第八章讨论了各种 WebSocket 回退和仿真选项。
有几种方法可以确定自己的浏览器是否支持 WebSocket。用来研究代码的一个便利工具是 web 浏览器的 JavaScript 控制台。每个浏览器都有不同的方式来启动 JavaScript 控制台。例如,在谷歌 Chrome 中,你可以通过选择视图开发者
开发者工具,然后点击控制台来打开控制台。有关 Chrome 开发者工具的更多信息,请参见
https://developers.google.com/chrome-developer-tools/docs/overview。
专业提示谷歌的 Chrome 开发者工具也能让你检查 WebSocket 流量。为此,在“开发工具”面板中,单击“网络”,然后在面板底部,单击“WebSockets”。附录 A 详细介绍了有用的 WebSocket 调试工具。
打开浏览器的交互式 JavaScript 控制台,评估表达式window.WebSocket。如果您看到 WebSocket 构造函数对象,这意味着您的 web 浏览器本机支持 WebSocket。如果您的浏览器支持 WebSocket,但是您的示例代码不工作,您将需要进一步调试您的代码。如果对同一个表达式求值,得到的结果是空的或未定义的,那么您的浏览器本身就不支持 WebSocket。
为了确保您的 WebSocket 应用在不支持 WebSocket 的浏览器中工作,您需要考虑回退或仿真策略。您可以自己编写(这非常复杂),使用 polyfill(一个为旧浏览器复制标准 API 的 JavaScript 库),或者使用 Kaazing 这样的 WebSocket 供应商,它支持 WebSocket 仿真,使任何浏览器(回到 Microsoft Internet Explorer 6)都支持 HTML5 WebSocket 标准 API。作为将 WebSocket 应用部署到企业的一部分,我们将在第八章中进一步讨论这些选项。
作为应用的一部分,您可以添加一个对 WebSocket 支持的条件检查,如清单 2-19 所示。
**清单 2-19 。**中的客户端代码确定浏览器中的 WebSocket 支持
if (window.WebSocket){
console.log("This browser supports WebSocket!");
} else {
console.log("This browser does not support WebSocket.");
}
注意网上有很多描述 HTML5 和 WebSocket 与浏览器兼容的资源,包括手机浏览器。两个这样的资源是
http://caniuse.com/和http://html5please.com/??。
通过 WebSocket 使用 HTML5 媒体
作为 HTML5 和 Web 平台的一部分,WebSocket API 被设计为能够很好地与 HTML5 的所有特性一起工作。可以用 API 发送和接收的数据类型对于传输应用数据和媒体非常有用。当然,字符串允许您表示像 XML 和 JSON 这样的 web 数据格式。二进制类型集成了 API,如拖放、FileReader、WebGL 和 Web Audio API。
让我们来看看如何通过 WebSocket 使用 HTML5 媒体。清单 2-20 显示了一个完整的客户端应用使用 HTML5 媒体和 WebSocket。您可以基于这段代码创建自己的 HTML 文件。
注意要构建(或简单地遵循)本书中的示例,您可以选择使用我们创建的虚拟机(VM ),它包含了我们在示例中使用的所有代码、库和服务器。有关如何下载、安装和启动虚拟机的说明,请参考附录 B。
清单 2-20 。 通过 WebSocket 使用 HTML5 媒体完成客户端应用
<!DOCTYPE html>
<title>WebSocket Image Drop</title>
<h1>Drop Image Here</h1>
<script>
// Initialize WebSocket connection
var wsUrl = "ws://echo.websocket.org/echo";
var ws = new WebSocket(wsUrl);
ws.onopen = function() {
console.log("open");
}
// Handle binary image data received on the WebSocket
ws.onmessage = function(e) {
var blob = e.data;
console.log("message: " + blob.size + " bytes");
// Work with prefixed URL API
if (window.webkitURL) {
URL = webkitURL;
}
var uri = URL.createObjectURL(blob);
var img = document.createElement("img");
img.src = uri;
document.body.appendChild(img);
}
// Handle drop event
document.ondrop = function(e) {
document.body.style.backgroundColor = "#fff";
try {
e.preventDefault();
handleFileDrop(e.dataTransfer.files[0]);
return false;
} catch(err) {
console.log(err);
}
}
// Provide visual feedback for the drop area
document.ondragover = function(e) {
e.preventDefault();
document.body.style.backgroundColor = "#6fff41";
}
document.ondragleave = function() {
document.body.style.backgroundColor = "#fff";
}
// Read binary file contents and send them over WebSocket
function handleFileDrop(file) {
var reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function() {
console.log("sending: " + file.name);
ws.send(reader.result);
}
}
</script>
在您最喜欢的现代浏览器中打开此文件。当 WebSocket 连接打开时,查看一下浏览器的 JavaScript 控制台。图 2-1 显示了运行在 Mozilla Firefox 中的客户端应用。注意,在这个图的底部,我们显示了 Firebug(一个强大的 web 开发和调试工具,可在http://getfirebug.com获得)中提供的 JavaScript 控制台。
图 2-1 。使用 HTML5 媒体的客户端应用,带有在 Mozilla Firefox 中显示的 WebSocket】
现在,试着将一个图像文件拖放到这个页面上。将图像文件拖放到页面上之后,您应该会看到呈现在网页上的图像,如图 2-2 所示。请注意 Firebug 是如何显示添加到页面中的图像文件的信息的。
图 2-2 。使用 HTML5 媒体和 Mozilla Firefox 中的 WebSocket 在客户端应用中显示的图像(PNG)
注意服务器
websocket.org目前只接受小消息,所以这个例子只适用于小于 65kb 的图像文件,尽管这个限制可能会改变。您可以在自己的服务器上试验更大的媒体。
这个演示的“惊喜”因素可能会因为媒体来自最终显示它的同一个浏览器而减少。你可以用 AJAX 甚至不用网络来实现同样的视觉效果。当客户端或服务器发送一些媒体数据,并由不同的浏览器显示时,事情变得非常有趣——甚至是成千上万的其他浏览器!在广播场景中,读取和显示二进制图像数据的机制与这个简化的 echo 演示相同。
摘要
在本章中,您学习了 WebSocket API 的各个方面,它使您能够从浏览器中运行的客户端应用启动 WebSocket 连接,并通过 WebSocket 连接从服务器向客户端发送消息。您了解了 WebSocket API 背后的基本概念,包括事件、消息和属性,还了解了一些 API 的实际例子。您还了解了如何使用公开可用的 WebSocket Echo 服务器创建自己的 WebSocket 应用,您可以使用它来进一步测试自己的应用。有关该接口的权威定义,请参见位于http://www.w3.org/TR/websockets/的完整 WebSocket API 规范。
在第三章中,您将学习 WebSocket 协议,并逐步构建您自己的基本 WebSocket 服务器。
三、WebSocket 协议
WebSocket 是一种网络协议,它定义了服务器和客户端如何通过 Web 进行通信。协议是商定的通信规则。组成互联网的一套协议由互联网工程任务组 IETF 发布。IETF 发布了称为 RFC 的征求意见稿,RFC 精确地指定了协议,包括 RFC 6455:web socket 协议。RFC 6455 发布于 2011 年 12 月,包含了实现 WebSocket 客户端或服务器时必须遵循的确切规则。
在前一章中,我们探讨了 WebSocket API,它允许应用与 WebSocket 协议进行交互。在这一章中,我们将带您了解互联网和协议的简史,为什么创建 WebSocket 协议,以及它是如何工作的。我们使用网络工具来观察和了解 WebSocket 网络流量。使用一个用 JavaScript 和 Node.js 编写的示例 WebSocket 服务器,我们研究 WebSocket 握手如何建立 WebSocket 连接,消息如何编码和解码,以及连接如何保持活动和关闭。最后,我们使用这个示例 WebSocket 服务器同时远程控制几个浏览器。
在 WebSocket 协议之前
为了更好地理解 WebSocket 协议,让我们通过快速浏览来看看 WebSocket 如何适应一个重要的协议家族,从而了解一些历史背景。
协议!
协议是计算最重要的部分之一。它们跨越编程语言、操作系统和硬件架构。它们允许由不同的人编写并由不同的代理操作的组件在房间内或世界范围内相互通信。在开放的、可互操作的系统中,许多成功的故事都归功于设计良好的协议。
在万维网及其组成技术(如 HTML 和 HTTP)出现之前,互联网是一个非常不同的网络。一方面,它要小得多,另一方面,它本质上是一个同辈人的网络。当在互联网主机之间进行通信时,两种协议过去是并且现在仍然是流行的:互联网协议(IP,其负责在互联网上的两个主机之间简单地传输分组)和传输控制协议(TCP,其可以被视为在互联网上延伸的管道,并且在两个端点之间的每个方向上承载可靠的字节流)。总之,TCP over IP (TCP/IP)一直是并将继续是无数网络应用使用的核心传输层协议。
互联网简史
一开始,互联网主机之间有 TCP/IP 通信。在这种情况下,任何一台主机都可以建立新的连接。TCP 连接一旦建立,任何一台主机都可以随时发送数据,如图图 3-1 所示。
图 3-1 。互联网主机之间的 TCP/IP 通信
网络协议中可能需要的任何其他功能都必须建立在传输协议之上。这些更高层被称为应用协议 。例如,在 Web 出现之前的两个重要的应用层协议是用于聊天的 IRC 和用于远程终端访问的 Telnet 。IRC 和 Telnet 显然需要异步双向通信。当另一个用户发送聊天消息或远程应用打印一行输出时,客户端必须收到提示通知。由于这些协议通常在 TCP 上运行,异步双向通信总是可用的。IRC 和 Telnet 会话保持持久的连接,客户机和服务器可以在任何时候自由地互相发送消息。TCP/IP 也是另外两个重要协议的基础:HTTP 和 WebSocket。不过,在我们开始之前,让我们先简要地看一下 HTTP。
Web 和 HTTP
1991 年,万维网项目以其最早的公开形式被宣布。网络是一个使用统一资源定位符(URL)链接超文本文档的系统。当时,URL 是一项重大创新。URL 中的 U 代表 universal,表明了当时革命性的想法,即所有超文本文档都可以相互连接。Web 上的 HTML 文档通过 URL 链接到其他文档。因此,网络协议是为获取资源而定制的是有道理的。HTTP 是一种简单的同步请求-响应式文档传输协议。
最早的 web 应用使用表单和整页重载。每次用户提交信息时,浏览器都会提交一个表单并获取一个新页面。每次有更新的信息要显示时,用户或浏览器都必须刷新整个页面,才能使用 HTTP 获取完整的资源。
有了 JavaScript 和 XMLHttpRequest API,一套被称为 AJAX 的技术被开发出来,以允许更无缝的应用,在每次交互过程中不会有突然的转变。AJAX 让应用只获取感兴趣的资源数据,并在没有导航的情况下更新页面。用 AJAX,网络协议还是 HTTP 尽管有 XMLHttpRequest 名称,但数据有时(但不总是)是 XML。
网络已经变得相当流行。如此受欢迎,事实上,许多人混淆了网络和互联网,因为网络通常是他们使用的唯一重要的互联网应用。 NAT(网络地址转换)、HTTP 代理和防火墙也变得越来越普遍。今天,许多互联网用户没有公开可见的 IP 地址。用户没有唯一的 IP 地址的原因有很多,包括安全措施、过度拥挤和缺乏必要。地址的缺乏妨碍了可寻址性;例如,需要公共地址的蠕虫无法访问未编址的用户。此外,没有足够的 IPv4 地址供所有 Web 用户使用。NAT 允许用户共享公共 IP 地址,同时仍能在网上冲浪。最后,占主导地位的协议 HTTP 不需要可寻址的客户端。HTTP 对于由客户端应用驱动的交互工作得相当好,因为客户端发起每个 HTTP 请求,如图图 3-2 所示:
图 3-2 。HTTP 客户端将连接到一个网络服务器
本质上,HTTP 通过其对文本 (从而支持我们互连的 HTML 页面)、URL 和 HTTPS(安全 HTTP over 传输层安全性(TLS) )的内置支持使 Web 成为可能。然而,在某些方面,HTTP 也因其流行而导致互联网倒退。因为 HTTP 不需要可寻址的客户端,所以网络世界中的寻址是不对称的。浏览器可以通过 URL 访问服务器上的资源,但是服务器端应用无法主动将资源发送给客户端。客户端只能发出请求,服务器只能响应未完成的请求。在这个不对称的世界里,要求全双工通信的协议并不奏效。
解决这一限制的一种方法是让客户机打开 HTTP 请求,以防服务器有更新要共享。使用 HTTP 请求来逆转通知流的总称称为“Comet”正如我们在前面章节中所讨论的,Comet 基本上是一组技术,通过轮询、长时间轮询和流式传输将 HTTP 扩展到极限。这些技术本质上模拟了 TCP 的一些功能,以便处理相同的服务器到客户端用例。由于同步 HTTP 和这些异步应用之间的不匹配,Comet 往往是复杂的、非标准的和低效的。
注在服务器对服务器的通信中,每台主机都可以寻址对方。当有新数据可用时,一个服务器可以简单地向另一个服务器发出 HTTP 请求,这就是用于服务器到服务器提要更新通知的 PubSubHubbub 协议的情况。PubSubHubbub 是一个开放协议,扩展了 RSS 和 Atom ,支持 HTTP 服务器之间的发布/订阅通信。虽然使用 WebSocket 可以实现服务器到服务器的通信,但本书主要关注实时 web 应用中的客户机-服务器通信。
介绍 WebSocket 协议
这一简短的互联网历史课程将我们带到了今天。现在,web 应用非常强大,具有重要的客户端状态和逻辑。通常,现代 web 应用需要双向通信。更新的即时通知更像是常规而非例外,用户越来越期望响应性的实时交互。让我们来看看 WebSocket 给了我们什么。
WebSocket:网络应用的互联网功能
WebSocket 保留了我们喜欢的 HTTP for web 应用的许多特性(URL、HTTP 安全性、更简单的基于消息的数据模型和对文本的内置支持),同时支持其他网络架构和通信模式。和 TCP 一样,WebSocket 是异步的,可以作为更高层协议的传输层。WebSocket 是消息协议、聊天、服务器通知、流水线和多路复用协议、自定义协议、压缩二进制协议和其他用于与 Internet 服务器互操作的标准协议的良好基础。
WebSocket 为 web 应用提供了 TCP 风格的网络功能。寻址仍然是单向的。服务器可以异步地向客户端发送数据,但是只有在有一个打开的 WebSocket 连接时。WebSocket 连接总是从客户端建立到服务器。WebSocket 服务器也可以充当 WebSocket 客户端。然而,使用 WebSocket,像浏览器这样的 web 客户端不能接受不是由它们发起的连接。图 3-3 显示了连接到服务器的 WebSocket 客户端,客户端或者服务器可以随时发送数据。
图 3-3 。连接到服务器的 WebSocket 客户端
WebSocket 桥接了网络世界和互联网世界(或者更具体地说,TCP/IP)。以前不容易与 web 应用一起使用的异步协议现在可以使用 WebSocket 轻松地进行通信。表 3-1 比较了 TCP、HTTP 和 WebSocket 的主要领域。
表 3-1 。TCP、HTTP 和 WebSocket 的比较
TCP 只传递字节流 ,所以消息边界必须用更高层的协议来表示。初学使用 TCP 的 socket 程序员常犯的一个错误是,假设每个对send()的调用都会导致一个成功的receive.,虽然对于简单的测试来说这可能是真的,但是当负载和延迟变化时,在 TCP socket 上发送的字节会不可预测地被分段。根据操作系统的判断,TCP 数据可以分布在多个 IP 数据包上,也可以组合成较少的数据包。TCP 中唯一的保证是到达接收端的单个字节将按顺序到达。与 TCP 不同,WebSocket 传输一系列离散的消息 。有了 WebSocket,多字节的消息会完整有序的到达,就像 HTTP 一样。因为消息边界内置于 WebSocket 协议中,所以可以发送和接收单独的消息,并避免常见的分段错误。
值得一提的是,在互联网出现之前,另一种网络模型正在被采用:开放系统互连(OSI) ,它包括七层:物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。然而,尽管术语可能相似,OSI 在设计时并没有考虑到互联网。TCP/IP 模型是为互联网设计的,它只包括四层:链路层、Internet 层、传输层和应用层,并且是今天驱动互联网的模型。
IP 位于互联网层,TCP 位于 IP 之上的传输层。WebSocket 位于 TCP(因此也是 IP)之上,它也被认为是传输层,因为您可以在 WebSocket 之上放置应用级协议。
检测 WebSocket 流量
在第二章中,我们使用了 WebSocket API,却没有真正看到网络层发生了什么。如果您想要查看网络上的 WebSocket 流量,您可以使用 Wireshark ( http://www.wireshark.org/)或 tcpdump ( http://www.tcpdump.org/)等工具,并检查通信堆栈内部的内容。Wireshark 使您能够“剖析”WebSocket 协议,这使您可以在一个方便的 UI 中查看我们将在本章稍后讨论的 WebSocket 协议的各个部分(例如,操作码、标志和有效载荷),如图图 3-4 所示。它甚至会显示从 WebSocket 客户端发送的消息的非屏蔽版本。我们将在本章后面讨论屏蔽。
图 3-4 。在 Wireshark 中查看 WebSocket 会话
注 附录 A 详细介绍了 WebSocket 流量调试工具。
WebKit (驱动谷歌 Chrome 和苹果 Safari 的浏览器引擎)最近也增加了对检查 WebSocket 流量的支持。在最新版本的 Chrome 浏览器中,你可以在开发者工具的网络标签中看到 WebSocket 消息。图 3-5 展示了这个用 WebSocket 开发的必备工具。
图 3-5 。使用谷歌浏览器开发工具查看网络套接字会话
我们强烈建议使用这些工具来观察 WebSockets 的运行,不仅是为了了解协议,也是为了更好地理解 WebSocket 会话期间发生的事情。
WebSocket 协议
让我们仔细看看 WebSocket 协议。对于协议的每个部分,我们将查看 JavaScript 代码来处理特定的语法。之后,我们将把这些片段组合成一个示例服务器库和两个简单的应用。
WebSocket 开场握手
每个 WebSocket 连接都以一个 HTTP 请求开始。这个请求与其他请求很相似,除了它包含一个特殊的头:Upgrade。Upgrade报头表示客户端想要将连接升级到不同的协议。在本例中,不同的协议是 WebSocket。
让我们看一个在连接到ws://echo.websocket.org/echo时记录的握手示例。在握手完成之前,WebSocket 会话符合 HTTP/1.1 协议。客户端发送如清单 3-1 所示的 HTTP 请求。
***清单 3-1。***来自客户端的 HTTP 请求
GET /echo HTTP/1.1
Host: echo.websocket.org
Origin:http://www.websocket.org
Sec-WebSocket-Key: 7+C600xYybOv2zmJ69RQsw==
Sec-WebSocket-Version: 13
Upgrade: websocket
清单 3-2 显示了服务器发回响应。
清单 3-2。 来自服务器的 HTTP 响应
101 Switching Protocols
Connection: Upgrade
Date: Wed, 20 Jun 2012 03:39:49 GMT
Sec-WebSocket-Accept: fYoqiH14DgI+5ylEMwM2sOLzOi0=
Server: Kaazing Gateway
Upgrade: WebSocket
图 3-6 说明了从客户端到服务器的对 WebSocket 的 HTTP 请求升级,也称为 WebSocket 开放握手。
图 3-6 。WebSocket 开放式握手示例
图 3-6 显示了必需和可选的接头。一些头是严格必需的,必须存在并且精确,WebSocket 连接才能成功。这个握手中的其他头是可选的,但是是允许的,因为握手是一个 HTTP 请求和响应。成功升级后,连接的语法切换到用于表示 WebSocket 消息的数据帧格式。除非服务器用101响应码、Upgrade报头和Sec-WebSocket-Accept报头响应,否则连接不会成功。Sec-WebSocket-Accept响应头的值来源于Sec-WebSocket-Key请求头,包含一个特殊的键响应,它必须与客户端期望的完全匹配。
计算按键响应
为了成功完成握手,WebSocket 服务器必须用一个计算出的密钥进行响应。这个响应表明服务器特别理解 WebSocket 协议。如果没有确切的响应,就有可能欺骗一些毫无戒心的 HTTP 服务器,使其意外地升级连接!
这个响应函数从客户端发送的Sec-WebSocket-Key头中获取键值,并在返回的Sec-WebSocket-Accept头中返回客户端期望的计算值。清单 3-3 使用 Node.js 加密 API 来计算组合密钥和后缀的 SHA1 散列:
清单 3-3。 使用 Node.jspto API Crypto API 计算密钥响应
var KEY_SUFFIX = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
var hashWebSocketKey = function(key) {
var sha1 = crypto.createHash("sha1");
sha1.update(key + KEY_SUFFIX, "ascii");
return sha1.digest("base64");
}
注清单 3-3 中的 KEY_SUFFIX 是协议规范中包含的一个常量 KEY 后缀,每个 WebSocket 服务器都必须知道。
在 WebSocket 开始握手和计算密钥响应时,WebSocket 协议依赖于 RFC 6455 中定义的 Sec-header。表 3-2 描述了这些 web socket Sec-header。
表 3-2 。WebSocket Sec- Headers 及其描述(RFC 6455
| 页眉 | 描述 |
|---|---|
| sec-web 套接字密钥 | 在 HTTP 请求中只能出现一次。用于从客户端到服务器的开放式 WebSocket 握手,以防止跨协议攻击。参见 Sec-WebSocket-Accept。 |
| sec-web socket-接受 | 在 HTTP 响应中只能出现一次。用于从服务器到客户端的 WebSocket 握手,以确认服务器理解 WebSocket 协议。 |
| sec-web socket-扩展 | 可以在 HTTP 请求中出现多次(逻辑上与包含所有值的单个 Sec-WebSocket-Extensions 头字段相同),但在 HTTP 响应中只能出现一次。用于 WebSocket 从客户端到服务器,然后从服务器到客户端的开放握手。这个头帮助客户机和服务器就一组协议级扩展达成一致,以便在连接期间使用。 |
| sec-web 套接字协议 | 用于从客户端到服务器的 WebSocket 握手,然后从服务器协商一个子协议。这个头通告了客户端应用可以使用的协议。服务器使用相同的报头来选择这些协议中的最多一个。 |
| sec-web socket-版本 | 在从客户端到服务器的开始 WebSocket 握手中使用,以指示版本兼容性。RFC 6455 的版本始终是 13。如果服务器不支持客户端请求的协议版本,它会用此报头进行响应。在这种情况下,服务器发送的标题列出了它支持的版本。这只有在客户端早于 RFC 6455 时才会发生。 |
消息格式
当 WebSocket 连接打开时,客户端和服务器可以随时相互发送消息。这些消息在网络上用二进制语法表示,该语法标记消息之间的边界,并包括简明的类型信息。更准确地说,这些二进制头标记了其他东西之间的边界,称为帧。帧是可以组合形成消息的部分数据。在关于 WebSocket 的讨论中,您可能会看到“帧”和“消息”交替使用。使用这两个术语是因为(至少目前)很少在每条消息中使用一个以上的帧。此外,在早期的协议草案中,帧是消息,网络上的消息表示被称为成帧。
你会回忆起第二章中的内容,WebSocket API 没有向应用公开框架级的信息。即使 API 是根据消息工作的,也可以在协议层处理子消息数据单元。一条消息中通常只有一个帧,但是一条消息可以由任意数量的帧组成。在全部数据可用之前,服务器可以使用不同数量的帧来开始传送数据。
让我们仔细看看 WebSocket 框架的各个方面。图 3-7 说明了 WebSocket 帧头。
图 3-7 。WebSocket 帧头
WebSocket 成帧代码负责:
- 操作码
- 长度
- 解码文本
- 掩饰
- 多帧消息
操作码
每个 WebSocket 消息都有一个指定消息有效负载类型的操作码。操作码由帧头第一个字节的后四位组成。操作码有一个数值,如表 3-3 所述。
表 3-3 。定义的操作码
| 操作码 | 消息负载的类型 | 描述 |
|---|---|---|
| one | 文本 | 消息的数据类型是文本。 |
| Two | 二进制的 | 消息的数据类型是二进制的。 |
| eight | 关闭 | 客户端或服务器正在向服务器或客户端发送结束握手。 |
| nine | 砰 | 客户端或服务器向服务器或客户端发送 ping 命令(参见第八章了解更多关于使用 ping 和 pong 的详细信息)。 |
| 10(十六进制 0xA) | 恶臭 | 客户端或服务器向服务器或客户端发送一个 pong(参见第八章关于使用 ping 和 pong 的更多细节)。 |
由于操作码使用四位,因此最多可以有 16 个不同的值。WebSocket 协议只定义了五个操作码,其余的操作码都是预留给将来在扩展中使用的。
长度
WebSocket 协议使用可变位数对帧长度进行编码,这允许小消息使用紧凑的编码,同时仍然允许协议承载中等大小甚至非常大的消息。对于 126 字节以下的消息,长度被打包成头两个字节中的一个。对于 126 和 216 之间的长度,使用两个额外的字节。对于大于 126 字节的消息,包括 8 个字节的长度。长度编码在帧头第二个字节的后七位中。该字段中的值 126 和 127 被视为特殊信号,附加字节将跟随该信号以完成编码长度。
解码文本
文本 WebSocket 消息使用 UCS 转换格式(8 位或 UTF-8)进行编码。UTF-8 是 Unicode 的可变长度编码,也向后兼容 7 位 ASCII。UTF-8 也是 WebSocket 文本消息中唯一允许的编码。将编码保持在 UTF-8 可以防止无数“纯文本”格式和协议中不同编码的混杂妨碍互操作性。
在清单 3-4 中,deliverText函数使用 Node.js 中的buffer.toString() API 将传入消息的有效载荷转换为 JavaScript 字符串。UTF-8 是buffer.toString()的默认编码,但为了清晰起见,在此指定。
清单 3-4。【UTF-8】文本编码
case opcodes.TEXT:
payload = buffer.toString("utf8");
屏蔽
从浏览器向上游发送到服务器的 WebSocket 帧被“屏蔽”以混淆其内容。屏蔽的目的不是防止窃听,而是出于特殊的安全原因,并提高与现有 HTTP 代理的兼容性。参见第七章进一步解释屏蔽旨在防止的跨协议攻击。
帧头第二个字节的第一位表示帧是否被屏蔽;WebSocket 协议要求客户端屏蔽它们发送的每个帧。如果有掩码,它将是帧头的扩展长度部分之后的四个字节。
WebSocket 服务器接收到的每个有效负载在处理之前首先被解除屏蔽。清单 3-5 显示了一个简单的函数,该函数在给定四个屏蔽字节的情况下,对 WebSocket 帧的有效载荷部分进行去屏蔽。
清单 3-5。 揭开有效载荷
var unmask = function(mask_bytes, buffer) {
var payload = new Buffer(buffer.length);
for (var i=0; i<buffer.length; i++) {
payload[i] = mask_bytes[i%4] ^ buffer[i];
}
return payload;
}
解除屏蔽后,服务器拥有原始的消息内容:二进制消息可以直接传送,文本消息将被 UTF-8 解码,并作为字符串通过服务器 API 公开。
多帧消息
帧格式中的 fin 位允许多帧消息或部分可用消息的流,这些消息可能是分段的或不完整的。要传输不完整的消息,您可以发送 fin 位设置为零的帧。最后一帧的 fin 位设置为 1,表示消息以该帧的有效载荷结束。
WebSocket 关闭握手
我们在本章的前面已经讨论了 WebSocket 开放式握手。在人际交往中,我们经常在初次见面时握手。有时我们分手时也会握手。本协议的情况也是如此。WebSocket 连接总是从开始握手开始,因为这是初始化对话的唯一方式。在 Internet 和其他不可靠的网络上,连接随时都可能关闭,因此不能说连接总是以关闭握手结束。有时候底层的 TCP 套接字会突然关闭。关闭握手优雅地关闭连接,允许应用区分有意和无意终止的连接。
当 WebSocket 关闭时,正在终止连接的端点可以发送一个数字代码和一个原因字符串来表明它为什么选择关闭套接字。代码和原因用 close 操作码(8)编码在帧的有效载荷中。该代码表示为一个无符号的 16 位整数。原因是一个短的 UTF-8 编码字符串。RFC 6455 定义了几个特定的结束代码。代码 1000–1015 指定用于 WebSocket 连接层。这些代码表示网络或协议中出现了故障。表 3-4 列出了该范围内的代码、它们的描述以及每种代码可能适用的场景。
表 3-4 。定义 WebSocket 关闭代码
| 密码 | 描述 | 何时使用此代码 |
|---|---|---|
| One thousand | 常闭 | 当您的会话成功完成时发送此代码。 |
| One thousand and one | 离开 | 在关闭连接时发送此代码,因为应用正在离开,并且不希望尝试后续连接。服务器可能正在关闭,或者客户端应用可能正在关闭。 |
| One thousand and two | 协议错误 | 由于协议错误而关闭连接时发送此代码。 |
| One thousand and three | 不可接受的数据类型 | 当您的应用收到它无法处理的意外类型的消息时,发送此代码。 |
| One thousand and four | 内向的; 寡言少语的; 矜持的 | 不要发送此代码。根据 RFC 6455,此状态代码是保留的,将来可能会被定义。 |
| One thousand and five | 内向的; 寡言少语的; 矜持的 | 不要发送此代码。WebSocket API 使用这个代码来表示没有收到代码。 |
| One thousand and six | 内向的; 寡言少语的; 矜持的 | 不要发送此代码。Websocket API 使用此代码来指示连接已异常关闭。 |
| One thousand and seven | 无效数据 | 收到格式与消息类型不匹配的消息后发送此代码。如果一条文本消息包含格式错误的 UTF-8 数据,连接应该用这个代码关闭。 |
| One thousand and eight | 邮件违反了策略 | 当您的应用由于其他代码未涉及的原因而终止连接时,或者当您不想公开消息无法处理的原因时,发送此代码。 |
| One thousand and nine | 消息太大 | 当收到的消息太大,应用无法处理时,发送此代码。(请记住,帧的有效载荷长度最长可达 64 位。即使你有一个大的服务器,一些消息仍然太大。) |
| One thousand and ten | 需要扩展 | 当您的应用需要一个或多个服务器没有协商的特定扩展时,从客户端(浏览器)发送此代码。 |
| One thousand and eleven | 意外情况 | 当您的应用由于不可预见的原因无法继续处理连接时,请发送此代码。 |
| One thousand and fifteen | TLS 失败(保留) | 不要发送此代码。WebSocket API 使用此代码在 WebSocket 握手之前指示 TLS 何时失败。 |
注 第二章描述了 WebSocket API 如何使用关闭代码。有关 WebSocket API 的更多信息,请参见
http://www.w3.org/TR/websockets/。
其他代码范围保留用于特定目的。表 3-5 列出了 RFC 6455 中定义的四类关闭代码。
表 3-5 。WebSocket 关闭代码范围
| 密码 | 描述 | 何时使用此代码 |
|---|---|---|
| 0-999 | 禁止 | 低于 1000 的代码无效,永远不能用于任何目的。 |
| 1000-2999 | 内向的; 寡言少语的; 矜持的 | 这些代码保留用于 WebSocket 协议本身的扩展和修订版本。按照标准规定使用这些代码。参见表 3-4。 |
| 3000-3999 | 需要注册 | 这些代码旨在供“库、框架和应用”使用这些代码应在 IANA(互联网号码分配机构)公开注册。 |
| 4000-4999 | 私人的 | 在您的应用中将这些代码用于自定义目的。因为它们没有注册,所以不要指望它们能被其他 WebSocket 软件广泛理解。 |
支持其他协议
WebSocket 协议支持更高级别的协议和协议协商。矛盾的是,RFC 6455 将可以与 WebSocket 一起使用的协议称为“子协议”, ,即使它们是更高级的、完全形成的协议。正如我们在第二章中提到的,在本书中,我们通常将可以与 WebSocket 一起使用的协议简称为“协议”,以避免混淆。
在第二章中,我们解释了如何用 WebSocket API 协商更高层的协议。在网络层,这些协议使用Sec-WebSocket-Protocol报头进行协商。协议名是客户端在初始升级请求中发送的头值 :
Sec-WebSocket-Protocol: com.kaazing.echo, example.protocol.name
该报头表示客户端可以使用协议(com.kaazing.echo或example.protocol.name)并且服务器可以选择使用哪个协议。如果您在升级请求中向ws://echo.websocket.org发送此报头,服务器响应将包括以下报头:
Sec-WebSocket-Protocol: com.kaazing.echo
这个响应表明服务器已经选择使用com.kaazing.echo协议。选择协议不会改变 WebSocket 协议本身的语法。相反,这些协议位于 WebSocket 协议之上,为框架和应用提供更高级别的语义。在接下来的章节中,我们将研究在 WebSocket 上分层广泛使用的、基于标准的协议的三种不同的用例。
为了简单地扩展 WebSocket 协议,还有另一种机制,称为扩展。
扩展名
像协议一样,扩展是用一个Sec-头协商的。连接客户端发送包含它支持的扩展名称的a Sec-WebSocket-Extensions头。
注意虽然您不能一次协商多个协议,但您可以一次协商多个扩展。
例如,Chrome 可能会发送下面的头来表明它将接受一个实验性的压缩扩展:
Sec-WebSocket-Extensions: x-webkit-deflate-frame
扩展如此命名是因为它们扩展了 WebSocket 协议。扩展可以向成帧格式添加新的操作码和数据字段。您可能会发现部署新的扩展比部署新的协议(或“子协议”)更困难,因为浏览器供应商必须显式地构建对这些扩展的支持。您可能会发现,编写一个实现协议的 JavaScript 库比等待所有浏览器供应商标准化一个扩展和所有用户更新他们的浏览器到支持该扩展的版本要容易得多。
用 Node.js 用 JavaScript 写一个 WebSocket 服务器
既然我们已经研究了 WebSocket 协议的要点,那么让我们一步一步地编写我们自己的 WebSocket 服务器。WebSocket 协议有许多现有的实现;您可以选择在应用中使用现有的实现。但是,出于必要或者仅仅因为可以,您可能需要编写一个新的服务器或者修改一个现有的服务器。编写自己的 WebSocket 协议实现既有趣又有启发性,并且可以帮助您理解和评估其他服务器、客户端和库。最重要的是,它能让你对网络、交流和网络有新的认识。
本章中的示例服务器是使用 Node.js 提供的 IO API 用 JavaScript 编写的。我们选择这些技术只是为了将本书中的代码示例限制为单一语言。因为您很可能在前端开发中使用 JavaScript 和 HTML5,所以您也很有可能能够流利地阅读这些代码。当然,您没有必要用 JavaScript 编写您的服务器,并且有充分的理由选择另一种语言。WebSocket 是一种语言无关的协议,这意味着您可以选择任何能够监听套接字的编程语言来创建服务器。
我们编写这个例子是为了使用 Node.js 0.8。它不能在 Node.js 的早期版本上运行,如果节点 API 发生变化,将来可能需要进行一些修改。websocket-example模块将前面的代码片段和一些额外的代码组合起来,形成一个 WebSocket 服务器。这个例子并不完全健壮,也不适合生产,但它确实是该协议的一个简单、独立的例子。
注意要构建(或简单地遵循)本书中的示例,您可以选择使用我们创建的虚拟机(VM ),它包含了我们在示例中使用的所有代码、库和服务器。关于如何下载、安装和启动虚拟机的说明,请参考附录 B 。
构建简单的 WebSocket 服务器
清单 3-6 让我们从构建一个简单的 WebSocket 服务器开始。您也可以打开文件websocket-example.js来查看示例代码。
清单 3-6。 用 Node.js 用 JavaScript 写的 WebSocket 服务器 API
// The Definitive Guide to HTML5 WebSocket
// Example WebSocket server
// See The WebSocket Protocol for the official specification
//http://tools.ietf.org/html/rfc6455
var events = require("events");
var http = require("http");
var crypto = require("crypto");
var util = require("util");
// opcodes for WebSocket frames
//http://tools.ietf.org/html/rfc6455#section-5.2
var opcodes = { TEXT : 1
, BINARY: 2
, CLOSE : 8
, PING : 9
, PONG : 10
};
var WebSocketConnection = function(req, socket, upgradeHead) {
var self = this;
var key = hashWebSocketKey(req.headers["sec-websocket-key"]);
// handshake response
//http://tools.ietf.org/html/rfc6455#section-4.2.2
socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
'Upgrade: WebSocket\r\n' +
'Connection: Upgrade\r\n' +
'sec-websocket-accept: ' + key +
'\r\n\r\n');
socket.on("data", function(buf) {
self.buffer = Buffer.concat([self.buffer, buf]);
while(self._processBuffer()) {
// process buffer while it contains complete frames
}
});
socket.on("close", function(had_error) {
if (!self.closed) {
self.emit("close", 1006);
self.closed = true;
}
});
// initialize connection state
this.socket = socket;
this.buffer = new Buffer(0);
this.closed = false;
}
util.inherits(WebSocketConnection, events.EventEmitter);
// Send a text or binary message on the WebSocket connection
WebSocketConnection.prototype.send = function(obj) {
var opcode;
var payload;
if (Buffer.isBuffer(obj)) {
opcode = opcodes.BINARY;
payload = obj;
} else if (typeof obj == "string") {
opcode = opcodes.TEXT;
// create a new buffer containing the UTF-8 encoded string
payload = new Buffer(obj, "utf8");
} else {
throw new Error("Cannot send object. Must be string or Buffer");
}
this._doSend(opcode, payload);
}
// Close the WebSocket connection
WebSocketConnection.prototype.close = function(code, reason) {
var opcode = opcodes.CLOSE;
var buffer;
// Encode close and reason
if (code) {
buffer = new Buffer(Buffer.byteLength(reason) + 2);
buffer.writeUInt16BE(code, 0);
buffer.write(reason, 2);
} else {
buffer = new Buffer(0);
}
this._doSend(opcode, buffer);
this.closed = true;
}
// Process incoming bytes
WebSocketConnection.prototype._processBuffer = function() {
var buf = this.buffer;
if (buf.length < 2) {
// insufficient data read
return;
}
var idx = 2;
var b1 = buf.readUInt8(0);
var fin = b1 & 0x80;
var opcode = b1 & 0x0f; // low four bits
var b2 = buf.readUInt8(1);
var mask = b2 & 0x80;
var length = b2 & 0x7f; // low 7 bits
if (length > 125) {
if (buf.length < 8) {
// insufficient data read
return;
}
if (length == 126) {
length = buf.readUInt16BE(2);
idx += 2;
} else if (length == 127) {
// discard high 4 bits because this server cannot handle huge lengths
var highBits = buf.readUInt32BE(2);
if (highBits != 0) {
this.close(1009, "");
}
length = buf.readUInt32BE(6);
idx += 8;
}
}
if (buf.length < idx + 4 + length) {
// insufficient data read
return;
}
maskBytes = buf.slice(idx, idx+4);
idx += 4;
var payload = buf.slice(idx, idx+length);
payload = unmask(maskBytes, payload);
this._handleFrame(opcode, payload);
this.buffer = buf.slice(idx+length);
return true;
}
WebSocketConnection.prototype._handleFrame = function(opcode, buffer) {
var payload;
switch (opcode) {
case opcodes.TEXT:
payload = buffer.toString("utf8");
this.emit("data", opcode, payload);
break;
case opcodes.BINARY:
payload = buffer;
this.emit("data", opcode, payload);
break;
case opcodes.PING:
// Respond to pings with pongs
this._doSend(opcodes.PONG, buffer);
break;
case opcodes.PONG:
// Ignore pongs
break;
case opcodes.CLOSE:
// Parse close and reason
var code, reason;
if (buffer.length >= 2) {
code = buffer.readUInt16BE(0);
reason = buffer.toString("utf8",2);
}
this.close(code, reason);
this.emit("close", code, reason);
break;
default:
this.close(1002, "unknown opcode");
}
}
// Format and send a WebSocket message
WebSocketConnection.prototype._doSend = function(opcode, payload) {
this.socket.write(encodeMessage(opcode, payload));
}
var KEY_SUFFIX = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
var hashWebSocketKey = function(key) {
var sha1 = crypto.createHash("sha1");
sha1.update(key+KEY_SUFFIX, "ascii");
return sha1.digest("base64");
}
var unmask = function(maskBytes, data) {
var payload = new Buffer(data.length);
for (var i=0; i<data.length; i++) {
payload[i] = maskBytes[i%4] ^ data[i];
}
return payload;
}
var encodeMessage = function(opcode, payload) {
var buf;
// first byte: fin and opcode
var b1 = 0x80 | opcode;
// always send message as one frame (fin)
// Second byte: mask and length part 1
// Followed by 0, 2, or 8 additional bytes of continued length
var b2 = 0; // server does not mask frames
var length = payload.length;
if (length<126) {
buf = new Buffer(payload.length + 2 + 0);
// zero extra bytes
b2 |= length;
buf.writeUInt8(b1, 0);
buf.writeUInt8(b2, 1);
payload.copy(buf, 2);
} else if (length<(1<<16)) {
buf = new Buffer(payload.length + 2 + 2);
// two bytes extra
b2 |= 126;
buf.writeUInt8(b1, 0);
buf.writeUInt8(b2, 1);
// add two byte length
buf.writeUInt16BE(length, 2);
payload.copy(buf, 4);
} else {
buf = new Buffer(payload.length + 2 + 8);
// eight bytes extra
b2 |= 127;
buf.writeUInt8(b1, 0);
buf.writeUInt8(b2, 1);
// add eight byte length
// note: this implementation cannot handle lengths greater than 2³²
// the 32 bit length is prefixed with 0x0000
buf.writeUInt32BE(0, 2);
buf.writeUInt32BE(length, 6);
payload.copy(buf, 10);
}
return buf;
}
exports.listen = function(port, host, connectionHandler) {
var srv = http.createServer(function(req, res) {
});
srv.on('upgrade', function(req, socket, upgradeHead) {
var ws = new WebSocketConnection(req, socket, upgradeHead);
connectionHandler(ws);
});
srv.listen(port, host);
};
测试我们的简单 WebSocket 服务器
现在,让我们测试我们的服务器。Echo 是网络的“Hello,World ”,所以我们用新的服务器 API 做的第一件事是创建一个服务器,如清单 3-7 所示。回显服务器简单地用连接的客户机发送的任何东西来响应。在这种情况下,我们的 WebSocket echo 服务器将使用它接收到的任何 WebSocket 消息进行响应。
清单 3-7。 使用新的服务器 API 构建 Echo 服务器
var websocket = require("./websocket-example");
websocket.listen(9999, "localhost", function(conn) {
console.log("connection opened");
conn.on("data", function(opcode, data) {
console.log("message: ", data);
conn.send(data);
});
conn.on("close", function(code, reason) {
console.log("connection closed: ", code, reason);
});
});
您可以在命令行上使用 node 启动该服务器。确保websocket-example.js在同一个目录中(或者作为一个模块安装)。
> node echo.js
如果您随后从浏览器将一个 WebSocket 连接到这个 echo 服务器,您将会看到您从客户端发送的每一条消息都被服务器回显。
注意当你的服务器监听本地主机时,浏览器必须在同一台机器上。您也可以使用第二章中的 Echo 客户端示例来尝试一下。
构建远程 JavaScript 控制台
JavaScript 最好的一个方面是它非常适合交互式开发。Chrome 开发工具和 Firebug 中内置的控制台是 JavaScript 开发如此高效的原因之一。控制台,也称为“真实求值打印循环”的 REPL,允许您输入表达式并查看结果。我们将利用 Node.js repl模块并添加一个定制的eval()函数。通过添加 WebSocket,我们可以通过互联网远程控制 web 应用!有了这个基于 WebSocket 的控制台,我们将能够从命令行界面远程评估表达式。更好的是,我们可以输入一个表达式,并查看对并发连接的每个客户端评估该表达式的结果。
在这个例子中,您将使用在清单 3-6 中显示的同一个服务器,然后构建两个小片段:一个作为远程控制,另一个作为您控制的对象。图 3-8 显示了你将在下一个例子中构建什么。
图 3-8 。远程 JavaScript 控制台
在构建这个例子之前,确保你已经构建了清单 3-6 中的例子。如果您还构建了 Echo 服务器部分(清单 3-7 ,您需要在测试随后的代码片段之前关闭 Echo 服务器。清单 3-8 包含了远程控制的 JavaScript 代码。
***清单 3-8。***web socket-repl . js
var websocket = require("./websocket-example");
var repl = require("repl");
var connections = Object.create(null);
var remoteMultiEval = function(cmd, context, filename, callback) {
for (var c in connections) {
connections[c].send(cmd);
}
callback(null, "(result pending)");
}
websocket.listen(9999, "localhost", function(conn) {
conn.id = Math.random().toString().substr(2);
connections[conn.id] = conn;
console.log("new connection: " + conn.id);
conn.on("data", function(opcode, data) {
console.log("\t" + conn.id + ":\t" + data);
});
conn.on("close", function() {
// remove connection
delete connections[conn.id];
});
});
repl.start({"eval": remoteMultiEval});
我们还需要一个简单的网页来控制。这个页面上的脚本简单地打开一个到我们的控制服务器的 WebSocket,评估它接收到的任何消息,并以结果作为响应。客户端还将传入的表达式记录到 JavaScript 控制台。如果你打开浏览器的开发者工具,你会看到这些表达式。清单 3-9 显示了包含脚本的网页。
清单 3-9。
<!doctype html>
<title>WebSocket REPL Client</title>
<meta charset="utf-8">
<script>
var url = "ws://localhost:9999/repl";
var ws = new WebSocket(url);
ws.onmessage = function(e) {
console.log("command: ", e.data);
try {
var result = eval(e.data);
ws.send(result.toString());
} catch (err) {
ws.send(err.toString());
}
}
</script>
现在如果你运行node websocket-repl.js,你会看到一个交互式解释器。如果你在几个浏览器中加载repl-client.html,你会看到每个浏览器都在评估你的命令。清单 3-10 显示了两个表达式navigator.userAgent和5+5的输出。
清单 3-10。 表情从控制台输出
> new connection: 5206121257506311
new connection: 6689629901666194
navigator.userAgent
'(result pending)'
> 5206121257506311: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:13.0) Gecko/20100101 Firefox/13.0.1
6689629901666194: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.15 Safari/537.1
5+5
'(result pending)'
> 6689629901666194: 10
5206121257506311: 10
建议的扩展名
远程 JavaScript 控制台是一些有趣项目的良好起点。下面是扩展这个例子的几种方法:
- 为远程控制台创建一个 HTML5 用户界面。使用 WebSocket 在用户界面和控制服务器之间进行通信。考虑一下,与使用 AJAX 的 HTTP 等通信策略相比,使用套接字如何简化发送流水线命令和接收延迟响应。
- 一旦你阅读了第五章,修改远程控制服务器来使用 STOMP。您可以使用主题向每个连接的浏览器会话广播命令,并在队列中接收回复。考虑如何将远程控制服务等新功能融入到消息驱动的应用中。
摘要
在这一章中,我们探讨了互联网和协议的简史,以及为什么会创建 WebSocket 协议。我们详细研究了 WebSocket 协议,包括线路流量、开始和结束握手以及帧格式。我们使用 Node.js 构建了一个示例 WebSocket 服务器,为一个简单的 echo 演示和一个远程控制台提供支持。虽然这一章很好地概述了 WebSocket 协议,但是您可以在这里阅读完整的协议规范:http://tools.ietf.org/html/rfc6455。
在接下来的章节中,我们将在 WebSocket 上使用更高级的协议来构建功能丰富的实时应用。
四、使用 XMPP 通过 WebSocket 构建即时消息和聊天
聊天是一个很好的例子,在一个只有 HTTP 的世界里,互联网应用变得越来越难构建。聊天和即时消息应用本质上是异步的:任何一方都可以随意发送消息,而不需要特定的请求或响应。这些应用是 WebSocket 的优秀用例,因为它们极大地受益于减少的延迟。当你与朋友和同事聊天时,你希望尽可能少的延迟,以便进行自然的对话。毕竟,如果有大量的延迟,它就很难成为即时消息。
即时通讯非常适合 WebSocket 聊天应用是这种技术的常见演示和例子。最常见的例子是使用简单的定制消息,而不是标准协议。在这一章中,我们将比这些基本演示更深入地探究,使用一个成熟的协议来挖掘大量不同的服务器实现、强大的功能以及经过验证的可伸缩性和可扩展性。
首先,我们探索 WebSocket 的分层协议,以及在构建使用 WebSocket 上的更高级协议的应用之前需要做出的一些关键选择。在这个例子中,我们使用 XMPP,它代表可扩展消息和存在协议,并且是在即时消息应用中广泛使用的标准。我们通过在 WebSocket 传输层上使用该协议来利用该协议进行通信。在我们的示例中,我们使用 XMPP over WebSocket 逐步将 web 应用连接到 Jabber Instant Messaging (IM) 网络,包括添加指示用户状态和在线状态的功能。
分层协议
在第三章中,我们讨论了 WebSocket 协议的简单演示,包括直接在 WebSocket 层发送和接收消息。我们的远程控制控制台示例演示了使用 WebSocket 构建涉及双向通信的简单应用是可能的。想象一下,扩展像遥控器这样的简单演示来构建更全功能的应用,如聊天客户端和服务器。WebSocket 的一个伟大之处在于,您可以在 WebSocket 的基础上构建其他协议,从而通过 Web 扩展您的应用。让我们来看看 WebSocket 上的分层协议。
图 4-1 显示了 TCP 上互联网应用层协议的典型分层。应用使用 XMPP 或 STOMP (简单的面向文本的消息协议 ??,我们将在第五章中讨论)之类的协议在客户端和服务器之间进行通信。XMPP 和 STOMP 依次通过 TCP 进行通信。使用加密通信时,应用层协议位于 TLS(或 SSL)之上,而 TLS 又位于 TCP 之上。
图 4-1 。互联网应用层图表
WebSocket 对世界的看法大同小异。图 4-2 显示了一个类似的图表,WebSocket 作为应用层协议和 TCP 之间的附加层被插入。XMPP 和 STOMP 分层在 WebSocket 之上,web socket 分层在 TCP 之上。在加密的情况下,使用wss://方案的安全 WebSocket 通信是通过 TLS 连接执行的。WebSocket 传输层是一个相对较薄的层,它使 web 应用能够建立全双工网络连接。WebSocket 层可以像图 4-1 中的 TCP 层一样处理,并用于所有相同的协议。
图 4-2 。Web 应用层图
图 4-2 包含 HTTP 有两个原因。首先,它说明了 HTTP 是作为 TCP 之上的应用层协议而存在的,可以直接在 web 应用中使用。AJAX 应用使用 HTTP 作为所有网络交互的主要或唯一协议。二、图 4-2 说明使用 WebSocket 的应用不需要完全忽略 HTTP。静态资源几乎总是通过 HTTP 加载。例如,即使您选择使用 WebSocket 进行通信,组成用户界面的 HTML、JavaScript 和 CSS 仍然可以通过 HTTP 提供服务。因此,在您的应用协议栈中,您可以通过 TLS 和 TCP 同时使用 HTTP 和 WebSocket。
当用作标准应用级协议的传输层时,WebSocket 确实大放异彩。这样做,您可以获得标准协议的惊人好处以及 WebSocket 的强大功能。让我们通过研究广泛使用的标准聊天协议 XMPP 来看看这些好处。
XMPP:一英里的 XML 流
您很有可能已经阅读和编写了 XML(可扩展标记语言)。XML 是基于尖括号的标记语言的悠久遗产的一部分,可以追溯到几十年前的 SGML、HTML 和它们的祖先。万维网联盟(W3C)发布了它的语法,许多 Web 技术都使用它。事实上,在 HTML5 之前,XHTML 是 HTML4 的继任者。XML 中的 X 代表可扩展,XMPP 利用了它提供的可扩展性。扩展 XMPP 意味着使用 XML 的扩展机制来创建名称空间,称为 xep(XMPP 扩展协议)。在http://xmpp.org有一个庞大的 xep 库。
XML 是一种文档格式;XMPP 是一种协议。那么,XMPP 是如何使用文档语法进行实时通信的呢?实现这一点的一种方法是在一个单独的文档中发送每条消息。然而,这种方法会不必要的冗长和浪费。另一种方法是将对话视为一个增长久而久之的长文档并传输消息,这就是 XMPP 处理文档语法的方式。XMPP 连接期间发生的双向会话的每个方向都由一个流式 XML 文档表示,该文档在连接终止时结束。该流式文档的根节点是一个<stream/>元素。流的顶层孩子是协议的单个数据单元,称为节。一个典型的小节可能看起来像清单 4-1 中的,去掉了空格以节省带宽。
清单 4-1。 XMPP 节
<message type="chat" to="desktopuser@localhost">
<body>
I like chatting. I also like angle brackets.
</body>
</message>
标准化
今天,您可以在 WebSocket 上使用 XMPP(XMPP/WS ),尽管没有这样做的标准。在工作和时间之后,IETF 有一个规范草案,也许有一天会激发出一个标准。还有几种 XMPP/WS 的实现,其中一些比另一些更具实验性。
WebSocket 上的 XMPP 标准将使独立的服务器和客户端实现以更高的成功概率进行互操作,并将确定将 XMPP 通信绑定到 WebSocket 传输层的所有选择。这些选择包括 WebSocket 和 TCP 之间的每个语义差异的选项,以及如何利用消息边界和操作码,如第三章中所讨论的。标准还将为 WebSocket 客户端和服务器能够识别的 WebSocket 握手中的协议头定义一个稳定的子协议名。在试验阶段,您找到或创建的使用 XMPP over WebSocket 的软件可能在这些选择上有所不同。每一种变化都有可能导致期望特定行为的客户机和服务器之间的不兼容。
虽然标准化的好处很多,但我们不需要等待一个完全成熟的标准来构建一个很酷的应用。我们可以选择一台客户机和一台服务器,我们知道它们可以很好地协同工作。例如,ejabberd-websockets 模块捆绑了一个 JavaScript 客户端库,它实现了 WebSocket 上 XMPP 的草案提案。或者,Kaazing WebSocket Gateway 是一个网关(服务器),包含一套兼容的客户端。
选择连接策略
使用 WebSocket 连接到 XMPP 服务器有两种方法:修改 XMPP 服务器以接受 WebSocket 连接或使用代理服务器。虽然您可以让 XMPP 服务器通过 WebSocket 接受 XMPP,但是这样做需要更新服务器,如果您不控制服务器操作,这可能是不可能的。像talk.google.com和chat.facebook.com这样的公共 XMPP 端点就是这种情况。在这些情况下,你需要根据http://tools.ietf.org/html/draft-moffitt-xmpp-over-websocket-01的规范创建你自己的模块。或者,在写这本书的时候,有一些实验性的模块:ejabberd-websockets 和 Openfire 的支持 WebSocket 的模块。图 4-3 显示了一个客户端连接到一个支持 WebSocket 的 XMPP 服务器。
图 4-3 。连接到支持 WebSocket 的 XMPP 服务器
第二种方法是使用一个代理服务器,它接受来自客户端的 WebSocket 连接,并与后端服务器建立相应的 TCP 连接。在这种情况下,后端服务器是标准的 XMPP 服务器,接受 TCP 连接上的 XMPP。Kaazing WebSocket 网关中的 XmppClient 采用了这种网关方法。在这里,应用可以通过 Kaazing 的网关连接到任何 XMPP 服务器,甚至是没有明确支持 WebSocket 的服务器。图 4-4 显示了一个 WebSocket 网关服务器接受 WebSocket 连接并对后端 XMPP 服务器进行相应的 TCP 连接的例子。
图 4-4 。通过 WebSocket 代理连接到 XMPP 服务器
节到消息对齐
在选择连接策略时,理解 WebSocket 消息(通常包含一个 WebSocket 框架)如何与 XMPP 节对齐是很重要的,因为这两种方法是不同的。在支持 WebSocket 的 XMPP 服务器的情况下,节被一对一地映射到 WebSocket 消息上。每个 WebSocket 消息只包含一个节,不能有重叠或分段。WebSocket 子协议的 XMPP 草案规定了这种一致性。在网关场景中,节到消息的对齐是不必要的,因为它将 WebSocket 转发给 TCP,反之亦然。TCP 没有消息边界,所以 TCP 流可能被任意分割成 WebSocket 消息。然而,在网关的情况下,客户机必须能够通过理解流式 XML 将字符整理成段。图 4-5 显示了 XMPP over WebSocket 子协议草案提案中描述的节到消息的对齐。关于注册 WebSocket 子协议草案的讨论,参见第二章和第三章。
下图显示了在 XMPP 服务器可以通过 WebSocket 直接与客户机通信的情况下,WebSocket 消息是如何与 XMPP 节对齐的。
图 4-5 。节到消息的一致性(XMPP over WebSocket 子协议草案提案)
图 4-6 显示了一个节与信息不一致的例子。该图显示了 WebSocket 消息如何不需要与节对齐,其中代理服务器接受 WebSocket 连接并通过 TCP 连接到后端 XMPP 服务器。
图 4-6 。没有节到消息的对齐(WebSocket 到 TCP 代理
联合会
许多即时通讯网络是有围墙的花园。拥有特定网络帐户的用户只能相互聊天。相反,Jabber ( http://www.jabber.org)是联合的,这意味着独立运行的服务器上的用户可以在服务器合作的情况下进行通信。Jabber 网络由不同域上的数千个服务器和数百万个用户组成。为联合配置服务器超出了本书的范围。在这里,我们着重于将客户机连接到一台服务器。您可以稍后将您的服务器连接到更大的联邦世界。
通过 WebSocket 构建聊天和即时消息应用
既然我们已经了解了在 WebSocket 上使用 XMPP 背后的一些重要概念,那么让我们来看一个工作示例,并深入研究更实际的细节。这里,我们将使用支持 WebSocket 的 XMPP 服务器,并构建一个典型的聊天应用,该应用通过 WebSocket 使用 XMPP 与服务器进行通信。
使用支持 WebSocket 的 XMPP 服务器
为了构建和运行本章的示例聊天应用,您需要一个支持 WebSocket 的 XMPP 聊天服务器,它与客户端库兼容。正如我们提到的,在撰写本书时,有几个选项,包括 ejabberd-websockets,一个 Openfire 模块,以及一个名为 node-xmpp-bosh 的代理,它理解 WebSocket 协议,这是一个由 Dhruv Matan 构建的开源项目。由于这些模块的实验性质,您的收获可能会有所不同。然而,这些模块正在被快速开发,在这本书出版(或你的阅读)之前,你可能会有许多可靠的选择。
注意对于这个前沿例子,我们选择 Strophe.js 作为客户端库。要自己构建这个示例,请选择一个支持 WebSocket 的 XMPP 服务器(或者更新您自己的 XMPP 服务器),并确保它与 Strophe.js 兼容。或者,如前所述,要构建(甚至遵循)本书中的示例,您可以使用我们创建的虚拟机(VM ),它包含我们在示例中使用的所有代码、库和服务器。关于如何下载、安装和启动虚拟机的说明,请参考附录 B 。由于本章中使用的技术的实验性质以及出于学习目的,我们强烈建议您使用我们提供的 VM。
设置测试用户
为了测试您的聊天应用,您需要一个至少有两个用户的消息传递网络来演示交互。为此,在支持 WebSocket 的聊天服务器上创建一对用户。然后,您可以使用这些测试用户使用您将在本章中构建的应用来来回回地聊天。
为了确保您的服务器配置正确,请尝试连接两个桌面 XMPP 客户端。例如,您可以安装以下任意两个客户端:Pidgin、Psi、Spark、Adium 或 iChat。你可以在http://xmpp.org找到更多信息。很可能你已经安装了一两个。在第一个聊天客户端中,您应该看到第二个用户的在线状态。同样,您应该在第二个客户机中看到第一个用户的状态。让其中一个用户保持登录状态,这样您就可以在开发 WebSocket 聊天应用时对其进行测试。
客户端库 : Strophe.js
要使您的聊天应用能够通过 WebSocket 使用 XMPP 与您的聊天服务器进行通信,您需要一个客户端库,使客户端能够与 XMPP 进行交互。在这个例子中,我们使用 Strophe.js,这是一个可以在 web 浏览器中运行的 JavaScript 的开源 XMPP 客户端库。js 提供了一个与 XMPP 交互的底层 API,并包含了构造、发送和接收节的函数。要构建像聊天客户端这样的高级抽象,您需要一些 XMPP 知识。然而,Strophe.js 具有天然的可扩展性,并为使用该库的开发人员提供了精确的控制。
在写这本书的时候,Strophe.js 的稳定分支使用了一个叫做 BOSH 的通信层。在 XEP-0124 扩展中指定的 BOSH 代表同步 HTTP 上的双向流。这是一种特定于 XMPP 的方式,通过半双工 HTTP 实现双向通信,类似于第一章中提到的 Comet 技术。BOSH 比 WebSocket 更早,是出于解决 HTTP 的局限性的类似需求而开发的。
WEBSOCKET,不是 BOSH
ejabberd-websocket 自述文件将 websocket 上的 XMPP 称为“对 Bosh 更优雅、更现代、更快速的替代”当然,现在 WebSocket 已经被标准化,并且即将被普遍部署,类似 Comet 的通信技术很快就会过时。
关于 WebSocket 仿真的讨论,请参见第八章,其中讨论了如何将 WebSocket 与没有本地支持的技术一起使用。
连接和开始使用
在开始聊天之前,您需要将客户端连接到 XMPP/WS 服务器。在这一步中,我们将建立一个从运行在 web 浏览器中的 HTML5 客户端应用到支持 WebSocket 的 XMPP 服务器的连接。一旦连接,套接字将在会话期间在客户机和服务器之间来回发送 XMPP 节。
首先,创建一个名为chat.html的新文件,如清单 4-2 所示。应用的 HTML 部分只是一个基本页面,包括 Strophe.js 库和组成聊天应用的 JavaScript。
清单 4-2。 聊天. html
<!DOCTYPE html>
<title>WebSocket Chat with XMPP</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="chat.css">
<h1>WebSocket Chat with XMPP</h1>
<!-- connect -->
<div class="panel">
<input type="text" id="username" placeholder="username">
<input type="password" id="password" placeholder="password">
<button id="connectButton">Connect</button>
</div>
<div id="presenceArea" class="panel"></div>
<div id="chatArea" class="panel"></div>
<div id="output"></div>
<!-- scripts -->
<script src="strophe.js"></script>
<script src="chat_app.js"></script>
我们将把这个 HTML 文档与一个小小的 CSS 文件链接起来,为用户界面增加一点风格,如清单 4-3 所示。
清单 4-3。 聊天. css
body {
font-family: sans-serif;
}
#output {
border: 2px solid black;
border-radius: 8px;
width: 500px;
}
#output div {
padding: 10px;
}
#output div:nth-child(even) {
background-color: #ccc;
}
panel {
display: block;
padding: 20px;
border: 1px solid #ccc;
}
我们将从最小版本的chat_app.js开始,随着我们扩展这个例子的功能,我们将增加它。首先,脚本将简单地用 Strophe.js 连接到 XMPP 服务器,并记录其连接状态。它还使用两个输入值:用户名和密码。这些值用于在建立连接时验证用户。
***清单 4-4。***chat _ app . js 初始版本
// Log messages to the output area
var output = document.getElementById("output");
function log(message) {
var line = document.createElement("div");
line.textContent = message;
output.appendChild(line);
}
function connectHandler(cond) {
if (cond == Strophe.Status.CONNECTED) {
log("connected");
connection.send($pres());
}
}
var url = "ws://localhost:5280/";
var connection = null;
var connectButton = document.getElementById("connectButton");
connectButton.onclick = function() {
var username = document.getElementById("username").value;
var password = document.getElementById("password").value;
connection = new Strophe.Connection(
{proto: new Strophe.Websocket(url)});
connection.connect(username, password, connectHandler);
}
请注意,这个示例要求用户输入他或她的凭证。在生产中,确保凭据不在未加密的情况下通过网络发送是非常重要的。实际上,根本不通过网络发送凭证要好得多。有关使用 WebSocket 加密和认证的信息,请参见第七章。如果您的聊天应用是一个更大的 web 应用套件的一部分,您可能希望使用单点登录机制,特别是如果您正在为一个更大的站点构建一个聊天小部件,或者如果您的用户使用外部凭证进行身份验证。
如果一切按计划进行,您应该看到页面上登录了“connected”。如果是这样,那么您已经成功地使用 XMPP over WebSocket 将用户登录到了聊天服务器。您应该会在之前保持连接的另一个 XMPP 客户端的花名册 UI 中看到连接的用户已经上线(参见图 4-7 )。
图 4-7 。从 chat.html 登陆,用洋泾浜语出现在网上。开发人员工具中显示的每个 WebSocket 消息都包含一个 XMPP 节
注意connect 处理程序中的
$pres()函数调用是必要的,用于指示用户已经在线登录。这些存在更新可以传达更多的细节,我们将在下一节中看到。
存在和状态
现在我们知道我们可以连接用户,让我们看一下跟踪用户的存在和状态。web 用户在桌面用户的联系人列表中看起来在线的方式是由于 XMPP 的在线特性。甚至当你不聊天的时候,在线状态信息也不断地从服务器中推出。当您的联系人在线登录、变为空闲状态或更改其状态文本时,您可能会收到状态更新。
在 XMPP 中,每个用户都有一个存在。存在具有可用性值,由显示标签和状态消息表示。要更改这个存在信息,发送一个存在节,如清单 4-5 所示:
清单 4-5。 在场小节示例
<presence>
<show>chat</show>
<status>Having a lot of fun with WebSocket</status>
</presence>
让我们为用户添加一个方法来改变他们的状态为chat_app.js(参见清单 4-6 )。首先,我们可以添加一些基本的表单控件来设置状态的在线/离线部分,在 XMPP 的说法中称为show。这些控件将显示为下拉菜单,其中包含四个可用性选项。下拉菜单中的值具有简短的指定名称,如“请勿打扰”的“dnd”我们还会给这些人类可读的标签,如“离开”和“忙碌”
清单 4-6。 存在更新 UI
// Create presence update UI
var presenceArea = document.getElementById("presenceArea");
var sel = document.createElement("select");
var availabilities = ["away", "chat", "dnd", "xa"];
var labels = ["Away", "Available", "Busy", "Gone"];
for (var i=0; i<availabilities.length; i++) {
var option = document.createElement("option");
option.value = availabilities[i];
option.text = labels[i];
sel.add(option);
}
presenceArea.appendChild(sel);
状态文本是自由格式的,所以我们将使用 input 元素,如清单 4-7 所示。
清单 4-7。 状态文本的输入元素
var statusInput = document.createElement("input");
statusInput.setAttribute("placeholder", "status");
presenceArea.appendChild(statusInput);
最后,我们将添加一个按钮,使得更新被发送到服务器(见清单 4-8 )。函数构建了一个 presence 节。为了更新连接用户的状态,presence 节包含两个子节点:show和status。尝试一下,注意桌面客户端几乎即时反映了 web 用户的状态。图 4-8 说明了到目前为止的例子。
清单 4-8。 按钮事件发送更新
var statusButton = document.createElement("button");
statusButton.onclick = function() {
var pres = $pres()
.c("show").t("away").up()
.c("status").t(statusInput.value);
connection.send(pres)
}
presenceArea.appendChild(statusButton);
图 4-8 。从浏览器更新在线状态。客户端发送的最新 WebSocket 消息包含客户端的 presence stanza.message
要在我们的 web 应用中查看其他用户的状态更新,我们需要理解传入的状态节。在这个简化的例子中,这些存在更新将被记录为文本。清单 4-9 展示了如何在chat_app.js中做到这一点。在成熟的聊天应用中,在线状态更新通常在聊天对话旁边更新。
清单 4-9。 处理状态更新
function presenceHandler(presence) {
var from = presence.getAttribute("from");
var show = "";
var status = "";
Strophe.forEachChild(presence, "show", function(elem) {
show = elem.textContent;
});
Strophe.forEachChild(presence, "status", function(elem) {
status = elem.textContent;
});
//
if (show || status){
log("[presence] " + from + ":" + status + " " + show);
}
// indicate that this handler should be called repeatedly
return true;
}
为了用这个函数处理存在更新,我们用连接对象注册了处理程序(见清单 4-10 )。对addHandler()的调用将把presenceHandler()函数与每个 presence 节关联起来。
清单 4-10。 注册出席处理器
connection.addHandler(presenceHandler, null, "presence", null);
图 4-9 显示了当 websocketuser 使用桌面客户端将他的在线状态更新为“去钓鱼-请勿打扰”时,浏览器客户端会立即显示出来。
图 4-9 。在浏览中观察存在变化 r
交换聊天信息
在这里,我们得到了任何即时消息应用的核心:聊天消息。聊天消息被表示为消息节,其类型属性被设置为chat。Strophe.js 连接 API 有一个addHandler()函数,让我们监听与该类型匹配的传入消息节,如清单 4-11 所示。
清单 4-11。 监听传入的“聊天”消息小节
function messageHandler(message) {
var from = message.getAttribute("from");
var body = "";
Strophe.forEachChild(message, "body", function(elem) {
body = elem.textContent;
});
// Log message if body was present
if (body) {
log(from + ": " + body);
}
// Indicate that this handler should be called repeatedly
return true;
}
我们还需要在连接后将这个处理程序与连接关联起来,如清单 4-12 所示。
清单 4-12。 将 addHandler 与连接关联
connection.addHandler(messageHandler, null, "message", "chat");
现在,试着从你的聊天客户端,比如 Pidgin,发送一条消息给网络用户。应该用消息节调用消息处理函数。图 4-10 说明了一个聊天消息交换。
图 4-10 。洋泾浜语和 chat.html 语的聊天
要将消息发送回 web 用户,您需要向服务器发送一个消息节。这个消息节必须有一个类型属性"chat"和一个包含实际聊天文本的主体元素,如清单 4-13 所示。
清单 4-13。 向服务器发送消息段
<message type="chat" to="desktopuser@localhost">
<body>
I like chatting. I also like angle brackets.
</body>
</message>
要用 Strophe.js 构建这个消息,使用$msg builder 函数。创建一个消息节,将类型属性设置为chat,将到属性设置为您想与之聊天的用户。在您通过连接发送消息后,其他用户应该会很快收到消息。清单 4-14 显示了这个消息节的一个例子。
清单 4-14。 用 Strophe.js 构建消息
// Create chat UI
var chatArea = document.getElementById("chatArea");
var toJid = document.createElement("input");
toJid.setAttribute("placeholder", "user@server");
chatArea.appendChild(toJid);
var chatBody = document.createElement("input");
chatBody.setAttribute("placeholder", "chat body");
chatArea.appendChild(chatBody);
var sendButton = document.createElement("button");
sendButton.textContent = "Send";
sendButton.onclick = function() {
var message = $msg({to: toJid.value, type:"chat"})
.c("body").t(chatBody.value);
connection.send(message);
}
chatArea.appendChild(sendButton);
现在,你们在聊天。当然,您可以在 web 客户端、桌面客户端或两者的组合之间聊天。这个聊天应用是 HTML5 和 WebSocket 的一个很好的例子,通过与标准网络协议的集成,在 web 浏览器中实现了桌面级的体验。这个 web 应用是桌面客户端的真正对等体。它们都是同一网络中的一流参与者,因为它们理解相同的应用层协议。是的,XMPP 是一个标准协议,即使这个特定的 WebSocket 层还没有标准化。它保留了 XMPP 相对于 TCP 的几乎所有优点,即使是作为草案。
任何数量的 web 和桌面客户端之间的对话都是可能的。相同的用户可以从任一客户端连接。在图 4-11 中,两个用户都在使用 web 客户端。
图 4-11 。网络客户之间的对话
乒乒乓乓
根据您的服务器配置,该应用可能会在一段时间后自动断开连接。断开连接可能是因为服务器发送了一个 ping,而客户端没有立即响应一个 pong。在 XMPP 中使用 Pings 和 pongs 的目的与在 WebSocket 中使用的目的相同:保持连接活动并检查连接的健康状况。Pings 和 pongs 使用 iq 节。在 XMPP 中,“iq”代表 info/query,是在异步连接之上执行请求/响应查询的一种方式。阿萍长得像清单 4-15 。
***清单 4-15。***XMPP 服务器 ping
<iq type="get" id="86-14" from="localhost"
to="websocketuser@localhost/cc9fd219" >
<ping />
</iq>
服务器将期待一个带有匹配 ID 的 iq 结果形式的响应(见清单 4-16 )。
***清单 4-16。***设置客户端响应
<iq type="result" id="86-14" to="localhost"
from "websocketuser@localhost/cc9fd219" />
为了处理 Strophe.js 中的 pings,我们需要注册一个函数来处理所有带有urn:xmpp:ping名称空间和type="get"的 iq 节(参见清单 4-17 )。和前面的步骤一样,我们通过在 connection 对象上注册一个处理程序来实现这一点。处理程序代码构建适当的响应,并将其发送回服务器。
清单 4-17。 为 iq 节注册一个处理程序
function pingHandler(ping) {
var pingId = ping.getAttribute("id");
var from = ping.getAttribute("from");
var to = ping.getAttribute("to");
var pong = $iq({type: "result", "to": from, id: pingId, "from": to});
connection.send(pong);
// Indicate that this handler should be called repeatedly
return true;
}
清单 4-18 显示了处理程序是如何注册的。
清单 4-18。 注册 addHandler
connection.addHandler(pingHandler, "urn:xmpp:ping", "iq", "get");
已完成的聊天应用
清单 4-19 显示了完整的端到端聊天应用,包括 pings 和 pongs。
清单 4-19。 最终版 chat_app.js
// Log messages to the output area
var output = document.getElementById("output");
function log(message) {
var line = document.createElement("div");
line.textContent = message;
output.appendChild(line);
}
function connectHandler(cond) {
if (cond == Strophe.Status.CONNECTED) {
log("connected");
connection.send($pres());
}
}
var url = "ws://localhost:5280/";
var connection = null;
var connectButton = document.getElementById("connectButton");
connectButton.onclick = function() {
var username = document.getElementById("username").value;
var password = document.getElementById("password").value;
connection = new Strophe.Connection({proto: new Strophe.Websocket(url)});
connection.connect(username, password, connectHandler);
// Set up handlers
connection.addHandler(messageHandler, null, "message", "chat");
connection.addHandler(presenceHandler, null, "presence", null);
connection.addHandler(pingHandler, "urn:xmpp:ping", "iq", "get");
}
// Create presence update UI
var presenceArea = document.getElementById("presenceArea");
var sel = document.createElement("select");
var availabilities = ["away", "chat", "dnd", "xa"];
var labels = ["Away", "Available", "Busy", "Gone"];
for (var i=0; i<availabilities.length; i++) {
var option = document.createElement("option");
option.value = availabilities[i];
option.text = labels[i];
sel.add(option);
}
presenceArea.appendChild(sel);
var statusInput = document.createElement("input");
statusInput.setAttribute("placeholder", "status");
presenceArea.appendChild(statusInput);
var statusButton = document.createElement("button");
statusButton.textContent = "Update Status";
statusButton.onclick = function() {
var pres = $pres();
c("show").t(sel.value).up();
c("status").t(statusInput.value);
connection.send(pres);
}
presenceArea.appendChild(statusButton);
function presenceHandler(presence) {
var from = presence.getAttribute("from");
var show = "";
var status = "";
Strophe.forEachChild(presence, "show", function(elem) {
show = elem.textContent;
});
Strophe.forEachChild(presence, "status", function(elem) {
status = elem.textContent;
});
if (show || status){
log("[presence] " + from + ":" + status + " " + show);
}
// Indicate that this handler should be called repeatedly
return true;
}
// Create chat UI
var chatArea = document.getElementById("chatArea");
var toJid = document.createElement("input");
toJid.setAttribute("placeholder", "user@server");
chatArea.appendChild(toJid);
var chatBody = document.createElement("input");
chatBody.setAttribute("placeholder", "chat body");
chatArea.appendChild(chatBody);
var sendButton = document.createElement("button");
sendButton.textContent = "Send";
sendButton.onclick = function() {
var message = $msg({to: toJid.value, type:"chat"})
.c("body").t(chatBody.value);
connection.send(message);
}
chatArea.appendChild(sendButton);
function messageHandler(message) {
var from = message.getAttribute("from");
var body = "";
Strophe.forEachChild(message, "body", function(elem) {
body = elem.textContent;
});
// Log message if body was present
if (body) {
log(from + ": " + body);
}
// Indicate that this handler should be called repeatedly
return true;
}
function pingHandler(ping) {
var pingId = ping.getAttribute("id");
var from = ping.getAttribute("from");
var to = ping.getAttribute("to");
var pong = $iq({type: "result", "to": from, id: pingId, "from": to});
connection.send(pong);
// Indicate that this handler should be called repeatedly
return true;
}
建议的延期
既然我们已经构建了一个基本的基于浏览器的聊天应用,您可以利用这个例子并做许多其他很酷的事情来将它变成一个成熟的应用。
构建用户界面
我们的示例网页chat.html,显然没有最漂亮或最有用的用户界面。考虑增强您的聊天客户端的 UI,加入更多用户友好的功能,如选项卡式对话、自动滚动和可见的联系人列表。将它构建为 web 应用的另一个好处是,您拥有许多强大的工具,可以用 HTML、CSS 和 JavaScript 来实现华丽而灵活的设计。
使用 XMPP 扩展
XMPP 有丰富的扩展生态系统。在http://xmpp.org上有数百个扩展提案或“xep”。这些功能从头像和群聊到 VOIP 会话初始化。
XMPP 是向 web 应用添加社交功能的好方法。对联系人、状态和聊天的内置支持提供了一个社交核心,您可以在此基础上添加协作、社交通知等。很多扩展都有这个目标。其中包括用于微博、评论、头像和发布个人事件流的 xep。
连接到 Google Talk
你可能从 Gmail 和 Google+中熟悉的聊天服务 Google Talk 实际上是 Jabber IM 网络的一部分。有一个可公开访问的 XMPP 服务器在端口5222上监听talk.google.com。如果你有一个 Google 帐户,你可以将任何兼容的 XMPP 客户端指向那个地址并登录。要使用您自己的 web 客户端连接到 Google Talk,请将 WebSocket 代理服务器指向该地址。该服务器需要加密,因此请确保该服务器配置为通过 TLS 进行连接。
摘要
在这一章中,我们探讨了如何在 WebSocket 上分层协议,特别是标准协议,以及 XMPP 这样的标准应用层协议如何适应标准的 web 架构。我们通过 WebSocket 构建了一个简单的聊天客户端,它使用了广泛使用的聊天协议 XMPP。在这样做的过程中,我们看到了使用 WebSocket 作为传输层以及这个标准应用层协议将 web 应用连接到交互式网络的强大功能。
在下一章中,我们将在 WebSocket 上使用 STOMP 来构建一个功能丰富的实时消息应用。