使用 XMPP 构建一个基于 web 的通知工具

使用 XMPP、PHP 和 JavaScript 编写实时 web 应用程序

实时 web 应用程序是联网的应用程序,带有基于 web 的用户界面,能够及时显示刚刚发布的 Internet 信息。这样的应用程序示例包括社会新闻聚合器和监控工具,它们能够使用来自外部源的数据持续更新。在本教程中,您将创建一个小型通知工具 Pingstream,它使用 PHP 和 JavaScript 通过 Extensible Messaging and Presence Protocol (XMPP) 进行通信,XMPP 是一组设计用于支持联机状态和实时通信功能的 XML 技术。

Ben Werdmuller, 顾问和作者, Freelance

Ben Werdmuller 的照片 Ben Werdmuller 是一位 Web 策划师和开发人员,他专注于开放源码平台。他是开源社交网络框架 Elgg 的共同创始人和技术带头人。Ben 的博客 http://benwerd.com/



2010 年 8 月 02 日

开始之前

本教程将向您介绍实时 web,并详细介绍之所以要构建实时 web 应用程序的几个原因。您将学到一些技术,这些技术将帮助您创建响应及时、持续更新的 web 应用程序,这些应用程序既能保护服务器资源,又能提供良好的用户体验。

关于本教程

常用缩略词

  • DOM: 文档对象模型
  • HTML: 超文本标记语言
  • HTTP: 超文本传输协议
  • REST: 具象状态传输
  • RSS: 真正简单聚合
  • URL: 统一资源定位符
  • XML: 可扩展标记语言

实时 web 应用程序允许用户在信息发布时及时接收通知,无需手动检查原始源获取更新。通过 Twitter 和 Friendfeed 这样的社交通知工具,Google Wave 这样的基于 web 的协作工具,以及 Meebo 这样的基于 web 的聊天客户端,实时 web 应用程序逐渐流行起来。

Extensible Messaging and Presence Protocol (XMPP) 是一组基于 XML 的技术,用于实时应用程序,定义为持续更新以响应新数据或更改数据的联网应用程序。它最初作为一个框架研发,以支持企业环境内的实时消息传递和联机状态(presence)应用程序。

在本教程中,您将构建一个简单的工具 Pingstream,它在 RSS 提要更新发布时使用它们持续更新自身(参见 下载 获取 Pingstream 源代码)。在此过程中,您将:

  • 了解 XMPP 为何特别适合 web 应用程序;
  • 了解 XMPP 通信的组件;
  • 安装和配置 Openfire XMPP 服务器;
  • 使用 PHP 和 XMPPHP 库连接到 XMPP 服务器;
  • 检查并通过 XMPP 传输 RSS 提要中的新项目;
  • 借助 Bidirectional-streams Over Synchronous HTTP (BOSH),使用 Strophe 和 jQuery 通过 HTTP 连接到 XMPP 服务器;
  • 在 web 页面中显示 XMPP 通知。

先决条件

本教程假定您比较熟悉使用 PHP 开发 web 应用程序,但也会涉及一些高级编程方法。您还应该拥有一定的 HTML 和 JavaScript 经验。拥有 jQuery JavaScript 框架经验可能会有所帮助。但本教程不要求熟悉 XMPP 或类似的技术。

要跟随本教程操作,必须安装和运行以下服务器软件:

  • PHP 5.2 或更高版本
  • Apache HTTP Server
  • MySQL

在本教程中,您还将下载和安装以下软件和库:

  • Openfire
  • jQuery
  • Strophe
  • XMPPHP
  • Last RSS

您可能会发现 MySQL 服务器工具 phpMyAdmin 能够派上用场。如果您使用一台桌面机器来本地测试您的实时 web 应用程序,您可能会发现 XAMPP 对于管理一个测试 web 服务器基础设施的安装和运行很有用。

参见 参考资料 获取所有这些工具下载的链接。


实时 web 简介

在本小节中,您将了解什么是实时 web 应用程序,您为何可能需要构建这样的应用程序,以及它们与典型的现代 web 应用程序模型的区别何在。

接受输入并提供反馈

实时 web 并不实时

持续更新的 web 应用程序 这个词汇比实时 web 应用程序 更贴切。在计算机科学中, 实时系统都必须满足操作期限的要求。当任务不能在其分配时间内完成时,硬实时系统将失败。实时 web 应用程序与软实时系统更相似,在软实时系统中,某个功能的延迟并不会导致系统失败(但可能会降低性能)。但您不应对实时 web 应用程序分配严格的任务计划。目前,web 并不是适合时间关键型应用程序的平台。

应用程序是帮助用户执行任务的专门软件,其特征是:从用户或其他源接收输入,然后提供可读的输出。应用程序也可能动态响应 — 以可视或编程方式 — 自动接收的输入数据中的变化。例如,当包含特定关键字的新闻出现在一个新闻监控应用程序连接到的新闻专线中时,该应用程序可能会通知用户。

由于 web 的起源是作为一个文档服务平台,因此它没有针对应用程序优化。HTML 非常适合表示和超链接文本内容,但不适合创建动态界面。web 应用程序能够接收和响应用户输入,这要归功于 PHP 这样的服务器端脚本语言与表单和 JavaScript 这样的 web 输入技术的结合。但是,要创建自动 更新的界面,您必须克服一些障碍。这些障碍比较难以克服,因为没有任何 web 技术在研发时考虑到这个功能。相比之下(以新闻监控为例),桌名软件无需刷新其界面就能够将通知发送给用户;桌面软件可以持续更新自身。相反,web 则局限于基于页面的模型。

然而,基于 web 的实时应用程序还是可以实现的,而且它们的好处显而易见。这样的应用程序包括企业聊天工具、联网的实时文档协作工具、以及搜索界面,它们能够及时显示新发布的内容。

