第4章 PHP与XML、WebService

云计算近年来非常火热,已经成为目前继SOA、Web 2.0之后又一个热门的话题。虽然云计算的定义还未尘埃落定,但并不妨碍Amazon.com、Google和Microsoft都相继推出自己的云计算平台。云计算中有一种被称为“提供者—消费者理论”的模式:模式中的一方通过硬件出让、软件租用、计算能力输出等方式对外提供服务,另一方则付费(或免费)使用这些服务以达到自己的目的。其中提供服务的一方被称为“提供者(Provider)”,使用服务的一方则称为“消费者(Consumer)”。在这种模式的计算能力输出方式中,经常使用到XML、WebService等技术。

XML(eXtensible Markup Language,可扩展标记语言)是一种标准化的数据格式,其语法类似于HTML。但与HTML不同的是,XML的设计目的是为了便于解析和交换数据。现在XML在出版、工程、医药等诸多领域已经成为一种标准化的数据格式,XML还可用于RPC(Remote Procedure Call,远程过程调用)、订单等场合。

WebService是一种新的Web应用程序分支,他们是自包含、自描述、模块化的应用,可以发布、定位和通过web调用。WebService可以执行从简单的请求到复杂商务处理的任何功能。一旦部署以后,其他WebService应用程序可以发现并调用它部署的服务。实际上,WebService的主要目标是跨平台的可互操作性。为了达到这一目标,WebService完全基于XML(可扩展标记语言)、XSD(XMLSchema)等独立于平台、独立于软件供应商的标准,是创建可互操作的、分布式应用程序的新平台。

WebService目前主要用于以下4个场合。

● 跨防火墙的通信

如果应用程序有成千上万的用户,而且分布在世界各地,那么客户端和服务器之间的通信将是一个棘手的问题。因为客户端和服务器之间通常有防火墙或者代理服务器。在这种情况下,传统的做法是,选择用浏览器作为客户端,写下一大堆程序页面,把应用程序的中间层暴露给最终用户。这样做的结果是开发难度大,程序很难维护。

而如果中间层组件换成WebService的话,就可以从用户界面直接调用中间层组件,从而省掉建立程序页面的那一步。不仅缩短了开发周期,还降低了代码复杂度,并能够增强应用程序的可维护性。同时,应用程序也不再需要在每次调用中间层组件时,都跳转到相应的“结果页”。在一个用户界面和中间层有较多交互的应用程序中,使用WebService这种结构,可以节省花在用户界面编程上的开发时间。另外,这样一个由WebService组成的中间层,完全可以在应用程序集成或其他场合下重用。最后,通过WebService把应用程序的逻辑和数据“暴露”出来,还可以让其他平台上的客户重用这些应用程序。

● 应用程序集成

有过企业级应用开发经验的开发者知道,企业里经常都要把用不同语言写成的、在不同平台上运行的各种程序集成起来,而这种集成将花费很大的开发力量。应用程序经常需要从运行在一些主机上的程序中获取数据;或者把数据发送到其他应用程序中去。即使在同一个平台上,不同软件厂商生产的各种软件也常常需要集成起来。通过WebService,应用程序可以用标准的方法把功能和数据“暴露”出来,供其他应用程序使用。在这种情况下,使用WebService是最省时省力的做法了。

● B2B的集成

用WebService集成应用程序,可以使公司内部的商务处理更加自动化。跨越供应商和客户、突破公司界限的交易更需要WebService。WebService是B2B集成(Business to Business Integration,跨公司商务交易集成)成功的关键。通过WebService,公司可以把关键的商务应用“暴露”给指定的供应商和客户。WebService运行在Internet上,在世界任何地方都可轻易实现,运行成本相对较低,而且可以轻易实现互操作性。只要把商务逻辑“暴露”出来成为WebService,就可以让任何指定的合作伙伴调用这些商务逻辑,而不管其系统在什么平台上运行,使用什么开发语言。

● 软件和数据重用

软件重用是一个很大的主题,重用的形式有很多,重用的程度有大有小。最基本的形式是源代码模块或者类一级的重用,另一种形式是二进制形式的组件重用。

当前,像表格控件或用户界面控件这样的可重用软件组件,在市场上都占有很大的份额。但这类软件的重用有一个很大的限制,就是重用仅限于代码,数据不能重用。原因在于,发布组件甚至源代码都比较容易,但要发布数据就没那么容易,除非是不会经常变化的静态数据。而WebService在允许重用代码的同时,可以重用代码背后的数据。使用WebService,再也不必像以前那样,要先从第三方购买、安装软件组件,再从应用程序中调用这些组件,只需直接调用远端的WebService即可。

另一种软件重用的情况是,把好几个应用程序的功能集成起来。现在互联网上有很多应用程序供应商,都在其应用中实现了以往要登录多个站点才能实现的多种功能,如果他们把这些功能都通过WebService“暴露”出来,就可以非常容易地把所有这些功能都集成到门户站点中,为用户提供一个统一的、友好的界面。

当前常见的WebService具体技术实现主要有三种类型。

