SOAP快速入门

目录

SOAP快速入门
基于CGI的服务器编写
客户端
传递值
自动调度
对象访问
错误处理
服务调度(同一服务器上的不同服务)
类型和名称
结论

本系列的第二部分

SOAP(简单对象访问协议)是一种远程调用远程服务器上存在的类和对象方法的协议。它是像CORBA、DCOM和XML-RPC等一系列类似项目中的最新项目。

SOAP指定了一种在XML中编码参数和返回值的标准方法,以及通过一些常见的网络协议(如HTTP(Web)和SMTP(电子邮件))传递它们的标准方法。然而,本文仅旨在快速介绍编写SOAP服务器和客户端。我们将几乎触及表面,了解所有可能的内容。

我们将使用来自CPAN的SOAP::Lite模块。不要被“Lite”后缀误导——这指的是使用模块的努力,而不是它的功能。

基于CGI的服务器编写

在此处下载本文中提到的源文件。

这是一个简单的基于CGI的SOAP服务器(hibye.cgi)

 #!perl -w

  use SOAP::Transport::HTTP;

  SOAP::Transport::HTTP::CGI   
    -> dispatch_to('Demo')     
    -> handle;

  package Demo;

  sub hi {                     
    return "hello, world";     
  }

  sub bye {                    
    return "goodbye, cruel world";
  }


Paul Kulchenko是即将于2001年7月23日至27日在加州圣地亚哥举行的O'Reilly开源大会的特邀演讲者。抓住这个机会,在美丽的海滩前沙顿圣地亚哥酒店和码头放松身心,同时与开源领袖们交流。有关更多信息,请访问我们的会议主页。您可以在网上注册。



这基本上分为两部分:前四行设置了一个围绕类的SOAP包装器。从‘package Demo’开始,都是被包装的类。

在先前的规范版本(1.0)中,SOAP通过HTTP应该使用一种新的HTTP方法,M-POST。现在通常首先尝试正常的POST,如果服务器需要,再使用M-POST。如果您不了解POST和M-POST之间的区别,不用担心,您不需要了解所有具体的细节才能使用该模块。

客户端

此客户端打印出hi()方法调用的结果(hibye.pl)

 #!perl -w

  use SOAP::Lite;

  print SOAP::Lite                                             
    -> uri('http://www.soaplite.com/Demo')                                             
    -> proxy('http://services.soaplite.com/hibye.cgi')
    -> hi()                                                    
    -> result;

uri()标识了服务器上的类,而proxy()标识了服务器本身的地址。由于两者看起来都像URL,我将花一分钟解释它们之间的区别,因为它非常重要。

proxy()

proxy()只是要联系以提供方法的服务器的地址。您可以使用http:、mailto:,甚至ftp: URL。

uri()

每个服务器都可以通过一个proxy() URL提供许多不同的服务。每个服务都有一个唯一的类似URI的标识符,您通过uri()方法指定给SOAP::Lite。如果您陷入SOAP文档的纠缠,那么“命名空间”对应于uri()方法。

如果您连接到互联网,您可以运行您的客户端,并且应该看到

 hello, world

就这样!

如果您的返回多个值的方法(hibye.cgi)

 #!perl -w

  use SOAP::Transport::HTTP;

  SOAP::Transport::HTTP::CGI      
    -> dispatch_to('Demo')        
    -> handle;

  package Demo;

  sub hi {                        
    return "hello, world";        
  }

  sub bye {                       
    return "goodbye, cruel world";
  }

  sub languages {                 
    return ("Perl", "C", "sh");   
  }

那么result()方法只会返回第一个。要访问其余的,请使用paramsout()方法(hibyeout.pl):

 #!perl -w

  use SOAP::Lite;

  $soap_response = SOAP::Lite                                  
    -> uri('http://www.soaplite.com/Demo')                                             
    -> proxy('http://services.soaplite.com/hibye.cgi')
    -> languages();

  @res = $soap_response->paramsout;

  $res = $soap_response->result;                               
  print "Result is $res, outparams are @res\n";

此代码将生成

 Result is Perl, outparams are Perl C sh

传递值

方法可以接受参数。这是一个将华氏度和摄氏度之间进行转换的SOAP服务器(temper.cgi)

 #!perl -w

  use SOAP::Transport::HTTP;

  SOAP::Transport::HTTP::CGI
    -> dispatch_to('Temperatures')
    -> handle;

  package Temperatures;

  sub f2c {
      my ($class, $f) = @_;
      return 5 / 9 * ($f - 32);
  }

  sub c2f {
      my ($class, $c) = @_;
      return 32 + $c * 9 / 5;
  }

