对于初学者,接下来的几篇专栏文章将研究 BEEP,它是众多潜藏在 Web 服务词汇表里的字首组合词中的一个。本文的目的就是介绍 BEEP 协议框架并就它适合在哪里使用提出建议。
BEEP 代表 Blocks Extensible Exchange Protocol(块可扩展交换协议),这样的全称和只说 BEEP一样没有意义,坦白地说更没有趣味。不过,XML 用户或许会被 可扩展的(extensible)这个词吸引,而的确正是扩展性使 BEEP 值得首先研究。稍后将会对此有更详细的讨论;让我们首先来看看 BEEP 所解决的问题。
假设您正在编写一个网络应用程序而且想让程序的实例能通过 TCP/IP 通信。在您开始考虑应用程序逻辑本身以前,需要解决程序如何连接,如何验证它们自己,如何发送消息,如何接收消息以及如何报告错误等问题。您花在这些问题上的累积时间可能会超过解决应用程序逻辑本身所需的时间。简单地说,这就是 BEEP 解决的问题。它实现了创建新的网络协议的所有“纯净因素”,因此您不必为它们担心。
这时您可能正在疑惑,这并不是没有道理的,为什么在有了 CORBA/IIOP,SOAP,XML-RPC 和其它协议以后还要加上另一种分布式计算协议呢?答案是,您需要认识到 BEEP 位于不同的层。它是一个框架。SOAP 可以恰当地在 BEEP 之上实现。BEEP 负责在消息的 TCP/IP 层连接、验证和封装 ― SOAP 有意回避的问题。BEEP 真正地和 HTTP 在同一层上竞争
重新使用和重新整理要素
最近的应用程序协议的设计者考察过 HTTP,发现它很好。是足够好。因此 WebDAV,也就是 Windows 中的“网络文件夹”特性背后的协议,为 HTTP 添加了几个动词以允许分布式编写。因特网打印协议(Internet Printing Protocol)则创造了一些 HTTP 头以便使用 HTTP/1.1 做它的工作,而 SOAP 到 HTTP 的绑定也做了类似的事情(有关这三个 HTTP 协议使用的背景知识,请参阅 参考资料)。
大体上,已经做了正确的事。HTTP 这个普遍存在并被广泛实现的协议,已经以一种有效的方式被重新使用。但也存在一些令人遗憾的结果:首先是造成 80 端口的超负荷。因为现在不仅有 Web 页面的请求而且还有潜在的强调安全的交易请求通过 80 端口传递,所以需要增强警惕性。许多影响 80 端口的 Web 高速缓存和其它设备的交互必须得到重视。这些问题在别的地方有详细的叙述(请参阅 参考资料),所以这里不再详细说明。
重新使用 HTTP 的第二个结果是只能使用它的交互模型。HTTP 是无状态的面向请求-响应的协议。不存在带响应的请求,也不存在不带请求的响应。而且,两个请求之间没有状态保存。不幸的是,这对许多交互模式而言不够好,因为它排除了象异步性、有状态的交互和对等通信这样的情况。这些问题可以而且已经通过在 HTTP 上分层解决了,但大部分解决方案都很难使用。
有经验的程序员就会在这个时候指出 重新整理要素的时候到了,即,将系统的职责置于它们正确的层并且提取共同的功能性。这是看待 BEEP 的最好方式:它本质上是对超负荷的 HTTP 重新整理要素以支持当今因特网应用协议的普遍要求。
那么它能做什么?
经过足够的背景知识描述之后,是看看 BEEP 能做什么和不能做什么的时候了,这样可以明白为什么需要使用它。
在 Marshall Rose(BEEP 规范的作者)所给的介绍中(请参阅 参考资料),BEEP 的应用程序“目标市场”在下列术语中描述:
尽管这些特征包含了大量的潜在的应用程序(例如,它们会恰当地允许 HTTP、FTP、SMTP 和各种即时消息传递协议的重新实现),还有许多应用程序不在 BEEP 的作用域之内。其中包括只有一次的交换,如 DNS 查找,这里由 BEEP 引起的成本将不成比例,还包括紧密耦合的 RPC 协议,如 NFS。
倘若一个应用程序符合目标市场,那么 BEEP 能提供什么呢?它的功能性的主要方面是:
不必担心这些事情这一事实使您能腾出手来给网络应用程序增加其它成分。例如,您可以开始考虑消息类型和结构。
BEEP 是一个对等协议,这就意味着它没有客户机或服务器的概念,不象 HTTP。然而,就象吵架或谈恋爱那样,某个人必须迈出第一步。为了便利我将把启动连接的一端称为 发起者,而把接收连接的一端称为 侦听者。当二者之间的连接建立以后,一个 BEEP 会话就被创建了。
通道
一个会话中的所有通信发生在一个或多个 通道中,正如 图 1中列举说明的那样。这些端只需要一个 IP 连接,这个连接随后被多路复用以创建通道。那个通道中可能的通信性质由它支持的 配置文件决定(每个通道有一个或多个)。
第一个通道(通道 0)有一个特殊目的。它支持 BEEP 管理配置文件,该配置文件用来协商更多通道的设置。所支持的配置文件决定特殊通道中端之间的精确交互。使用 BEEP 定义协议归结为配置文件的定义。
两类配置文件
会话建立后,发起者为它希望使用的特殊配置文件或配置文件集请求启动一个通道。如果侦听者支持配置文件(配置文件集),通道将被创建。配置文件本身采取两种格式中的一种:用于初始化调整的和用于数据交换的。
在通信开始时设置的调整配置文件以某种方式影响会话的其余部分。例如,请求 TLS 配置文件确保通道使用“传输层安全性(Transport Layer Security)”加密。其它的调整配置文件执行象认证这样的步骤。
数据交换配置文件在两端间建立关于通道中允许什么种类的交换的期望,类似于 Java 接口在交互对象间建立关于什么方法可用的期望。如 XML 的名称空间一样,配置文件由一个 URI 标识。例如,BEEP Java 工具中的示例“Echo”配置文件的 URI 为 http://xml.resource.org/profiles/NULL/ECHO。
数据类型
BEEP 对通道能够携带的数据类型没有限制。BEEP 使用 MIME 标准来支持任意类型的有效负载。这个方法巧妙地避开了 SOAP 所引起的这类问题:如何在 SOAP 消息中发送 XML 文档或二进制文件。
XML 连接
在本文的开头我预示过 BEEP 利用 XML,此时对在哪里进行利用感到疑惑是可以理解的。事实上,负责通道初始化的 BEEP 管理配置文件是作为 XML DTD 定义的(有关管理配置文件定义的指示,请参阅 参考资料)。这就是为什么 XML 和 BEEP 一起配合得这么好的原因:因为 BEEP 负责协议的基础设施,XML 负责数据的结构组织。因此对于 BEEP 配置文件中的消息语法的定义而言,XML 是自然而然的选择(尽管,如以上提到的,配置文件可以使用任何 MIME 类型)。
除了通道管理配置文件以外,许多新出现的 BEEP 应用程序配置文件使用了 XML 作为它们消息的编码。这就是一个好处,因为它意味着任何根据 XML 文档定义的现有消息传递标准都有一个相当直接的对 BEEP 配置文件的映射。
BEEP 是一种对等协议框架。它不象 HTTP 那样本身就是一个现成的协议, 但它是可用于构建这种协议的框架。它处理这种协议的许多功能,使得不必彻底改造它们。功能性的主要方面是:
在上一篇文章中,我解释了 BEEP 会话中的通信发生在一个或多个通道上,按传输协议在这些通道上进行多路复用。 (我将假设,对于每个 RFC 3081,正在 TCP/IP 上使用 BEEP。这并不需要是以下情况:在另一个面向连接的传输协议上映射 BEEP。)
第一个通道(通道 0)扮演一个特殊角色并用于会话管理。 通道上的所有通信都是由一个概要文件定义的,该概要文件基本上是可允许的交互的描述。 对于基于 XML 的协议,概要文件可以基于指定消息语法的 DTD 或模式。(很显然,要完整定义概要文件,不仅仅只需要语法规范。)
当对等点使用 BEEP 连接时,为了构成其通信,它们请求彼此的概要文件。 概要文件分为两个类别:调整和数据交换。调整概要文件会影响整个会话,通常负责安全性和认证。 象上面指出的那样,数据交换概要文件定义特殊通道上的交换。
具体剖析
既然您对 BEEP 所做的事情有了高层次的了解, 那么就让我们看一下另一种极端情况并研究协议的基本原理是如何实现的。 不必完全理解接下来的内容,但这有助于理解什么样的交换方式可供概要文件使用。
BEEP 将 消息的概念具体化为有用的通信单元。 这种作为应用程序协议交互的一部分发送的消息可以是“我的 CPU 温度为 80 度”, 或者“这里是一个 JPEG 图像”。MIME 用于消息信封,所以消息可以是任何内容类型。 对于消息应该多大或多小,也没有真正的限制;它是由特定的应用程序决定的。
由于这个原因,BEEP 实现比消息小的通信单元,这个通信单元就是组成消息的帧。 虽然常常可以合理地将消息以一个帧发送,但也可能需要将它分解成许多帧。 帧包含标识它所属的通道和消息的头及其在消息中的顺序。 我们现在将不讨论有关帧这一主题,但请记住:消息可以由多个帧组成。
为了支持上面概述的基本功能,BEEP 提供了三种交互样式:
MSG/RPY
:客户机发送 MSG
消息,通常要求服务器执行任务。服务器执行任务并发送 RPY
消息,表明成功并可能传达更多信息。 MSG/ERR
:这类似于 MSG/RPY
,除了服务器会遇到错误。它返回 ERR
消息,表明失败并可能描述问题,而不是执行客户机要求的任务。 MSG/ANS
:客户机发送请求 MSG
, 服务器用任意数量(包括 0)的 ANS
消息回答它。 当服务器完成其任务之后,它发送 NUL
消息,表明其回答现在已完成。 回页首
示例交互
为了演示这些交换类型,我们将考虑一个高度简化的地址簿存储示例。 我们所需的简单操作是存储、检索和查询。将托管地址簿的对等点表示为 S:
,将客户机表示为 C:
。为了这个示例, 我自己创造了一个用于定义协议的实际 XML, 虽然灵感来自 Evolution 电子邮件客户机的调查研究。假设实际连接、调整和通道建立都已经实施。
示例 1:存储一个项
C: MSG <person> <name>Edd</name> <tel>1-234-567-7890</tel> </person> S: RPY <person uid="edd_0"/> |
客户机将一个完整的地址簿项发送给服务器。服务器发觉没有指定标识,就假设它是一个新项, 存储它并将新项的标识返回给客户机。
示例 2:检索一个项
C: MSG <person uid="joe_2"/> S: RPY <person uid="joe_2"> <name>Joe</name> <tel>1-634-222-2333</tel> </person> |
客户机希望检索标识为“joe_2”的项的全部细节。它将其所知道的信息尽可能多地发送给服务器, 服务器填充详细信息并将它们发回客户机。
示例 3:查询
C: MSG <query> <match><tel>1-*</tel></match> <return><name /></return> </query> S: ANS <person uid="edd_0"> <name>Edd</name> </person> S: ANS <person uid="joe_2"> <name>Joe</name> </person> C: NUL |
客户机希望找出在美国/加拿大有电话号码的人的姓名。 它发送查询,服务器开始搜索其数据库 — 这个操作可能需要一些时间。为了给予及时响应,每当它找到一项,就会把这个项发回客户机。 查询完成时,它发送 NUL
。
示例 4:错误发生
C: MSG <person uid="joe_2"/> S: ERR <error code="403"> You are not allowed to access this information. </error> |
客户机希望检索一个项,但已经对服务器编程不允许该客户机访问这个项,并返回一个错误条件。
双向性
BEEP 的独特功能之一是它的双向性。 这可以在上面示例的上下文中阐明。例如,设想正在通信的两台机器严格来说不是客户机和服务器, 而是两台服务器。我们的地址簿协议可用来使它们的数据保持同步。 在上面的示例中,想象一下 M1 始终扮演了 S
角色,M2 扮演了 C
角色。M2 已经连接到 M1 并请求地址簿协议概要文件。如果两台机器都支持地址簿协议概要文件, 那么 M1 可以使用地址簿概要文件请求至 M2 的通道,并且角色将会颠倒。 两个连接可以同时打开:每台机器可以向另一台机器查询地址簿项列表,并检索它没有的项。
回页首
编写一些代码
幸运的是,为了使用 BEEP,我们不需要从头开始实现协议栈。 在 beepcore.org(请参阅 参考资料)上, 有使用 Java、C 和 Tcl 的实现,还有正在为 Python 和 Ruby 开发的实现。在我们的示例中将使用 Java。
首先,让我们看一下 BEEP 的概念是如何映射到 Java BEEP 核心 API 的。
Session
:每个对等连接都需要这个类的一个实例。 会话对象用于启动任何调整概要文件(如加密),还用于启动通道。 想要支持交互的服务器端的会话有一个相关的 ProfileRegistry
, 它将受支持的概要文件 URI 映射到其相应的概要文件“启动通道”侦听器(请参阅下面的 Profile
描述)。 Channel
:通道对象作为调用会话的 startChannel()
方法的结果返回。它可以通过 sendMSG()
方法发送 MSG
消息。 Profile
:概要文件接口只定义一个方法, 该方法为特殊概要文件返回“启动通道”事件的侦听器。 概要文件的所有实现都实现这个接口。“启动通道”侦听器接收 Channel
对象并在该通道上注册消息(或帧)的侦听器。 Message
:这个类封装 MSG
、 RPY
、 ANS
、 ERR
和 NULL
消息类型。可以询问它的内容,它有用于响应消息的 sendRPY()
、 sendANS()
、 sendERR()
和 sendNUL()
方法。 Reply
: Reply
对象用于处理来自对等点的异步回答。 它最有用的方法是 getNextReply()
,这个方法返回一个 Message。 根据您在交互的服务器端还是客户机端,类的使用方法会不同。 服务器在通道上注册 MSG
消息的侦听器,而客户机在发送 MSG
时将指定 ReplyListener
( Reply
类是其中之一)。 如果应用程序是对称的(即,两个对等点都可以启动并侦听消息), 那么两个对等点将同时实现消息和回答侦听器。
为了显示代码,我们将开始根据上面的示例建立日历协议的原型。
客户机端
我们将在 Java 应用程序的 main()
方法中实现客户机端的主要部分。 首先,我们将建立与正在端口 6000 上运行的服务器 calhost
的会话:
import org.beepcore.beep.core.*; import org.beepcore.lib.Reply; import org.beepcore.transport.tcp.*; ... Session session; try { session = AutomatedTCPSessionCreator.initiate("calhost", 6000, new ProfileRegistry()); } catch (BEEPException e) { // error handling } |
我们使用其中一个 Java API 的助手类 AutomatedTCPSessionCreator
来创建会话。 请注意,我们传递一个空的 ProfileRegistry:
― 这个对等点仅打算扮演客户机角色, 所以它确保不过多宣称对任何概要文件的支持。现在,我们尝试启动通道:
Channel channel; try { channel = session.startChannel( "http://example.org/profiles/ADDRESSBOOK"); } catch (BEEPError e) { // catch and deal with errors based on // e.getCode() } catch (BEEPException e) { // catch lower level errors } |
如果一切工作正常,那么我们现在有了一个使用地址簿概要文件支持通信的 channel
对象。 现在,我们将发送一个服务器要存储的地址簿项。
String message = "<person><name>Edd</name>" + "<tel>1-234-567-7890</tel></person>; // for the sake of example, we've decided we'll // send all messages encoded in UTF-16 // using org.beepcore.beep.core.ByteDataStream // we could use StringDataStream if we wanted UTF-8 ByteDataStream msgst=new ByteDataStream( request.getByes("utf-16")); msgst.setContentType("text/xml"); msgst.setTransferEncoding("utf-16"); Reply reply = new Reply(); channel.sendMSG(msgst, reply); |
到目前为止,我们已经创建了消息并对它进行编码,正确地设置了消息有效负载的 MIME 类型,并将消息发送到服务器。现在,我们可以期待来自服务器的回答,向我们提供已发送的新项的标识。
Message repmsg = reply.getNextReply(); DataStream ds = repmsg.getDataStream(); InputStream is = ds.getInputStream(); // the reply is a stream, not a string byte inputbuf[]=new byte[256]; int inputlen; String reptxt; while (ds.isComplete() == false || is.available() >0) { inputlen=is.read(inputbuf); if (inputlen>0) { reptxt=reptxt.concat(new String(inputbuf, 0, inputlen, "utf-16"); } } System.out.println("got back:" +reptxt); Thread.currentThread.sleep(6000); } while (true); |
我们假设没有任何错误的事情发生,如果发生了,不必担心。 无论我们取回了什么,都要将它转储给控制台。 对消息进行译码的过程可能看上去有点复杂, 但请记住:BEEP 是一种通用框架,并且不对消息的性质作任何假设。 例如,如果您正在使用 HTTP,您必须做相同的编码工作。 实际上,我已经发现自己编写了实用程序类来为特殊概要文件处理消息的组合和译码。
服务器端
现在, 我们浏览实现交互的服务器端的要点。尤其将集中讨论通道和消息侦听器。 在本文的末尾,我将提供 beepcore-java 项目的参考资料,您可以从中找到设置服务器端的完整示例。
public class AddressbookProfile implements Profile, StartChannelListener, MessageListener { // we're rolled all the interfaces into one class // for convenience public StartChannelListener init(String uri, ProfileConfiguration config) throws BEEPException { return this; } public void startChannel(Channel channel, String encoding, String data) throws StartChannelException { // just register ourself as interested in incoming // messages channel.setDataListener(this); } public void closeChannel(Channel channel) throws CloseChannelException { // say we're not interested any more channel.setDataListener(null); } |
到目前为止,我们已经设置了内务处理。为了简单一点,每个会话只创建一个消息侦听器实例, 因此,不管打开多少个通道,都使用同一个对象。
public void receiveMSG(Message message) throws BEEPError, AbortChannelException { DataStream ds=message.getDataStream(); AddressbookEntry newentry; // we then do the same processing as we did // in the client example to get some data out // of the stream. imagine that we then store // the new addressbook entry in some database // somewhere and fill the "newentry" with it // it's now time to reply with an "ok" String replytxt="<person uid=\"" + String.valueOf(newentry.getUID()) + \""/>"; DataStream reply=new ByteDataStream( replytxt.getBytes("utf-16")); reply.setContentType("text/xml"); try { message.sendRPY(reply); } catch (BEEPException e) { // handle the error } } |
此外,如果接收到不正确的消息或发生了某些其它错误,我将省略错误处理,包括调用 sendERR()
的代码。正如您看到的那样,考虑 MIME 类型、字符编码和传输编码很重要。 在许多设置中,您将无权控制另一个对等点正在运行的软件,所以使用因特网标准对确保互操作性是重要的 ― 即使您所做的唯一一件额外事情是 拒绝那些没有用 ERR
消息显式支持的配置。
进一步挖掘
最好的开始端是通过下载 Java BEEP 核心实现。 其中包含的示例将提供您一个开始;它实现 BEEP 上的简单“ping”协议,并演示如何在连接上设置加密。
从实际观点出发,我发现 Java BEEP 核心 API 正是那个核心。 一旦您构建了一些助手类来处理编码和译码概要文件有效负载的共同工作, 特定于 BEEP 的代码将变成一个“给定的”,并且您可以全神贯注于应用程序的真实逻辑 ― 它首先是使用 BEEP 的全部要点。
回页首
结束语
BEEP 为因特网协议世界(包括不断成长的 Web 服务区域)提供了一种 HTTP 持续超载的似乎可能的替代方法。 其灵活性和对 MIME 根深蒂固的支持意味着它应该很好地适合面向连接的协议需求。 正如 BEEP 用户社区在稳定地逐步增加一样,可自由使用的 BEEP 的实现数目正在稳定地逐步增加。 创建新应用程序协议的开发人员应该认真地考虑将 BEEP 用作其工作基础。
参考资料