级别: 初级 Roy Miller (roy@roywmiller.com), 独立顾问
2004 年 2 月 06 日 应用程序间通信对程序员来说可能是个不好对付的问题。而许多可用的选择(如 JNI)又难于掌握。XML-RPC 提供了一种非常简单的解决方案。该方法简洁、易于实现,且得到了大多数流行编程语言(例如 Java 语言和 C++)的开放源代码库的良好支持。例如,如果您有一个 Java 应用程序需要与另一个用 C++ 编写的应用程序进行对话,那么 XML-RPC 正好可能是最简单的方法。在本文中,软件开发人员兼培训师 Roy Miller 谈论了 XML-RPC 是什么以及如何有效地使用它。
我无数次从开发同伴那里听说最新的热门技术就是对软件开发世界中使人烦恼的问题的解决方法。XML 首次出现时,许多人就是这样说的。此时,我没有感到兴奋,而且从那时起,我的态度也没有太多改变。我一直认为
XML 是种极佳的方法,用以定义结构化数据,而无需笨拙地将之转化为关系型结构。但是,XML 不是一种编程语言 ―― XLST 在语法上较为复杂,且至少对于我来说有点奇怪。因此,我一直在等待出现需要进行结构化数据交换的问题,而这才是创造
XML 的真正目的。在最近的项目中就出现了这个特殊问题,XML(用作 XML-RPC)正是合适于该工作的工具。
编程挑战
我们的客户制造了一种硬件设备。我们加入该项目之前,用户配置每台设备的惟一方法就是用命令行接口。要不是每个客户在每个网络上可能有 20 台或更多(也许甚至成百或上千)这样的硬件设备,该方法也并非必定糟糕。迫使客户用命令行接口一个接一个地配置每台设备很可能会削减销售。当客户在订货到达后不得不对多台设备进行初始设置和配置的时候,该问题将会更为尖锐。每个设备的配置包含在一个
XML 文件中,而设备在启动时将读取该文件。
客户聘请我们创建一个配置应用程序,用以在一个或更多位置集中的管理机器上运行。该应用程序需要简化所有设备的初始设置,将之重新配置为要进行固件升级、纠正错误等等,以及监控现有设备。其中有点困难的问题就是设备上的软件是用
C 编写的,而我们的台式机应用程序却需要用 Java 编程语言进行编写。
我们首先考虑的是 JNI,但是觉得应该存在更简单的东西。那就是称作 XML-RPC 的有用的小东西。
XML-RPC 入门
XML-RPC 网站(请参阅
参考资料)是这样描述的:
它是允许运行在不同操作系统、不同环境中的软件进行基于
Internet 过程调用的规范和一组实现。这种远程过程调用使用 HTTP 作为传输协议,XML 作为编码格式。XML-RPC 的定义尽可能简单,但能够传送、处理和返回复杂的数据结构。
在阅读该描述时,我们就知道我们有了答案。每台设备的配置保存在一个文件之中(其内容也是 XML,但这对于该论述无关紧要)。这意味着我们已经拥有告诉每台设备如何配置自身的语义。如果我们给设备发送它所期待的配置文件,那就会很好了。但是将如何发送呢?我们可以仅仅发送字节,但那样会危及安全性,况且用字节操作来完成这一切也不是谁所真正需要的。我们意识到可以用定义良好的
XML-RPC 消息来发送字符串有效负荷,而 XML-RPC 消息将允许我们调用每台设备上严格限制的软件公共接口中的 C 函数。
XML-RPC 重点
概括地说,您可以将 XML-RPC 认为是简化的 SOAP。它可能是您曾需要的惟一的应用程序间的通信。XML-RPC 网站上有个极佳的“入门”文档,该网站还提供了一些发展历史以及各种语言的实例。但是,您可能只需要阅读其规范。在不到六页的内容上包括了一个简单的模型。本节中,我们将介绍一些重点,以便为如何在项目中使用
XML-RPC 做好准备。
一个 XML-RPC 消息就是一个请求体为 XML 的 HTTP-POST 请求。您需要一个 XML-RPC 客户程序来创建消息,以及一个
XML-RPC 服务程序来接收消息。服务程序一旦完成了请求,就同样以 XML 格式送回一个 XML-RPC 响应消息。请求可以包含参数(整数、字符串、日期以及其他类型,如果需要还可以包括数组和复杂记录)。每个请求的格式都极其简单,如清单 1 所示:
清单 1. XML-RPC 请求示例
POST /RPC2 HTTP/1.0
User-Agent: Frontier/5.1.2 (WinNT)
Host: betty.userland.com
Content-Type: text/xml
Content-length: 181
<?xml version="1.0"?>
<methodCall>
<methodName>examples.getStateName</methodName>
<params>
<param>
<value><i4>41</i4></value>
</param>
</params>
</methodCall>
|
您需要一个指定“处理程序”名(清单 1 中为
examples )的字符串
methodName
和一个调用该处理程序的方法(清单 1 中为
getStateName )。无论如何,服务程序可以解释这个名字字串。我们所使用的 Java 服务程序(我们将稍候讨论)将用处理程序名
examples 找到一个对象,并且调用该对象之上的
getStateName 方法。
其响应也很简单,如清单 2 所示:
清单 2. XML-RPC 响应示例
HTTP/1.1 200 OK
Connection: close
Content-Length: 158
Content-Type: text/xml
Date: Fri, 17 Jul 1998 19:55:08 GMT
Server: UserLand Frontier/5.1.2-WinNT
<?xml version="1.0"?>
<methodResponse>
<params>
<param>
<value><string>South Dakota</string></value>
</param>
</params>
</methodResponse>
|
当您发出一个 XML-RPC 调用时,您将获得一个 XML 响应,其中包含一个
<params> 元素,该元素中又依次包含一个
<param> 元素,其中再包含一个
<value> 元素,该元素中则包含一个需要进行处理的返回值。大多数情况下,这就是您所希望获得的响应。但是现实从来都不是那么简单的。如果发生某些错误,服务程序则会返回“故障”响应,如清单
3(反映在 RPC 中发送太多参数的故障)所示:
清单 3. XML-RPC 故障响应示例
HTTP/1.1 200 OK
Connection: close
Content-Length: 426
Content-Type: text/xml
Date: Fri, 17 Jul 1998 19:55:02 GMT
Server: UserLand Frontier/5.1.2-WinNT
<?xml version="1.0"?>
<methodResponse>
<fault>
<value>
<struct>
<member>
<name>faultCode</name>
<value><int>4</int></value>
</member>
<member>
<name>faultString</name>
<value><string>Too many parameters.</string>
</value>
</member>
</struct>
</value>
</fault>
</methodResponse>
|
<fault> 元素的
<value> 元素中包含一个带有
faultCode
member 成员和
<faultString> 成员的结构。这就像是 Java 类中的
toString() 。如果发生错误,并且是由编码所致,
toString()
则会通过错误代码和出错消息告诉您是什么。而 XML-RPC 故障响应完成同样的工作。
这也涉及您需要用以理解 XML-RPC 相关处理的所有内容。实际上,您真的无需了解 XML 消息布局的细节。只要提供了有效输入,您所选择的
XML-RPC 实现库将为您完成所有工作。因此,您所缺乏的用以读取规范的惟一工具就是客户程序和服务程序实现。在这个应用程序中,我们需要 Java
实现的客户程序和 C 实现的服务程序。
使之工作
XML-RPC 网站包含多种语言编写的规范的客户程序和服务程序实现的链接,这些语言包括 Java 编程语言、Ruby、Python、C/C++ 和 Perl。
有一个由 Apache 小组编写的实现,还有一些是由单个开发人员编写的。在查看其中一些的代码之后,我们选择了 Greger Ohlson
所编写的 Marquee XML-RPC 客户程序实现。Ohlson 也编写了一个服务程序实现,但是为我们将配置的硬件设备编写代码的同事选择了
Apache XML-RPC 服务程序。只要输入和输出是可预测的,这也的确不要紧。
所有的开发是在 Eclipse 中进行的,因此我们仅仅下载 Marquee 库,为它创建一个项目并加载到我们的工作区中。还将它置于应用程序的类路径中使我们可以访问
Marquee 接口。此时,我们就只需要使用它。为了简化该方法,我们为设备创建了一个包装器,使应用程序剩下部分只需处理一个域对象而无需担忧
XML-RPC 的细支末节,并且创建了一个 XML-RPC 对象来包装请求和解码响应。
当应用程序由于某种理由(例如检查其状态)需要与设备进行交互时,它仅需调用该设备的包装器上的方法,从而与 XML-RPC 对象交互以完成 XML-RPC
的神奇功能。清单 4 显示了说明包装器模样的简化实例。
清单 4. 设备包装类
public class Device {
protected DeviceConfiguration configuration;
protected Status status = Status.UNREACHABLE;
protected Device(DeviceConfiguration configuration) {
this.configuration = configuration;
}
public Status getStatus() {
return obtainRpcClient().getStatus();
}
public void setStatus(Status status) {
if (this.status != status) {
this.status = status;
}
}
public void reboot() {
Status status;
try {
status = obtainRpcClient().reboot();
} finally {
makeUnreachable();
}
setStatus(status);
}
public DeviceConfiguration getConfiguration() {
this.configuration =
DeviceConfigurationBuilder.toConfig(
obtainRpcClient().getDeviceConfiguration());
makeOk();
return this.configuration;
}
public Status putConfiguration() {
Status status =
obtainRpcClient().replaceDeviceConfiguration(
DeviceConfigurationBuilder.toData(this.configuration));
setStatus(status);
return status;
}
protected RpcClient obtainRpcClient() {
return new RpcClient(
this.configuration.getIpAddress(),
80,
this.configuration.getUserPassword(),
100);
}
public void makeOk() {
setStatus(Status.OK);
}
public void makeUnreachable() {
setStatus(Status.UNREACHABLE);
}
}
|
Device 类使用了三个辅助类:
DeviceConfiguration 、
Status
和
DeviceConfigurationBuilder 。
这三个类的详细说明超出了本文的范围,可是我将介绍由
DeviceConfiguration 类来保存从每个硬件设备的
XML 配置中提取的值的实例。这正是追踪那些值的便利方法。如您所能看到的,
Device 类用
DeviceConfigurationBuilder
将原始配置数据和
DeviceConfiguration 实例进行相互转换。
该示例包括用以向设备询问其状态,告诉设备进行重新启动,以及 get 和 put 配置数据的方法。但是
Device
实例不处理与它所仿效的设备进行的对话,而是授权给 XML-RPC 包装类,而这正是 XML-RPC 真正令人激动的地方。清单 5 显示了说明
XML-RPC 包装类样子的简化示例。我们将其称为
RpcClient 以区分 Marquee
XmlRpcClient 。
清单 5. XML-RPC 客户程序包装类
import java.util.Hashtable;
import marquee.xmlrpc.XmlRpcClient;
import marquee.xmlrpc.XmlRpcException;
public class RpcClient {
protected static final Object[] EMPTY_ARRAY = new Object[0];
protected XmlRpcClient xmlRpcClient;
protected String ipAddress;
protected String password;
protected int port;
protected int timeout;
public RpcClient(
String ipAddress,
int port,
String password,
int timeout) {
super();
this.ipAddress = ipAddress;
this.port = port;
this.password = password;
this.timeout = timeout;
xmlRpcClient = new XmlRpcClient(ipAddress, port, "/RPC2");
}
protected Object invoke(final String rpcMethodName) {
return invoke(rpcMethodName, EMPTY_ARRAY);
}
protected Object invoke(final String rpcMethodName, Object[] parameters) {
try {
Object result = xmlRpcClient.invoke(rpcMethodName, parameters);
if (result instanceof Hashtable) {
Hashtable fault = (Hashtable) result;
int faultCode = ((Integer) fault.get("faultCode")).intValue();
throw new RuntimeException(
"Unable to connect to device via XML-RPC. \nFault Code: "
+ faultCode
+ "\nFault Message: "
+ (String) fault.get("faultString"));
}
if (result instanceof Integer) {
return Status.getStatus((Integer) result);
}
return result.toString();
} catch (XmlRpcException e) {
throw new RuntimeException(e);
}
}
public Status getStatus() {
return (Status) invoke("Device.getStatus");
}
public String getDeviceConfiguration() {
return (String) invoke("Device.getConfiguration");
}
public Status replaceDeviceConfiguration(String configurationData) {
return (Status) invoke(
"Device.replaceConfiguration",
new Object[] { configurationData });
}
public Status reboot() {
return (Status) invoke("Device.reboot");
}
}
|
请注意,
RpcClient 的公共接口包括构造函数共有五个方法。每个方法调用
invoke()
的一种版本。一种版本不带参数(例如由
reboot() 调用),而另一种则以
Object
数组为参数(例如由
replaceDeviceConfiguration() 调用)。
不带参数的版本用一个空的
Object 数组调用另一版本。而获取参数的
invoke()
方法才是我们与 Marquee 库进行交互的惟一场所。该方法调用包含于
RpcClient 的
XmlRpcClient
实例上的
invoke() 。Marquee 库将完成其神奇功能:在 XML 中包装方法字符串(请记住根据该规范,它看上去像是
handlerName.methodName )和参数
Object 数组,然后将之发送给服务程序。它所返回的结果可能是一个
哈希表 (Marquee 为 XML-RPC 响应的“故障”版本进行的选择)、一个
整数 包装器(用于数字返回值)或一个
字符串 (XML-RPC
的默认返回类型)。
本例中,如果得到一个错误返回,我们就发出带有从故障
哈希表 中提取的细节信息的
RuntimeException 。如果得到一个数字状态值(例如调用
reboot() 时将得到),我们就实例化
Status 对象来保存该值,并很好地进行文本转换以在用户界面(UI)中显示。如果得到一个
字符串 返回(调用
getDeviceConfiguration() 时将得到),我们就将之返回。
重启 aDevice
既然您已经了解了所有事项,就让我们将之连接起来。比如说我们的应用程序告诉一个特殊的
设备 (我们将称之为
aDevice )来进行自我重启。用户界面(UI)区域中的某类将调用
aDevice 上的
reboot() 。以下是接下来所发生的事情:
-
aDevice 创建一个
RpcClient 实例 ――
anRpcClient ,以连接物理设备上的
XML-RPC 服务程序(使用
aDevice 的
DeviceConfiguration
实例的 IP 地址和用户口令)。
-
aDevice 调用
anRpcClient 上的
reboot() 。
-
anRpcClient 用空的参数列表调用其 Marquee
XmlRpcClient
实例 ――
xmlRpcClient 上的
invoke() 。
-
xmlRpcClient 在从服务程序得到一个 XML-RPC 故障消息时返回一个
哈希表 ,在得到一个数字返回值时返回一个
整数 ,在所有其他情况下返回一个
字符串 。
-
anRpcClient 返回从
xmlRpcClient 所得到的内容,本例中仅为显示一切运行良好的返回代码(即服务程序本身未返回任何数据)。
- 如果出现错误,
aDevice 就将其
Status 实例设置为
UNREACHABLE
以使之在用户界面(UI)中报告。
- 如果一切运行良好,
aDevice 就只是将其
Status 实例更新为
anRpcClient
发送给它的内容。
向 aDevice 询问其当前状态
向
aDevice 询问某些数据不会困难很多。最基本的实例就是应用程序向
aDevice
询问其当前状态。本例中,用户界面(UI)区域中的某类将调用
aDevice 上的
getStatus() 。以下是接下来所发生的事情:
-
aDevice 创建一个
RpcClient 实例 ――
anRpcClient ,以连接物理设备上的
XML-RPC 服务程序。
-
aDevice 调用
anRpcClient 上的
getStatus() 。
-
anRpcClient 用空的参数列表调用其 Marquee
XmlRpcClient
实例 ――
xmlRpcClient 上的
invoke() 。
-
xmlRpcClient 在从服务程序得到一个 XML-RPC 故障消息时返回一个
哈希表 ,在得到一个数字返回值时返回一个
整数 ,在所有其他情况下返回一个
字符串 。
-
anRpcClient 返回从
xmlRpcClient 获得的状态。
- 如果出现错误,
aDevice 就将其
Status 实例设置为
UNREACHABLE
以使之在用户界面(UI)中报告。
- 如果一切运行良好,
aDevice 就只是将其
Status 实例更新为
anRpcClient
发送给它的内容。
向
aDevice 询问其当前配置数据的实例实质上也相同,惟一区别就是我们必须获取由 XML-RPC 调用返回的原始配置数据(为一个
字符串 ),并将之适配到
DeviceConfiguration 实例中。当我们将新的配置数据发送给
aDevice
时,我们通过从
DeviceConfiguration 实例中提取配置数据并从中构建一个字符串有效负荷完成了与之相反的工作。
请注意在该示例代码中,我们无需进行任何 XML 操纵。完全不是的。因为 Marquee 库为我们完成了这一切。现在,XML-RPC 规范相当简单,因此,您大概可以滚动自己的客户程序,却无需做什么 ―― Marquee 库非常好,并且还拥有本文中未曾探索的功能。文档是直观、完整的。在我看来,永远不必解析 XML 是让人高兴的。
服务端
直到此时,我都没有提及太多该等式的服务端的相关内容。那是因为是由小组中的其他人为该应用程序创建的 XML-RPC 服务端(使用 Apache
的 C 实现)。那是很棒的,但是如果也必须用 Java 语言开发 XML-RPC 服务该怎么办呢?实际上也很容易。在该项目中,我们可以使用同样用
Java 语言编写的 Marquee XML-RPC 服务程序实现(请参阅
Other
uses for XML-RPC以了解实际该如何做,但那是出于另一目的)。
清单 6 显示了一个简单的处理先前讨论的 XML-RPC 客户端请求的 XML-RPC 服务程序。它仅仅用于示例,模拟了一台真实的物理设备。让我们剖析该代码以了解 XML-RPC 服务程序的详情。
清单 6. 简单的 XML-RPC 服务程序
import java.io.IOException;
import marquee.xmlrpc.XmlRpcServer;
import marquee.xmlrpc.handlers.ReflectiveInvocationHandler;
public class DeviceServer {
public static final String USERNAME = "username";
public static final String PASSWORD = "password";
protected boolean isShuttingDown;
protected String configuration = "initial configuration";
protected String host;
protected String password = PASSWORD;
protected XmlRpcServer rpcServer;
protected Thread rpcThread;
protected int port;
protected Status status = Status.OK;
public DeviceServer(String theHost, int thePort) {
host = theHost;
port = thePort;
createRpcServer();
startRpcServer();
}
public void shutDown() {
isShuttingDown = true;
if (rpcServer != null)
rpcServer.shutDown();
}
public Object getStatus() {
return new Integer(status.getCode());
}
public Object getConfiguration() {
return "valid configuration data";
}
public Object replaceConfiguration(String xml) {
configuration = xml;
return new Integer(status.getCode());
}
public Object reboot() {
return new Integer(status.getCode());
}
public void setStatus(Status newStatus) {
status = newStatus;
}
protected void startRpcServer() {
rpcThread = new Thread(new Runnable() {
public void run() {
try {
rpcServer.runAsService(port);
} catch (IOException ioe) {
if (!isShuttingDown)
ioe.printStackTrace();
}
}
});
rpcThread.setName("DeviceServer[" + this.host + "] on " + this.port);
rpcThread.start();
}
protected void createRpcServer() {
rpcServer = new XmlRpcServer();
rpcServer.registerInvocationHandler(
"Device",
new ReflectiveInvocationHandler(this));
}
}
|
服务程序的构造函数给出了处理过程的概要内容。我们保存传入的主机和端口信息,然后调用
createRpcServer()
以实例化 Marquee
XmlRpcServer 并用其注册
ReflectiveInvocationHandler (马上将详细介绍)。然后,我们调用
startRpcServer() 来给该服务起一个帮助性的名字并在其自身的线程中运行。在启动该线程之前,
Marquee XmlRpcServer 需要用端口号
int 调用
runAsService() 。一旦启动,该服务就侦听从该端口进入的来自该端口上所连接的任何客户端的请求。
大多数请求是直接进行的,但是这个
ReflectiveInvocationHandler 又是什么呢?XML-RPC
规范没有讲述如何实现客户程序或服务程序。但它
确实
规定了 XML-RPC 服务程序必须处理用
<methodcall> 元素进行的新来请求。该元素中是表单
handlername.methodname 的调用字符串。当您调用在 Marquee 客户端调用
invoke() 时,它就制造合适的
XML 将方法字符串和若干参数以格式良好的 XML-RPC 消息传递给服务程序。在服务端,Marquee XML-RPC 服务程序:
- 将方法字符串解析为处理程序名和在该处理程序上调用的方法
- 用指示名称找到已注册的处理程序
- 通过传递请求中所发送的参数来调用该处理程序上的方法
- 将结果包装在 XML-RPC 响应中并送回给客户端
要使用 Marquee XML-RPC 服务,运行的服务实例就必须知道如何译解方法字符串。它必须知道哪个对象与
handlername 键值进行通信。虽然这样说,但是显然除非您告诉服务程序名字“Device”与给定的对象进行通信,否则服务程序将不知道如何译解
handlername 。那可以是任何实例。如清单
7 所示,我们仅仅让服务程序处理所有新来的方法请求。此代码中,我们在
createRpcServer() 里完成该工作。
清单 7. 简单的 XML-RPC 服务程序
rpcServer.registerInvocationHandler("Device", new
ReflectiveInvocationHandler(this));
|
现在,无论何时服务端获得一个请求,而其方法字符串如
Device.someMethod 一般,它都知道在自身中查找
someMethod() 以处理请求。在示例服务程序中,我们只需完成“在要求的对象上找到要求的方法”这个基本行为,因此我们使用了
Marquee
ReflectiveInvocationHandler 。而 Marquee 中基本的处理程序就挺好了,所以我们无需编写自己的。该处理程序仅仅在其被实例化的对象上查找要求的方法。如果查看代码,您将发现所有的
Java 反射(Reflection)逻辑 Marquee 为您避免了编写代码的麻烦。
处理程序概念对于 XML-RPC 规范不是必须的,但是 Marquee 是围绕其构建的并且运行良好。如果基本的
ReflectiveInvocationHandler 不能完成您的工作,您可以用子类
XmlRpcInvocationHandler 来实现自己的处理程序。
XML-RPC 的其他用法
在本文所描述的项目中为了进行测试,我们使用 XML-RPC 来帮助应用程序进行外部脚本编制。我们用 Ruby 编写了一个简单的测试架构并使之向应用程序发出
XML-RPC 请求,这都包含在 Marquee Java XML-RPC 服务端上。当到了要配送该应用程序的时候,我们仅关闭 XML-RPC
服务程序就可以了。
 |
SWT
与 Ruby
在我所参与的项目部分中,我们使用 XML-RPC 创建一个带有 SWT 用户界面(UI)和 Ruby 后端的应用程序。对于我们编写的应用程序来说,Ruby 使用起来更简单,但是 Ruby 的 UI 库却没有向 SWT 开放。而 XML-RPC 却允许我们轻易地将两者融合。
|
|
结束语
本文中所介绍的实例显然是十分简单的。Marquee XML-RPC 库所包含的许多功能我都没有论述(如调用预处理器和串行器以将 Java 对象转化为
XML-RPC 传输结构)。虽然那些附加功能是补贴性的。您无需它们来获得 XML-RPC 的巨大价值。XML-RPC 确实很简单,这使值得在分布式应用程序中考虑它们。一般来说,如果您需要在两个应用程序间通信,特别是如果那些应用程序是用不同的语言编写的,XML-RPC
就值得注意。XML-RPC 网站引用了一个
Byte
杂志评论员的话:“难道分布式计算一定比这还难吗?我不这么认为。”我也同意,这少对于这里所谈论的项目是如此。本例中,XML-RPC 让我们完成了所需工作,而没有挡路。好工具就是如此。
参考资料
关于作者
对本文的评价
|