下面是一个示例查询(temp.pl):

 #!perl -w

  use SOAP::Lite;

  print SOAP::Lite                                            
    -> uri('http://www.soaplite.com/Temperatures')                                    
    -> proxy('http://services.soaplite.com/temper.cgi')
    -> c2f(37.5)                                              
    -> result;

您还可以创建一个代表远程类的对象,然后在该对象上调用方法(tempmod.pl):

 #!perl -w

  use SOAP::Lite;

  my $soap = SOAP::Lite                                        
    -> uri('http://www.soaplite.com/Temperatures')                                     
    -> proxy('http://services.soaplite.com/temper.cgi');

  print $soap                                                  
    -> c2f(37.5)                                               
    -> result;

自动分发

由于是Perl,做这件事的方式不止一种:SOAP::Lite提供了另一种客户端语法(tempauto.pl)。

 #!perl -w

  use SOAP::Lite +autodispatch =>

    uri => 'http://www.soaplite.com/Temperatures',
    proxy => 'http://services.soaplite.com/temper.cgi';

  print c2f(37.5);

指定uri和代理参数后,您可以使用与本地相同的语法调用远程函数(例如,c2f)。这是通过UNIVERSAL::AUTOLOAD完成的,它捕获所有未知的方法调用。请注意,所有对未定义方法的调用都将尝试使用SOAP。

对象访问(这应该是“简单对象访问协议”,对吧?)

方法也可以返回真实对象。让我们扩展我们的类,使其具有面向对象的接口(temper.cgi):

 #!perl -w

  use SOAP::Transport::HTTP;

  SOAP::Transport::HTTP::CGI
    -> dispatch_to('Temperatures')
    -> handle;

  package Temperatures;

  sub f2c {
      my ($class, $f) = @_;
      return 5/9*($f-32);
  }

  sub c2f {
      my ($class, $c) = @_;
      return 32+$c*9/5;
  }

  sub new {
      my $self = shift;
      my $class = ref($self) || $self;
      bless {_temperature => shift} => $class;
  }

  sub as_fahrenheit {
      return shift->{_temperature};
  }

  sub as_celsius {
      my $self = shift;
      return $self->f2c( $self->{_temperature} );
  }

这是一个访问此类的客户端(tempobj.pl):

 #!perl -w

  use SOAP::Lite;

  my $soap = SOAP::Lite
    -> uri('http://www.soaplite.com/Temperatures')
    -> proxy('http://services.soaplite.com/temper.cgi');

  my $temperatures = $soap
    -> call(new => 100) # accept Fahrenheit  
    -> result;

  print $soap
    -> as_celsius($temperatures)
    -> result;

具有自动分发的类似代码更短,更易于阅读(tempobja.pl):

 #!perl -w

  use SOAP::Lite +autodispatch =>
    uri => 'http://www.soaplite.com/Temperatures',
    proxy => 'http://services.soaplite.com/temper.cgi';

  my $temperatures = Temperatures->new(100);
  print $temperatures->as_fahrenheit();

错误处理

SOAP调用可能因多种原因而失败,例如传输错误、参数不正确或服务器错误。以下处理传输错误(例如,如果客户端和服务器之间出现网络中断,则可能发生这种情况)。所有其他错误都由fault()方法表示(temperr.pl):

 #!perl -w

  use SOAP::Lite;

  my $soap = SOAP::Lite
    -> uri('http://www.soaplite.com/Temperatures')
    -> proxy('http://services.soaplite.com/temper.cgi');

 my $result = $soap->c2f(37.5);

  unless ($result->fault) {
    print $result->result();
  } else {
    print join ', ', 
      $result->faultcode, 
      $result->faultstring, 
      $result->faultdetail;
  }

faultcode()提供了有关错误主要原因的信息。可能的值可能包括:

客户端:您在请求中提供了不正确的信息。

当远程调用参数不正确时,可能会发生此错误。参数可能超出范围,例如,当期望正整数时提供了负数;或者类型不正确,例如,提供了字符串而不是期望的数字。

服务器:服务器端有问题。

这意味着提供的信息是正确的,但由于暂时困难(例如,数据库不可用),服务器无法处理请求。

MustUnderstand:具有mustUnderstand属性的header元素,但服务器没有理解。

服务器能够解析请求,但客户端请求的功能无法提供。例如,假设请求需要执行SQL语句,并且客户端想要确保多个请求将在一个数据库事务中执行。这可以通过具有一个公共TransactionID的三次不同调用来实现。

