使用 WSDL 部署 Web 服务

WSDL 和 SOAP

为更好理解 WSDL 是如何工作的,我将首先描述 SOAP 和 HTTP 是如何使用 WSDL 工作的。WSDL 的用途是“描述”您的 Web 服务。业务之间将通过交换 WSDL 文件来理解对方的服务。一旦知道您伙伴的服务并希望调用它们,SOAP 就派上用场了。可以将服务看作是通过 SOAP 访问的对象。

最有可能的情况是,您将通过因特网或电子邮件与潜在伙伴通信。当然,因特网使用 HTTP 而电子邮件以 SMTP 方式工作,这使得 HTTP 和 SMTP 成为作为 SOAP 的“传输服务提供者”的有利候选人。

WSDL 编写

现在,我将讲述为 Web 服务编写 WSDL 的过程。目的是公开现有的 Web 服务。您所处的情况也许就是下列情况之一:

  • 您有一个现存的服务(例如,一个网站),并希望表示它的功能性。
  • 您有一个 WSDL,并且希望依照已经决定表示的功能性来实现 Web服务器端的逻辑。(有些人也许会认为这是一个不可能的方案,但是 UDDI 的指纹概念使它变得极为可能;我将在本系列的第四部分讨论 UDDI)。
  • 您正在从零开始,并且既无网站又无 WSDL 界面。

本文中所涵盖的信息适用于这些可能性中的任意一种或全部。

WSDL 编写的四个步骤

我将把 WSDL 编写分成四个简单步骤。遵循每个步骤,您的 Web 服务将准备就绪用于部署。

步骤 1:服务接口

您将构建一个移动电话销售公司的服务接口作为样本项目(我将这个服务称为 MobilePhoneService )。该公司销售不同型号的移动电话,所以公司 Web 服务的后端数据存储库中将包含一个具有两列( model number 和 price )的表格。(为了将焦点保持在 WSDL 本身,我保持该表格的简单性)。有两个关于要使用 WSDL 表示的服务的方法:

  • getListOfModels ()
  • getPrice (modelNumber)

GetListOfModels 方法提供了一个字符串数组,其中每个字符串表示一种移动电话的型号。 GetPrice 获得型号,然后返回它的价格。WSDL 将这些方法作为操作调用。现在将开始构建“WSDL 接口文件( WSDL interface file )”。

每个 WSDL 文件的根元素都是 <definitions> ,必须在其中提供服务的完整描述。首先,必须在 <definitions> 元素中提供各种名称空间的声明。三个必须做的外部名称空间声明是 WSDL、SOAP 和 XSD(XML 模式定义)。还有一个名称空间 ― TNS,它指您的 MobilePhoneService(这表示 TNS(targetNamespace 的缩写)包含专为 MobilePhoneService 定义的所有元素和属性的名称)。但是 WSDL 是您将在 WSDL 编写中使用得最多的主要名称空间。在本系列文章中使用到其它名称空间时,我将提到它们的效用。

关于名称空间只要注意一点:WSDL 广泛地使用名称空间这一概念。我鼓励您到 W3C 的官方网站去学习关于名称空间的更多知识(请参阅 参考资料)。WSDL 是这种思想的一种实现,因为名称空间提供了无限的灵活性,而这恰恰是用于电子数据交换的可移植格式所需要的。

<definitions> 元素包含一个或多个 <portType> 元素,实际上,每个元素都是您希望表示的一系列 operation 。或者,您也可以将单个 portType 元素看作是将各种方法组成类的一个逻辑分组。例如,如果您的供应链管理解决方案需要在客户和供应商之间进行交互,您最可能做的是分别定义与他们交互的功能性;也就是说,您将为用户和供应商各定义一个 portType。应该将每个 portType 称为 服务,因此整个 WSDL 文件将成为一个服务集合。

必须为每个服务提供一个名称。在本例中,仅有一个服务(因此只有一个 <portType> )。 需要使用该 portType 元素的 name 属性为移动电话销售服务指定名称。

在每个服务内可以有几个方法、或者 operation ,WSDL 通过 <operation> 元素来引用它们。样本应用程序有两个要表示的方法:getListOfModels 和 getPrice 。因此,您需要提供两个 <operation> 元素,每个元素有一个 name 。 我已经使用 <operation> 元素的 name 属性命名了每个操作。

此时,WSDL 文件看上去象 清单 1

清单 1:定义操作
<?xml version="1.0" encoding="UTF-8" ?>
<definitions  name="MobilePhoneService"
     targetNamespace="www.mobilephoneservice.com/MobilePhoneService-interface"
     xmlns="http://schemas.xmlsoap.org/wsdl/"
     xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
     xmlns:tns="http://www.mobilephoneservice.com/MobilePhoneService"
     xmlns:xsd="http://www.w3.org/1999/XMLSchema">
     <portType name="MobilePhoneService_port">
             <operation name="getListOfModels ">
                     .......
                     .......
             </operation>
             <operation name="getPrice">
                     .......
                         .......
             </operation>
     </portType>
</definitions>

步骤 2:指定参数

定义好操作(或方法)以后,现在需要指定将向它们发送和从它们返回的参数。在 WSDL 术语中,所有参数称为“消息”。认为您是在递送消息而结果得到返回的消息是有用的。方法调用是这样一种操作:它准备返回“消息”来响应进入的消息。

请回忆,在第一步骤中有两个操作要表示。第一个操作 getListOfModels 不必获得任何参数并且返回一个字符串数组,其中每个字符串表示移动电话的型号。因此,必须定义一个包含字符串数组的 <message> 元素。

看看 清单 2 中的各种 <message> 元素。其中的第一个元素有一个等于 ListOfPhoneModels 的名称属性(该消息的逻辑名称),以及名称为 models 的单个 <part> 元素,这意味着该 ListOfPhoneModels 是一个“只含有一个 part 的”消息,其中仅有的这个 part 的名称是“models”。消息可以有任意多个 part ― 只要为它们起不同的名称,以唯一标识。

