SOAP 简介

作者:Reuven M. Lerner

在“锻造”栏目的1月和2月文章中,我演示了一个简单的三层Web应用程序,它使用数据库、Web服务器和基于mod_perl的Mason模板系统。我们能够看到三层Web应用程序的一些优点和缺点,尤其是与两层应用程序相比。

但正如我上个月指出的那样,我们的三层架构是不完整的,并且不一定是一个公平的演示。这是因为我们的Perl中间件对象层必须与我们为HTML::Mason编写的组件位于同一台计算机上,HTML::Mason是一个构建在mod_perl之上的模板系统。根据你的计算方式,这可能被认为是一个两层应用程序,尽管它在层之间有一个面向对象的抽象层。

为了将Mason组件和Perl对象放在不同的计算机上,我们需要某种能力来跨网络调用对象方法。也就是说,以下Perl代码应该可以工作,无论$object是位于与我们的Apache服务器相同的计算机上,还是位于Internet上的其他地方

$object->method($arg1, $arg2);

分布式对象技术和远程过程调用已经在各种平台上存在多年。在几乎每种情况下,这项技术都被限制在特定的语言或平台上。DCOM(分布式组件对象模型)允许任何语言的对象进行通信,但仅限于Windows。Java的RMI(远程方法调用)只能与其他Java对象通信。CORBA是一个例外,它允许对象跨平台和语言进行通信,但CORBA很复杂,已经花费了一段时间才能起步,并且尚未成为大多数程序员知识库的一部分。

为了响应这些专有和复杂的协议,Internet社区中的许多人创建了SOAP(简单对象访问协议),它使得创建分布式应用程序变得非常容易。SOAP最大的两个倡导者是Dave Winer(以其Scripting News“网络日志”而闻名)和Microsoft,后者通常与开放标准和跨平台协议无关。无论Linux社区中的人怎么想,Microsoft都公开拥抱了SOAP,并将其作为其.NET工作的基础。

SOAP 历史和概念

SOAP依赖于Internet上的任何两台计算机都可以使用HTTP(Web的动力协议)进行通信的想法。(实际上,SOAP可以通过几乎任何高级协议传输,包括SMTP和POP3,但HTTP是迄今为止最常见的。)然后它使用XML(允许我们创建标签和文档标准的标记语言)传输信息。服务器将传入的XML转换为对象方法调用,然后将对象的响应转换为XML文档,该文档作为HTTP响应返回。由于HTTP和XML都是开放标准,由万维网联盟发布,因此它们可以在各种平台上实现(并且已经实现),因此可以毫无问题地进行交互。

SOAP的前身,简单地称为XML-RPC,提供了一种使用以XML格式格式化的数据并通过HTTP传输的远程过程调用(RPC)的简单机制。由于多种原因,包括XML-RPC无法处理高级数据结构这一事实,W3C采用了SOAP。

许多语言和平台继续支持XML-RPC,并且在某些情况下可能需要使用它,因为它具有较小的开销。实际上,SOAP受到如此多的关注,导致其库的开发、使用和调试比XML-RPC的库更大。但是,在撰写本文时,SOAP的实现比XML-RPC更多,这意味着您对平台或语言的选择可能会迫使您选择一种协议或另一种协议。

顾名思义,SOAP希望与对象而不是简单的过程调用一起工作。因此,SOAP客户端在服务器上的特定对象上调用方法。该方法在XML文档本身的主体中指定,而与之关联的对象在HTTP“SOAPAction”标头中命名。当然,我们还需要指定一个计算机名称和端口,SOAP请求可以定向到该端口。

服务器本身,包括其名称和传输SOAP请求的端口号,称为SOAP代理。当您考虑到HTTP服务器只是中继对象方法调用而没有自己完成任何工作时,这是有道理的。不要将SOAP代理与HTTP代理混淆。HTTP代理将来自HTTP客户端的请求中继到HTTP服务器,并且通常执行安全检查和缓存。相比之下,SOAP代理在SOAP客户端和代理计算机上的对象之间中继消息。

SOAP服务器充当代理的对象有时被称为端点,并在“SOAPAction”HTTP标头中指定。端点的名称实际上可以是任何文本字符串,包括层级分隔符,例如::和/。在实践中,端点与SOAP代理编写的语言关联的对象层次结构有直接连接。在Perl中,端点可能类似于“Foo/Bar”,它指的是位于文件Foo/Bar.pm中的Foo::Bar对象。