● XML-RPC

XML-RPC是一种简单的、轻量级的通过HTTP进行RPC通信的规范。一个XML-RPC消息就是一个内容为XML的HTTP POST请求,被调用的方法在服务器端执行并将执行结果以XML格式编码后返回。

在Web服务发展的初期,XML格式化消息的第一个主要用途是应用于XML-RPC协议,其中RPC代表远程过程调用。在XML远程过程调用(XML-RPC)中,客户端发送一条特定消息,该消息中必须包括名称、运行服务的程序以及输入参数。

● SOAP

相对于XML-RPC只能使用有限的数据类型和一些简单的数据结构,SOAP(Simple Object Access Protocol,简单对象访问协议)走进了开发者们的视野。XML-RPC只有简单的数据类型集,取而代之,SOAP通过利用XML Schema的不断发展来定义数据类型。同时,SOAP也能够利用XML命名空间,这是XML-RPC所不需要的,这样,SOAP消息的开头部分就可以是任何类型的XML命名空间声明,其代价是在系统之间增加了更多的复杂性和不兼容性。

● REST

REST(Representational State Transfer,表述性状态转移)是针对Web应用而设计的架构,其目的是为了降低开发的复杂性,提高系统的可伸缩性。REST基于Web的架构,实际上就是各种规范的集合,这些规范共同组成了Web架构。Restful Web Service面向资源,而不是面向动作(Action),这里的资源指的不是数据,而是数据和表现形式的组合,比如“最新访问的10位会员”和“最活跃的10位会员”,在数据上可能有重叠或者完全相同,而由于他们的表现形式不同,所以被归为不同的资源,这也就是REST这个名称的来源。

REST基于HTTP,任何对资源的操作行为都是通过HTTP来实现的。以往的Web开发大多数用的都是HTTP中的GET方法和POST方法,而对其他方法很少使用,实际上HTTP把对一个资源的操作限制在4个方法以内:GET、POST、PUT和DELETE,而这正是对资源CRUD(Create、Retrieve、Update、Delete,建立/获取/更新/删除)操作的实现。由于资源是和URI一一对应的,执行这些操作的时候URI没有变化,因此和以往的Web开发有很大的区别。

因为REST要求所有的操作都是无状态的,所以可以提高系统的可伸缩性。没有了上下文(Context)的约束,使用REST实现分布式和集群就更为简单,也可以让系统更有效地利用缓冲池(Pool);并且由于服务器端不需要记录客户端的一系列访问,也降低了服务器端的开销。

PHP 5能够很好地支持这三种方式的WebService技术:通过xmlrpc-epi项目的xmlrpc扩展xmlrpc扩展默认是不可用的,xNix下需要在编译PHP时加上“-with-xmlrpc”选项来启用该扩展,Windows下需要修改php.ini来开启该扩展。提供SOAP和XML-RPC的访问途径;通过soap扩展提供对SOAP的支持;通过URL重写、地址路由以及方法调度来支持REST。

很多场合下会用到XML,多种程序语言都对XML提供了良好的支持,PHP也不例外,有多种方式读取和写入XML的内容。同时,XML也是WebService的必需技术。

4.1 使用PHP生成XML文档

PHP可以生成(创建)动态HTML,也可以生成动态XML。可以为其他程序生成基于表单或数据库查询的XML,也可以为PHP中可以做的任何事生成XML。动态XML的一个典型应用是RSS(丰富站点摘要,Rich Site Summary;另一种解释为真正简易聚合,Really Simple Syndication),一种用来同步新闻站点内容的文件格式。可以通过读取数据库或者HTML文件中的文章信息,生成一个基于这些信息的XML摘要文件。

用PHP生成XML文档很简单,只需用header()函数把文档的MIME类型改成“text/xml”,然后按照XML的格式输出内容即可。为了避免“<?xml…?>”声明被解释为一个PHP标签,需要编辑php.ini,将short_open_tag选项设为不启用,或者直接用echo把这一行打印出来:

<?php
    echo '<?xml version="1.0" encoding="gbk"?> ';
?>

例4-1是使用PHP生成一个RSS文档,这个RSS文档是一个包含有若干个channel元素的XML文档,每个channel包含有几个item元素,每个item元素又包含有一个title、一个link和一个description元素。实际上,RSS所支持的属性比例4-1中所展示的要多。就像PHP生成HTML没用到什么特殊函数一样,用PHP生成XML也只需用echo打印内容即可。

实例演练

例4-1 使用PHP输出一个RSS文档的内容

<?php
header("Content-type:application/xml");
echo '<?xml version="1.0" encoding="gbk"?>';
?>
<rss version="2.0">
   <channel>
   <?php
   //将数据内容存储于数组中
   $items = array(
                           array("title" => "新浪网",
                                          "link" => "http://www.sina.com.cn/",
                                          "desc" => "国内新闻门户站点"),
                           array("title" => "大渝网",
                                          "link" => "http://cq.qq.com/",
                                          "desc" => "重庆城市生活第一网"),
   );
   foreach($items as $item) {
          echo "<item>\n";
          echo "  <title>".$item["title"]."</title>\n";
          echo "  <link>".$item["link"]."</link>\n";
          echo "  <description>".$item["desc"]."</description>\n";
          echo "  <language>zh-CN</language>\n";
          echo "</item>\n";
   }
   ?>
   </channel>