面向健壮的应用程序开发的 Web 技术

通常,web 应用程序通过使用 Asynchronous JavaScript and XML (Ajax) 工具来模拟持续更新的界面。在这个模型中,应用程序的 web 页面中包含 JavaScript,它在后台反复请求一个服务器回拨。尽管 Ajax 应用程序的响应性在很多情况下都够用,但这种技术还是有一些缺陷。

Ajax 不能容忍不稳定的 Internet 连接:一次临时掉线可能会导致整个界面失败。它在服务器负载方面也效率低下。假设您的后台 Ajax 轮询函数每 10 秒检查一次服务器。每一次都将建立一个新的 HTTP 连接,包括初始化服务请求所需的资源,即使没有新数据可以向用户显示。结果是应用程序不必要地使用过多的处理器时间和带宽。

基于 XML 的技术向 web 应用程序提供了巨大的优势。XML 解析器现在是大多数环境的一个标准组成部分;无需其他软件就能支持以适当的格式读写数据。XML 是自我描述型语言;使用它的文档不需要外部架构。最后,正如 web 是独立于平台的一样,XML 作为一种技术支持在不同平台间互操作。因此,开发人员可以将精力集中于特定于他们的应用程序的逻辑。

web 基于 HTML、Cascading Style Sheets (CSS) 和 JavaScript 等可互操作的免费开源标准。如果针对 web 上的实时通信的新标准出现,那么该标准也应是免费、开源和可互操作的。基于 XML 的 XMPP 满足这些标准。在本教程中,您将使用 XMPP 来构建一个客户端库,它通过标准方法(比如一个 web hook)来接收输入,并将适当的数据实时中继到用户。


XMPP 简介

本小节将简要介绍 XMPP,它的起源,以及为何它是一个适合实时 web 通信的协议。您将检查 XMPP 通信设置的组件,并查看展示这些组件如何使用的示例。

Web 标准和 XMPP

XMPP 是一组基于 XML 的技术,用于实时应用程序。最初,XMPP 作为一个框架开发,目标是支持企业环境内的即时消息传递和联机状态应用程序。当时的即时消息传递网络是私有的,非常不适合企业使用。例如,AOL Instant Messenger 不能针对公司内的安全通信进行调整。尽管存在一些商业解决方案,但它们固定的特性集通常不能进行调整,以满足组织的特殊需求。XMPP,当时名为 Jabber,允许组织构建自己的定制工具来促进实时通信,并允许安装现成的第三方解决方案。

XMPP 是一个分散型通信网络,这意味着,只要网络基础设施允许,任何 XMPP 用户都可以向其他任何 XMPP 用户传递消息。多个 XMPP 服务器也可以通过一个专门的 “服务器-服务器” 协议相互通信,提供了创建分散型社交网络和协作框架的有趣可能性,但这个主题已超出了本教程的讨论范围。

顾名思义,XMPP 可用于满足广泛的、对时间敏感的特性要求。实际上,Google Wave,一个大型多用户协作环境,将 XMPP 作为其联合协议的基础。尽管 XMPP 的出现是为了满足 “个人-个人” 即时消息传递的要求,但它完全不必局限于此任务。

XMPP 通信的结构

要促进消息传递,每个 XMPP 客户端用户必须拥有一个全局惟一标识符。基于历史原因,这些标识符称为 Jabber IDs,或称为 JIDs。鉴于这个协议的分布式特征,重要的是 JID 应包含联系用户所需的所有信息:不存在将用户链接到他们连接到的服务器的中央知识库。JID 的结构类似于电子邮件地址(但不要求 JID 同时也是有效的电子邮件收件人)。

客户端和服务器节点,我将它们统称为 XMPP 实体,都拥有 JIDs。SomeCorp 公司的员工 John Doe 可能拥有 JID John.Doe@somecorp.com。这里,somecorp.com 是 SomeCorp 公司的 XMPP 服务器的地址,John.Doe 是 John Doe 的用户名。

JIDs 还拥有连接到它们的资源。这允许在一个 XMPP 实体标识符之外进一步处理细粒度;例如,尽管上面的示例总体上能够表示 John Doe,但 John.Doe@somecorp.com/Work 可以用于将数据发送到与他的工作相关的工具。

这些资源可以采用任意用户定义的名称,一个 XMPP 实体可以拥有任意数量的资源。除了可以是上下文依赖的外,它们还可以绑定到设备、工具或工作站。对于您的 Pingstream 示例,web 站点的每个访问者都将作为同一个用户登录 XMPP 服务器,但他们拥有不同的资源。

通信类别

使用 XMPP 的实时消息传递系统包含三大通信类别:

  • 消息传递,其中数据在有关各方之间传输;
  • 联机状态,它允许用户广播其在线状态和可用性;
  • 信息/查询请求,它允许 XMPP 实体发起请求并从另一个实体接收响应。

这些类别是互补的。例如,如果用户或实体离线(尽管在许多用例中,理想的状态是服务器在用户返回之前一直持有用户的消息),则没有将数据发送给用户或发起一个实体的信息/查询请求的点。这些消息中的每一条都将通过一个完整的 XML 传递 — XML 节是以 XML 表达的独立信息项。

这三种类型的 XMPP 节都拥有以下公共属性:

  • from:源 XMPP 实体的 JID;
  • to:目标接收者的 JID;
  • id:这次对话的可选标识符;
  • type:节的可选子类型;
  • xml:lang:如果内容是人们可读的,则为消息语言的描述。

基于 XMPP 的数据传输发生在一些 XML 流上,默认在端口 5222 上操作。这些 XML 流实际上是两个完整的 XML 文档,每个文档对应一个通信方向。一旦会话建立,stream 元素将打开。这个元素将封装整个通信文档。然后,一些节被注入这个文档的第二层。最后,一旦通信结束,stream 元素将关闭,形成一个完整的文档。