SOAP剖析

让我们看一个简单的SOAP对话。我们的示例将演示在HTTP之上的SOAP,这是最常见的配置。使用其他协议时可能会有细微的差异。

HTTP是无状态的,这意味着两台计算机之间的每个连接都包含一个请求(从客户端到服务器)和一个响应(从服务器返回到客户端)。请求和响应分别分为两部分,称为标头和正文。当然,客户端和服务器可以添加他们想要的任何其他标头,从而为各种专门的通信协议打开大门。

SOAP请求或响应的正文将采用XML格式。(如果您以前从未使用过XML,请不要担心;虽然它可能是一个深刻而有趣的课题,但您不需要了解太多XML即可使用SOAP。)每个SOAP消息(请求或响应)都包含一个可选的SOAP标头和一个强制性的SOAP正文,它们都包装在SOAP信封中。信封将内容标识为属于SOAP,并设置将用于消息其余部分的命名空间。标头描述正文中的数据,正文包含方法调用或其结果。

为了通过SOAP调用远程服务器上的对象,我们将不得不打开与相应URL的HTTP连接,并通过SOAPAction标头标识该对象。我们发送一个包含SOAP信封的XML文档,在其中我们的SOAP标头和正文标识将在此对象上调用的方法,以及该方法可能需要的任何参数。客户端还必须准备好解析SOAP服务器返回的响应,提取包含在该响应中的数据结构并根据需要使用它们。

SOAP服务器执行补充操作,接收SOAP请求,解析其内容,并使用传递的参数在本地计算机上调用适当的方法。服务器还将SOAP响应返回给客户端,其中包含根据需要的一个或多个值。

现在您了解了与SOAP相关的术语,您可以忘记几乎所有这些术语。SOAP实现为我们提供了一个抽象层,使我们可以忽略它通过HTTP通信以及请求和响应使用XML的事实。当您的程序使用SOAP调用远程对象方法时,它关心的是接收响应;请求和响应的打包方式无关紧要。

我们的后端对象

我将使用Paul Kulchenko编写的出色的SOAP::Lite模块用Perl编写一些演示程序。这应该让您了解如何编写SOAP客户端和服务器,以及如何将它们集成到您的Web应用程序中。尽管SOAP::Lite名称如此,但它提供了丰富的功能,并且是向Perl程序添加SOAP功能的绝佳方法。大多数主要编程语言都有类似的SOAP库和对象,因此不要认为SOAP仅适用于Perl。

因为SOAP充当对象的代理,所以我们首先必须创建一个其方法可以通过网络使用的对象。清单1包含简单的“Text::Caps”Perl模块,它处理两个相当无用的方法

清单 1. Caps.pm, Perl 模块

  • capitalize,它将单个字符串作为参数并返回该字符串的大写版本

  • capitalize_array,它执行与capitalize相同的操作,但针对字符串列表中的每个元素而不是针对单个字符串

请注意,虽然SOAP用对象和方法描述所有内容,但此示例模块使用标准的Perl模块和子例程,而不是面向对象的语法。因此,当我提到Text::Caps对象的capitalize方法时,我的真正意思是我们将调用Text::Caps::Capitalize子例程。

独立的SOAP服务器

SOAP通常通过HTTP传输,HTTP位于TCP/IP之上。这意味着我们可以利用Perl自带的TCP/IP套接字代码创建一个简单的SOAP服务器。但是,SOAP::Lite为我们做了很多繁琐的工作;我们不必创建套接字或等待它。相反,我们创建一个类型为SOAP::Transport::HTTP::Dæmon的对象,它知道如何充当适当类型的SOAP服务器。您可以在清单2中看到这种简单服务器的源代码。

清单 2. 简单的独立 SOAP 服务器

这段代码相对简单,但即使对于最有经验的 Perl 程序员来说,也会觉得奇怪。这是因为与 SOAP::Lite 关联的对象通常会返回自身来表示成功。这允许我们在单个调用中调用多个方法。换句话说,我们可以说:

$object->method1()->method2();

而不是传统的:

$object->method1();
$object->method2();
您可以选择使用任一种语法来使用 SOAP::Lite,但第一种版本在文档中很常见。