在这种情况下,SOAP头可能通过一个名为“TransactionID”的新头元素进行扩展,该元素在3次单独调用中携带一个公共标识符。然而,如果服务器不理解提供的TransactionID头,它可能无法在调用之间保持事务完整性。为此,客户端可以指示服务器“必须理解”元素“TransactionID”。如果服务器看到这一点并且不理解该元素的含义,它将不会尝试处理请求。

此功能使服务更可靠,分布式系统更健壮。

VersionMismatch:服务器无法理解客户端使用的SOAP版本。

这为(可能的)未来扩展提供,当SOAP的新版本可能具有不同的功能时,并且只有了解这些功能的客户端才能正确使用它。

其他错误

服务器允许创建自己的错误,例如Client.Authentication

faultstring()提供可读的解释,而faultdetail()提供对更详细信息的访问,这可能是一个字符串、对象或更复杂的结构。

例如,如果您将uri更改为其他内容(让我们尝试使用'Test'而不是'Temperatures'),此代码将生成:

 Client, Bad Class Name, Failed to access class (Test)

默认情况下,客户端会在传输错误带诊断信息终止,对于失败的调用不采取任何行动,因此,您可以从结果中获取故障信息。您可以使用on_fault()处理程序来更改此行为,可以是针对单个对象,这样它就会在传输错误和SOAP故障(temperrh.pl)时终止。

 #!perl -w

  use SOAP::Lite;

  my $soap = SOAP::Lite
    -> uri('http://www.soaplite.com/Temperatures')
    -> proxy('http://services.soaplite.com/temper.cgi')

    -> on_fault(sub { my($soap, $res) = @_; 
         die ref $res ? $res->faultdetail : $soap->transport->status, "\n";
       });

或者您可以全局设置它(temperrg.pl)

 #!perl -w

  use SOAP::Lite

    on_fault => sub { my($soap, $res) = @_; 
      die ref $res ? $res->faultdetail : $soap->transport->status, "\n";
    };

  my $soap = SOAP::Lite
    -> uri('http://www.soaplite.com/Temperatures')
    -> proxy('http://services.soaplite.com/temper.cgi');

现在,将您的SOAP调用包装在eval {}块中,并捕获传输错误和SOAP故障(temperrg.pl)

 #!perl -w

  use SOAP::Lite

    on_fault => sub { my($soap, $res) = @_; 
      die ref $res ? $res->faultdetail : $soap->transport->status, "\n";
    };

  my $soap = SOAP::Lite
    -> uri('http://www.soaplite.com/Temperatures')
    -> proxy('http://services.soaplite.com/temper.cgi');

  eval { 
    print $soap->c2f(37.5)->result; 
  1 } or die;

您还可以考虑这种变体,它在失败时返回undef并设置$!,就像许多Perl函数所做的那样(temperrv.pl)

 #!perl -w

  use SOAP::Lite
    on_fault => sub { my($soap, $res) = @_; 
      eval { die ref $res ? $res->faultdetail : $soap->transport->status };
      return ref $res ? $res : new SOAP::SOM;
    };

  my $soap = SOAP::Lite
    -> uri('http://www.soaplite.com/Temperatures')
    -> proxy('http://services.soaplite.com/temper.cgi');

  defined (my $temp = $soap->c2f(37.5)->result) or die;

  print $temp;

最后,如果您想忽略错误(然而,您仍然可以使用fault()方法调用检查它们)

 use SOAP::Lite
    on_fault => sub {};

或者

 my $soap = SOAP::Lite
    -> on_fault(sub{})
    ..... other parameters

服务调度(一个服务器上的不同服务)

到目前为止,我们的CGI程序只有一个类来处理传入的SOAP调用。但我们可能有一个CGI程序将SOAP调用调度到许多类。

什么是SOAP调度?当一个SOAP请求被服务器接收时,它会绑定到请求中指定的类。这个类可能是已经在服务器端加载的(在服务器启动时,或作为先前调用的结果),或者根据服务器配置按需加载。《调度》是指确定哪个类应该处理给定的请求,并在必要时加载该类的过程。《静态》调度意味着类名在配置中指定,而《动态》调度意味着只指定一个类池,例如,在特定目录中,并且该目录中的任何类都可以访问。

想象一下,您想在服务器端提供对两个不同类的访问,并想为两者提供相同的‘代理’地址。您应该怎么做?有几种选项可供选择

静态内部

… 您已经熟悉了(hibye.cgi)

  use SOAP::Transport::HTTP;

  SOAP::Transport::HTTP::CGI   
    -> dispatch_to('Demo')     
    -> handle;

  package Demo;

  sub hi {                     
    return "hello, world";     
  }

  sub bye {                    
    return "goodbye, cruel world";
  }

  1;

静态外部