例如,清单 1 展示了一个 stream 元素,它建立了从客户端到服务器的通信。

清单 1. 建立从客户端到服务器的通信的 stream 标记
<stream:stream from="[server]" id="[unique ID over conversation]" 
xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams" version="1.0">

消息

一旦通信建立,客户端就能使用 message 元素将消息发送到另一个用户,message 元素包含以下任意子元素:

  • subject:一个可读的字符串,表示消息主题。
  • body:一个可读的字符串,表示消息体。如果每个消息体标记都拥有一个不同的 xml:lang 值,那么可以包含多个消息体标记。(xml:lang 是惟一可能的属性。)
  • thread:一个惟一标识符,表示一个消息线程。客户端软件可以使用这个子元素将相关消息串联在一起。

但是,消息也可以非常简单,如 清单 2 所示:

清单 2. 样例消息
<message from="sendinguser@somedomain" to="recipient@somedomain" xml:lang='en'>
  <body>
    Body of message
  </body>
</message>

对于提供实时 web 界面而言,消息节是最有用的节。“发布-订阅” 模型 — 在实时 web 应用程序中使用消息来传输数据的一种替代方法 — 将稍后介绍。

信息/查询

信息/查询节拥有广泛的功能。一个例子就是 “发布-订阅” 模型,在该模型中,发布者通知服务器某个特定资源进行了更新,服务器则通知已选择订阅这些通知并拥有适当授权的所有 XMPP 用户。

来自发布者的一系列项目被编码为一些节,格式为基于 XML 的 Atom 发布格式。每个项目都包含在一个 item 元素内,然后合并到一个 pubsub 元素中,最后成为一个信息/查询节。在 清单 3(选自 XMPP 发布-订阅规范)中,Shakespeare's Hamlet(JID 为 hamlet@denmark.lit/blogbot)用他著名的独白发布一个更新到 pubsub.shakespeare.lit pubsub 更新节点:

清单 3. 对 pubsub.shakespeare.lit pubsub 更新节点的更新
<iq type="set"
    from="hamlet@denmark.lit/blogbot"
    to="pubsub.shakespeare.lit"
    id="pub1">
  <pubsub xmlns="http://jabber.org/protocol/pubsub">
    <publish node="princely_musings">
      <item>
        <entry xmlns="http://www.w3.org/2005/Atom">
          <title>Soliloquy</title>
          <summary>
To be, or not to be: that is the question:
Whether 'tis nobler in the mind to suffer
The slings and arrows of outrageous fortune,
Or to take arms against a sea of troubles,
And by opposing end them?
          </summary>
          <link rel="alternate" type="text/html"
                href="http://denmark.lit/2003/12/13/atom03"/>
          <id>tag:denmark.lit,2003:entry-32397</id>
          <published>2003-12-13T18:30:02Z</published>
          <updated>2003-12-13T18:30:02Z</updated>
        </entry>
      </item>
    </publish>
  </pubsub>
</iq>

信息/查询节也用于请求一个特定 XMPP 实体的有关信息。例如,在 清单 4 中的节中,boreduser@somewhere 正在查找 friendlyuser@somewhereelse 拥有的公共项目。

清单 4. 用户查找由 friendlyuser@somewhereelse 拥有的公共项目
<iq type="get"
    from="boreduser@somewhere"
    to="friendlyuser@somewhereelse"
    id="publicStuff">
  <query xmlns="http://jabber.org/protocol/disco#items"/>
</iq>

反过来,friendlyuser@somewhereelse 使用一列可被订阅到使用 “发布-订阅” 的项目进行响应,如 清单 5 所示:

清单 5. 使用一列项目响应
<iq type="result"
    from="friendlyuser@somewhereelse"
    to="boreduser@somewhere"
    id="publicStuff">
  <query xmlns="http://jabber.org/protocol/disco#items">
    <item jid="stuff.to.do"
          name="Things to do"/>
    <item jid="stuff.to.not.do"
          name="Things to avoid doing"/>
  </query>
</iq>

清单 5 中的信息/查询节中的每个返回项目都拥有一个可以订阅到的 JID。信息/查询还允许超出本教程范围的广泛的服务器信息请求。它们中的许多在针对多服务器环境的 web 应用程序上下文中有用,或者作为复杂的分散型协作框架的基础。

联机状态

联机状态信息包含在一个联机状态(presence)节中。如果 type 属性省略,那么 XMPP 客户端应用程序假定用户在线且可用。否则,type 可设置为 unavailable,或者特定于 pubsub 的值:subscribesubscribedunsubscribeunsubscribed。它也可以是针对另一个用户的联机状态信息的一个错误或探针。

一个联机状态节可以包含以下子元素:

  • show:一个机器可读的值,表示要显示的在线状态的总体类别。这可以是 away(暂时离开)、chat(可用且有兴趣交流)、dnd(请勿打扰)、或 xa(长时间离开)。
  • status:一个可读的 show 值。该值为用户可定义的字符串。
  • priority:一个位于 -128 到 127 之间的值,定义消息路由到用户的优先顺序。如果值为负数,用户的消息将被扣留。

例如,清单 6 中的 boreduser@somewhere 可以用这个节来表明聊天意愿:

清单 6. 样例联机状态通知
<presence xml:lang="en">
  <show>chat</show>
  <status>Bored out of my mind</status>
  <priority>1</priority>
</presence>

注意 from 属性此处省略。

另一个用户 friendlyuser@somewhereelse 可以通过发送 清单 7 中的节来探测 boreduser@somewhere 的状态:

清单 7. 探测用户状态
<presence type="probe" from="friendlyuser@somewhereelse" to="boreduser@somewhere"/>
Boreduser@somewhere's server would then respond with a tailored presence response:
<presence xml:lang="en" from="boreduser@somewhere" to="friendlyuser@somewhereelse">
  <show>chat</show>
  <status>Bored out of my mind</status>
  <priority>1</priority>
