新的 JXTA 版本在实用性方面做了改变,更适合于现实世界网络拓扑
简介: JXTA 2 是开放源代码 P2P 网络的第二个主要版本,它利用流行的、基于 Java 的参考实现作为构建基础。在设计方面进行了重要的修改,以获得更高的性能、海量伸缩性和可维护的 P2P 网络。本文建立在 Sing Li 于两年前发表的 JXTA 系列文章 Makng P2P interoperable的基础上,为您介绍了这个平台上最新的重要改变。
自 JXTA 1.0 推出以后,开放源代码点对点(P2P)和分布式计算团体对它给予了热情欢迎( developerWorks 上有关 JXTA 的早期讨论,请参阅 参考资料)。在日益增长的独立开放源代码项目下汇集了众多的 JXTA 团体,这些项目的目的就是为了验证由 JXTA 平台开发小组所做出的声明:即 JXTA 可以并应该成为未来互操作的 P2P 网络应用程序的首选构建标准。
从积累的经验中学习
在最近的两年中,JXTA 平台设计小组一直在帮助 JXTA 开发团体更好地理解基本设计概念,以及在使用 JXTA 平台 API 提供的功能时的最佳实践。应用程序开发团体的反馈和经验为小组提供了宝贵的意见和建议。这些反馈来源于现实生活中的部署工作,包括对平台最初设计的局限性、API 库的笨拙或者矛盾之处的观察,以及使 JXTA 更适合于构建现实世界 P2P 应用程序的特定要求。如果不先推出一个可工作的参考实现,那么就不可能收集这些宝贵的积累经验。因此第一个版本的 JXTA 很好地达到了它的目的。
根据这些反馈意见,JXTA 平台小组将主要精力放到了 JXTA 2 的设计和实现中的一组主要目标上。表 1 显示了最主要的设计目标,以及 JXTA 2 实现如何满足这些设计目标的简要说明(对于 JXTA 1 和 JXTA 2 之间的区别给予了强调)。
表 1. JXTA2 的设计目标和实现
目标 | JXTA 2 实现 |
在当今大多数典型的网络配置上的海量伸缩性 |
|
提高性能 |
|
改进的开发人员友好性 | 对 API 做了大量重新设计和改进,使得 API 更容易理解并具有更多的一致性。主要的 API 改变包括:
|
改进可靠性 | 对节点的自适应行为进行了更改,从而改进了整个网络的可靠性(因为聚集和中继对网络可靠性来说是最关键的服务,所以大多数更改是围绕它们进行的):
|
提高可管理性 | 支持对节点的详细远程监视。在 JXTA 2 中内建了广泛的测试和计量能力。这是基准测试的关键,也是使网络调整和性能 / 伸缩性改进成为可能的关键。还提供了一个 GUI 监视实用程序。它标志着使 JXTA 节点更具可管理性和更容易管理这一长期计划的开始。 |
兼容移动 | 为 Java 2 mobile edition (JXME)的 JXTA 实现提供了代理能力。 |
一个 网络地址转换设备(NAT)使多台计算机和终端可以通过一个网络地址连接到 Internet (通过转换输出包上的网络地址并对输入包上的地址重新进行转换)。家庭用户或者远程工作的业务用户广泛使用这些设备,它们常被称为 高速 Internet 路由器或者 Internet 共享设备。
在本文中,我们将详细考察在 JXTA 2 中使海量伸缩性成为可能的关键改变。我们还将完成一些使用新的平台配置 API 的实际编码,为 JXTA shell 创建一个寂静启动装置(可以在自己的 JXTA 应用程序中重用的装置)。最后,我们将探讨几个新的 shell 命令,并看一下在 JXTA 2 网络中是如何索引和定位资源的。
回页首
迈向海量伸缩性
现实世界的网络拓扑随着时间而不断发展。它的演变常常受到外部基础设施和经济因素的影响,这些通常是软件系统设计者无法直接控制的。JXTA 1 采取了单纯的和相当理论化的方式使用底层物理网络拓扑。相反,JXTA 2 采取了更多编程式的方式设计覆盖网络,它在今天最常见的网络拓扑上可以有更高的性能和伸缩性。
JXTA 2 推出了 聚集超级节点网络的概念:动态而自适应地将 P2P 网络成员划分为边缘节点(edge peer)和聚集节点(rendezvous peer)。传播只发生在更稳定的(且成员数更少的)聚集节点中。这种限制极大地增强了伸缩性,并降低了网络范围内消息风暴或者泛洪的可能性。
让我们更深入地看一下这种新重叠网络(overlay network)的实现。
边缘节点与聚集超级节点的对比
在 JXTA 2 中,边缘节点本质上是易变的,有可能频繁地加入和退出。JXTA 2 网络中大多数节点都会是边缘节点。一个边缘节点不转发查询请求,它可能维护、也可能不维护自己的本地公告缓存。大多数边缘节点将维护一个本地缓存,并需要使用 JXMEJxtaProxy的服务(有关 JXME 的更多内容请参阅 参考资料)。
虽然一些边缘节点与聚集网络直接连接,但是许多边缘节点是通过作为中继的中间节点或者 JxtaProxy连接到聚集网络的。 要参与 P2P 网络,一个边缘节点只需知道如何到达单个聚集节点。通常,一个边缘节点维护一个已知聚集节点的列表(称为 种子聚集节点),并参与新聚集节点的动态发现,这使得该节点可以故障切换到备用聚集节点,并增强整个网络的可靠性。
聚集节点一般变化较少并且更稳定(即当它们连接到网络上后,它们就停留在那里并保持连接)。可以想到,任何时候在 JXTA 网络中边缘节点的数量要比聚集节点多得多。聚集节点在本质上是直接连接节点,这意味着它们不应该需要中继或者 JxtaProxy,以连接到网络中另一个聚集节点。每一个聚集节点都是聚集超级节点网络中的成员(在特定的 JXTA 组环境中)。聚集节点把无法根据自己的索引缓存进行解析的查询转发给其他聚集节点,它们参与维护网络中所有可访问资源的一个松散一致的 分布式散列表(DHT)。每一个聚集节点维护一个网络中所有已知聚集节点的动态视图,并可以在网络拓扑改变时连接或者故障切换到另一个聚集节点。
每个 JXTA 节点都可以假定为边缘节点或者聚集节点。事实上,一个边缘节点如果不能在一个延长的时间(extended period of time)内连接到任何聚集节点,那么它可以自适应地变为一个聚集节点。网络中所有聚集节点也可以发出查询,意味着边缘节点功能实际上是聚集节点功能的一个完全子集。一般来说,在 JXTA 2 网络中应用的是自适应式和编程式的对称方法,它受底层物理网络拓扑以及定制的用户配置影响。
共享资源分布式索引
JXTA 2 网络的操作依赖于其解析分布式查询的能力(有关这一概念的更多内容,请参阅侧栏“ P2P 网络的实质”)。在 JXTA 2 中,聚集超级节点网络形成了松散一致的 DHT,从而解析分布式查询。
JXTA 2 使用了一种称为 共享资源分布式索引(shared resource distributed index SRDI) 的分布式算法,以创建并维护网络中资源的一个总体索引。在 JXTA 中,资源是用公告形式的元数据(实质上就是 XML 文档)来描述的。通过一组特定的属性,用 SRDI 在网络范围索引这些公告。维护的分布式索引类似于一个散列表,其索引的属性作为散列键,而散列值映射回包含实际公告的源节点。因而可以在聚集网络上任何地方根据这些属性进行查询。这样,通过定位具有所需公告的节点,SRDI 就可以答复在网络中的公告查询。例如,一个节点可能向具有数千个节点的网络发送叫做 “ LotteryService的管道”的查询。SRDI 可以快速地对这种查询进行解析,并使保存有“ LotteryService管道”公告的节点给出回复。
在 SRDI 中,不再需要远程发布任何公告。发布的只是储存在节点上的公告的索引。这个索引信息通过单个连接的聚集节点“推出”到 DHT(聚集超级网络)。
松散一致的 DHT
JXTA 2 网络的作用是一个持续可用的、网络范围内的、动态的、分布式的数据结构:这是一个虚拟的散列表,包含了整个 JXTA 组中所有已发布公告的索引。一个边缘节点可以在任何时候通过提供一组属性(表中的散列键)来查询散列表。该查询是由网络(实际上是聚集超级节点网络)通过将该散列键混编为所需的值(即包含所请求公告的节点)来进行解析的,如图 1 所示:
在图 1 中,边缘节点 1 (EP1) 创建了一个管道,并在本地储存其公告。通过 SRDI 更新这个索引,DHT 现在就知道了这个公告。之后,边缘节点 2 (EP2) 对 EP1 的管道进行查询。聚集超级节点网络解析这个查询,并通知 EP1 对该公告的请求。结果,EP1 会将所请求的公告作为响应发送给 EP2。
要创建这个 DHT,每个节点在本地缓存公告,所有本地存储的公告都已索引。索引推送到聚集节点(在任何时侯,JXTA 2 边缘节点只连接到一个聚集节点)。聚集超级节点网络维护包含混合索引的 DHT。查询总是发送到聚集节点,如图 2 所示:
图 2 中显示的步骤如下:
至止,我们已经描述了由 JXTA 2 聚集网络维护的 DHT,它好像总是以一种完美的一致性方式维护的。这在目前是不可能的,因为在 P2P 网络中,通过相互协作来实现 DHT 的一组聚集节点可能会出现加入和退出现象(尽管不像边缘节点那样频繁)。当聚集节点离开时,它所维护的一组索引就会在一定时间内无法使用(直到可靠的节点再次发布它)。基于一种自适应性的方式,JXTA 2 可以维护一个松散一致的 DHT,它可以处理 P2P 网络中节点的易变特性。
JXTA 2 采取的方式保证了 DHT 在突然分离的网络(即由于连接节点的消失而产生的节点孤岛)以及新合并的网络(即出现了将以前分离的节点孤岛连接起来的连接节点)中也可以继续工作。JXTA 2 的方法包括了一个 聚集节点查看(rendezvous peerview,RPV) 和一个插入式聚集节点遍历器(rendezvous walker)。
RPV 和 RPV 遍历器
RPV 是由超级节点网络中每一个聚集节点维护的。RPV 是该节点的已知聚集节点列表,按各个聚集节点的惟一节点 ID 排序。在 DHT 算法中使用的散列函数在每一个节点上都是相同的,它用于确定一个(本地不能解析的)查询请求应该转发到哪一个聚集节点。
所有变得不可达的聚集节点都会从节点的 RPV 中删除。超级节点网络中每一个聚集节点都定期向其 RPV 中随机选择的聚集节点发送其已知聚集节点的随机子集。这样做是为了保证 RPV 最终覆盖整个网络,并适应底层物理网络所发生的任何分离或者合并。注意,在任何给定时间,由网络中不同聚集节点所维护的 RPV 可能彼此不同。
为了维护 DHT,基于一个固定的散列函数,SRDI 会将收到的索引信息存储到所选择的聚集节点上。为了应付松散一致的特点,这个索引信息冗余地复制到 RPV 中相邻的其他聚集节点上(回顾一下,RPV 是一个已知聚集节点的有序列表,按全局惟一节点 ID 排序)。这可以保证即使目标聚集节点出现崩溃,仍然很有可能在查询时对这一索引信息进行成功地混编。
在解析查询时,散列函数是针对聚集节点自己的 RPV 执行的。因为可能有多个现有的聚集节点断开连接,或者多个新的聚集节点加入超级节点网络,所以如果散列没有立即解析这个查询,就引入一个 RPV 遍历器,并将查询转发给有限数量的其他聚集节点。这种限定范围遍历器所使用的算法被设计为“插入式的”,也可以根据特定的网络方案进行定制。图 3 解释了索引信息的冗余储存。
在这个图中,散列函数将收到的索引信息映射到 R5。从边缘节点收到这个索引信息的聚集节点 R1 将索引信息发送给 R5,并复制给 R4 和 R6,以增加索引信息的可用性。假设 R3 收到了匹配这条存储索引的查询,那么执行的散列会将查询发送给 R3 的 RPV 中的 R5。不过,如果此时 R5 不存在,那么 RPV 将会收缩,并将原来 R5 驻留的空间关闭。这意味着以前的 R6 将成为新的 R5。
如果 R6 没有所需的索引信息,那么网络的拓扑可能有了重大的改变。在这种情况下,RPV 遍历算法就要发挥作用了。遍历器将在 RPV 列表中上下遍历以查找该信息。
我们将在下面实际配置和使用一个聚集超级节点网络。在这之前,我们需要看一看 JXTA 2 中的一些 API 和 shell 的改进。
回页首
配置器 API 的主要改进
API 的一个主要改进之处是在对自动配置的支持方面。现在终于可以使用平台 API 通过编程创建 PlatformConfig 文件(节点公告),并启动 JXTA 平台。这意味着以前令人眼花缭乱的众多用户配置参数完全可以隐匿不管。
有多个 API 用于进行自动配置,可以根据您的特定需要选用。大多数与配置相关的 API 在 net.jxta.util.config 包中。net.jxta.util.config.Configurator类是普通的包装器类,用于简化 JXTA 2 配置编程,在许多情况下它已经足够了。在内部,net.jxta.util.config.Configurator 类使用同一包中的更低级的类来完成其工作。
在即将发布的 JXTA 2.2 中(在写作本文时尚未发布),这些配置 API 将统一到一个外部的 net.jxta.ext.config包中。特定角色(如边缘节点、聚集节点、中继)的节点配置文件将进一步方便了通过编程来进行配置。
配置的核心目标是创建一个描述要启动的节点的节点公告。这个节点公告在默认情况下保存在与 PlatformConfig 文件相同的 .jxta 目录中。
回页首
为寂静启动对 JXTA 配置进行编程
为了进行我们的试验,我们需要启动总共六个 JXTA 节点,其中五个是聚集节点,一个是边缘节点。为了方便您复制这个网络,我们将在一台计算机上运行所有这六个节点。在以前,这需要花费很大的力气来描述所有需要在 JXTA GUI 中手工输入的配置参数 ―― 这种描述轻易就可以达到本文的整个长度。
为了绕过这种复杂性,我们将创建一个名为 com.ibm.devworks.jxta2.shell.ShellStarter的 starter 类。这个类将:
步骤 1 和 2 只有在没有找到现有节点公告的情况下才执行。
清单 1 显示 com.ibm.devworks.jxta2.shell.ShellStarter类的部分代码。参阅 参考资料一节,可以下载完整的代码。
public class ShellStarter { private static final String TLS_PRINCIPAL_PROP = "net.jxta.tls.principal"; private static final String TLS_PASSWORD_PROP = "net.jxta.tls.password"; private static final String ADDR_SEP = ":"; private static final String PORT_PRE = "97"; ... public ShellStarter() { } public static void main(String[] args) throws Exception { ... String tpFname = PlatformConfigurator.getPlatformConfigFileName(); File tpFile = new File(PlatformConfigurator.getPlatformConfigFileName()); // only perform config if not already configured if (!tpFile.exists()) { tcpAddress = args[1]; rdvNode = args[3]; int rdvNodeNum = Integer.parseInt(rdvNode); myPort = Integer.parseInt(PORT_PRE + args[2] + PORT_POST); Vector rdvList = new Vector(); if (rdvNodeNum < 10) rdvList.add(TCP_PRE + tcpAddress + ADDR_SEP + PORT_PRE + rdvNode + PORT_POST); pa = PlatformConfigurator.createTcpEdge( args[0], // peername "A dwPeer - " + args[0], // description tcpAddress, // ip myPort, // ports rdvList , // rdvs USER_NAME, USER_PASS ); // disable multicast // pass in to preserve settings TcpConfigurator tc = new TcpConfigurator(pa); tc.setMulticastState(false); // enable incoming connection tc.setServer(tcpAddress); tc.setServerEnabled(true); tc.save(pa); // save to pa only, not file // configure the rendezvous if (isRdv) { // pass in to preserve rdv settings created by PlatformConfig RdvConfigurator rdv = new RdvConfigurator(pa); rdv.setIsRendezVous(true); rdv.save(pa); } PlatformConfigurator.save(pa); } // if config exists System.setProperty(TLS_PRINCIPAL_PROP, USER_NAME); System.setProperty(TLS_PASSWORD_PROP, USER_PASS); Boot.main(args); } } |
在清单 1 中,突出显示的代码展示了如何使用 net.jxta.util.config.PlatformConfigurator 类来准备一个节点公告,该节点公告将用于寂静启动 shell 的一个实例,而无需调用 GUI 配置器。我们首先用若干命令行参数调用帮助器方法PlatformConfigurator.createTcpEdge()创建一个边缘节点。不过,边缘节点在默认情况下启用多播(multicast),因此我们要在单机环境中禁用它。我们用 net.jxta.util.config.TcpConfigurator类关闭多播状态。我们还使用同一个 TcpConfigurator启用进入的 TCP 连接。最后,我们检查命令行是否指定了这是一个聚集节点。如果是的话,我们就使用net.jxta.util.config.RdvConfigurator实例将节点设置为聚集节点。还要注意 net.jxta.tls.principal 和net.jxta.tls.password系统属性的设置,以绕过登录提示。
ShellStarter的命令行使用以下的参数:
ShellStarter <peer name> <local IP or hostname> <port index> <rdv port index> [edge | rdv] |
我们创建的所有边缘节点和聚集节点都运行在同一台主机上,但是它们使用不同的 TCP 端口。参数 <port index>表明节点将运行在 97?1 端口,其中“?”是索引。这使我们可以在 9701、9711、9721 等端口上配置多达 10 个节点和聚集节点。例如,要在 9711 端口上用节点名 rdv1、IP 地址 192.168.23.17 创建一个聚集节点,我们使用下面的命令行:
ShellStarter rdv1 192.168.23.17 1 99 rdv |
注意使用端口索引 99 以表明这个聚集节点没有其他已知的种子聚集节点。
要在 9701 端口上创建名为 peer1、具有同一 IP 的边缘节点,并以上面的聚集节点为种子,使用以下的命令行:
ShellStarter peer1 192.168.23.17 0 1 edge |
对于 rendezvous, 在 test 目录中有五个启动目录 —— 分别为 rdv1 到 rdv5。还有一个称为 peer1 的节点启动目录。每一个目录包含一个 runshell.bat 文件,它有针对 ShellStarter的相应参数。您将需要编辑这些文件以修改 IP 地址。图 4 显示了这个网络的配置。
在 JXTA 2 shell 发布中,有几个新命令。其中对开发者最有用的是 kdb命令。可以在平台运行的时候,用这个命令打开和关闭不同 JXTA 组件的调试日志。
kdb命令是菜单驱动的,可用于设置 16 个组件中任何一个的日志优先级(结果是生成的调试信息量差别很大)。
新的 route命令可用于显示或者操作 JXTA 路由表信息。
JXTA 2 shell 中另一个有用的命令是新的 rdv命令,它特别有助于理解聚集超级节点网络是如何操作的。这个命令有许多选项,在我们的试验过程中对其中一些选项进行了探索。
要启动这个系统,首先从一到五按顺序启动所有聚集节点。一定要等每一个聚集节点完全启动后再启动下一个,然后再启动 peer1。
回页首
观察 RPV 和遍历器的活动
使用 rdv命令(参见侧栏“ JXTA 2 中的新 shell 命令”),可以看到由任何一个聚集节点维护的 RPV。
rdv -rpv |
作为例子,下面的代码片段显示了 rdv1 给出的结果:
rdv5 rdv4 rdv3 rdv2 |
RPV 顺序反映了节点的 JXTA 节点 ID 顺序。我们现在可以观察 RPV walker 的活动了。 rdv命令有一个运行字符串索引服务的选项(只用于测试和诊断)。要启动这个服务,在所有六个节点上运行下面的命令:
rdv -start |
现在,在其中一个聚集节点上创建一个字符串。在我们的例子中,我们在 rdv4 上创建“treasure”:
JXTA> rdv -add treasure |
您可以用下面的 list 选项来确认 rdv4 现在包含这个字符串:
JXTA> rdv -list treasure |
现在,让我们看一下 RPV 遍历器的活动。在 peer1上搜索以下的字符串:
JXTA>rdv -search treasure Sending test message rdv has sent search query for treasure JXTA>rdv received from : jxta://uuid-59616261646162614A78746150325 03369170C5E92004D0DB2E48AAA571741C803 found: treasure |
立刻就在 rdv4上找到了 treasure 字符串。可以在 rdv4上键入 whoami 验证这个节点 ID 确实就是 rdv4。
看一下 rdv4shell 窗口,您将看到回复确实是直接从 rdv4发出的:
Replying search query= treasure send reply sent |
在其他的聚集节点上,您会看到转发这个查询的证据:
Forwarding search query= treasure |
下面是所发生的过程:当查询“treasure”时,就调用聚集节点遍历器,“遍历”聚集超级节点网络。作为一台聚集节点客户机,这个请求被传递给唯一连接的聚集节点 ―― rdv1。从 rdv1 开始的“遍历”引起了对查询的传播。每一个没有“treasure”字符串的聚集节点都通过遍历器转发这个请求给其他已知的聚集节点(转发次数不超过任意设置的生存时间 / 跳数限制值 10,并受循环检测控制),直到这个请求到达 rdv4。收到这个查询后,rdv4 直接回复给 peer1。
回页首
观察 SRDI 的活动
使用 rdvshell 命令,可以容易地试验 RPV 和聚集节点的遍历机制。要实际看到 SRDI 的活动,我们可以设置 SRDI 消息日志机制的LOG级别为 DEBUG,并引起一些 SRDI 活动(即一个节点上创建一个公告)。
为了避免来自诊断字符串索引服务的伪消息,首先要关闭这个服务。关闭从 rdv1 到 rdv5 以及 peer1 的服务:
rdv -stop |
然后在每一个节点上,用 kdb命令将 SRDI 消息的 LOG级别设置为 DEBUG:
JXTA>kdb KDB Main Menu 1 Change LOG configuration q Quit MAIN> 1 LOG Menu 1 Global 2 EndpointRouter 3 Endpoint 4 HTTP 5 TCP 6 TLS 7 Rendezvous 8 Discovery 9 Resolver 10 Pipe 11 Relay 12 Messengers 13 Messages 14 Quota Listener 15 SRDI 16 CM q Quit LOG> 15 Level [w,i,f,d,e,q or ?])> d LOG Menu 1 Global 2 EndpointRouter 3 Endpoint 4 HTTP 5 TCP 6 TLS 7 Rendezvous 8 Discovery 9 Resolver 10 Pipe 11 Relay 12 Messengers 13 Messages 14 Quota Listener 15 SRDI 16 CM q Quit LOG> |
现在,我们将在 peer1 上创建一个公告,它会导致 SRDI 索引信息被推送到 rdv1,并随后复制到 DHT 中。创建公告的最容易方式是创建一个新的 peergroup(实际上是一个 peergoup 公告):
newpgrp -n supergroup |
如果再看 rdv1 输出的消息,您将会看到类似于下面的 SRDI 消息:
<DEBUG 14:36:07,078 Srdi:521> Pushing deltas in group NetPeerGroup <DEBUG 14:36:07,078 Srdi:494> waiting 30000ms before sending deltas in group NetPeerGroup |
当散列和冗余索引复制发生时,其他聚集节点可能也会收到由 rdv1 发送的其他 SRDI 消息。
回页首
结束语
P2P 网络对当今计算的影响是深远的,并且已经有大量的计算用户受到其影响。不要指望在完全隔离的条件下设计一个可行的 P2P 应用程序创建环境。对用户反馈、连续需求分析、重新考虑设计和设计迭代的考虑,是创建一个可使用的设计环境的重要因素。JXTA 遵从了这种理念。在第二代版本中,JXTA 根据其早期使用者群体的现实需求进行了改进和调整。
通过编程式地将重叠网络分为通过核心 聚集节点互连的边缘节点,在今天常见的网络拓扑上部署 JXTA 2 更容易了。同时缓解了在“平面的”体系结构中常见的消息广播风暴和洪流的问题。
JXTA 2 的核心 聚集节点网络实现了松散一致的 DHT。在 P2P 网络上可用的所有资源都可在这个 DHT 的帮助下索引并动态获取。由 DHT 维护的索引是 SRDI。与第一版相比,不再在网络中传播公告了,相反,在需要时只传播索引信息。这种方法显著地改进了可用网络带宽的使用,而代价只是在查询解析时计算量稍有增加。
另一个正在完善的领域是 JXTA 平台 API。全功能节点配置 API 现在可以使用了,它允许开发人员创建寂静启动的应用程序,而无需启动 JXTA 的默认(通常是令人困惑的)节点配置器。我们在创建寂静 shell 启动脚本进行试验时使用了这个 API。使用新的 shell 命令 rdv,我们可以观察聚集超级节点网络的活动。通过使用新的 kdbshell 命令设置 DEBUG 日志级别,还可以观察 SRDI 消息传递和操作。
创建可伸缩的、高性能的并且(最重要的) 可使用的P2P 网络需要不懈地追求。JXTA 2 是在正确的方向上迈出的重要一步。
参考资料
原文地址:http://www.ibm.com/developerworks/cn/java/j-jxta2/index.html#2