正如我们在本系列文章的第 1 部分中讨论的那样,基于 SMS 的消息传递对于无线电话用户来说非常方便。但是,它不是适合于非电话式移动设备(如 PDA 和连接到 WAN 的手持设备)的消息传递平台。不同蜂窝网络之间的 SMS 消息传递(比如国际长途)也会很贵,甚至于在某些情况下是不可行的。在我们有关移动 P2P 消息传递的系列文章的第二部分也是最后一部分中,我们将向您介绍两个通用的对等网络 - JXTA P2P 和 Jabber 即时消息传递网络,您在不适于使用 SMS 的情况下可能使用它们。本文中的资料还会出现在我即将发表的 Prentice Hall 出版的“Java Mobile Enterprise Application Development”一书中(请参阅下面的参考资料一节以获取相关链接)。
JXTA 框架
JXTA 定义了一个用于对等网络的开放协议组。这些基于 XML 的协议描述了复杂的操作,比如对等点发现、端点路由、连接绑定、基本查询/应答消息交换和通过集中对等点进行的网络传播。对这些协议进行完整的描述超出了本文的范畴;感兴趣的读者应当参考 JXTA 官方网站以获取更多的信息或者阅读一些有关本主题的其它 developerWorks 文章(请参阅下面的参考资料一节以获取相关链接)。总的来说,JXTA 网络是由下面几个组件组成的:
对等点是任何 JXTA 网络的基本元素。对等点实现核心 JXTA 协议,并且独立于所有其它的对等点并且异步地与之进行操作。对等点提供应用程序和/或网络服务,并且能公布自己。
对等点组是对等点的集合。JXTA 定义了对等点用来创建、连接和监控各组的协议。对等点组提供服务,比如对等点发现、成员资格、访问控制、管道、解析器和监控。
管道表示对等点之间的虚拟连接。点对点管道连接两个对等点;传播管道连接广播对等点与多个侦听器。管道连接的对等点不一定要在物理上直接链接;可以通过多个中间管道来连接它们。
消息是通过管道在对等点和端点之间交换的数据。JXTA 为所有消息定义了信封格式。每个对等点都可以定义自己的消息内容格式,只要该格式符合 XML 规范即可。但是,为了让两个对等点交换有意义的信息,所以它们必须理解彼此的消息格式。
公告是用来描述 JXTA 网络资源的元数据结构。所有的对等点和服务都理解公告。
JXTA 是通用的 P2P 框架,不只是用于处理简单的消息传递。它解决了象 P2P 文件共享、P2P 应用程序服务和协作的分布式计算之类的问题。JXTA 协议的设计旨在与任何实现技术无关,而 JXTA 的参考实现是构建于 Java 平台之上的。该参考实现使用 Java API 来封装 JXTA 协议消息,并提供了从 Java 应用程序访问 JXTA 网络的可编程方法。
用于移动设备的 JXTA
JXTA 的强大功能和灵活性是有代价的:复杂性。JXTA 对等点需要处理许多任务并在基于套接字的 XML 级上处理消息。这样的对等点可能太复杂,而无法在大多数移动设备上运行。另外,XML 或原始套接字支持都不是标准的 J2ME/MIDP 规范的组成部分。要使移动 P2P 用户可以使用 JXTA 网络,我们需要一组用于移动设备的轻量级 JXTA API。JXME 项目的目的是为 CLDC 和 MIDP 平台提供 JXTA API。它也能用于更高端的 J2ME 框架,比如 Personal Profile。
JXME 使用中继来连接轻量级的移动对等点与 JXTA 网络的其余部分。中继本身是集中的 JXTA 对等点,它们完全有能力处理管道、公告和对等点组服务。移动对等点使用符合 JXTA 二进制消息格式的消息,通过基于 HTTP 的二进制连接来与中继进行通信。中继为移动对等点提供了许多对等点服务:
为了节约带宽,中继滤除不必要的公告,并去除那些已发送的公告。
中继在移动对等点之间来回路由(中继)消息。
为了使移动对等点和普通对等点之间能相互操作,中继将中继消息从 JXTA XML 格式转换成 JXTA Binary Message 格式,或逆向转换。
中继担当代表其移动对等点的代理。该中继与其它对等点及管道相互操作,并使用组服务。
作为开发人员,我们不必担心移动-到-中继二进制协议的确切格式和中继对等点的确切实现。我们只要使用 JXME 所提供的 API 类就行了。
图 1. JXME 体系结构
JXME API
您可以研究最新的来自该项目公共 CVS 服务器的 JXME 源代码 - 中继和 J2ME 对等点库(请参阅参考资料)。您还可以从 JXME 网站下载稳定的二进制代码和源代码。
一旦下载了 JXME 包,您就可以开始使用它所附带的演示程序。让我们使用下载包中包含的 Ant 构建脚本来简要地看一下运行演示程序的步骤。更详细的循序渐进教程,请参考 JXME 网站。
首先,您需要在计算机上运行中继对等点(必须可以从移动设备和目标 JXTA 社区访问该计算机)。proxy 目录中的 build.xml 文件包含了将运行 JXME 中继的 runproxy 任务。请确保您选择了适当的复选框以将其作为集合点、中继和 JxtaProxy 来运行。现在您可以运行 JXME 对等点了。下载包包括了两个您可以使用的样本 JXME 应用程序 - chat 和 tictactoe。
现在,让我们研究一下 JXME 包本身。JXME API 只有三个类,都在 net.jxta.j2me 包中:
Element 表示 JXTA 消息内的元素。元素包含了名称、名称空间、MIME 类型和二进制数据数组。
Message 表示由几个元素组成的 JXTA 消息。Message 提供了用于访问那些元素的方法。
PeerNetwork 是最有用的类。它指定了移动对等点可以通过中继执行的 JXTA 任务。在 PeerNetwork 类中有几个很有用的方法:
createInstance() 是个工厂(factory)方法,它返回一个带有指定对等点名称的 PeerNetwork 实例。
connect() 方法连接至指定 HTTP URL 上的中继。它返回持久状态信息的字节数组;这个信息应该通过 connect() 方法在所有到该中继的后续连接中进行传递。
create() 方法通过中继代理在 JXTA 网络上创建对等点、组和管道。
search() 方法搜索对等点、组和管道。
poll() 方法在中继上轮询发送给这个移动对等点的消息。在服务器线程中可以迭代地调用它。
listen() 和 close() 方法分别打开和关闭输入管道。
send() 方法将消息发送给指定的管道。
图 2. net.jxta.j2me 包中的类
示例
在这一节中,我将使用教程示例 mySend.java(它包括在可从 JXME 网站获得的 JXME 源代码包中)来演示我们上面所概述的 API 的用法。JXME 包由许多人对 Project JXTA 项目所做的自愿贡献工作合作完成。源代码由 Sun Project JXTA Software License 授权使用,它基于 BSD 许可证。
在 J2ME 仿真器上运行的示例移动对等点通过在仿真器的主机 PC(localhost)的 9700 端口运行的 JXME 中继来执行许多操作。当然,在实际部署中,移动对等点可以在任何移动设备上运行,而且您可以只需将 localhost 更改成您的中继计算机的 IP 地址。JXME 中继的代码包含于下载包中。
下面的示例对等点需要一些步骤:
1、建立新的管道。
3、搜索并查找它刚建立的管道。
5、通过那条管道将消息发送给它自己。
7、接收那条消息。
让我们研究一下这些步骤的代码。在清单 1 中,方法 peer.listen() 指示中继创建新的管道。
清单 1. 创建新管道
// Create a peer and have it connect to the relay.
// A connection to relay is required before any other
// operations can be invoked.
String relayUrl = "http://localhost:9700/";
PeerNetwork peer = PeerNetwork.createInstance("mySendPeer");
byte [] persistentState = peer.connect(relayUrl, null);
// Have the peer create and open a Unicast pipe; PipeID will
// be returned asynchronously in a response message
int listenQueryId = peer.listen("myPipe", null, PeerNetwork.UNICAST_PIPE);
// Have peer search for this Pipe and then send it a Message
String pipeID = findMyPipe();
sendMyMessage(pipeID);
// Finally, have the peer poll for Messages sent to it
recvMessage();
}
成功地创建了管道之后,中继生成了到移动对等点的二进制消息。方法 findMyPipe() 在中继上反复轮询直到它接收到该消息,表明成功了为止。然后它解析该消息并获取管道标识。该二进制消息具有类 XML 的结构。清单 2 演示该过程。
清单 2. 获取我们刚创建的管道
public String findMyPipe() throws IOException {
// Now poll for all messages addressed to us. Stop when we
// find the response to our listen command
int rid = -1;
String id = null;
String type = null;
String name = null;
String arg = null;
String response = null;
Message msg = null;
while (true) {
// do not use a timeout of 0, 0 means block forever
msg = peer.poll(1);
if (msg == null) {
continue;
}
// look for a response to our search query
for (int i = 0; i < msg.getElementCount(); i++ ) {
Element e = msg.getElement(i);
if (Message.PROXY_NAME_SPACE.equals(e.getNameSpace())) {
String elementName = e.getName();
if (Message.REQUESTID_TAG.equals(elementName)) {
String rids = new String(e.getData());
try {
rid = Integer.parseInt(rids);
} catch (NumberFormatException nfx) {
System.err.println("Recvd invalid " +
Message.REQUESTID_TAG +
": " + rids);
continue;
}
} else if (Message.TYPE_TAG.equals(elementName)) {
type = new String(e.getData());
} else if (Message.NAME_TAG.equals(elementName)) {
name = new String(e.getData());
} else if (Message.ARG_TAG.equals(elementName)) {
arg = new String(e.getData());
} else if (Message.ID_TAG.equals(elementName)) {
id = new String(e.getData());
} else if (Message.RESPONSE_TAG.equals(elementName)) {
response = new String(e.getData());
}
}
}
// PIPE_NAME: myPipe
// PIPE_TYPE: PeerNetwork.UNICAST_PIPE
if (rid == listenQueryId &&
response.equals("success") &&
type.equals("PIPE") &&
name.equals(PIPE_NAME) &&
arg.equals(PIPE_TYPE)) {
return id;
}
}
}
在清单 3 中,方法 sendMyMessage() 获取从方法 findMyPipe() 返回的管道标识并通过该管道发送消息。
清单 3. 通过该管道发送消息
public void sendMyMessage(String pipeID)
throws IOException {
Element[] elm = new Element[2];
// Our Message will contain two elements. Receiver should look for
// elements with the same names ("mySend:Name" and "mySend:Message")
// PEER_NAME: mySendPeer
elm[0] = new Element("mySend:Name", PEER_NAME.getBytes(),
null, null);
elm[1] = new Element("mySend:Message", "Hello there".getBytes(),
null, null);
Message msg = new Message(elm);
// PIPE_NAME: myPipe
// PIPE_TYPE: PeerNetwork.UNICAST_PIPE
sendRequestId = peer.send(PIPE_NAME, pipeID, PIPE_TYPE, msg);
System.out.println("send request id: " + sendRequestId);
}
正如您所记得的那样,mySendPeer 是自己侦听管道的。因此,我们上面所发送的消息对于来自该中继的同一移动对等点来说将是可用的。方法 recvMessage() 在中继上轮询,直到它接收到该消息为止,一旦它接收到了该消息就显示其内容,如清单 4 所示。
清单 4. 接收消息
public void recvMessage() throws IOException {
Message msg = null;
while (true) {
msg = peer.poll(1);
if (msg == null) {
continue;
}
for (int i = 0; i < msg.getElementCount(); i++) {
Element e = msg.getElement(i);
if ("mySend".equals(e.getNameSpace()) &&
"Name".equals(e.getName()))
System.out.println("Message from: " +
new String(e.getData()));
if ("mySend".equals(e.getNameSpace()) &&
"Message".equals(e.getName()))
System.out.println("Message: " +
new String(e.getData()));
}
}
}
Jabber 即时信使
归根结底,任何 P2P 系统的成功都取决于其吸引用户的能力。虽然 JXTA 是功能很强大且技术很先进的框架,但是它的技术复杂性却阻碍了其被用户所采用。Jabber 是比 JXTA 简单得多的 P2P 系统;它的设计主要用于即时消息传递。Jabber 比 JXTA 具有大得多的对等点网络。
Jabber 最初的设计旨在提供流行的因特网即时消息传递系统(AOL、MSN、Yahoo! 和 ICQ 等等)之间的互操作性。它是一个强大灵活但简单的协议,它可以包含所有现有的 IM 协议。因为其强大的特性和完全开放的 XML 协议,Jabber 是目前为止最高级的可用的 IM 系统。Jabber 还能支持许多高级的 P2P 应用程序,比如日历群件和文件共享。
Jabber 对等点通过 Jabber 服务器彼此通信。Jabber 服务器还可以彼此“交谈”,以便将不直接连接至同一服务器的对等点组成大型对等点域。Jabber 对等点和服务器之间的所有通信都采用这种形式:在原始套接字连接上发送开放的 XML 格式消息(这些消息的规范已经提交给了 IETF)。结果,Jabber 服务器和对等点的实现可以与任何平台无关。Jabber 开发库可用于 Java 平台、C++、C#、Perl、Python、PHP,甚至是 Flash。
J2ME 设备(尤其是 MIDP 设备)的 Jabber 客户机开发并不是无意义的。任何 MIDP Jabber 客户机必须捆绑它自己的轻量级 XML 解析器。另外,MIDP VM 和底层网络必须支持原始套接字连接。如今有几个可用的符合 MIDP 1.0 的 Jabber 客户机或库:
Al Sutton 的 KVMJab 是用于 MIDP 平台的开放源码 Jabber 库。它为开发 MIDP Jabber 应用程序提供了一个简单的 API。虽然源代码是可用的,但是您在可以将 KVMJab 合并到您的商业软件中之前必须为 KVMJab 的开发作出贡献。
uppli 的 uMessenger 是用于 MIDP 的功能完备的 Jabber IM 客户机。它还支持文件共享。
结束语
在本文中,我们讨论了将 JXTA 和 Jabber P2P 网络扩展至移动对等点所需的基本技术和工具。您现在应当能通过任何支持 TCP/IP 的无线网络将您的移动设备连接至大型的现有 P2P 社区。
阅读本系列文章之后,您应当会理解:为何 P2P 对移动应用程序如此重要,什么样的 P2P 网络是可用于移动对等点的,以及如何使用 J2ME 工具来开发移动 P2P 应用程序。现在您可以将 P2P 特性添加到您的 J2ME 游戏或企业应用程序中,准备迎接 2003 年繁荣的移动商业吧!