当我们为 SOAP::Transport::HTTP::Dæmon 调用 “new” 构造函数时,我们会传递两个参数:计算机的名称和服务器应侦听连接的端口。

一旦我们创建了服务器对象,我们必须告诉它对象位于何处。这是一个安全特性,尽管需要一些时间才能理解。通常,Perl 在 @INC(一个目录名称数组)中查找模块。当我们导入一个模块时,Perl 会依次搜索 @INC 的每个元素,直到找到我们的模块。如果找不到我们的模块,Perl 将返回一条错误消息。

但是,由于 SOAP 将我们的模块暴露给整个世界,因此我们在使其可用之前必须小心。也许我们的一些模块返回机密数据或操作关系数据库中的信息。为了确保只有我们希望暴露的模块才能通过 SOAP 实际可用,SOAP::Lite 在处理传入的 SOAP 请求时会完全忽略 @INC。只有在对 dispatch_to() 的调用中明确提到的模块,或者在 dispatch_to() 中命名的目录中的模块,才能通过 SOAP 使用。

从某种意义上说,dispatch_to() 有效地定义了传入 SOAP 请求的等效 @INC。如果一个模块位于 dispatch_to() 中未提及的目录中,那么它对于 SOAP 请求将是不可见的。这与修改 @INC 不同。

请注意,虽然我在本文的示例中使用 /tmp,但在实际的开发或生产系统中以这种方式使用 /tmp 是一个很糟糕的主意。如果您想将与 SOAP 相关的 Perl 模块放在与 /usr/lib/perl 不同的目录中,我强烈建议您将其保留在主文件系统上,例如 /usr/lib/soaplite 中。

测试我们的服务器

现在我们的独立 SOAP 服务器正在运行,我们应该测试它以查看它是否工作。为此,我们必须创建一个 SOAP 请求,将其发送到服务器,然后解析它返回的 XML 编码的 SOAP 响应。幸运的是,SOAP::Lite 包含这样一个实用程序 SOAPsh.pl。这个小程序允许我们以交互方式创建和发送 SOAP 请求,并显示结果。即使您计划使用另一个 Perl 的 SOAP 库,仅凭 SOAPsh.pl 就足以证明下载 SOAP::Lite 是值得的。

如果我们在 localhost(即,与运行 SOAPsh.pl 的同一台计算机)上运行我们的独立 SOAP 服务器,并且如果我们在端口 8080 上运行它,我们可以按如下方式调用它:

perl SOAPsh.pl http://localhost:8080/ Text/Caps

请注意,SOAPsh.pl 的第一个参数是 SOAP 服务器的 URL,第二个参数是我们想要调用的对象。请记住,第二个参数必须使用 URL 样式的对象层次结构分隔符(即斜杠 (/))来传递,这样可以避免很多麻烦。键入 “Text::Caps” 而不是 “Text/Caps” 会使 SOAP 服务器感到困惑,并导致难以调试的错误。

如果您的 SOAPsh.pl 调用成功,您将看到以下提示:

Usage: method[(parameters)]
>

“>” 符号表示轮到您键入,您可以为您已连接的对象调用任何方法。您现在可以调用该对象支持的任何方法,包括任何参数。所以要将一个单词大写,我只需键入:

> capitalize('abc')
因为我的 SOAP 客户端和服务器都在同一台计算机上,所以响应几乎是瞬间的。SOAPsh.pl 打印出:
--- SOAP RESULT ---
$VAR1 = 'ABC';
嘿,这太棒了!我只是通过网络调用了一个对象方法。这并不难,不是吗?

如果我们能够来回发送简单的标量,SOAP 就会很棒。但是我们可以发送和接收各种数据类型。例如,我们可以调用 capitalize_array,发送一个参数列表:

> capitalize_array('abc', 'def', >'GHi')

返回值是一个数组引用:

--- SOAP RESULT ---
$VAR1 = bless( [
                'ABC',
                'DEF',
                'GHI'
                         ], 'Array' );
返回的数组引用看起来有点奇怪,因为它已被转换为 SOAP::Lite 可以发送和检索的格式。我们很快就会看到我们的程序如何忽略这种中间格式,无缝地通过 Internet 交换复杂的数据结构。
检查 SOAP