</rss>

例4-1的运行结果如图4-1所示使用IE(Internet Explorer)7或更高版本的浏览器可能显示的内容有所不同,这是因为IE 7或更高版本的浏览器默认开启了“源阅读视图”。关闭方法为:打开“工具”菜单→“Internet选项”→“内容”→选项卡→“源”,在对话框中取消勾选“打开源阅读视图”选项。

图4-1 例4-1的运行结果

4.2 使用SimpleXML解析XML

SimpleXML扩展是处理XML最简单的方法,在PHP 5中默认开启。使用SimpleXML不需要记忆繁多的DOM API,只需通过数据结构的形式访问XML即可。

4.2.1 创建一个SimpleXML对象

可以使用下面三种方法中的任意一种创建一个SimpleXML对象。

1.使用simplexml_load_file()函数载入指定文件并且将其内容解析到内存中,如例4-2所示。

实例演练

例4-2 使用simplexml_load_file()函数创建一个SimpleXML对象

$xml = simplexml_load_file('xml.xml');
echo '<pre>';
print_r(xml2array($xml));
echo '</pre>';
function xml2array($xml) {
   if(get_class($xml) == 'SimpleXMLElement') {
           $attributes = $xml->attributes();
           foreach($attributes as $k=>$v) {
                   if($v) {
                           $a[$k] = (string) $v;
                   }
   }
   $x = $xml;
   $xml = get_object_vars($xml);
   }
   if(is_array($xml)) {
           if(count($xml) == 0) {
                   return (string) $x; //
           }
           foreach($xml as $key=>$value) {
                   $r[$key] = xml2array($value);
                   if( !is_array( $r[$key] ) ) {
                   //$r[$key] = simplexml2ISOarray($value);
                   if( !is_array( $r[$key] ) ) {
                           //$r[$key] = utf8_decode( $r[$key] );
                           $r[$key] = $r[$key];
                   }
           }
           if(isset($a)) {
                   $r['@'] = $a;    //属性值
           }
           return $r;
   }
   return (string) $xml;
}

例4-2中的函数xml2array()用于遍历XML文档的内容并将其转换为数组。

2.使用simplexml_load_string()函数从字符串中获取内容并建立XML对象,如例4-3所示。

实例演练

例4-3 使用simplexml_load_string()函数创建一个XML对象

$string = <<<EOT
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
   <channel>
          <item>
                <title>新浪网</title>
                <link>http://www.sina.com.cn/</link>
                <description>国内新闻门户站点</description>
                <language>zh-CN</language>
          </item>
          <item>
                <title>大渝网</title>
                <link>http://cq.qq.com/</link>
                <description>重庆城市生活第一网</description>
                <language>zh-CN</language>
          </item>
   </channel>
</rss>
EOT;
$xml = simplexml_load_string($string);
echo '<pre>';
print_r(xml2array($xml));
echo '</pre>';

3.使用simplexml_import_dom()函数导入一个用DOM扩展创建的DOMDocument对象,如例4-4所示。

实例演练

例4-4 使用simplexml_import_dom()函数导入一个DOMDocument对象

$dom = new DOMDocument();
$dom->load('./xml.xml');
$xml = simplexml_import_dom($dom);
echo '<pre>';
print_r(xml2array($xml));
echo '</pre>';

simplexml_import_dom()函数有一个在DOM扩展中的兄弟函数——dom_import_simplexml(),使用这些相关的函数可以在两个扩展中共享相同的XML结构。例如,可以用SimpleXML修改简单的文档并且用DOM处理更加复杂的类型。

以上三种方式,PHP都返回一个SimpleXML对象。例4-2、例4-3和例4-4的运行结果完全相同,如图4-2所示。

图4-2 例4-2、例4-3和例4-4的运行结果

4.2.2 浏览SimpleXML对象

使用SimpleXML需要遵循的一些约定如下:

1.属性表示元素的迭代器。例如,循环所有在<body>中的<p>标记,可以使用以下代码:

foreach($xml->body->p as $p) {
   //对每一个<p>标记的内容进行操作
}

2.数字索引表示元素。例如,使用以下方式访问<p>标记的第一个元素:

$xml->body->p[0];

3.非数字索引表示属性。例如,使用以下方式访问<body>标记的背景属性:

$xml->body['background'];

4.允许用字符串转换访问文本数据。这意味着可以从元素中访问所有的文本数据。需要注意的是,从一个节点访问文本数据将不会自动包含子节点。因此往往需要使用asXML()方法,例如下面的代码可以输出第二个<p>标记的内容:

$string = <<<EOT
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<p>this is <a href="http://www.sina.com.cn/">link</a></p>
<p>this is <a href="http://www.163.com/">link</a></p>
</rss>
EOT;
$xml = simplexml_load_string($string);
Echo $xml->p[1]->asXML();

如果需要遍历body节点所有的子元素,可使用“children()”方法。以下代码可以遍历body节点所有的子元素:

foreach($xml->body->children() as $element) {
   //对该元素进行一些操作
}

如果需要遍历一个元素所有的属性,则可以使用“attributes()”方法。以下代码可以遍历第一个<a>标记的属性:

foreach($xml->body->p[0]->a->attributes() as $attribute) {
   //对这些属性进行操作
}

4.3 HTTP简介

Web的运行是基于HTTP(HyperText Transfer Protocol,超文本传输协议)的。HTTP规定了浏览器如何向Web服务器请求文件,以及服务器如何根据请求返回文件。当一个Web浏览器请求一个Web页面时,它会发送一个HTTP请求消息给Web服务器。这个请求消息总是包含某些头部信息(header information),有时也会有消息主体(body)。Web服务器将返回一个回应消息作为响应,回应消息也包含头部信息和消息主体。HTTP请求的第一行通常是这样的:

GET /index.html HTTP/1.1

这一行指定了一个称为方法(method)的HTTP命令,其后指明了文档的地址和正在使用的HTTP版本。这个例子中,请求是用GET方法,并采用HTTP 1.1协议来请求名称为index.html的服务器端文档。在第一行之后,请求还可能包含一些可选的头部信息,给服务器附加的数据,例如:

User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; zh-CN)
AppleWebKit/528.1 (KHTML, like Gecko) Version/4.0 Safari/528.1.1
Accept: image/gif, image/jpeg, text/*, */*