我已包括了 <part> 元素的另一个属性,它就是 type 。将这个“type”属性当作 C++ 或 Java 中的数据类型。我已经将 models 的数据类型指定为 tns:Vector。(请回忆,我在 <definitions> 根元素中指定了一些名称空间,其中之一是 tns 。)这个类型即指MobilePhoneService 名称空间。这意味着当编写 WSDL 时,您可以创建自己的名称空间。现在您也许会问两个逻辑问题:为什么?和怎么做?

要回答 为什么,让我们以 getListOfModels 操作返回的字符串数组为例。WSDL 使用 XML 模式定义(XSD)定义的一些原始数据类型(诸如 int、float、long、short、byte、string、Boolean 等等),并允许您直接使用它们,或者以这些原始数据类型构建复杂数据类型后,在消息中使用它们。这就是为什么当引用复杂数据类型时,您需要定义自己的名称空间。在本例中,需要为 array of strings 构建一个复杂数据类型。

现在来看 怎么做问题,您将使用 XSD 创建自己的名称空间。为实现这个目的,我在 <types> 元素中使用了 xsd:complexType 元素用来定义称为 Vector 的数据类型。 Vector 使用两个原始数据类型:string(元素数据)和 Integer(元素计数)。因此, Vector 成为名称空间的一部分并可以通过别名 tns 来引用。

在 清单 2 中,我以类似的方式定义了另外两个消息 PhoneModel 和 PhoneModelPrice 。这两个消息只使用了 xsd 名称空间中的原始数据类型 string,因此您不必为使用它们而定义任何更复杂的数据类型。

您也许已经注意到当创建 <message> 元素时,没有指定这些消息是进入参数还是返回值。这是一个您将在 <portType> 元素内的<operation> 元素中完成的工作。因此,正如您在 清单 2 中所看到的,我已经将 <input> 和 <output> 元素都添加到这两个操作中。每个 input 元素通过消息名来引用它并将它当作用户调用该操作时要提供的参数。类似地,每个 <output> 元素引用一个消息;它将该消息当作操作调用的返回值。

至今, 清单 2准确地限定了目前的讨论的范围。

步骤 3:消息传递和传输

我以一种抽象方式定义了操作和消息,而不考虑实现的细节。实际上,WSDL 的任务是定义或描述 Web 服务,然后提供一个对外部框架的引用来定义 WSDL 用户将如何实现这些服务。可以将这个框架当作 WSDL 抽象定义和它们的实现之间的“绑定( binding )”。

当前,最流行的绑定( binding )技术是使用简单对象访问协议(SOAP)。WSDL 将指定能够访问 Web 服务实际实现的 SOAP 服务器,并且从那时起 SOAP 的整个任务就是将用户从 WSDL 文件带到它的实现。SOAP 是本系列文章中下一部分的主题,所以我将暂时避免讨论 SOAP 细节而继续集中讲述 WSDL 编写。

WSDL 编写的第三个步骤是描述将 SOAP 与 WSDL 文件绑定到一起的过程。您将把 <binding> 元素包括到 <definitions> 元素内。这个 binding 元素应该有 name 和 type 属性。 name 将标识这个绑定而 type 将标识您希望与这个绑定相关联的 portType(一组操作)。在 清单 3中,您会发现 <portType> 元素的 name 与 <binding> 元素的 type 属性值相匹配。

WSDL binding 元素包含您将用于绑定用途的外部技术的声明。因为正在使用 SOAP,所以这里将使用 SOAP 的名称空间。WSDL 术语中,对外部名称空间的使用称为 extensibility 元素。

在 清单 3 中,您将看见一个空的 <soap:binding/> 元素。该元素的用途是声明将把 SOAP 作为绑定和传输服务使用。

<soap:binding> 元素有两个属性:style 和 transport。style 是一个可选属性,它描述该绑定内操作的性质。transport 属性指定 HTTP 作为该绑定将使用的级别较低的传输服务。

SOAP 客户机将从 WSDL 文件中读取 SOAP 结构并与另一端的 SOAP 服务器协调,所以必须特别关注 interoperability 。我打算在本系列文章的第三部分详细讲述该问题。

在空的 <soap:binding/> 元素后面,有两个 WSDL <operation> 元素,分别表示步骤 1 的操作。每个 <operation> 元素提供各自操作的绑定细节。因此,我提供了另一个 extensibility 元素,即 <soap:operation/> (仍然是一个空元素,与它发生的那个操作相关)。该 <soap:operation/> 元素有一个 soapAction 属性,SOAP 客户机将使用该属性创建 SOAP 请求。

请回忆步骤 2 中, getListOfModels 操作只有输出而无任何输入。因此,必须为该操作提供一个 <output> 元素。该输出包含<soap:body/> 元素(仍然是一个空元素,与它发生的那个操作相关)。SOAP 客户机需要该信息来创建 SOAP 请求。 <soap:body/> 的名称空间属性值应该与您将部署到 SOAP 服务器上的 service 的名称相对应,SOAP 服务器将在在本系列文章的下一部分中讲述。

您已几乎要完成步骤 3 了。只要将下一个操作复制到这个操作的后面,您将完成 清单 3

步骤 4:概括

您已经生成了一个完整描述服务 interface 的 WSDL 文件。现在,WSDL 需要一个附加步骤来创建该 WSDL 文件的概要。WSDL 将该文件称为 implementation 文件,在本系列文章的第四部分中,当您在 UDDI 注册中心发布 Web 服务时,会使用它。请看 清单 4― 这个 WSDL 实现文件。它的主要特性如下:

  • 除了 
    清单 4(实现文件)引用不同的 targetNamespace 去引用实现文件以外, <definitions> 根元素和 清单 3(WSDL 接口文件)中的完全相同。
  • 有一个 
    <import> 元素,该元素引用 清单 3的接口文件(文件名 MobilePhoneService-interface.wsdl)和它的名称空间。
  • 有一个 
    <service> 标记,其中有一个表示该服务的逻辑名 name 。在 service 元素内有一个引用在 清单 3中创建的 SOAP 绑定的 port 元素。