如您所见,无需了解底层 XML 编码的数据即可使用 SOAP。但是,调试 SOAP 问题通常需要您查看 XML 以及在请求和响应中发送的 HTTP 标头。

SOAP::Lite 对象支持 on_debug( ) 方法,该方法接受一个子例程引用作为参数。对于每个 SOAP 事务,都会调用此子例程,这意味着我们可以将信息记录到磁盘或屏幕。on_debug( ) 的最简单用法如下:

on_debug(sub{print STDERR @_})

换句话说,我们要求 SOAP::Lite 将所有内容的副本发送到 STDERR。这为我们提供了一个绝佳的机会来了解幕后发生了什么。在我们调用此方法后,SOAPsh.pl 会提醒我们,我们调用的是本地方法而不是 SOAP 方法:

--- METHOD RESULT ---
SOAP::Lite=HASH(0x82e1174)
启用调试后,我们之前对 capitalize(abc) 的调用将被转换为 SOAP 请求(参见清单 3):

清单 3. SOAP 请求

如您所见,与所有 HTTP 请求一样,该请求分为标头和正文。与正常的 HTTP 请求一样,我们指示一个动作(“POST”)以及一个 URL,作为 Content-Length(指示请求中的字节数)和 Content-Type(始终为 “text/xml”)。

然后乐趣开始了:最后一个标头是 SOAPAction,它命名了正在调用的对象和方法。SOAPAction 标头旨在允许公司防火墙过滤掉正在调用的危险对象和方法。但是,目前,对 SOAPAction 的支持似乎很难找到。此外,有关对象及其方法的信息都隐藏在 XML 请求和响应本身中,使得该标头对于解析目的而言是不必要的。

XML 本身以 XML 声明和 SOAP 信封开始。信封内是一个可选的标头(在此特定调用中未显示)和一个强制性的正文。正文命名了我们希望调用的对象和方法,以及我们可能传递的任何参数。

此 XML 被解析为本机操作系统和语言格式,然后传递给目标对象。该对象将响应值返回给 SOAP 服务器,然后该服务器以 XML 格式创建 SOAP 响应,如清单 4 所示。

清单 4. XML 中的 SOAP 响应

与请求一样,响应使用 HTTP 和 HTTP 标头来传递一些元数据,包括服务器类型、日期、内容长度、类型(“text/xml”)甚至正在运行的 SOAP 服务器的类型。

此特定响应的信封(如请求一样)不包含标头。但是,它确实包含一个正文,其中返回返回值(类型为 “xsd:string”)。虽然请求使用 “namesp3:capitalize” 的命名空间,但响应使用 “namesp1:capitalizeResponse” 的命名空间。这是 SOAP 中的标准做法;XML 命名空间用于标识消息是否包含请求或响应,以及为哪个方法发送响应。

在没有任何解释的情况下,清单 5 是对 capitalize_array(reuven, shira, atara) 的调用的类似调试输出:

清单 5. 调试输出

SOAP 客户端

SOAPsh.pl 可以演示交互式请求,但当我们的程序可以创建和发出自己的请求时,SOAP 会更有用。清单 6 演示了一个简单 SOAP 客户端的代码,该客户端连接到我们在 localhost 的端口 8080 上的服务器。请注意,URI 再次是对象的名称,而代理是 SOAP 服务器的名称。

清单 6. soap-client.pl

SOAP::Lite 特别令人惊叹的是它允许我们在我们的对象上调用仅存在于网络上的方法。也就是说,“uri”、“proxy” 和 “result” 方法显然存在于 SOAP::Lite 对象上。但是 “capitalize” 方法仅存在于我们的远程 Text/Caps 对象上。SOAP::Lite 通常足够智能,可以找出差异,并传递任何无法在本地解析的方法。

基于 CGI 的 SOAP 服务器

我们的独立服务器旨在简单,而且它确实如此。但是,当我们每天开始收到数百万个请求时会发生什么?那么我们的独立服务器将无法跟上,用户的例程调用将无法得到服务。

清单 7. cgi-soap.pl

一个实际的解决方案是使用针对接收大量传入 HTTP 事务进行了优化的软件,即 Apache。使用 Apache 来处理我们传入的 SOAP 事务意味着我们可以根据需要将其扩展到尽可能高或尽可能低。我们的服务器程序不再需要考虑这一点;它可以专注于接收 SOAP 数据包并将它们传递给 Perl 模块的平凡细节。