User-Agent头部信息提供Web浏览器相关的信息,而Accept头部信息指定了浏览器接受的MIME类型。在所有头部信息之后,请求会包含一个空白行,说明头部信息已经结束,如果采用了相对应的方法,则请求也可以包含附加的数据,如果请求不包含任何数据则将以一空白行结束。

Web服务器接收请求后,处理并返回一个响应。HTTP响应的第一行看起来是这样的:

HTTP/1.1 200 OK

这一行指定了协议的版本、状态码和状态码的描述。本例中状态码为200,说明请求成功(因此状态码的描述是OK)。在状态行之后,响应消息包含了一些头部信息,用于向客户端浏览器提供附加信息。例如:

Date: Thu, 14 Aug 2008 10:12:45 GMT
Server: Apache 1.3.33 (Windows) PHP/5.2.4
Content-Type: text/html
Content-Length: 130

Server这一行提供了Web服务器软件的相应信息;Content-Type指定响应中数据的MIME类型。在这些头部信息之后是一个空行,如果请求成功,空行之后就是所请求的数据。

最常用的两种HTTP方法是GET和POST。GET方法用于从服务器中获得文档、图像或数据库检索结果的信息。POST则用于向服务器发送信息,例如,用户名、密码、邮件地址或其他信息要提交到服务器,或者存储到服务器上的数据库中。当用户在浏览器的地址栏中输入一个URL并访问或者单击网页上的一个链接时,浏览器都使用GET方法。而用户提交一个表单时,既可以使用POST方法,也可以使用GET方法,具体使用哪种方法由form标签的method属性确定。

4.4 使用SOAP方式建立与调用WebService

最初,SOAP是作为XML-RPC的扩展而发展起来的,其主要强调通过从WSDL文件中所获得的方法和变量名来进行远程过程调用。SOAP的消息被称为一个SOAP Envelope,包括SOAP Header和SOAP Body。其中,SOAP Header可以方便地插入各种其他消息来扩充Web Service的功能,比如Security(采用证书访问Web Service),SOAP Body则是具体的消息正文,也就是编码后的信息。

调用SOAP时,实际上是向一个URL地址发送HTTP POST报文(根据SOAP规范,HTTP GET报文也可被支持),调用方法的名字在HTTP Request Header SOAP-Action中给出,接下来就是SOAP Envelope了。服务端接到请求,执行计算,将返回结果转换成XML,以HTTP方式返回给客户端。

目前使用PHP做SOAP开发一般有三种方式:

● PEAR SOAP扩展

● PHP SOAP扩展

● NuSOAP(纯PHP代码实现,在PHP 4时期使用较多)

PHP 5中新增了被称为“ext/soap”的内置SOAP扩展,作为PHP标准发行包的一部分,不需要另外单独下载、安装和管理。它比PEAR SOAP扩展具备更多的特性,而且与PEAR SOAP扩展使用PHP编写不同的是,它使用C语言编写,因此它的速度要比纯PHP代码实现的方式快。

PHP SOAP扩展还支持WSDL(发音参考“wizdel”,Web Services Description Language,网络服务描述语言)。WSDL是一个用来描述Web服务的XML词汇集,通过载入WSDL文件,SOAP扩展可以确定例如端点、程序和可以用来连接到一个端点的信息类型等内容。

一个典型的SOAP应用分为服务器端和客户端两部分,例4-5是一个简单的服务器端程序的代码。

实例演练

例4-5 一个简单的SOAP服务器端程序