</presence>

这些联机状态值源自 “个人-个人” 消息传递软件。show 元素的值 — 通常用于确定将向其他用户显示的状态图标 — 在聊天应用程序之外如何使用现在还不清楚。状态值可能会在微博工具中找到用武之地;例如,Google Talk(一个 XMPP 聊天服务)中的用户状态字段的更改可以被导入为 Google Buzz 中的微博条目。

另一种可能性就是将状态值用作每用户应用程序状态数据的携带者。尽管此规范将状态定义为可读,但没有什么能够阻止您在那里存储任意字符串来满足您的要求。对于某些应用程序而言,它可以不是可读的,或者,它可以携带微格式形态的数据负载。

您可以为一个 XMPP 实体拥有的每个资源独立设置联机状态信息,以便访问和接收连接到一个应用程序中的单个用户的所有工具和上下文的数据只需一个用户帐户。每个资源都可以被分配一个独立的优先级;XMPP 服务器将首先尝试将消息传递给优先级较高的资源。

XMPP 使用 BOSH 越过 HTTP

要通过使用 JavaScript 的 XMPP 进行通信的 web 应用程序必须符合一些特殊要求。出于安全考虑,不允许 JavaScript 从 web 页面的域与不同域上的多个服务器通信。如果您的 web 应用程序界面被托管在 application.mydomain.com,所有 XMPP 通信也必须发生在 application.mydomain.com

防火墙是另一个问题所在。理想情况下,如果您将 XMPP 用作您的 web 界面的实时元素的基础,那么您希望它对防火墙后面的用户有效。但是,公司防火墙通常只对少数几个协议开放几个端口,以便允许 web 数据、电子邮件和类似的通信通过。默认情况下,XMPP 使用端口 5222,这很可能是公司防火墙阻止的端口。

假设您知道您的用户前面的防火墙在端口 80 上允许 HTTP(这是用于访问 web 的默认协议和端口)。理想情况是您的 XMPP 通信能够越过该端口上的 HTTP。但是,HTTP 的设计并不针对持续连接。web 的架构不同于实时数据所需的通信架构。

下面我们看看 Bidirectional-streams Over Synchronous HTTP (BOSH) 的标准,该标准为双向同步数据提供一个模拟层。借助这个标准,可以与一个 XMPP 服务器建立一个较长的 HTTP 连接(时长一分钟或两分钟)。如果新数据在那个期间到达,则 HTTP 请求返回数据并关闭;否则,该请求只是失效。不管是哪种情况,一旦一个请求关闭,另一个请求将重新建立。尽管结果是对一个 web 服务器的一系列重复连接,但它是一个比 Ajax 轮询更有效的数量级,特别是因为连接到的是一个专业服务器而不是直接连接到 web 应用程序。

BOSH 上的 XMPP 允许 web 应用程序通过一个原生连接持续与 XMPP 服务器通信。客户端通过端口 80 上的 HTTP 上的一个标准 URL 连接。然后,web 服务器将这个连接代理到由 XMPP 服务器操作的一个不同端口 — 通常是 7070 — 上的 HTTP URL。这样,无论何时数据被发送到 XMPP 服务器,web 应用程序只需使用一些资源,而 web 客户端可以使用通常支持的 web 标准从防火墙后操作。维持 BOSH 的较长 HTTP 轮询的开销主要由 XMPP 服务器而不是 web 服务器或 web 应用程序承担。web 服务器和 XMPP 服务器都不会受到与使用 JavaScript 进行通信一样的域限制,正是因为这一点,消息才能够被发送到其他 XMPP 服务器和客户端。

现在,您理解了 XMPP 如何适合实时 web,可以下载并设置它,以便开始创建这个 Pingstream 应用程序。


获取和安装一个 XMPP 服务器

在本小节中,您将安装 Openfire XMPP 服务器并配置它来支持您的实时 web 应用程序。

选择一个 XMPP 服务器

有两个领先的开源 XMPP 服务器可以免费下载。它们都应用广泛并通过 GNU Public License version 2 许可,每个服务器都有自己的优势和缺点:

  • ejabberd:ejabberd 中的 e 指的是 Erlang,一种软实时编程语言。这一技术基石使 ejabberd 非常快。它还与 XMPP 核心和相关标准高度兼容。ejabberd 可以安装在大多数环境中。
  • Openfire:Openfire 用 Java™ 语言编写,用户友好,安装方便。

本教程使用 Openfire。

创建 Openfire 数据库

为您的 Openfire 用户和配置创建一个新的 MySQL 数据库。通过使用 MySQL,您可以以编程方式从您的 PHP web 应用程序添加、编辑、删除和查询您的 XMPP 服务器用户,以及调节您的 XMPP 基础设施以匹配您的 web 基础设施。

如果您安装了 phpMyAdmin,比如作为您的 XAMPP 安装的一部分,那么您可以按照以下步骤创建数据库:

  1. 从主界面选择 Privileges
  2. 选择 Add a new user
  3. 添加用户细节(确保主机是 localhost;本教程假定您是在 localhost 上测试),选择 Create database with same name and grant all privileges,如 图 1 所示。不要向您的 Openfire 数据库新用户授予全局数据库特权。
图 1. 在 phpMyAdmin 中添加一个 Openfire 数据库
'Add a new User' 界面的屏幕截图:为 phpMyAdmin 中的用户添加用户名、主机、密码和数据库

添加用户和数据库后,就可以安装 Openfire 服务器了。

安装 Openfire

下载 Openfire 安装程序并运行它,将 Openfire 安装到您选择的位置(参见 参考资料)。(您也可以选择从 Openfire 的 Subversion 源代码知识库检查 Openfire 的最新版本并本地构建它,但这个主题超出了本教程的范围。)收到提示时,告知 Openfire 安装程序在安装完成时启动服务器。