类似于静态内部,但是模块位于服务器代码之外(hibyeout.cgi)

  use SOAP::Transport::HTTP;

  use Demo;

  SOAP::Transport::HTTP::CGI   
    -> dispatch_to('Demo')     
    -> handle;

以下模块当然应该在@INC列表中的一个目录中(Demo.pm)

 package Demo;

  sub hi {                     
    return "hello, world";     
  }

  sub bye {                    
    return "goodbye, cruel world";
  }

  1;

动态

如您在静态内部静态外部模式中看到的那样,模块名称是在服务器代码中硬编码的。但是,如果您想在不需要修改服务器的情况下动态添加新模块怎么办?动态调度允许您这样做。指定一个目录,该目录中的任何模块都可以用于调度(hibyedyn.cgi)

 #!perl -w

  use SOAP::Transport::HTTP;

  SOAP::Transport::HTTP::CGI

    -> dispatch_to('/home/soaplite/modules')

    -> handle;

然后将Demo.pm放在/home/soaplite/modules目录中(Demo.pm)

 package Demo;

  sub hi {                     
    return "hello, world";     
  }

  sub bye {                    
    return "goodbye, cruel world";
  }

  1;

就这样。现在/home/soaplite/modules目录中的任何模块都可用,但不要忘记,客户端指定的URI应与您想调度调用到的模块/类名匹配。

混合

我们需要它做什么?不幸的是,动态调度也有一个显著的缺点:出于安全原因,动态调度禁用了对@INC的访问。为了解决这个问题,您可以将动态和静态方法结合起来。您需要做的就是这样做(hibyemix.cgi)

 #!perl -w

  use SOAP::Transport::HTTP;

  SOAP::Transport::HTTP::CGI

    -> dispatch_to('/home/soaplite/modules', 'Demo', 'Demo1', 'Demo2')

    -> handle;

现在,Demo、Demo1和Demo2都从@INC中的任何位置预加载,但动态访问已启用,以便在/home/soaplite/modules中的任何模块,并且它们将在需要时加载。

类型和名称

截至目前,Perl是一种无类型语言(在这个意义上,整数123和字符串'123'之间没有区别),这大大简化了从SOAP消息到Perl数据的转换过程。对于大多数简单数据,我们在这个阶段可以忽略类型。然而,这种方法也有缺点:我们在生成SOAP消息时需要提供额外的信息,因为其他服务器或客户端可能期望类型信息。SOAP::Lite不会强迫你显式地为每个参数指定类型,而是根据实际值尝试猜测每个数据类型(根据Perl的另一个口号,DWIM,“按我所想”)。

例如,具有值123的变量在SOAP消息中成为类型为int的元素,而具有值'abc'的变量成为类型为string。然而,还有更复杂的案例,例如包含二进制数据的变量,这必须进行Base64编码,或者对象(blessed references),例如,根据它们的Perl包指定类型和名称(除非指定)。

尽管自动类型化可能不适用于所有情况。例如,没有默认方法可以从值123创建类型为string或类型为long的元素。你可以通过多种方式改变这种行为。首先,你可以完全禁用自动类型化(通过调用带有值为0的autotype()),或更改不同类型的自动类型化。

或者,你可以使用SOAP::Data类的对象来显式指定特定变量的类型

 my $var = SOAP::Data->type( string => 123 );

$var成为一个类型为string、值为123的元素。你可以在这个变量可以在使用普通Perl变量进行SOAP调用的地方使用。这也允许你提供不仅特定的数据类型,还可以提供特定的名称属性

由于许多服务依赖于参数的名称(而不是位置),你可以使用相同的语法为请求参数指定名称。要为$var变量添加名称,请调用$var->name('myvar'),或者甚至使用type()方法进行链式调用

 my $var = SOAP::Data->type(string => 123)->name('myvar');

  # -- OR --
  my $var = SOAP::Data->type('string')->name(myvar => 123);

  # -- OR --
  my $var = SOAP::Data->type('string')->name('myvar')->value(123);

你可以始终使用value()方法获取或设置变量的值

 $var->value(321);            # set new value

  my $realvalue = $var->value; # store it in variable

结论

这应该足以让你开始构建SOAP应用程序。你可以阅读man手册(或者如果你勇敢的话,甚至源代码)来了解更多信息,并不要忘记定期检查www.soaplite.com以获取更多文档、示例和SOAP相关的乐趣。

本文的第二部分可以在这里找到


主要贡献者

Nathan Torkington

基本上开始了这项工作,并推动了整个过程。

Tony Hong

宝贵的评论和输入帮助我保持这些材料的最新和简单。

这篇作品继续这里

标签

反馈

这篇文章有什么问题吗?请通过在GitHub上打开问题或拉取请求来帮助我们。