Soap 是 PHP 开发 RPC 数据通讯的常用方式,有 WSDL 和 NON-WSDL 两种模式。这里主要介绍 WSDL 模式 RPC 深层工作原理。以下是引用维基百科的两段说明:

WSDL(Web 服务描述语言Web Services Description Language )是为描述 Web 服务发布的 XML 格式。

WSDL 描述 Web 服务的公共接口。这是一个基于 XML 的关于如何与 Web 服务通讯和使用的服务描述;也就是描述与目录中列出的 Web 服务进行交互时需要绑定的协议和信息格式。通常采用抽象语言描述该服务支持的操作和信息,使用的时候再将实际的网络协议和信息格式绑定给该服务。

使用 PHP 创建 SOAP-WSDL 服务

HelloServerWsdl.php:

<?php

function hello($someone) { 
    #return file_get_contents('php://input');
    #return print_r($_SERVER, true);
    return "Hello " . $someone . "! - With WSDL";
} 

ini_set('soap.wsdl_cache_enabled', 0); 
$server = new SoapServer(
    'http://php.laijinman.com/soap/Hello.wsdl', 
    [
        'soap_version' => SOAP_1_2,
    ]
);
$server->addFunction('hello'); 
$server->handle();

Hello.wsdl:

<?xml version="1.0"?>
<definitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns="http://schemas.xmlsoap.org/wsdl/">

  <message name="req">
    <part name="someone" type="xsd:string"/>
  </message>

  <message name="res">
    <part name="return" type="xsd:string"/>
  </message>

  <portType name="portTypeName">
    <operation name="hello">
      <input message="tns:req"/>
      <output message="tns:res"/>
    </operation>
  </portType>

  <binding name="bindingName" type="tns:portTypeName">
    <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
    <operation name="hello">
      <soap:operation soapAction=""/>
      <input>
        <soap:body use="encoded" namespace="urn:inputNamespace" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
      </input>
      <output>
        <soap:body use="encoded" namespace="urn:outputNamespace" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
      </output>
    </operation>
  </binding>

  <service name="serviceName">
    <port name="portName" binding="tns:bindingName">
      <soap:address location="http://php.laijinman.com/soap/HelloServerWsdl.php"/>
    </port>
  </service>

</definitions>

HelloClientWsdl.php:

<?php
ini_set('soap.wsdl_cache_enabled', 0); 

class Client extends SoapClient
{

    public function __doRequest($request, $location, $action, $version, $one_way = NULL) {
        #var_dump(func_get_args());
        #die();

        return parent::__doRequest($request, $location, $action, $version, $one_way);
    }
}

$client = new SoapClient(
#$client = new Client(
    'http://php.laijinman.com/soap/Hello.wsdl',
    [
        'soap_version' => SOAP_1_2,
        'trace' => 1,
    ]
);

var_dump($client->__getFunctions());
var_dump($client->__getTypes());

$res = $client->hello('world');
#$res = $client->__soapCall('hello', ['world']);
var_dump($res);

var_dump($client->__getLastRequestHeaders());
var_dump($client->__getLastRequest());
var_dump($client->__getLastResponseHeaders());
var_dump($client->__getLastResponse());

运行程序 http://php.laijinman.com/soap/HelloClientWsdl.php,输出结果:

/home/laijinman/data/websites/php/soap/HelloClientWsdl.php:24:
array (size=1)
  0 => string 'string hello(string $someone)' (length=29)

/home/laijinman/data/websites/php/soap/HelloClientWsdl.php:25:
array (size=0)
  empty

/home/laijinman/data/websites/php/soap/HelloClientWsdl.php:29:string 'Hello world! - With WSDL' (length=24)

/home/laijinman/data/websites/php/soap/HelloClientWsdl.php:31:string 'POST /soap/HelloServerWsdl.php HTTP/1.1
Host: php.laijinman.com
Connection: Keep-Alive
User-Agent: PHP-SOAP/7.0.14
Content-Type: application/soap+xml; charset=utf-8; action=""
Content-Length: 453

' (length=204)

/home/laijinman/data/websites/php/soap/HelloClientWsdl.php:32:string '<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:ns1="urn:inputNamespace" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:enc="http://www.w3.org/2003/05/soap-encoding"><env:Body><ns1:hello env:encodingStyle="http://www.w3.org/2003/05/soap-encoding"><someone xsi:type="xsd:string">world</someone></ns1:hello></env:Body></env:Envelope>
' (length=453)

/home/laijinman/data/websites/php/soap/HelloClientWsdl.php:33:string 'HTTP/1.1 200 OK
Server: nginx/1.10.2
Date: Sat, 24 Dec 2016 06:43:32 GMT
Content-Type: application/soap+xml; charset=utf-8
Content-Length: 565
Connection: close
X-Powered-By: PHP/7.0.14