SOAP 和 WSDL

我在本系列文章的 第 1 部分介绍了 WSDL。WSDL 描述了 Web 服务的接口。Web 服务所有者将用 SOAP 来实现他们的接口。因此, WSDL 服务实际上作为 SOAP 服务一样存在。一旦 Web 服务用户拥有 WSDL 文件,他或者她就知晓接口的细节。他或者她就会用 SOAP 来与 Web 服务通信。

可以把 Web 服务考虑为对象,可以通过 WSDL 接口公开并且使用 SOAP 通过因特网远程访问。既然服务是对象,那么肯定有每种服务的相关属性和每种服务调用的行为。SOAP 消息是 XML 文档,可通过 HTTP 工作。

为什么用 SOAP?

B2B(Business-to-business)和 A2A(application-to-application )需求表明企业之间为交换信息而相互通信。这种概念被用在 B2B、工作流和跨企业集成中。例如,设想一条垂直供应链,在链上一家企业为了满足它的客户需求而需要调用其提供者的服务。而一些提供者需要沿供应链进一步下行来调用其它企业的服务。

很明显,在此应用程序中互操作性是最为重要的。任何单个企业只能实现 SOAP 通信通道的一端。另一端将是因特网上 任何地方的实体

在最近几年里,企业之间的集成和互操作性已经成为软件工程师和企业的一个挑战性任务。平台相关性也成为取得集成和互操作性的一个大问题。SOAP 依然是在企业间取得集成和互操作性最简单的机制。

SOAP 体系结构

有了对 SOAP 和它的用途的基本理解,我现在就展开对其体系结构的讨论以了解一些深层知识。请参阅 图 1, 在此图里面您可以识别典型 SOAP 通信体系结构中的一些组件:

  1. SOAP 客户机
  2. SOAP 服务器
  3. 实际服务
图 1. 一个典型 SOAP 通信体系结构的组件
一个典型 SOAP 通信体系结构的组件

让我解释上面所提到的每个实体的体系结构角色。 下面的讨论参照 图 1

SOAP 客户机

SOAP 客户机是一台有 SOAP 机制的机器,它可以产生 SOAP 请求并通过 HTTP 发送到服务器。一条 SOAP 请求是一种类型的 SOAP 消息,通常只有两种类型的 SOAP 消息:一条 SOAP 请求就是一台 SOAP 客户机发送给 SOAP 服务器的内容,一条 SOAP 响应就是 SOAP 服务器对 SOAP 客户机响应的内容。 清单 1是典型的 SOAP 请求,请参阅 清单 2来回顾 SOAP 响应。

清单 1:一条简单的 SOAP 请求
<SOAP-ENV:Envelope  xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" >
   <SOAP-ENV:Body>
    <m:getListOfModels xmlns:m = "uri reference" >
    </m:getListOfModels>
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

SOAP 服务器

SOAP 服务器也是一台有 SOAP 机制的机器,能够接收来自 SOAP 客户机的请求,并对之作出适当的响应。这些编过码的响应会返回发出请求的 SOAP 客户机。在 SOAP 服务器内部有三个实体:

  1. 服务管理器
  2. 被部署服务的列表
  3. XML 转换程序

服务管理器负责根据请求管理服务。请参阅 清单 1 的服务请求,在这里元素 <m:getListOfModels xmlns:m="urn:MobilePhoneservice" > 包含了服务的名称。服务管理器会读取 SOAP 客户机想调用的 SOAP 服务的名称并检查所需的服务实际上是否驻留于这台 SOAP 服务器上。此后,它会查询被部署服务的列表(SOAP 服务器所托管的所有服务的列表)。若存在,服务管理器将把 SOAP 请求传送给 XML 转换程序。XML 转换程序就负责将 SOAP 请求的 XML 结构转换成程序员用来实现实际服务的编程语言(例如,Java 编程语言)的结构。还要负责将来自实际服务的响应转换回 SOAP 响应的 XML 结构。请参阅 清单 2获得 SOAP 响应的说明。

清单 2:一条简单的 SOAP 响应
<SOAP-ENV:Envelope  xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
   <SOAP-ENV:Body>
    <m:getListOfModelsResponse xmlns:m="urn:MobilePhoneservice">
        <Model>M1</Model>
        <Model>M2</Model>
        <Model>M3</Model>
    </m:getPriceResponse>
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

实际服务

图 1中标有 actual service的框就是实际服务驻留的位置。服务实现可以是:例如,COM 组件或 JavaBeans 组件的形式。XML 转换程序负责将 XML 结构转换成合适的方法调用。当 XML 转换程序调用了实际服务实现的某个方法时,这个方法就会完成它的工作并且将结果信息返回 XML 转换程序。

请看一看 图 1中连接 XML translator 和 actual service 的箭头。箭头的两端同在一个企业内,这意味着同一个组织控制着通信两端的接口。与穿过企业边界的在 SOAP 客户机和 SOAP 服务器之间的箭头相比,这正是 SOAP 的目的所在。

SOAP 请求响应机制

