实时消息传递
想要向所有连接到您网站的浏览器发送消息吗?通过 Web sockets 运行的发布-订阅模式可能正是解决方案。
早在 20 世纪 80 年代,BSD UNIX 就引入了“socket”的概念,这是一种数据结构,其功能类似于文件句柄,您可以从中读取或写入数据。但是,文件句柄允许程序处理文件,而 socket 连接到另一个进程——可能在同一台计算机上,但很可能在另一台计算机上,在 Internet 上的其他地方运行。Sockets 引发了一场通信革命,这在很大程度上是因为它们使编写跨网络通信的程序变得容易。
今天,我们认为这是理所当然的。在任何给定时刻,我的计算机上都打开了数十个或数百个 sockets,我不知道它们是否正在与本地或远程程序通信。但这正是重点——使用 sockets 非常容易,我们不再认为联网程序是任何特殊或不寻常的东西。创建 sockets 的人们不可能想象到使用他们的发明构建的各种协议、应用程序和业务。
我的重点不是赞扬 sockets,而是指出技术的发明者,特别是那些提供基础设施支持和新的抽象层的技术,无法预先知道它将如何被使用。
有鉴于此,请考虑一种名为 Web sockets 的新型网络通信协议,它是统称为 HTML5 的标准的一部分。至少对我而言,Web sockets 是 HTML5 套件中最被低估、最少被讨论的部分,它有可能将 Web 浏览器转变为成熟的应用程序平台。
Web sockets 不会取代 HTTP。相反,很像 BSD sockets,它们在两台计算机之间提供双向、长期通信。“双向”和“长期”方面使 Web sockets 与 HTTP 区分开来,在 HTTP 中,客户端发送请求,服务器发送响应,然后连接终止。建立 Web socket 的开销非常小——一旦建立通信,它可以无限期地继续下去。
现在 Web sockets 已经存在,甚至受到越来越多的浏览器的支持,您可以使用它们做什么?这个问题仍然很难回答,这在很大程度上是因为 Web sockets 是如此之新。毕竟,如果您在 20 世纪 80 年代问某人您可以使用 BSD sockets 做什么,那么流媒体视频不太可能浮现在脑海中。
也就是说,对于某些应用,Web sockets 已经显示出其优势。特别是,受益于实时数据更新的应用程序,例如股票市场行情自动收录器,现在可以接收稳定的数据流,而不是重复执行对服务器的 Ajax 调用。实时聊天系统是 Web sockets 可以发挥作用的另一个例子,而 HTTP 在这里表现不佳。实际上,任何处理或显示恒定数据流的 Web 应用程序都可以从 Web sockets 中受益。
但是您可以更进一步。请记住,Web sockets 提供单个服务器和单个客户端之间的通信。然而,在许多应用程序中,服务器可能希望“广播”信息给大量客户端。您可以想象这如何与 Web sockets 一起工作,在服务器和每个客户端之间创建 Web socket 连接,然后向每个客户端发送消息,也许通过迭代 Web sockets 数组并在每个 Web socket 上调用 send() 来实现。
这当然是可能的,但是自己实现这样的系统将是耗时且困难的,并且可能不容易扩展。幸运的是,现在有第三方服务(收费)将为您处理此类连接。这种发布-订阅(“pub-sub”)系统使服务器几乎可以同时向任意数量的客户端发送消息,从而为各种 Web 应用程序打开了大门。
在本文中,我回顾了 Web sockets 背后的基本原理,然后继续演示一个使用发布-订阅模式的简单应用程序。即使您目前在 Web 应用程序中不需要这种功能,我也毫不怀疑您最终会遇到可以从中受益的情况。到那时,您希望意识到将其付诸实施并不太困难。
使用 Web SocketsWeb sockets,与 HTML5 标准的其余部分一样,与浏览器内的编程有关——当然,这发生在 JavaScript 或编译为 JavaScript 的语言中。要创建新的 Web socket,您只需说
var ws = new WebSocket("ws://lerner.co.il/socket");
这个 API 的美妙之处在于它的简单性。我不知道你怎么想,但我厌倦了期望我记住哪个参数代表主机名、哪个参数代表协议以及哪个参数代表端口(如果有的话)的协议。在 Web sockets 的情况下,正如您对 Web 标准的期望一样,您将所有这些都传递到一个 URL 中,该 URL 的协议定义为“ws”或“wss”(用于 SSL 加密的 Web sockets)。另请注意,您不必将 Web socket 定义为只读、只写或读/写;Web sockets 都是双向的。
您可以通过调用“send”方法向 Web socket 的另一端发送数据
ws.send("Hello");
或者,如果您想发送更复杂的东西,通常使用 JSON
var stuff_to_send = {a:1, b:2};
ws.send(JSON.stringify(stuff_to_send));
当您的 Web socket 接收到一些数据时会发生什么?目前什么也没有。您必须告诉浏览器,每次接收到数据时,它都应该对数据执行某些操作,例如在屏幕上显示数据。您使用回调告诉它要对数据执行什么操作,正如您在函数式语言(例如 JavaScript)中所期望的那样。也就是说,您告诉 Web socket,当它接收到数据时,它应该执行一个函数,并将接收到的数据作为参数传递给该函数。一个简单的例子可能是
ws.onmessage = function(message) {
alert("Received ws message: '" + message.data + '"');
};
您还可以使用数据做一些更有趣和令人兴奋的事情
ws.onmessage = function(message) {
$("#wsdata").html(message.data);
};
当然,传入的数据不一定是字符串。相反,它可能是一堆 JSON,在这种情况下,它可能包含带有字段的 JavaScript 对象。在这种情况下,您可以说
ws.onmessage = function(message) {
parsed_message = JSON.parse(message)
$("#one").html(parsed_message.one);
$("#one").html(parsed_message.two);
};
现在,重要的是要记住,Web sockets 协议是与 HTTP 不同的协议。这意味着当我说我想连接到 ws://lerner.co.il/socket 时,我需要确保我在 lerner.co.il 上运行 Web socket 服务器,该服务器响应该 URL 上的项目。这与 Apache、nginx 或您最喜欢的 HTTP 服务器不同。
所以,当我说您的浏览器连接到服务器时,您需要提供这样的服务器。本文的“资源”部分描述了许多使创建 Web sockets 服务器成为可能且相当简单的系统。
发布-订阅如您所见,使用 Web sockets 非常简单。但是,如果您想向多个客户端发送消息会发生什么?例如,假设您的公司从事股票交易,并且您希望公司网站的主页显示某些股票和股票指数的最新价值,并持续更新。
最简单且看似最直接的方法是使用我上面描述的策略——即,服务器可以将 Web sockets 存储在数组(或类似的数据结构)中。在设定的时间间隔,服务器然后可以对每个客户端执行 ws.send()
,发送简单的文本字符串或包含一个或多个信息片段的 JSON 数据结构。客户端在接收到此数据后,然后执行 onmessage
回调函数,该函数然后相应地更新用户的浏览器。
这种方法存在许多问题,但让我困扰的主要问题是缺乏真正的抽象层。作为应用程序开发人员,您希望发送消息,而不是考虑消息是如何发送的,甚至是谁在接收消息。这是看待发布-订阅(pub-sub)设计模式的一种方式。发布者和订阅者不是直接相互连接,而是通过中间人对象或服务器进行连接,该中间人对象或服务器负责连接。当发布者想要发送消息时,它通过代理发送消息,然后代理使用现有的 Web socket 连接向每个客户端发送消息。
现在,这听起来可能有点像消息队列,我大约一年前在这个空间中描述过消息队列。但是消息队列和发布-订阅系统的工作方式彼此截然不同,它们的用途也不同。
您可以将消息队列视为类似于电子邮件的工作方式,具有单个发送者和单个接收者。在接收者检索消息之前,它会在消息队列中等待。消息出现的顺序和时间不一定得到保证,但是消息的传递是得到保证的。
相比之下,发布-订阅系统有点像群组 IM 聊天。每个连接到发布-订阅系统并订阅特定频道的人都会收到发送到该频道的消息。如果您恰好在发送消息时未订阅,您将不会收到它;发布-订阅中没有存储或重放消息的机制。
如果您有兴趣从您的主页向人们提供实时股票更新,那么发布-订阅的方式是让每个客户端将自己注册为发布-订阅服务器的订阅者。然后服务器会定期向发布-订阅服务器发送新消息,发布-订阅服务器会将消息传递给相应的客户端。请记住,在发布-订阅系统中,发布者不知道有多少订阅者或他们何时进入/离开系统。发布者需要知道的只是用于向发布-订阅服务器发送数据的 API,该 API 会将数据传递给相应的客户端。
实施发布-订阅发布-订阅长期以来存在于 Web 之外,并且是向各种客户端“广播”信息的相当标准的架构。您可以自己创建发布-订阅系统,但至少有两家商业服务——Pusher 和 PubNub 是最知名的——可以非常轻松地在您的 Web 应用程序中实施实时消息传递。Pusher 使用 Web sockets,当浏览器不支持 Web sockets 时,则替换为基于 Flash 的解决方案。PubNub 使用不同的系统,称为“HTTP 长轮询”,这避免了浏览器对 Web sockets 的支持问题。如果您正在寻找商业发布-订阅服务,两者都值得考虑,但我在这里(以及在我的咨询工作中)使用 Pusher,部分原因是我更喜欢使用 Web sockets,部分原因是 Pusher 允许您使用事件类型标记每条消息,从而为您提供更丰富的发送数据机制。
由于 Pusher 是一项商业服务,因此您需要先注册才能使用它。它有一个免费的“沙箱”系统,对于开发中的系统来说绰绰有余。一旦您超出其每天 20 个连接和 100,000 条消息的限制,您就需要支付月费。注册 Pusher 后,您需要三个信息来实施应用程序
-
密钥:这相当于您的用户名。发布者使用它来发送消息,客户端使用它来检索消息。这不能是秘密,因为它将位于用户在其浏览器中显示的 HTML 文件中。
-
秘密:相当于密码。这不会在客户端或订阅者方面使用。但是,它将在服务器(发布者)方面使用,该服务器正在向客户端发送数据。这确保只有您在发送数据。
-
最后,您与 Pusher 一起使用的每个应用程序都有自己的应用程序 ID,这是一个唯一的数字代码。如果您有不同的应用程序与 Pusher 一起运行,您需要注册额外的应用程序 ID。
一旦您拥有这三条信息,您就可以开始创建您的 Web 应用程序了。您将在此处创建的简单应用程序是一个 Web 页面,用于显示有关特定股票的最新信息。哪个股票?发布者决定向您展示的任何股票。此类股票名称和价值的更新将通过发布-订阅发送到您的浏览器,使您无需刷新或用户的任何输入即可更新页面。
为了使这一切正常工作,您需要一个由三个部分组成的应用程序。首先,让我们使用基于 Ruby 的 Sinatra 框架创建一个非常简单的 Web 应用程序。这是整个应用程序,我将其放在名为 stock.rb 的文件中
#!/usr/bin/env ruby
require "sinatra"
require 'erb'
get "/" do
erb :index
end
该程序表示 Web 应用程序响应 / URL 上的 GET 请求,仅此而已。对任何其他 URL 或使用任何其他方法的请求都将遇到错误。如果要求您显示 / URL,您将显示名为 views/index.erb 的 ERb(嵌入式 Ruby)文件。您可以通过键入以下内容来启动您的 Web 应用程序
./stock.rb
在我的系统上,我得到以下响应
== Sinatra/1.3.3 has taken the stage on 4567
↪for development with backup from Thin
>> Thin web server (v1.5.0 codename Knife)
>> Maximum connections set to 1024
>> Listening on 0.0.0.0:4567, CTRL+C to stop
换句话说,如果我现在请求 http://localhost:4567/,我会收到一个错误,因为模板不存在。创建一个名为“views”的子目录,然后我可以在其中创建文件 index.erb,如列表 1 所示。
列表 1. index.erb
<!DOCTYPE html>
<!-- -*-html-*- -->
<head>
<h3>Stock Market</h3>
<script src="https://ajax.googleapis.ac.cn/ajax/libs/
↪jquery/1.8.0/jquery.min.js"
↪type="text/javascript"></script>
<script src="http://js.pusher.com/1.12/pusher.min.js"
↪type="text/javascript"></script>
<script type="text/javascript">
var pusher = new Pusher('KEY_FROM_PUSHER');
var channel = pusher.subscribe('stock_ticker');
channel.bind('update_event', function(data) {
$("#name").html(data['name']);
$("#price").html(data['price']);
});
</script>
</head>
<body>
<h1>Stock Market</h1>
<p>Current value of <span id="name">NAME</span> is
↪<span id="price">PRICE</span>.</p>
</body>
如您所见,index.erb 是一个简单的 HTML 文件。它的主体由标题和单个段落组成
<p>Current value of <span id="name">NAME</span> is
↪<span id="price">PRICE</span>.</p>
上面的行是原始股票行情自动收录器。当您的发布系统将发送新的股票名称和价格时,您将更新此行以反映该消息。
正如您使用回调来处理 Web socket 上的传入消息一样,您还需要定义一个回调来处理发布者发送到您的 Pusher“频道”的消息,正如它所知。(每个应用程序可以有任意数量的频道,每个频道可以有任意数量的事件。这允许您区分不同类型的消息,即使在同一应用程序中也是如此。)
为了做到这一点,您需要加载 JavaScript 库(来自 pusher.com),然后使用您创建的帐户的密钥创建一个新的 Pusher 对象
var pusher = new Pusher('cc06430d9bb986ef7054');
然后,您指示您要订阅特定频道,频道的名称不需要预先设置
var channel = pusher.subscribe('stock_ticker');
最后,您定义一个回调函数,指示当您在 stock_ticker 频道上收到类型为“update_event”的消息时,您希望替换本文档主体中的 HTML
channel.bind('update_event', function(data) {
$("#name").html(data['name']);
$("#price").html(data['price']);
});
请注意,我在这里使用 jQuery 是为了替换页面上的 HTML。为了使其工作,我还引入了 jQuery 库,从 Google 的服务器下载它。
有了这个 HTML 页面,并且我的 Sinatra 应用程序正在运行,我现在准备好接收消息了。我运行 Sinatra 应用程序并将我的浏览器指向 localhost:4567。我应该看到页面的静态版本,段落中包含 NAME 和 PRICE。
发布消息几乎与接收消息一样容易。不同的应用程序将有不同的用例。有时,您会希望从 Web 应用程序本身发送消息,表明已向论坛发布新消息,或者已登录用户的数量已更改。在其他情况下,您希望这些更新来自外部进程——可能是一个通过 cron 运行或独立于 Web 应用程序监控数据库的进程。
对于这个特定示例,我编写了一个小的 Ruby 程序 update-stocks.rb,如列表 2 所示。该程序使用 Pusher 人员免费提供的“pusher”gem。然后,您从您的列表(常量数组 COMPANIES)中选择一家公司,然后选择一个最多为 100 的随机数。接下来,您将消息发送给“stock_ticker”频道上的所有订阅者,表明您已发送“update_event”。由于发布者和订阅者之间通信的解耦性质,如果您拼错了频道或事件名称,您将不会收到错误消息。相反,消息将不会传递给任何人。因此,您需要在编写这些内容时格外小心,并确保在您的客户端和服务器中使用相同的名称。
列表 2. update-stocks.rb
#!/usr/bin/env ruby
COMPANIES = %w(ABC DEF GHI JKL MNO)
require 'pusher'
Pusher.app_id = APP_ID_FROM_PUSHER
Pusher.key = 'KEY_FROM_PUSHER'
Pusher.secret = 'SECRET_FROM_PUSHER'
loop do
company = COMPANIES.sample
price = rand 100
Pusher['stock_ticker'].trigger('update_event',
↪{ :name => company, :price => price})
sleep 5
end
结论
Web sockets 将极大地改变 Web,但目前尚不清楚如何或何时改变。能够使用发布-订阅几乎同时更新大量客户端显示已经改变了人们看待 Web 应用程序的方式——正如您可以从这个小型示例应用程序中看到的那样,这并不难做到。发布-订阅并不适用于所有应用程序,但如果您要向许多人发送相同的数据,并且他们可能希望自动将更新接收到他们的浏览器中,那么这是一种简单直接的方法。
资源您可以从各种来源了解 Web sockets。W3C 的 API 和定义位于 http://www.w3.org/TR/2009/WD-websockets-20090423,文档出奇地可读。另一个很好的信息来源是我同事 Zach Kessin 撰写并由 O'Reilly 出版的书 Programming HTML5 Applications。
Web socket 服务器几乎已使用您可以想象的每种语言编写。我在 Wikipedia 上找到了一个相对最新的列表,其中包含链接,位于“Web socket”条目下,因此,我不会尝试在此处重现它。
您可以在 http://pusher.com 上了解有关 Pusher 的更多信息,或在 http://pubnub.com 上了解有关流行的竞争对手 PubNub 的更多信息。
插图图形,来自 Shutterstock.com。