' (length=193)

/home/laijinman/data/websites/php/soap/HelloClientWsdl.php:34:string '<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:ns1="urn:outputNamespace" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:enc="http://www.w3.org/2003/05/soap-encoding"><env:Body xmlns:rpc="http://www.w3.org/2003/05/soap-rpc"><ns1:helloResponse env:encodingStyle="http://www.w3.org/2003/05/soap-encoding"><rpc:result>return</rpc:result><return xsi:type="xsd:string">Hello world! - With WSDL</return></ns1:helloResponse></env:Body></env:Envelope>
' (length=565)

分析

SOAP-WSDL RPC 实际上是基于 HTTP 协议,向服务端http://php.laijinman.com/soap/HelloServerWsdl.php以 POST 方式发送了一段 XML 数据。

主要可以看两个关键点:

向服务器发送 XML 数据(var_dump($client->__getLastRequest());):

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:ns1="urn:inputNamespace" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:enc="http://www.w3.org/2003/05/soap-encoding">
  <env:Body>
    <ns1:hello env:encodingStyle="http://www.w3.org/2003/05/soap-encoding">
      <someone xsi:type="xsd:string">world</someone>
    </ns1:hello>
  </env:Body>
</env:Envelope>

接收服务器返回的 XML 数据(var_dump($client->__getLastResponse());):

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:ns1="urn:outputNamespace" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:enc="http://www.w3.org/2003/05/soap-encoding">
  <env:Body xmlns:rpc="http://www.w3.org/2003/05/soap-rpc">
    <ns1:helloResponse env:encodingStyle="http://www.w3.org/2003/05/soap-encoding">
      <rpc:result>return</rpc:result>
      <return xsi:type="xsd:string">Hello world! - With WSDL</return>
    </ns1:helloResponse>
  </env:Body>
</env:Envelope>
  • 客户端把 RPC 函数调用封装成一个 XML 格式的字符串,通过 HTTP 调用 POST 到服务器的接口端。
  • 服务器接收到 XML 数据,解析内容,并调用对应的接口进行处理,把结果再次封装成 XML 内容,并输出。
  • 客户端把服务器端输出的 XML 内容,并解析出返回结果,并返回给函数调用者。

其它方式实现

通过分析 SOAP 的底层实现原理知道,所谓的 SOAP RPC 调用,其实就是向服务器以 HTTP+POST 方式发送一段 XML 数据,并获取到服务器端输出到一段 XML 数据,通过解析 XML 就可以获取到需要的调用结果了。

这里举例使用其它方式调用 SOAP RPC 的方法:

  • PHP+Curl 方式:
<?php

$ch = curl_init();

$req = '<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:ns1="urn:inputNamespace" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:enc="http://www.w3.org/2003/05/soap-encoding"><env:Body><ns1:hello env:encodingStyle="http://www.w3.org/2003/05/soap-encoding"><someone xsi:type="xsd:string">world</someone></ns1:hello></env:Body></env:Envelope>';

$opts = [
    CURLOPT_URL => 'http://php.laijinman.com/soap/HelloServerWsdl.php',
    CURLOPT_RETURNTRANSFER => 1,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => $req,
];

curl_setopt_array($ch, $opts);
$res = curl_exec($ch);

var_dump($res);

输出:

/home/laijinman/data/websites/php/soap/curl.php:18:
string(565) "<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:ns1="urn:outputNamespace" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:enc="http://www.w3.org/2003/05/soap-encoding"><env:Body xmlns:rpc="http://www.w3.org/2003/05/soap-rpc"><ns1:helloResponse env:encodingStyle="http://www.w3.org/2003/05/soap-encoding"><rpc:result>return</rpc:result><return xsi:type="xsd:string">Hello world! - With WSDL</return></ns1:helloResponse></env:Body></env:Envelope>
"
  • Shell+Curl 方式:
curl http://php.laijinman.com/soap/HelloServerWsdl.php \
-d '<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:ns1="urn:inputNamespace" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:enc="http://www.w3.org/2003/05/soap-encoding"><env:Body><ns1:hello env:encodingStyle="http://www.w3.org/2003/05/soap-encoding"><someone xsi:type="xsd:string">world</someone></ns1:hello></env:Body></env:Envelope>'

输出:

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:ns1="urn:outputNamespace" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:enc="http://www.w3.org/2003/05/soap-encoding"><env:Body xmlns:rpc="http://www.w3.org/2003/05/soap-rpc"><ns1:helloResponse env:encodingStyle="http://www.w3.org/2003/05/soap-encoding"><rpc:result>return</rpc:result><return xsi:type="xsd:string">Hello world! - With WSDL</return></ns1:helloResponse></env:Body></env:Envelope>