创建基于 CGI 的 SOAP 代理与创建独立程序没有太大区别。可能要记住的最重要的事情是,你不应该使用 CGI.pm 或你可能熟悉的任何其他与 CGI 相关的模块。请记住,此处的 CGI 程序是 SOAP 代理,并且 CGI 协议用于来回传输 XML 编码的请求和响应。

其他好东西

SOAP::Lite 附带了很多好东西,很难知道什么时候停止描述它们。例如,那些已经确信使用 mod_perl 代替 CGI 的人会很高兴地知道 SOAP::Lite 具有本机 mod_perl 支持,以及 CGI 和独立支持。

您自己的程序可以利用我们在上面看到的 “自动分发” 机制,其中任何未在本地识别的方法名称都会传输到远程对象。

SOAP::Lite 可以处理 SOAP 支持的大多数数据结构的传输,包括对象。换句话说,您可以在远程对象上调用 new( ),然后在 new( ) 返回的对象上调用各种方法。此功能已在许多特定平台上存在多年,但 SOAP 使其与平台无关的事实确实令人惊叹。

最后,SOAP 已经超越了其对 HTTP 的专属使用,现在支持各种其他协议,包括一些我们可能没想到的协议,例如 POP3 和 SMTP。SOAP::Lite 支持所有这些协议;在您阅读本文时,它无疑会支持更多协议。

SOAP 和三层应用程序

既然我们已经了解了 SOAP 如何在简单情况下使用,让我们考虑一下它如何在更复杂的情况下使用。例如,假设我必须创建一个非常大的网站,该网站依赖于后端关系数据库。在许多情况下,正如本专栏的定期读者所知,我更喜欢使用 mod_perl 和 HTML::Mason 来实现。

但是随着服务器端 Java 市场的增长,部分或全部后端功能可能会使用 JavaBeans 来实现。此外,随着 Enterprise JavaBeans (EJB) 作为一种需要事务的分布式应用程序技术变得越来越普及和有趣,我甚至可能更喜欢用 Java 进行部分实现。

有了 SOAP,我现在可以随意混合和匹配语言和平台。如果我创建一个 SOAP 服务器来访问适当的 Java 对象,那么我的 Mason 组件完全可以与 Java 中间件层通信。在某些情况下,即使系统最初是单一语言的,这可能也是更可取的。鉴于 Perl 不支持网络事务,我们可能希望先用 Perl 进行初始实现,然后在将来转向 EJB。使用 SOAP,这可能而且甚至是很理想的。

最后,Web 应用程序服务器已经开始与 SOAP 协同工作。这不仅允许其他计算机上使用其他语言编写的对象与给定的服务器通信,而且还为不一定基于 Web 的 Internet 服务打开了大门。也许基于 Web 的报纸会开始提供基于 SOAP 的标题系统,获取与其网站上相同的内容,但以这样一种方式进行打包,以便有人可以通过单个 SOAP 调用下载自定义的标题集。有了这样的服务,用户可以安装一个桌面(非 Web)应用程序,该应用程序会每隔几分钟更新自身以显示最新的标题。

结论

SOAP 预示着一种新型分布式 Internet 应用程序的开始,即可以在操作系统和编程语言之间执行远程过程调用。RPC 不再必须是一个专有的、难以理解或难以调用的过程;在一个下午的时间里,您就可以创建一个简单的分布式应用程序。这对 Web 和 Internet 的未来意味着什么是一个好问题,但已经有人声称桌面应用程序将越来越多地成为 GUI 外壳,这些外壳将 SOAP 请求发送到中央服务器。无论未来如何,Perl 和其他自由语言可以使用 SOAP 这一事实意味着我们将很快能够比以往任何时候都更容易地进行通信。嘿,这不就是互联网的全部意义吗?

资源

Introducing SOAP
Reuven M. Lerner 拥有一家小型咨询公司,专门从事 Web 和 Internet 技术。他和他的妻子 Shira 最近庆祝了他们女儿 Atara Margalit 的出生。您可以通过 reuven@lerner.co.il 或 ATF 主页 http://www.lerner.co.il/atf/ 与他联系。
加载 Disqus 评论