<?php
ini_set("soap.wsdl_cache_enabled", "0");
class ExampleService {
   /*
    * @param string $name
    * @return string
    */
   public function hello($name) {
          if($name) {
                  return '你好,'.$name;
          } else {
                  throw new SoapFault("Server", "没有输入昵称");
          }
   }
}
$server = new SoapServer('ExampleService.wsdl', array('location' =>
'http://localhost/soap/ExampleService.php', 'uri' => "ExampleService",
'encoding' => 'UTF-8'));
$server->setClass("ExampleService");
$server->handle();
?>

第二行使用函数ini_set()将soap.wsdl_cache_enabled设置为0,表示不缓存WSDL内容。接着定义了一个类ExampleService,这里需要注意的是,为类的方法编写注释信息,如果注释信息不够全面或完善,就会导致使用Zend Studio该功能在Zend Studio 6.0版本下运行正常,在7.0或更新版本中存在问题。生成的WSDL文档内容不够完整,从而客户端在调用SOAP的时候无法获得完整的方法信息。接下来使用PHP SOAP类SoapServer和方法setClass()实例化了一个服务器端SOAP对象并将其提交到Web服务器运行。

接着可以使用Zend Studio生成一个WSDL文档,以供客户端程序调用,单击File→Export…命令,选择WSDL File,并选择类文件和其中要作为接口提供给客户端使用的类名称,以及确定生成的WSDL文档名称即可,如图4-3所示。

图4-3 使用Zend Studio生成WSDL文档

生成的WSDL文档内容如例4-6所示。

实例演练

例4-6 使用Zend Studio生成的WSDL文档内容

<?xml version='1.0' encoding='UTF-8'?>
<!-- WSDL file generated by Zend Studio.-->
<definitions name="ExampleService" targetNamespace="urn:ExampleS ervice" xmlns:typens="urn:ExampleService" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/wsdl/ soap/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns="http://schemas.xmlsoap.org/wsdl/">
   <message name="hello">
          <part name="name"/>
   </message>
   <message name="helloResponse">
          <part name="helloReturn"/>
   </message>
   <portType name="ExampleServicePortType">
          <operation name="hello">
                 <input message="typens:hello"/>
                 <output message="typens:helloResponse"/>
          </operation>
   </portType>
   <binding name="ExampleServiceBinding"
                  type="typens:ExampleServicePortType">
          <soap:binding style="rpc"
                  transport="http://schemas.xmlsoap.org/soap/http"/>
          <operation name="hello">
                 <soap:operation soapAction="urn:ExampleServiceAction"/>
                 <input>
                        <soap:body namespace="urn:ExampleService"
                        use="encoded" encodingStyle=
                        "http://schemas.xmlsoap.org/soap/encoding/"/>
                 </input>
                 <output>
                        <soap:body namespace="urn:ExampleService"
                        use="encoded" encodingStyle=
                        "http://schemas.xmlsoap.org/soap/encoding/"/>
                 </output>
          </operation>
   </binding>
   <service name="ExampleServiceService">
          <port name="ExampleServicePort"
                binding="typens:ExampleServiceBinding">
                <soap:address location=""/>
          </port>
   </service>
</definitions>

最后就是编写客户端代码,相对于服务器端程序,客户端代码要简单得多,如例4-7所示。

实例演练

例4-7 SOAP客户端代码

<?php
$param = array('name' => '超人');
$client = new SoapClient('ExampleService.wsdl', array('location' =>
'http://localhost/soap/ExampleService.php', 'uri' => "ExampleService",
'encoding' => 'UTF-8'));
try {
   echo $client->__soapCall('hello', $param)."\n";//调用hello()方法
} catch(SoapFault $e) {
   echo 'Code: '.$e->faultcode."<br />".'Info: '.$e->faultstring."\n";
}
?>

首先要使用PHP SOAP类SoapClient实例化一个客户端SOAP对象,接下来即可采用try…catch…语句结构调用服务器端的SOAP方法,同时若程序出错可以捕捉到相应的异常信息。这里需要注意的是,客户端在调用SOAP接口的方法时,方法__soapCall()的第二个参数一定要为数组,否则可能出错。

运行以上客户端程序,结果如图4-4所示。

图4-4 SOAP客户端调用服务器端方法的运行结果

若程序出现错误或Web服务器配置不当,则客户端会输出错误信息,如图4-5所示。

图4-5 调用SOAP方法出错时的提示信息

由于SOAP是跨语言和平台的技术,所以PHP完全可以调用.NET或Java等语言编写的WebService,从而能够更好地促进不同语言开发的系统之间的沟通与联系,使不同的平台之间紧密集成,如图4-6所示的就是一个.NET编写的WebService的介绍信息页面,虽然看不到其实际代码,但是根据其“暴露”出来的接口信息,依然可以使用PHP调用它完成所需的功能。

图4-6 使用.NET编写的WebService

单击页面中的“服务说明”链接,可以看到其对应的WSDL文档信息,如图4-7所示。

图4-7 图4-6中的WebService的WSDL文档

