XML观察:剖析BEEP
--块可扩展交换协议标准介绍的第二部分
Edd Dumbill(
edd@xml.com)
编辑兼发行人,xmlhack.com
2002 年 3 月
在研究 BEEP — 块可扩展交换协议(Blocks Extensible Exchange
Protocol)的第二篇文章中,Edd 根据他上一篇文章中概述的 BEEP 的主要原理, 解释该协议是如何实现的,并提供如何在 Java
中使用它的示例。
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
Edd
1-234-567-7890
S: RPY
客户机将一个完整的地址簿项发送给服务器。服务器发觉没有指定标识,就假设它是一个新项,
存储它并将新项的标识返回给客户机。
示例 2:检索一个项
C: MSG
S: RPY
Joe
1-634-222-2333
客户机希望检索标识为“joe_2”的项的全部细节。它将其所知道的信息尽可能多地发送给服务器,
服务器填充详细信息并将它们发回客户机。
示例 3:查询
C: MSG
1-*
S: ANS
Edd
S: ANS
Joe
C: NUL
客户机希望找出在美国/加拿大有电话号码的人的姓名。 它发送查询,服务器开始搜索其数据库 —
这个操作可能需要一些时间。为了给予及时响应,每当它找到一项,就会把这个项发回客户机。 查询完成时,它发送 NUL。
示例 4:错误发生
C: MSG
S: ERR
You are
not allowed to access this information.
客户机希望检索一个项,但已经对服务器编程不允许该客户机访问这个项,并返回一个错误条件。
双向性
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 = "Edd"
+
"1-234-567-7890;
// 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="
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 用作其工作基础。
参考资料
beepcore.org 是 Web 上 BEEP 规范和工具的主页。
RFC 3081 中指定了 BEEP 至 TCP
的映射。
作者有关 BEEP
的上一篇文章提供了协议框架的高级概述。
beepcore-java 是
RFC 3080 和 3081 的 Java 实现。
还可以获得用 Tcl 和 C 编写的 BEEP 实现:beepcore-c 和 beepcore-tcl。
Python 和 Ruby 的实现正处于开发阶段。
ClipCode 在 .NET 框架中使用
BEEP。
最后,请看一下 IBM
WebSphere Studio Application Developer, 它是构建、测试和部署 J2EE 应用程序(包括从 DTD 和模式生成
XML 文档)的一种易于使用的集成开发环境。
如果您希望与本文章的作者或其所在机构,进一步交流,请联系:畅享网 姜小姐
jill.jiang@amteam.org | 021-51096826-112 |
在线联系