当 SOAP 客户机向 SOAP 服务器发送 SOAP 消息时,用 HTTP 协议传输。这就叫做 SOAP 与 HTTP 绑定。当 SOAP 服务器收到消息时,将消息交给服务管理器。服务管理器检查被部署服务的列表,查找在 SOAP 消息中所需的服务。若没有查找到所请求的服务,它将请求失败返回给 SOAP 客户机。但是若此项服务可以提供,控制权由服务管理器转移给 XML 转换程序(转换程序完成合适语言的转换并访问实际服务实现)。服务实现会处理请求并将结果返回给 XML 转换程序。XML 转换程序将结果转换成 SOAP 客户机能够理解的 SOAP 响应(XML 文档)。然后又一次用 HTTP 绑定来传输 SOAP 响应。 现在让我们看一下 SOAP 与 HTTP 的绑定细节。

SOAP 与 HTTP 绑定

当您将 SOAP 和 HTTP 绑定在一起或在 HTTP 上操作 SOAP 时,您实际上将 HTTP 报头加到了 SOAP 请求和响应上了。 清单 1是典型 SOAP 请求的结构,而清单 3、 4、 5和 6都是完整的 HTTP 请求,用来演示如何将 HTTP 报头添加到 清单 1上。相似地, 清单 7是一条完整的 HTTP 响应,针对于来自 清单 2的 SOAP 响应。

无论您何时在 HTTP 上使用 SOAP,Content-Type 字段必须是 text/xml。现在您可以察看 清单 3到 清单 7的详情。

使用 HTTP 的 SOAP 请求

您可以将 SOAP 和 HTTP 的 POST请求方法连用。为了发送一条 SOAP HTTP 请求,您需要在 HTTP 中提供一个  SOAPAction 报头字段。 SOAPAction 指定了 SOAP 请求的目的。服务器(例如过滤 HTTP 中 SOAP 请求消息的防火墙)可以用字段 SOAPAction 的值来做决定。

HTTP 客户机在发送一条 SOAP HTTP 请求时必须用此报头字段。SOAPAction 可以有如下几种值:SOAPAction:"URI-Reference" 
SOAPAction:"filename" 
SOAPAction:"" 
SOAPAction:

清单 3:演示 SOAPAction 报头字段中的 URI 引用
POST /Vendors HTTP/1.1
Host: www.mobilephoneservice.com
Content-Type:"text/xml";Charset="utf-8"
Content-Length: nnnn
SOAPACtion:"www.mobilephoneservice.com/Vendors/MobilePhoneservice#getListOfModels"
<?xml version="1.0"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"  >
    <SOAP-ENV:Body>
        <m:getListOfModels xmlns:m="urn:MobilePhoneservice" >
        </m:getListOfModels>
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

清单 3 在 SOAPAction 中包括如下 URI 引用: www.mobilephoneservice.com/Vendors/MobilePhoneservice#getListOfModels

这个 SOAPAction 展示了两部分内容。第一部分是一个特别 SOAP 部署的地址:www.mobilephoneservice.com/Vendors/MobilePhoneservice