可以使用如例4-8所示的代码来调用这个WebService。

实例演练

例4-8例4-8和图4-6中的某些不影响读者阅读的敏感信息已被屏蔽。使用SOAP方式调用图4-6中的WebService的PHP代码

<?php
$dc_userno = '**********';
$client = new SoapClient('http://************/login.asmx?WSDL', array('encoding' => 'GBK'));
$param = array('userNo' => $dc_userno); //传入的参数
$result = $client->__soapCall('GetUserName', array('parameters' =>$param)); //调用GetUserName()方法
$dcTrueName = $result->GetUserNameResult;
echo '姓名: '.$dcTrueName.'<br />';
$result = $client->__soapCall('GetUserSex', array('parameters' =>$param)); //调用GetUserSex()方法
$dcGender = $result->GetUserSexResult;
echo '性别: '.$dcGender.'<br />';
$result = $client->__soapCall('GetUserDw', array('parameters' =>$param)); //调用GetUserDw()方法
$dcUnit = $result->GetUserDwResult;
echo '单位: '.$dcUnit.'<br />';
$result = $client->__soapCall('GetUserKind', array('parameters' =>$param)); //调用GetUserKind()方法
$dcUserKind = $result->GetUserKindResult;
echo '用户类型: '.$dcUserKind.'<br />';
?>

例4-8的执行结果如图4-8所示。

图4-8 例4-8的执行结果

4.5 边学边练

本章的两个实例功能相近,差别在于做法不同,4.5.1节使用XML-RPC的方式,而4.5.2节使用SOAP方式,读者可对比揣摩。

4.5.1 实例一:两个数的简单四则运算——XML-RPC服务器与客户端

本例的目标是客户端程序通过调用服务器端程序暴露出来的4个远程方法。

● plus:返回两个数的和

● minus:返回第一个数与第二个数的差

● multi:返回两个数的积

● devide:返回第一个数与第二个数的商

下面来完成两个数的简单四则运算。例4-9是本例的服务器端程序。

实例演练

例4-9 处理两个数的四则运算的服务器端程序

<?
function plus($method, $args) {
   re t u r n $ a r g s [ 0 ] .' + ' .$ a r g s [ 1 ] .' = ' .( i n t v a l ( $ a r g s [ 0 ] ) +
intval($args[1]));
}
function minus($method, $args) {
   r e t u r n $ a r g s [ 0 ] .' - ' .$ a r g s [ 1 ] .' = ' .( i n t v a l ( $ a r g s [ 0 ] ) -
intval($args[1]));
}
function multi($method, $args) {
   r e t u r n $ a r g s [ 0 ] .' * ' .$ a r g s [ 1 ] .' = ' .( i n t v a l ( $ a r g s [ 0 ] ) *
intval($args[1]));
}
function devide($method, $args) {
   r e t u r n $ a r g s [ 0 ] .' / ' .$ a r g s [ 1 ] .' = ' .( i n t v a l ( $ a r g s [ 0 ] ) /
intval($args[1]));
}
$server = xmlrpc_server_create();
if(!$server) die("建立 XML-RPC 服务器失败!");
//在服务器端注册四则运算对应的方法
xmlrpc_server_register_method($server, 'plus', 'plus');
xmlrpc_server_register_method($server, 'minus', 'minus');
xmlrpc_server_register_method($server, 'multi', 'multi');
xmlrpc_server_register_method($server, 'devide', 'devide');
extract($_POST);
$request = xmlrpc_encode_request($method, $arguments, array("encoding" =>"utf-8"));
$response = xmlrpc_server_call_method($server, $request, $arguments);
$decoded = xmlrpc_decode($response, "utf-8");
if(xmlrpc_is_fault($decoded)) {
   die("调用发生错误");
} else {
   echo $decoded;
}
xmlrpc_server_destroy($server);
?>

在定义服务器端函数的时候需要注意,这些函数实际上是从第二个参数开始处理的,如果忽视这一点将可能导致未知的错误。

函数xmlrpc_server_create()用来建立一个XML-RPC的服务器端对象,若此过程失败,将会返回一条错误信息。

函数xmlrpc_server_register_method()用来将服务器端定义的函数添加到将暴露给客户端程序调用的远程方法列表中。其第一个参数是前边建立的XML-RPC服务器端对象,第二个参数是XMLRPC客户端调用的方法名称,第三个参数是对应的XML-RPC服务器端定义的函数名称。

函数xmlrpc_encode_request()根据客户端提交的信息生成符合服务器端需要的XML文档(其中包含远程方法名和该方法要求的参数数组),此文档被传递给函数xmlrpc_server_call_method(),由其负责调用服务器端定义的函数并将客户端提交的远程方法参数数组传递给该函数。函数xmlrpc_server_call_method()默认的返回内容是XML格式的文档,其第4个参数是一个将选项名称映射到其值的数组,这些选项如表4-1所示。

表4-1 函数“xmlrpc_server_call_method()”第4个参数数组的可选项