服务器启动后,您应该看到服务器状态窗口,如 图 2 所示:

图 2. Openfire 的状态窗口
Openfire 状态窗口的屏幕截图,显示服务器版本、日期和状态

单击 Launch Admin 打开一个基于 web 的向导,如 图 3 所示,该向导将带您逐步配置您的 Openfire 服务器:

图 3. 配置向导
配置向导的屏幕截图,显示 Database Setting - Standard Connection

配置向导允许您选择使用标准数据库连接或嵌入式数据库连接。选择标准数据库连接,以便您能够使用您的 MySQL 数据库。

Database Driver Presets 列表选择 MySQL。将您的服务器和数据库名称插入 Database URL 字段。例如,对于在 localhost 上设置、名为 openfire 的 MySQL 数据库而言,应输入:

    jdbc:mysql://localhost:3306/openfire

在向导的下一屏幕上,选择将用户帐户存储在数据库中。输入此前创建的数据库用户的用户名和密码,然后一直继续到配置向导结束。此时,您应该已经为您的 XMPP 服务器创建了一个服务器管理员并设置了域位置。

每用户通知的插件

根据本教程的演示目的,您将只需使用在这里创建的两个用户。但是,如果您希望在您的应用程序中支持复杂的每用户通知,那么您需要能够以编程方式从您的应用程序的 PHP 部分添加和移除用户。Openfire 的 User Service 插件通过一个用于 XMPP 用户管理的 REST 界面向您提供这个功能。要安装这个插件,从 Openfire 插件站点(参见 参考资料)下载它。插件本身是单个文件:userservice.jar。您必须将其放置到您的 Openfire 安装的 /plugins 目录中。

使用您建立的管理员凭证登录到管理屏幕。单击 Edit Properties(位于 Server ports 下方),记录列示的服务器名称。这个名称将形成您的 JIDs 的域部分。这个名称是不可互换的,比如,不能使用 localhost 替代 127.0.0.1,反之也不行。

单击顶部导航菜单中的 Users/Groups 并创建两个新用户。这些用户将您在开发过程中的测试用户。

单击 Server settings,然后单击 Offline messages。由于您将 XMPP 用于界面通知,因此应将 Offline Message Policy 设置为 Drop,如 图 4 所示。您不想保存用户没有登录时收到的消息,否则,当他们返回时,可能会被数千条通知所 “淹没”。

图 4. 丢弃离线消息
配置向导的屏幕截图,显示 Offline Messages 屏幕,其中 Drop 单选按钮选中

Server settings 中,单击 Server to Server。对于本文,您不必连接到外部服务器,因为您不需要作为更大的 XMPP 网络的一个连接部分操作。因此,将 Service Enabled 设置为 DisabledAllowed to Connect 设置为 White List。这些设置将阻止未授权的连接造成破坏。

配置 Apache 以通过 BOSH 转发 XMPP

Openfire 在 http://localhost:7070/http-bind 维护了一个 HTTP 绑定 URL,以便通过 BOSH 访问。要在端口 80 上使用这个 URL,您必须配置 Apache HTTP Server 以将一个 URL 转发到这个位置。为此,您需要启动代理模块。