第二部分是一个片段标识符,它给出了我们感兴趣的方法的名字(#getListOfModels)。

清单 4:演示 SOAPAction 报头字段中的一个文件名
POST /Vendors HTTP/1.1
Host: www.mobilephoneservice.com
Content-Type:"text/xml";Charset="utf-8"
Content-Length: nnnn
SOAPAction:"MobilePhoneservice#getListOfModels"
<?xml version="1.0"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"  >
    <SOAP-ENV:Body>
        <m:getListOfModels xmlns:m="urn:MobilePhoneservice" >
        </m:getListOfModels>
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

清单 4 在 SOAPAction 中包含一个文件名( MobilePhoneservice#getListOfModels )。 MobilePhoneservice 文件必须出现在主机 URI( www.mobilephoneservice.com/Vendors )中。 这个主机 URI 是在 HTTP 报头中 host 字段( www.mobilephoneservice.com)和文件夹名( /Vendors )的结合。

清单 5:演示 SOAPAction 报头中的空字符串
POST /Vendors HTTP/1.1
Host: www.mobilephoneservice.com
Content-Type:"text/xml";Charset="utf-8"
Content-Length: nnnn
SOAPAction:""
<?xml version="1.0"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"  >
    <SOAP-ENV:Body>
        <m:getListOfModels xmlns:m="urn:MobilePhoneservice" >
        </m:getListOfModels>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

清单 5 在 SOAPAction 中包含一个空字符串("")。空字符串值表明 SOAP 的目的和 Host URI(www.mobilephoneservice.com/Vendors )的目的是一样的。

清单 6:演示无值 SOAPAction 报头
POST /Vendors HTTP/1.1
Host: www.mobilephoneservice.com
Content-Type:"text/xml";Charset="utf-8"
Content-Length: nnnn
SOAPAction:
<?xml version="1.0"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"  >
    <SOAP-ENV:Body>
        <m:getListOfModels xmlns:m ="urn:MobilePhoneservice" >
        </m:getListOfModels>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

清单 6没有包含 SOAPAction 值。这表明没有关于消息目的的信息。

用 HTTP 的 SOAP 响应

响应将可能是两种类型的 SOAP 响应中的一种:

  • 一个成功的 SOAP 操作产生 SOAP 结果
  • 一个不成功的 SOAP 操作产生一条 SOAP 错误消息
清单 7:一条带有 HTTP 报头的成功 SOAP 响应
HTTP/1.1 Content-Type:"text/xml"; Charset="utf-8"
Content-Length: nnnn
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" >
<SOAP-ENV:Body>
    <m:getListOfModelsResponse xmlns:m = "URI-Reference">
        <model>m1</model>
        <model>m2</model>
    </m:getListOfModels>
</SOAP-ENV:Body>

清单 7是第一种情况,在此可以从 SOAP 服务器取得有意义的结果。

清单 8是一条典型的 SOAP 错误消息。SOAP HTTP 响应遵循 HTTP 中通信状态信息的 HTTP 状态码的语义。若在处理一条请求时发生一个 SOAP 错误,SOAP HTTP 服务器必须发出一条 HTTP 500 "Internal Server Error" 响应,同时在响应中包括一条带有 SOAP 出错元素的 SOAP 消息。

清单 8:一条带有 HTTP 报头的典型 SOAP 错误消息
HTTP/1.1 500 Internal Server Error
Content-Type: "text/xml"; Charset="utf-8"
Content-Length: nnnn
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" >
    <SOAP-ENV:Body>
        <SOAP-ENV:Fault> 
        <faultstring>Failed to process the request</faultstring>
        </SOAP-ENV:Fault>
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

使用电子邮件的 SOAP

HTTP 不是唯一绑定 SOAP 消息的解决方案。若 HTTP 不合适,您可以用诸如 SMTP 的其它机制来用于 SOAP 绑定。将 SOAP 和 SMTP 绑定,您可以建立一条单向传输路由。两条单向消息可以用来建立请求/响应通信。 用 SMTP 来发送一条 SOAP 消息,您需要遵从以下步骤:

  • 使用 MIME-Version 报头字段
    MIME-Version用一个版本号来区别不同的 MIME 版本。它应用邮件处理代理(例如一个 POP 服务器)来区别旧版本和新版本所生成的邮件消息。请参阅 清单 9,它使用了一个 MIME-Version 报头字段。

    清单 9:一个使用电子邮件的 SOAP 示例

    TO: <info@waxsys.com>
    From: <abc@punjab.com>
    Reply-To: <abc@punjab.com>
    Date: SAT, 2 Feb 2002 16:00:00 
    Message-Id: <4FAB345C8D93E93B7A6E9@punjab.com> MIME-Version: 1.0
    Content-Type: text/xml; charset=utf-8
    Content-Transfer-Encoding: QUOTED-PRINTABLE
    <?xml version ="1.0" encoding="UTF-8"?>
    <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
        <SOAP-ENV:Body>
            <prnt:echoString xmlns:prnt="http://waxsys.com">
                <msgString>Put your mail Message</msgString>
            </prnt:echoString>
        </SOAP-ENV:Body>
    </SOAP-ENV:Envelope>
  • 使用 Content-Type 报头字段:
    Content-Type用来标识消息主体中的数据类型。对于 SOAP 消息 Content-Type 应该有一个值“text/xml”。请参阅 清单 9,它使用了 Content-Type。
  • 使用 Content-Transfer-Encoding 字段:
    Content-Transfer-Encoding 用来指定传输编码的类型,也就是您所要传输的数据是字符格式还是二进制格式。 清单 9使用 Quoted-Printable 编码,这种编码符合依照 ASCII 字符集的可打印字符。这种对数据的编码方式使邮件传输代理不可能修改结果八位元。请参阅 清单 9,它使用了 Content-Transfer-Encoding 。

SOAP 模式与实现

SOAP 消息

一条 SOAP 消息只是一个 XML 文档,由一个强制性的 SOAP Envelope 组成,SOAP Envelope 有一个可选的 SOAP Header 和一个必须有的 SOAP Body。

SOAP 模式的元素:

  • Envelope
  • Header
  • Body
  • Fault

Envelope:
Envelope 是表示一条 SOAP 消息的顶层元素。为了发送一条 SOAP 消息,必须包括此元素。Envelope 使用必要的 SOAP 名称空间标识符(http://schemas.xmlsoap.org/soap/envelope/ )。若 Envelope 包含了错误的名称空间,会产生一个关于 Envelope 名称空间版本的错误。 清单 10是一个空 Envelope。称其为“空 Envelope”是为了强调在通过“投递”发出它之前,它最终应该包含一封“信”(也许是商业信)。SOAP 模式中的“信”就是指“SOAP Body”,HTTP POST(在 HTTP 与 SOAP 的绑定一部分讨论过)就是传输机制。

清单 10:一个空 SOAP Envelope
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
</SOAP-ENV:Envelope>

Header:
SOAP Header 是可选的。您可以直接将 SOAP Body 放到 SOAP Envelope 中并完全忽略报头。报头提供了一个扩展 SOAP 消息功能的机制。例如,认证就是由 SOAP Header 条目所提供的一种典型扩展。在此情况下,将有一个认证框架,它会使用 SOAP 作为更低级别的传输。请参阅 清单 11来查看在 SOAP 中的报头实现。

清单 11:在一个 SOAP Envelope 中的报头实现
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header>
    <m:Order xmlns:m="some URI" SOAP-ENV:mustUnderstand="1">
    </m:Order>
</SOAP-ENV:Header>
</SOAP-ENV:Envelope>

Body:
Body 元素包含您实际要发送的消息。它是一个强制性的元素且其子元素通常属于一个用户定义的名称空间。 清单 12 展示了一条引用一个用户定义的名称空间 “u” 的 SOAP 消息。Body 元素是必要信息的容器。这个元素必须在 SOAP 消息中出现并且必须是 SOAP Envelope 元素的一个直接子元素。它也必须直接跟在 SOAP Header 元素的后面。若没有 Header 元素,那么它应直接跟在 Envelope 元素的后面。主体可以包含子元素并且子元素可能是受限于名称空间的。

清单 12: SOAP Envelope 内有 Header,还有 Body
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header>
    <m:Order xmlns:m="some URI" SOAP-ENV:mustUnderstand="1">
    </m:Order>
</SOAP-ENV:Header>
<SOAP-ENV:Body>
    <u:GetPrice xmlns:u="some URI" >
        <model>m1</model>
    </u:GetPrice>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Fault:
这个元素表明一条错误消息。它应作为一个主体条目出现并且不能在 Body 元素中出现一次以上。通常,Fault 元素会在一条 SOAP 响应消息中出现,以表明在 SOAP 请求中出现错误。

Fault 的子元素:

  • faultcode (错误的标识)
  • faultstring (错误的描述)
  • faultactor (标识由谁导致的错误)
  • detail (错误细节。通常是一个应用程序特定错误,也就是说,它相当于在 SOAP 请求主体中用到地用户定义的名称空间)

清单 13是一条典型的错误消息。

清单 13: 当应用程序出现错误时,SOAP Fault 的使用
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header>
<m:Order xmlns:m="some URI" SOAP-ENV:mustUnderstand="1">
</m:Order>
</SOAP-ENV:Header>
<SOAP-ENV:Body>
<SOAP-ENV:Fault>
<faultcode>SOAP-ENV:Server</faultcode>
<faultstring>Not necessary information</faultstring>
<detail>
    <d:faultdetail xmlns:d = "uri-referrence">
        <msg>
            application is not responding properly.
        </msg>
        <errorcode>12</errorcode>
    </d:faultdetail>
</detail>
</SOAP-ENV:Fault>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

来自第 1 部分的一条对 WSDL 文件的 SOAP 请求

已经解释了 SOAP 消息(请求和响应)的常规语法,我将展示如何对本系列 第 1 部分中的 MobilePhoneservice 开发一条 SOAP 请求。在第 1 部分中您设计一个完整的 WSDL 接口来解释 MobilPhoneservice。移动公司在 MobilePhoneservice 中提供了两种方法,一种是getListOfModels() ,另一种是 getPrice(modelNumber) 。 GetListOfModels() 没有参数但是返回手机型号的一张列表,而getPrice(modelNumber) 有一个参数 modelNumber 并返回需求型号的 price 。您将用 SOAP 请求格式对它作成文档,但是首先让我展示给您一般的 SOAP 请求和响应格式。

清单 14:SOAP 请求的一般格式
<SOAP-ENV:Envelope xmlns:SOAP-ENV ="SOAP schema's URI"
<SOAP-ENV:Body>
    <Instance:"Method Name" xmlns:Instance= "URI where method is located">
        <parameter1>value</parameter1>
        <parametern>value</parametern>
    </Instance:"Method Name">
</SOAP_Envelop:Body>
</SOAP-ENV:Envelope>

一条简单的 SOAP 请求或响应只能表明一种服务的一个方法。包含一条 SOAP 请求的 Envelope 的一般格式遵从 清单 14。将这种一般格式与清单 16 中的 getListOfModels() 的方法调用请求比较。在清单 16 中,我已经提供了方法和 URI 的名称。既然在 getListOfModels()中不需要参数,所以 <m:getListOfModels> 在 清单 16中是一个空元素。

清单 15:一条 SOAP 响应的一般格式
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Body>
    <Instance:"Method Name"+"Response" 
    xmlns:Instance="URI where method is located">
    <return>
        <responseparameter1>value</responseparameter1>
        <responseparametern>value</responseparametern>
    </return>
    </Instance: "Method Name"+"Response">
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

清单 15 是一条一般的 SOAP 响应。Apache SOAP 服务器在方法名称的后面增加了"Response"关键字并将返回值封入元素 <return> 中作为一个直接子方法元素。若返回值是复合型结构,那么 <return> 元素包含一个或多个 <item> 元素。将 清单 15与 清单 17 相比,清单 17 是来自getListOfModels() 的实际响应。 清单 17包含一系列项目,作为 Vector 数据类型,它是返回参数。 相似地, 清单 18和 19 展示了针对 MobilePhoneservice 的方法 getPrice() 的 SOAP 请求和响应。

清单 16:调用 getListOfModels() 方法的 SOAP 请求
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Body>
    <m:getListOfModels xmlns:m = "www.mobilphoneservice.com" >
    </m:getListOfModels>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
清单 17:针对于来自清单 16 请求的 SOAP 响应
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope 
    xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
    xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" 
    xmlns:xsd="http://www.w3.org/1999/XMLSchema">
<SOAP-ENV:Body>
    <ns1:getListOfModelsResponse xmlns:ns1="urn:MobilePhoneservice" 
    SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
    <return xmlns:ns2="http://xml.apache.org/xml-soap" 
        xsi:type="ns2:Vector">
        <item xsi:type="xsd:string">M1</item>
        <item xsi:type="xsd:string">M2</item>
        <item xsi:type="xsd:string">M3</item>
        <item xsi:type="xsd:string">M4</item>
        <item xsi:type="xsd:string">M5</item>
    </return>
    </ns1:getListOfModelsResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
清单 18:对于 getPrice 方法的 SOAP 请求
<SOAP-ENV:Envelope 
    xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
    xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" 
    xmlns:xsd="http://www.w3.org/1999/XMLSchema">
<SOAP-ENV:Body>
    <m:getPrice xmlns:m ="www.mobilphoneservice.com">
        <modelNumber xsi:type ="xsd:String">M1</modelNumber>
    </m:getPrice>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
清单 19:对于来自清单 18 请求的 SOAP 响应
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope 
    xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" 
    xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" 
    xmlns:xsd="http://www.w3.org/1999/XMLSchema">
<SOAP-ENV:Body>
    <ns1:getPriceResponse xmlns:ns1="urn:MobilePhoneservice" 
        SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
            <return xsi:type="xsd:string"> 5000 </return>
    </ns1:getPriceResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

在 SOAP 服务器上部署基于 WSDL 的服务

在此部分您将在 Apache SOAP 服务器上部署来自第 1 部分的 WSDL 服务。Apache SOAP 工具箱将 WSDL 服务信息保存在一个部署描述符文件里面。部署描述符包含了 WSDL 服务的名称和它拥有的所有方法。在运行时部署描述符会将这些名称提供给 SOAP 服务器。同样的部署描述符文件还包含了实现接口的 JavaBean 组件的地址。

清单 20:一个部署描述符的框架
<isd:service xmlns:isd="http://xml.apache.org/xml-soap/deployment"
    id="URN:SERVICE-URN">
<isd:provider type="java" 
    scope="Request" 
    methods="EXPOSED-METHODS">
    <isd:java class="IMPLEMENTING-CLASS"/> 
</isd:provider>
<isd:faultListener>org.apache.soap.server.DOMFaultListener
</isd:faultListener>
</isd:service>

清单 20是一个部署描述符的框架,为了作为基于 WSDL 服务的部署描述符使用,它需要三项信息( URN:SERVICE-URN、EXPOSED-METHODS和 IMPLEMENTING-CLASS)。 URN:SERVICE-URN 是被部署服务的名称。在此例中它是 “urn:MobilePhoneservice” 。EXPOSED-METHODS 是一个单空格分隔的由服务提供的方法的列表。 在此部署中它是 getListOfModels getPrice 。

IMPLEMENTING-CLASS 是带有全路径的 Java 类名称。例如, samples.phonequote.MobilePhoneservice 。 在此例中测试应用程序时,您有如下目录结构: 
Apache SOAP 服务器: C:\foo\SOAP-2_2
Mobile phone 服务实现:
C:\foo\SOAP-2_2\samples\phonequote\MobilePhoneservice

因此,IMPLEMENTING-CLASS 路径请参照您安装 SOAP 工具箱的目录。 我没有提供 Java 类的实际实现。 它取决于业务逻辑并且可以是任何东西。

清单 21:MobilePhoneservice 的部署描述符
<isd:service xmlns:isd="http://xml.apache.org/xml-soap/deployment"
id="urn:MobilePhoneservice">
<isd:provider type="java"
    scope="Request"
    methods="getListOfModels getPrice">
    <isd:java class="samples.phonequote.MobilePhoneservice"/>
</isd:provider>
<isd:faultListener>
    org.apache.soap.server.DOMFaultListener
</isd:faultListener>
</isd:service>

清单 21是来自第 1 部分对 WSDL 文件的完整部署描述符。

SOAP 客户机与 SOAP 服务器的通信

我已经提供过一个应用程序样本来演示一台 SOAP 客户机与一台 SOAP 服务器的通信。 为此我给过三个列表:Startup.html( 清单 22)、Operation.html( 清单 23)和 Execute.jsp( 清单 24)。

StartUp.html( 清单 22)是一个简单的 HTML 文件,提供给用户一个 GUI 并询问他将要调用哪一个 SOAP 方法。用户会选择一个他需要的方法。

清单 22:一个作为前端的简单 HTML 页
<HTML>
<BODY bgcolor="Teal">
<br/>
<p align="center">
    <font size="5" face="Arial" color="white"><b> 
    SOAP method invocation demo </b></font>
</p>
<hr/>
<font face="Arial" color="whitesmoke" size="3">
<br/><b>
    Click any of the method name to execute.<br/>
    1. Get the List of all Models that we manufacture.... 
        <a href="execute.jsp?index=1"> 
        <font color="orange"> GetListOfModels </font></a> <br/>
    2. Get the Price of any particular model......................
        <a href="operation.html"> 
        <font color="orange"> GetPrice </font></a>
</b>
</BODY>
</HTML>

Operation.html( 清单 23)将询问客户提供方法调用所需的参数。

清单 23:根据他或她所选择的方法给予客户一个 GUI
<HTML>
<BODY bgcolor="Teal">
<br/>
<p align="center"> 
<font size="5" face="Arial" color="white"><b>
    GetPrice Operation input Form </b>
</font></p>
<hr/>
<p align="center"> 
<form action="execute.jsp" method="POST">
<input type="hidden" name="index" value="0">
<table textColor="white">
<tr><td>
<font color="whitesmoke"><b>Description :</b></font>
</td><td><font color="whitesmoke">
    Method GetPrice is used to Get Price of given Model Number</font>
</td></tr>
<tr><td>
<font color="whitesmoke"><b>Parameter(s)</b></font></td><td>
</td></tr>
<tr><td><font color="whitesmoke">Model Number </td></font>
<td><font color="whitesmoke">
    <input type="text" name="parameter" size="30"> 
    (required) </font>
</td></tr>
<tr><td>
    </td><td><input type="Submit" value="Invoke">
</td></tr>
</font>
</table>
</form>
</p>
</BODY>
</HTML>

Execute.jsp( 清单 24)包含了所有的令人感兴趣的代码。它检测所调用的方法和所传递的参数。然后发送给远程服务器一个方法调用。

清单 24:检测方法并发送给远程服务器一个调用
<%@ page language="java" import="java.util.Vector" %>
<%@ page import="java.net.MalformedURLException, java.net.URL" %>
<%@ page import="java.util.Vector" %>
<%@ page import="org.apache.soap.SOAPException, 
        org.apache.soap.Constants" %>
<%@ page import="org.apache.soap.rpc.Call, org.apache.soap.rpc.Response, 
        org.apache.soap.rpc.Parameter" %>
<%@ page import="org.apache.soap.transport.http.SOAPHTTPConnection" %>
<%@ page import="org.apache.soap.Fault" %>
<HTML>
<BODY bgcolor="Teal">
<br/>
<p align="center">
<font color="whitesmoke">
<% 
    boolean isParameter = false ;
    SOAPHTTPConnection soapTransport = new SOAPHTTPConnection();
    // Address of the remote server. 
    // Normally this should be dynamically passed and detected.
    // We have hard coded it only for demonstration.
    URL url = new URL ("http://localhost:8080/soap/servlet/rpcrouter");
    // Build the call.
    Call call = new Call ();
    call.setTargetObjectURI ("urn:MobilePhoneservice");
    call.setSOAPTransport (soapTransport);
    call.setEncodingStyleURI(Constants.NS_URI_SOAP_ENC);
    // We'll detect which method user selected
    // and give a call accordingly.
    // We'll pass parameters if present.
    if (request.getParameter("parameter")!=null)
    isParameter = true;
    if (request.getParameter("index").equals("0")) 
    {
        call.setMethodName("getPrice");
        Vector params = new Vector();
        String message = new String (request.getParameter("parameter"));
        params.addElement (new Parameter("message", String.class, 
        message , null));
        call.setParams(params);
    }
    else
        call.setMethodName("getListOfModels");
    Response resp = call.invoke ( url, /* actionURI */ "" );
    out.println("<p align=left> 
    <font size=\"4\" face=\"Arial\" color=\"white\">
    Response of [ "+call.getMethodName()+" ]
    </font><hr/>");
    
    // Check the response.
    if (resp.generatedFault ()) {
        Fault fault = resp.getFault ();
        out.println("<b>Fault is:</b>"+ fault.getFaultCode ()
        +" ["+fault.getFaultString ()+"]");
    } else {
        Parameter result = resp.getReturnValue ();
        out.println("<b>Response is: </b>"+ result.getValue ()+"");
    }
%>
<font>
</p>
</BODY>
</HTML>

为了运行此应用程序,您需要两台 Apache SOAP 服务器。 一台服务器将用来与用户通信并托管 清单 22、 23和 24。 另一台服务器(也称为远程服务器)就是我们需要部署第 1 部分所讲的基于 WSDL 服务的地方(在前一节描述,“ 在 SOAP 服务器上基于 WSDL 服务的部署”)。 仅仅是为了演示,远程服务器的地址 http://localhost:8080/soap/servlet/rpcrouter 已经硬编码在 Execute.jsp( 清单 24)中。在实际操作中您可以从 WSDL 文件中读取它。

SOAP 中的简单与复合数据类型

在此节中,我将从解释简单与复合数据类型的不同开始。然后展示如何在 SOAP 中对它们编码。

简单类型包括字符串、浮点数、整数、枚举等。 例如一部手机的“name”的数据类型就是 “string” 。 复合类型由简单类型组成但只代表一个实体。例如, “Student” 类型记录可以有不同的属性,如 “studentName” 属于类型 “string” , “studentRollNumber” 属于类型 “int”但都只代表一个实体 “Student” 。

清单 25 包含了一个名称为 “Mobile” 的复合数据类型。 您会在后面的 SOAP 请求中用到。

清单 25: “Mobile”类型的模式定义结构
1<? xml version="1.0" ?>
2<xsd:schema xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" 
3   xmlns:xsd="http://www.w3.org/1999/XMLSchema">
4   targetNameSpace= "www.mobilephoneservice.com/phonequote">
5 <xsd:element name ="Mobile">
6 <xsd:complexType>
7   <xsd:element name="modelNumber" type="xsd:int">
8   <xsd:element name="modelName" type="xsd:string">
9   <xsd:element name="modelWeight" type="xsd:int">
10  <xsd:element name="modelSize" type="xsd:int">
11  <xsd:element name="modelColor">
12      <simpleType base="xsd:string">
13      <enumeration value="white" />
14      <enumeration value="blue" />
15      <enumeration value="black" />
16      <enumeration value="red" />
17      <enumeration value="pink" />
18      </simpleType>
19  </xsd:element>
20 </complexType>
21 </xsd:element>
22</xsd:schema>

在 清单 25 中的第 5 行展示了我们的类型名称(Mobile),而第 6 行说明它是复合数据类型。因复合数据类型有属性,所以在第 7 行到第 12 行展示了定义为子元素的 “Mobile” 数据类型的属性。

第 7 行声明的元素展示了 “Mobile” 类型有一个名称为 “modelNumber” 的属性且其类型为 “int” (也就是说, “modelNumber” 只能采用整数值)。 类似的,第 9 行和第 10 行声明的元素具有同样的类型但有不同的属性名称。在第 8 行定义的元素具有名称为 “modelName” 的属性且其类型是 “string” 。

第 11 行的元素因有位于第 12 行的、名称为 “simpleType” 的子元素,所以需要更好的理解。这里您在复合类型 Mobile 中定义了一个简单类型。simpleType 的名称为 “modelColor” 且它的类型是 “enumeration” 。它有一个属性 “base” 具有的值为 "xsd:string" ,这表明简单类型 “modelColor” 具有在 SOAP 模式中定义的类型 “string” 的功能。在第 13 行到第 17 行中的每一个 <enumeration> 标记都具有一个属性: “value” ( "white""blue" 、 "black" 、 "red" 和 "pink" )。 枚举类型使我们能够从多项选项中选择一个值。

在 SOAP 请求中使用复合数据类型

清单 26 演示了在 SOAP 请求中复合类型的使用。 它展示了一个在 Body 元素中的携带请求的 Envelope,在 Body 元素中,您正调用 “m” 名称空间的 addModel 方法。 清单 26 使用数据类型 “Mobile” ,此数据类型在 清单 25中定义。

AddModel 方法携带一个类型为 “Mobile” 的参数。我们以 “msd” 名称空间引用来引用 “Mobile” 结构。请参阅清单 26 的 <SOAP-ENV:Envelope> 元素中的 "xmlns:msd" 声明。 这是一个在 SOAP 请求中使用用户定义数据类型的示例。

清单 26:实现在清单 25 中定义的“Mobile”结构
1 <SoAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
2 xmlns:xsd="http://www.w3.org/1999/XMLSchema" 
3 xmlns:msd="www.mobilephoneservice.com/phonequote">
4 <SOAP-ENV:Body>
5   <m:addModel xmlns:m="www.mobilephoneservice.com">
6   <msd:Mobile>
7       <modelNumber>1</modelNumber>
8       <modelName>mlr97</modelName>
9       <modelWeight>10</modelWeight>
10      <modelSize>4</modelSize>
11      <modelColor>white</modelColor>
12  </msd:Mobile>
13  </m:addModel>
14 </SOAP-ENV:Body>
15<SOAP-ENV:Envelope>

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值