函数xmlrpc_decode()将xmlrpc_server_call_method()的调用结果转换为PHP格式的内容,而函数xmlrpc_is_fault()则用来在这些内容中检测是否包含错误信息,如果其返回值为真,则表明XMLRPC服务器端程序返回了一个错误响应。

函数xmlrpc_server_destroy()用来销毁前边建立的XML-RPC对象,释放资源。

相对于服务器端程序,本例的客户端程序则简单得多,例4-10是本例的客户端程序。

实例演练

例4-10 处理两个数的四则运算的客户端程序

<?
$url = 'http://localhost/xml-rpc.php';
$methods = array('plus', 'minus', 'multi', 'devide', 'error_test');
$arguments = array(1, 2);
foreach($methods as $method) {
//向服务器端提交参数
   $result = post_request($url, array('method' => $method, 'arguments' =>$arguments));
   if($result[0] == 0) {
          echo $result[1].'<br />';
          continue;
   } else {
          echo '提交过程中发送错误,信息如下:<br />'.$result[1];
          continue;
   }
}
function post_request($url, $params) {
   $str = '';
   foreach ($params as $k=>$v) {
           if (is_array($v)) {
                   foreach ($v as $kv => $vv) {
                           $str .= '&' .$k .'[' .$kv  .']=' .urlencode($vv);
                   }
           } else {
                   $str .= '&' .$k .'=' .urlencode($v);
           }
   }
   if(function_exists('curl_init')) {
          // 如果安装了 CURL 扩展
          $ch = curl_init();
          curl_setopt($ch, CURLOPT_POST, 1);
          curl_setopt($ch, CURLOPT_URL, $url);
          curl_setopt($ch, CURLOPT_POSTFIELDS, $str);
          curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
          curl_setopt($ch, CURLOPT_USERAGENT, 'XML-RPC Client 0.1 (curl)' .phpversion());
          $result = curl_exec($ch);
          $errno = curl_errno($ch);
          curl_close($ch);
          return array($errno, $result);
   } else {
          // 如果未安装 CURL 扩展
          $context =
          array('http' =>
          array('method' => 'POST',
                  'header' => 'Content-type: application/x-www-formurlencoded'."\r\n".
                  'U s e r - A g e n t : X M L - R P C C l i e n t 0 .1 ( n o n - c u r l )'.phpversion()."\r\n".
                  'Content-length: ' .strlen($str),
                  'content' => $str));
          $contextid = stream_context_create($context);
          $sock = fopen($url, 'r', false, $contextid);
          if($sock) {
                  $result = '';
                  while(!feof($sock)) {
                  $result .= fgets($sock, 4096);
                  }
                  fclose($sock);
          }
   }
   return array(0, $result);
}
?>

数组$methods中列出了将要调用的远程方法名称,其中前4种是服务器端提供的,第5个则是一个不存在的远程方法,用于测试发生错误的情况。

函数post_request()则使用两种方法模拟了使用POST方式提交数据的过程:使用CURL扩展或数据流的读/写。该函数的返回值是一个数组,数组的第一个元素表示是否出错(值为“0”表示未出错,否则表示错误编号),第二个元素表示XML-RPC服务器端的实际返回信息。

本实例的执行结果如图4-9所示。

图4-9 两个数的简单四则运算实例的执行结果

图4-9中,前4行是分别调用plus()、minus()、multi()和devide()4个服务器端方法后的执行结果,第5行则是调用了一个不存在的方法“error_test()”后的执行结果。

4.5.2 实例二:使用SOAP方式重写4.5.1节的实例

在4.5.1节中使用XML-RPC的方式实现了客户端程序调用服务器端提供的两个数的四则运算,在本例中将重写其服务器端和客户端代码,以SOAP方式提供两个数的四则运算的WebService。

使用SOAP方式的WebService服务器端代码与XML-RPC方式大同小异,主要内容还是4个可以被客户端调用的函数。服务器端代码如例4-11所示。

实例演练

例4-11 SOAP方式的WebService服务器端代码

<?php
ini_set("soap.wsdl_cache_enabled", "0");
class soapinterface{
   /*
    * @param int $opnum1
    * @param int $opnum2
    * @return $result
    */
   public function plus($opnum1, $opnum2) {
          $opnum1 = intval($opnum1);
          $opnum2 = intval($opnum2);
          $result = $opnum1.'+'.$opnum2.'='.(intval($opnum1) + intval($opnum2));
          return $result;
   }
   /*
    * @param int $opnum1
    * @param int $opnum2
    * @return $result
    */
   public function minus($opnum1, $opnum2) {
          $opnum1 = intval($opnum1);
          $opnum2 = intval($opnum2);
          $result = $opnum1.'-'.$opnum2.'='.(intval($opnum1) -intval($opnum2));
          return $result;
   }
   /*
    * @param int $opnum1
    * @param int $opnum2
    * @return $result
    */
   public function multi($opnum1, $opnum2) {
          $opnum1 = intval($opnum1);
          $opnum2 = intval($opnum2);
          $result = $opnum1.'*'.$opnum2.'='.(intval($opnum1) * intval($opnum2));
          return $result;
   }
   /*
    * @param int $opnum1
    * @param int $opnum2
    * @return $result
    */
   public function devide($opnum1, $opnum2) {
          $opnum1 = intval($opnum1);
          $opnum2 = intval($opnum2);
          $result = $opnum1.'/'.$opnum2.'='.(intval($opnum1) / intval($opnum2));
          return $result;
   }
}
$soapinterface = new SoapServer('server.wsdl', array('location' =>'http://localhost/soap/server.php', 'uri' => "soapinterface", 'encoding'=> 'UTF-8'));
$soapinterface->setClass("soapinterface");
$soapinterface->handle();
?>