打开您的 http.conf Apache 配置文件并找到 mod_proxy.somod_proxy_http.soLoadModule 条目,它们默认被注释掉。移除前导的井字符(#),取消注释。这个配置文件的 Dynamic Shared Object (DSO) Support 部分中的多个适当的行(不一定在一起)现在应该类似于 清单 8

清单 8. 启用 Apache HTTP Server 中的代理支持
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule proxy_module modules/mod_proxy.so

在配置文件的末尾,添加 清单 9 中的行(如果您没有将 locahost 作为您的测试服务器环境,则应将 127.0.0.1 替换为您的服务器 IP 地址):

清单 9. httpd.cof 中的 XMPP 代理规则
# XMPP proxy rule
ProxyRequests Off
ProxyPass /xmpp-httpbind http://127.0.0.1:7070/http-bind/
ProxyPassReverse /xmpp-httpbind http://127.0.0.1:7070/http-bind/

注意,在 清单 9 中,您在端口 80 上使用了一个稍微不同的 URL:/xmpp-httpbind。这个 URL 是 strophe.js(您稍后将用到的客户端 JavaScript 框架)分配给一个用于设置 BOSH 端点的变量的值。

重启服务器。现在,您可以开始编写使用 XMPP 的 web 应用程序了。


创建使用 XMPPHP 的服务器应用程序

在前面的小节中,您设置了服务器和插件。在本小节中,您将创建您的实时应用程序的服务器端部分。

服务器端功能

您的应用程序的 PHP 端将执行以下两个主要任务:

  1. 一个 PHP 脚本将获取一个 RSS,并将在第一个条目自您上次检查以来发生改变时通知您。
  2. 一个前端脚本将初始化您的 JavaScript XMPP 客户端。

首先在您的测试服务器的 web 根中创建一个名为 pingstream 的 PHP 新项目空间。如果您使用针对 Eclipse 开发环境的 PHP Development Tools 项目扩展,那么要注意,要在 web 根、而不是 Eclipse 的默认工作空间中创建这个项目(参见 图 5):

图 5. 创建一个新的 Eclipse PHP 项目
PHP Project > New PHP Project > Create a PHP Project 对话框的屏幕截图

图 5 显示了这个 Pingstream 项目的以下值:

  • Project name 字段:Pingstream
  • Contents 单选按钮:Create project from existing source
  • Directory 字段:c:\xampp\htdocs\pingsstream
  • PHP Version 单选按钮:Use default PHP settings

XMPPHP 是应用最广泛的针对 PHP 的 XMPP 库。与 ejabberd 和 Openfire 一样,它也是免费和开源的,因此它是首次使用 XMPP 开发人员的一个不错的起点。

下载 XMPPHP(参见 参考资料)。解包这个归档并将其插入您的新项目的 lib/xmpphp 子文件夹中。

最后,为节约您的 RSS 提要的解析时间,下载并安装 Last RSS PHP 解析器(参见 参考资料)。

存储服务器端设置

您将使用此前创建的两个测试用户之一作为您的通知发送方。在您的 /pingstream 的根中创建一个名为 config.inc.php 的文件,并添加 清单 10 中的内容:

清单 10. 服务器端配置设置
<?php

    // Pingstream configuration file

    // Define global $CONFIG array - don't change this!
        global $CONFIG;
        $CONFIG = array();
        $CONFIG['send'] = new stdClass();

    // Set account details for sending party
        $CONFIG['send']->user = 'testuser';    // User portion of JID
        $CONFIG['send']->host = '127.0.0.1';    // Host portion of JID
        $CONFIG['send']->resc = 'pingstream';// Resource portion of JID
        $CONFIG['send']->pass = 'mypass';    // Password for user

config.inc.php 文件还必须包含关于您的接收方的细节,如 清单 11 所示:

清单 11. 关于接收方的细节
// Set the receiving account details
    $CONFIG['receive'] = 'receivinguser@127.0.0.1';

您还需将少量数据缓存在一个文件中,因此 config.inc.php 需要包含一个可写入的文件路径,如 清单 12 所示:

清单 12. 设置存储临时文件的位置
// Full path to your cache directory, with trailing slash
    $CONFIG['cachedir'] = '/tmp/';

如果您位于一台 Microsoft® Windows® 机器上,那么您可以将这个位置设置为一个空字符串。否则,确保您指定的目录包含一个完整路径和一个结束斜杠,并被设置为全局可写入(world-writeable)。

定义数据源

您的 Pingstream 应用程序将检查 IBM developerWorks Web development 专区提要获取更新,因此,将 清单 13 中的内容添加到 config.inc.php:

清单 13. 检查一个 developerWorks 提要获取更新
// Set the RSS feed you're going to check
    $CONFIG['rss'] = 'http://www.ibm.com/developerworks/views/web/rss/libraryview.jsp';

现在,创建另一个新文件:/lib.inc.php。这将是您的库文件,包含在您的应用程序的主控制器页面和用户界面页面中。

lib.inc.php 的顶端必须引用 config.inc.php(主 XMPPHP 库)和 lastRSS,如 清单 14 所示:

清单 14. 加载必要的库
<?php

    // Load libraries
        require_once('XMPPHP/XMPP.php');
        require_once('config.inc.php');
        require_once('lastRSS.php');

接下来,您将创建一个函数来通过 XMPP 将一条消息发送到客户端。通过 XMPPHP 完成这个任务很简单,只需使用在您的配置文件中保存的凭证建立一个连接,发送消息,然后关闭连接。

在这个函数的第一部分中,您创建了一个新的 XMPPHP_XMPP 对象,如 清单 15 所示:

清单 15. 创建一个新的 XMPP 连接对象
// Load configuration
        global $CONFIG;

        $conn = new XMPPHP_XMPP(
                    $CONFIG['connect']->host,
                    5222, 
                    $CONFIG['connect']->user,
                    $CONFIG['connect']->pass, 
                    $CONFIG['connect']->resc);

注意,XMPPHP 通过 XMPP 通信的默认端口 5222 连接到您的 XMPP 服务器。尽管您的客户端通信需要使用 BOSH,但服务器端没有这个要求。

要连接到 XMPP 服务器,需要发送一个初始连接请求,一直等到接收到您的 XMPP 会话已经启动的通知,然后发送一个联机状态节来声明您处于在线状态(参见 清单 16):

清单 16. 建立一个 XMPP 连接
$conn->connect();
$conn->processUntil('session_start');
$conn->presence();

下一步是发送消息本身,该消息已被预先填充到一个名为 $message 的变量中(参见 清单 17):

清单 17. 发送消息
$conn->message($CONFIG['receive'], $message);

然后您使用 $conn->disconnect(); 断开连接。

在此过程中,您可能会遇到错误。清单 18 将清单 151617 中的代码放到一个将插入到 lib.inc.php 中的函数中。在这个过程中,它将发送一条消息的业务封装到一个 try/catch 语句中,该语句将把任何异常消息写入错误日志中。

清单 18. 完整的 send_notification 函数,包含 try/catch 语句以记录错误
/**
 * Updates everyone's user interface with a message
 */
function send_notification($message) {

    // Load configuration
    global $CONFIG;

    $conn = new XMPPHP_XMPP(
                $CONFIG['connect']->host,
                5222, 
                $CONFIG['connect']->user,
                $CONFIG['connect']->pass, 
                $CONFIG['connect']->resc);

    try {
        $conn->connect();
        $conn->processUntil('session_start');
        $conn->presence();
        $conn->message($CONFIG['receive'], $message);
        $conn->disconnect();
    } catch(XMPPHP_Exception $e) {
        error_log($e->getMessage());
    }

}

您可以对任意数量的公共通知应用程序使用这个简单的机制。这里,您向访问站点的所有用户发送相同的通知,但通过以下机制来定制通知是件麻烦事:

  1. 每个用户被赋予一个惟一会话字符串,或者一个特定于某个特殊兴趣或搜索的共享字符串;
  2. 这个字符串作为资源片段被添加到接收方的 JID;
  3. 消息然后被发送到 user@domain/session-string JID。

获取外部数据

接下来,您需要一个函数(参见 清单 19)来从您的指定提要检索最新的 RSS 条目:

清单 19. 获取 RSS 提要中的最新条目
function get_last_feed_item() {
    global $CONFIG;         // Load configuration
    $rss = new lastRSS;     // Initialize lastRSS 
       $rss->CDATA = 'content';
    if ($rs = $rss->get($CONFIG['rss'])) {
        if (isset($rs['items'][0]))
            return $rs['items'][0];
        else
            return false;
    } 
}

这个函数初始化 lastRSS,加载您配置的 RSS 提要(这里是 IBM developerWorks Web development 专区的最新文章提要),将最顶端的条目返回为一个数组。拥有这个最新提要条目后,您需要知道自从上次检查以来,该条目是否被更改。为此,您需要使用一个小型文本文件。为保护应用程序,最好使用数据库或另一种方法,但对于测试目的,可以放心使用一个小型文件缓存。这个缓存在您每次检查时都存储最新提要条目的 URL;如果这个 URL 更改,那么您就有一个新条目,应该通知用户。另一个函数 feed_has_changed 将据此返回 truefalse。不管是哪种情况,它都会将这个最新 URL 保存到缓存文件中,为下次检查做好准备。清单 20 展示了 feed_has_changed 函数:

清单 20. 返回任何新提要数据
function feed_has_changed($url) {

    global $CONFIG;
    $changed = false;

    // Check to see if the file exists, and if it does, if
    // the URL has changed
    if (!file_exists($CONFIG['cachedir'] . 'cache.txt')) {
        $changed = true;
    } else if (file_get_contents($CONFIG['cachedir'] . 'cache.txt') != $url) {
        $changed = true;
    }

    // If the URL has indeed changed, update the version in the cache
    // and return true
    if ($changed) {
        file_put_contents($CONFIG['cachedir'] . 'cache.txt', $url);
        return true;
    }

    // Otherwise return false
    return false;

}

您将把最新条目的一个简单的 HTML 编码版本传递给客户端。在更高级的应用程序中,可以以 JavaScript Object Notation (JSON) 或 XML 编码该条目,多包括一些元数据,并允许客户端 JavaScript 根据设备和浏览器适当格式化它。但是,对于现在,清单 21 中的版本就够用了。

清单 21. 将一个提要条目封装在简单的 HTML 中
function last_item_html($item) {
    return <<< END
    <div class="item">
        <div class="item_title">
            <h2><a href="{$item['link']}"
 target="_blank">{$item['title']}</a></h2>
        </div>
        <div class="item_body">
            {$item['description']}
        </div>
    </div>
END;
}

最后,在您的 /pingstream 目录中创建一个名为 backend.php 的新 PHP 文件。使用 清单 22 中的简单 PHP 脚本作为文件内容。这个脚本负责检索 RSS 提要并通过 XMPP 将最新提要条目的 JSON 编码版本发送到您的接收方。

清单 22. 通过 XMPP 发送封装的提要条目
<?php

    require_once('lib.inc.php');

    if ($lastitem = get_last_feed_item()) {
        if (feed_has_changed($lastitem['link'])) {
            send_notification(last_item_html($lastitem));
        }
    }

这就是将动态通知发送给应用程序的公共用户所需的全部内容。理想情况下,您应该将 backend.php 脚本作为一个定期时间任务运行。但是,对于测试目的,您可以通过一个 web 浏览器手动执行该脚本。


浏览器应用程序:Strophe.js 和 jQuery

在本小节中,您将编写一些 JavaScript 函数,以便通过 BOSH 上的 XMPP 接收消息,并构建一个 HTML 用户界面来显示接收到的通知。

创建用户界面

现在您需要创建用户界面来接收通知。Strophe.js 是用于通过 BOSH 发送和接收 XMPP 数据的常用 JavaScript 库。对于 Pingstream 中的目的,您只需接收数据,尽管有一点是显而易见的:双向通信允许您快速构建丰富的协作环境。

尽管有几个版本,但 Strophe 的 JavaScript 版本作为一个基于浏览器的 XMPP 客户端对您而言是最有用的。下载压缩包(参见 参考资料)并将其解压缩到 pingstream 的 strophejs 文件夹中。

jQuery JavaScript 框架极大地简化了事件处理和 DOM 操作。本文提供的 Strophe.js 示例广泛使用该框架,这两者简直是 “天生一对”。下载 jQuery(参见 参考资料 中的链接)并将这个缩微版放到 pingstream 中的 jquery 文件夹中。

新建一个 index.html 文件。在该文件中包含对刚才下载的 Strophe 和 jQuery 库的引用,以及对稍后即将定义的 pingstream.js 库的引用。在 body 元素中,添加一个 ID 为 notifications 的 div 元素,如 清单 23 所示:

清单 23. 客户端 HTML 页面
<!DOCTYPE html>
<html>
    <head>
        <title>Latest content</title>
        <script type="text/javascript"  
src="jquery/jquery-1.4.2.min.js"></script>
        <script type="text/javascript"  
src="strophejs/strophe.js"></script>
        <script type="text/javascript" 
src="pingstream.js"></script>
    </head>
    <body>
        <h1>Latest content:</h1>
        <div id="notifications"></div>
    </body>
</html>

创建 JavaScript 文件 — pingstream.js — 您刚才在 清单 23 中引用的。在 pingstream.js 的顶端,定义此前在 Apache 中配置的 BOSH 代理端点,如 清单 24 所示:

清单 24. 设置 BOSH 端点
var BOSH_SERVICE = '/xmpp-httpbind';
var connection = null;

当页面完全加载后,您想自动连接到 XMPP 服务器。您可以使用 jQuery 的 $(document).ready 调用实现这个目标;其中,您新建一个 strophe.js Strophe.Connection 对象并用它连接到服务器,如 清单 25 所示:

清单 25. 建立一个通过 BOSH 的连接
$(document).ready(function () {
    connection = new Strophe.Connection(BOSH_SERVICE);
    connection.connect(    "sendinguser@127.0.0.1",
                            "sendingpass",
                            onConnect);
    });

更健壮的选项

对于本教程的目的,您正在使用此前定义的发送方。对于一个更健壮的应用程序,更好的方法可能是为每个注册应用程序用户创建一个新用户,并将每个用户订阅到一个 “发布-订阅” 界面。或者,如果您将用户名和密码留空并将 XMPP 服务器配置为接受这种类型的连接,那么 Strophe.js 可以匿名登录。在这些情况下,将针对每个匿名用户动态创建一个 JID;这些 JID 必须受到管理。最后,您还可以扩展 XMPP 聊天室。

清单 25 中,Strophe.Connection.connect 方法包含一个对 onConnect 函数的引用,作为它的一个参数。onConnect 将在连接建立后立即启动。您可以利用这个机会来为入向消息添加一个通知处理程序;您在这里注册了一个名为 notifyUser 函数。随后,您发送了一个简单的联机状态节。

要确保您可以连接并接收新消息,您还需向用户发送一个友好通知。

清单 26 中的代码添加到您的 JavaScript 文件中的 $(document)ready 调用上方:

清单 26. 处理入向 XMPP 消息
function onConnect(status)
{
    $('#notifications').html('<p class="welcome">Hello! 
Any new posts will appear below.</p>');
    connection.addHandler(notifyUser, null, 'message', null, null,  null);
    connection.send($pres().tree());
}

最后,由于您注册了通知处理程序,因此,只要 XMPP 客户端接收到消息节,Strophe.js 就会调用 notifyUser(msg) 函数。msg 参数是 XML 节本身的一个表示,可以如 清单 27 所示查询:

清单 27. 查询 msg 参数
var elems = msg.getElementsByTagName('body');
var body = elems[0];
$('#notifications').append(Strophe.getText(body));

理想情况下,您希望对消息进行限制,以便只显示您的服务器端发送用户发送的消息。您可以将它封装到构成 notifyUser 函数主体的一个 if 语句中,如 清单 28 所示:

清单 28. notifyUser 函数
function notifyUser(msg) 
{
    if (msg.getAttribute('from') == "testuser@127.0.0.1/pingstream") {
        var elems = msg.getElementsByTagName('body');
        var body = elems[0];
        $('#notifications').append(Strophe.getText(body));
    }
    return true;
}

这个函数应位于在 清单 26 中定义的 onConnect 函数上方。

最终效果

在一个 web 浏览器中打开您的 index.html 文件。您应该会看到一个简单的标题和一条消息,该消息称更新将在下面显示(这可能会使您回想起您发给自己的测试通知,称 XMPP 连接正在成功运行)。

现在加载 backend.php。就像变戏法一样,来自 IBM developerWorks Web development 专区的最新更新将显示在您的页面上。其他带有 RSS 提要的示例源包括 Twitter 帐户、通讯社、以及来自服务器监控软甲的更新提要。

这是开发一个强大平台的简单起点。Strophe.js 能够促进应用程序的双向通信,尽管更简单的方法是使用标准的 jQuery HTTP 回拨来将用户输入送入系统,从而避免为您的应用程序编写一个 XMPP 后台监控进程的麻烦。更令人兴奋的是,当您 web 服务器用作 BOSH 代理时,完全无需太多来自服务器端 web 应用程序的输入,两个或更多 web 客户端就能通过 XMPP 相互通信。这种技术将对从办公室协作软件到游戏的很多软件产生深远影响。


结束语

本教程讨论了实时 web 应用程序的必要性,以及 XMPP 如何克服现有技术的缺点。为展示这种方法的效果,您使用 XMPPHP、Last RSS、Strophe.js、Openfire 和 PHP 开发了一个简单的 RSS 更新通知应用程序。

尽管需要一个附加服务器层和一些 JavaScript 新技术,但 XMPP 比传统 Ajax 轮询模型更加适合实时 web 应用程序。XMPP 更快,在开发和系统基础设置方面需要的开销更少,并且使用一个强大的新兴 web 开发标准。


下载

描述名字大小
Pingstream 源代码pingstream.zip238KB

参考资料

学习

获得产品和技术

  • Openfire:下载 Openfire,这是一个基于 XMPP (Jabber) 协议的跨平台实时协作服务器。
  • PHP:访问这个 PHP 站点,获取这个应用广泛的脚本语言,该语言非常适合 Web 开发,可以嵌入到 HTML 中。本教程使用 PHP 5.2 或更高版本。
  • Apache HTTP Server:下载这个 Apache web 服务器。
  • MySQL:下载这个开源事务型数据库。
  • DB2 Express-C:下载这个免费版 IBM DB2 数据库服务器,它是中小型企业应用程序开发的一个坚实基础。
  • Openfire 插件:获取用于 Openfire 的 User Service 插件。
  • XMPPHP:从这个项目的 Google Code 站点下载 PHP XMPP Library。
  • Last RSS:获取 Vojtech Semecky 针对 PHP 的 RSS 解析器(经过 GNU Public License version 2 许可)。
  • jQuery:下载 jQuery JavaScript 库,经过 MIT 或 GNU Public License 许可。
  • Strophe:下载 Strophe.js,这是一系列用于编写 XMPP 客户端的库。它的许可允许对其进行自由使用、修改和共享。
  • phpMyAdmin:获取这个免费软件工具,它支持通过 web 管理 MySQL。
  • IBM 产品评估试用版软件:下载或 在线试用 IBM SOA Sandbox,并开始使用来自 DB2®、Lotus®、Rational® 、Tivoli® 和 WebSphere® 的应用程序开发工具和中间件产品。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML, Web development, Open source
ArticleID=503485
ArticleTitle=使用 XMPP 构建一个基于 web 的通知工具
publish-date=08022010