完成服务器端代码后,同样,使用Zend Studio自动生成所需的WSDL文档,其内容如例4-12所示。

实例演练

例4-12 使用Zend Studio自动生成的WSDL文档内容

<?xml version='1.0' encoding='UTF-8'?>
<!-- WSDL file generated by Zend Studio.-->
<definitions name="server" targetNamespace="urn:server" xmlns:typens="urn:server" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:soapenc="http:// schemas.xmlsoap.org/soap/encoding/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns="http://schemas.xmlsoap.org/wsdl/">
  <message name="devide">
         <part name="opnum1"/>
         <part name="opnum2"/>
  </message>
  <message name="devideResponse">
         <part name="devideReturn"/>
  </message>
  <message name="minus">
         <part name="opnum1"/>
         <part name="opnum2"/>
  </message>
  <message name="minusResponse">
         <part name="minusReturn"/>
  </message>
  <message name="multi">
         <part name="opnum1"/>
         <part name="opnum2"/>
  </message>
  <message name="multiResponse">
         <part name="multiReturn"/>
  </message>
  <message name="plus">
         <part name="opnum1"/>
         <part name="opnum2"/>
  </message>
  <message name="plusResponse">
         <part name="plusReturn"/>
  </message>
  <portType name="soapinterfacePortType">
         <operation name="devide">
                <input message="typens:devide"/>
                <output message="typens:devideResponse"/>
         </operation>
         <operation name="minus">
                <input message="typens:minus"/>
                <output message="typens:minusResponse"/>
         </operation>
         <operation name="multi">
                <input message="typens:multi"/>
                <output message="typens:multiResponse"/>
         </operation>
         <operation name="plus">
                <input message="typens:plus"/>
                <output message="typens:plusResponse"/>
         </operation>
         </portType>
                <binding name="soapinterfaceBinding"
                type="typens:soapinterfacePortType">
                <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/ soap/http"/>
                <operation name="devide">
                       <soap:operation soapAction="urn:soapinterfaceAction"/>
                       <input>
                             <soap:body namespace="urn:server" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
                       </input>
                       <output>
                             <soap:body namespace="urn:server" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
                       </output>
                </operation>
                <operation name="minus">
                       <soap:operation soapAction="urn:soapinterfaceAction"/>
                       <input>
                             <soap:body namespace="urn:server" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
                       </input>
                       <output>
                             <soap:body namespace="urn:server" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
                       </output>
                </operation>
                <operation name="multi">
                       <soap:operation soapAction="urn:soapinterfaceAction"/>
                       <input>
                             <soap:body namespace="urn:server" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
                       </input>
                       <output>
                             <soap:body namespace="urn:server" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
                       </output>
                </operation>
                <operation name="plus">
                       <soap:operation soapAction="urn:soapinterfaceAction"/>
                       <input>
                             <soap:body namespace="urn:server" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
                       </input>
                       <output>
                             <soap:body namespace="urn:server" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
                       </output>
                </operation>
         </binding>
  <service name="serverService">
         <port name="soapinterfacePort"
                binding="typens:soapinterfaceBinding">
                <soap:address location=""/>
         </port>
  </service>
</definitions>

最后就是客户端程序,代码如例4-13所示。

实例演练

例4-13 SOAP方式的WebService客户端代码

<?php
$param = array('opnum1' => 2, 'opnum2' => 4);
$client = new SoapClient('server.wsdl', array('location' => 'http:// localhost/soap/server.php', 'uri' => "soapinterface", 'encoding' => 'UTF-8'));
try {
//传入参数,调用四则运算对应的方法
   echo $client->__soapCall('plus', $param)."<br />";
   echo $client->__soapCall('minus', $param)."<br />";
   echo $client->__soapCall('multi', $param)."<br />";
   echo $client->__soapCall('devide', $param)."<br />";
   echo $client->__soapCall('not_exist_function', $param)."<br />";
} catch(SoapFault $e) {
   echo 'Code: '.$e->faultcode."<br />".'Info: '.$e->faultstring."\n";
}
?>

例4-13中,在正确调用了服务器端提供的4个方法后,也测试性地调用了一个不存在的服务器端方法——not_exist_function()以查看SOAP的返回结果,结果是程序捕捉到了发生的异常,抛出了相应的信息。本实例的执行结果如图4-10所示。

图4-10 使用SOAP方式重写的WebService的执行结果