Skip to content

12. Web服务

12.1. Introduction

我们在上一章介绍了多个 TCP/IP 客户端-服务器应用程序。由于客户端和服务器之间交换的是文本行,因此它们可以使用任何语言编写。客户端只需了解服务器所期望的通信协议即可。

Web服务同样属于TCP/IP服务器应用程序。它们具有以下特征:

  • 它们由Web服务器托管,其客户端-服务器通信协议是HTTP(HyperText传输协议),该协议构建在TCP-IP之上。
  • 无论提供何种服务,Web服务均采用标准交互协议。一个Web服务提供多种服务,如S1、S2、……、Sn。每项服务均需接收客户端提供的参数,并向客户端返回结果。对于每项服务,客户端需要了解:
    • 服务的确切名称(如果
    • 需提供的参数列表及其类型
    • 服务返回的结果类型

一旦掌握了这些信息,无论调用哪个Web服务,客户端与服务器的交互都将遵循相同的格式。因此,客户端的编写得以标准化。

  • 出于防范互联网攻击的安全考虑,许多组织都拥有私有网络,且仅向互联网开放服务器上的特定端口:主要是Web服务的80端口。 所有其他端口均被锁定。因此,如前一章所述的客户端-服务器应用程序是在私有网络(内网)中构建的,通常无法从外部访问。将服务托管在Web服务器上,则使其对整个互联网社区开放。
  • Web服务可以被建模为一个远程对象。此时,所提供的服务便成为该对象的方法。客户端可以像访问本地对象一样访问该远程对象。这隐藏了所有的网络通信部分,并允许构建一个独立于该层的客户端。如果该层发生变化,客户端无需修改。
  • 与上一章介绍的 TCP/IP 客户端-服务器应用程序类似,客户端和服务器可以使用任何编程语言编写。它们通过文本行进行通信。这些文本行包含两部分:
    • HTTP协议所需的头部
    • 消息正文。对于服务器对客户端的响应,其格式为 XML(eXtensible 标记语言)。对于客户端向服务器的请求,消息正文可以有多种形式,其中包括 XML。 客户端的 XML 请求可能采用一种称为 SOAP(简单对象访问协议)的特殊格式。在这种情况下,服务器的响应也遵循 SOAP 格式。

基于Web服务的客户端/服务器应用程序架构如下:

这是对三层架构的扩展,其中添加了专门的网络通信类。我们在第 11.9.1 节中已经遇到过类似的架构,即基于 Windows 图形客户端和 TCP 服务器的税务应用程序。

让我们通过一个初步示例来阐明这些概括性内容。

12.2. 使用 Visual Web Developer 构建首个 Web 服务

我们将构建一个具有以下简化架构的第一个客户端/服务器应用程序:

12.2.1. 服务器端

我们曾提到,Web 服务由 Web 服务器托管。编写 Web 服务属于 Web 编程的总体范畴,即服务器端编程。此前我们曾有机会编写 Web 客户端,这也属于 Web 编程,但属于客户端编程。 “Web 编程”一词通常指服务器端编程,而非客户端编程。若要开发 Web 服务或更广泛的 Web 应用程序,Visual C# 并非合适的工具。 我们将使用 Visual Developer——这是 Visual Studio 2008 的 Express 版本之一,可从 [2] 处下载,地址为 [1]:[http://msdn.microsoft.com/fr-fr/express/future/bb421473(en-us).aspx](2008 年 5 月):

  • [1]:下载地址
  • [2]:下载选项卡
  • [3]:下载 Visual Developer 2008

要创建第一个Web服务,启动Visual Developer后,可按以下步骤操作:

  • [1]:选择“文件”/“新建网站”
  • [2]:选择“Web Service”类型的应用程序
  • [3]:选择开发语言:C#
  • [4]:指定创建项目的文件夹
  • [5]:在 Visual Web Developer 中创建的项目
  • [6]:项目在磁盘上的文件夹

在 Web Developer 中,Web 应用程序的结构如下:

  • 一个根目录,其中包含网站文档(静态 HTML 网页、图片、动态 .aspx 网页、.asmx Web 服务等)。 该目录中还包含文件 [web.config],这是 Web 应用程序的配置文件。它与 Windows 应用程序中的 [App.config] 文件具有相同的作用,且结构也相同。
  • 一个名为 [App_Code] 的文件夹,其中包含待编译的网站类和接口。
  • 一个名为 [App_Data] 的文件夹,用于存放 [App_Code] 类所调用的数据。例如,其中可能包含一个 SQL Server *.mdf 数据库。

[Service.asmx] 是我们请求创建的 Web 服务。它仅包含以下一行代码:


<%@ WebService Language="C#" CodeBehind="~/App_Code/Service.cs" Class="Service" %>

上述源代码是为托管该应用程序的 Web 服务器准备的。在生产环境中,该服务器通常是 IIS(Internet Information Server),即微软的 Web 服务器。Visual Web Developer 内置了一个轻量级 Web 服务器,用于开发模式。上述指令指示 Web 服务器:

  • [Service.asmx] 是一个 Web 服务(指令 WebService
  • 使用 C# 编写(属性 Language
  • Web 服务的 C# 代码位于文件 [~/App_Code/Service.cs] 中(属性 CodeBehind)。Web 服务器将从该位置获取代码并进行编译。
  • 实现 Web 服务的类名为 Service(属性 Class

Visual Developer 生成的 Web 服务 C# 代码 [Service.cs] 如下:


using System.Web.Services;

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
// 若要允许从脚本调用此 Web 服务,请使用 ASP.NET AJAX,并取消注释以下行。 
// [System.Web.Script.Services.ScriptService]
public class Service : System.Web.Services.WebService
{
    public Service () {

        //如果使用设计好的组件,请取消以下行注释 
        //InitializeComponent(); 
    }

    [WebMethod]
    public string HelloWorld() {
        return "Hello World";
    }
    
}

Service 类看起来像一个典型的 C# 类,但有几点需要注意:

  • 第 7 行:该类继承自定义在 System.Web.Services 命名空间中的 WebService 类。这种继承并非总是必需的。特别是在本示例中,可以省略它。
  • 第 3 行:类本身前面带有 [WebService(Namespace="http://tempuri.org/")] 属性,用于为 Web 服务指定命名空间。类供应商会为其类指定命名空间,以便赋予类一个唯一名称,从而避免与其他供应商可能使用相同名称的类发生冲突。对于 Web 服务,情况也是如此。 每个 Web 服务都必须通过一个唯一名称来标识,此处为 http://tempuri.org/。该名称可以是任意名称,并不一定必须采用 HTTP URI 的形式。
  • 第 15 行:方法 HelloWorld 前带有 [WebMethod] 属性,该属性告知编译器该方法应向 Web 服务的远程客户端公开。 未带此属性的方法对 Web 服务客户端不可见。这可能是被其他方法调用的内部方法,但不打算对外发布。
  • 第 9 行:Web 服务的构造函数。在我们的应用程序中,它没有用处。

生成的 [Service.cs] 类被转换如下:


using System.Web.Services;

[WebService(Namespace = "http://st.istia.univ-angers.fr")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class Service : System.Web.Services.WebService
{
    [WebMethod]
    public string DisBonjourALaDame(string nomDeLaDame) {
        return string.Format("Bonjour Mme {0}", nomDeLaDame);
    }
    
}

为 Web 应用程序生成的配置文件 [web.config] 如下:


<?xml version="1.0"?>
<!-- 
    Note: As an alternative to hand editing this file you can use the 
    web admin tool to configure settings for your application. Use
    the Website->Asp.Net Configuration option in Visual Studio.
    A full list of settings and comments can be found in 
    machine.config.comments usually located in 
    \Windows\Microsoft.Net\Framework\v2.x\Config 
-->
<configuration>


    <configSections>
      <sectionGroup name="system.web.extensions" type="System.Web.Configuration.SystemWebExtensionsSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
...
      </sectionGroup>
    </configSections>  


    <appSettings/>
    <connectionStrings/>
...
</configuration>

该文件共 140 行。由于内容较为复杂,我们在此不予详细说明,将保持文件原样。在上文中,我们可以看到 <configuration>、 <configSections>、<sectionGroup>、<appSettings>、<connectionString>,这些标签我们在Windows应用程序的[App.config]文件中也遇到过。

我们有一个可运行的Web服务,其执行路径为:

  • [1,2]:右键单击 [Service.asmx],并选择在浏览器中查看页面
  • [3]:Visual Web Developer 启动其内置的 Web 服务器,并将该服务器的图标显示在任务栏的右下角。 Web 服务器在随机端口上启动,此处为 1906。显示的 URI /WsHello 是网站 [4] 的名称。

Visual Web Developer 还启动了一个浏览器来显示所请求的页面,即 [Service.asmx]:

  • 在 [1](即该页面的 URI)中,我们可以看到网站 URI [http://localhost:1906/WsHello] 之后紧跟页面 URI /Service.asmx。
  • 在 [2] 中,后缀 .asmx 向 Web 服务器表明,这并非生成 HTML 页面的普通网页(后缀为 .aspx),而是一个 Web 服务的页面。 随后,系统会自动生成一个网页,其中包含指向每个带有 [WebMethod] 属性的 Web 服务方法的链接。这些链接可用于测试相关方法。

点击上方的 [2] 链接,将跳转至以下页面:

  • 在 [1] 中,请注意新页面的 URI 为 [http://localhost:1906/WsHello/Service.asmx?op=DisBonjourALaDame]。 这是带有参数 op=M 的 Web 服务 URI,其中 M 是该 Web 服务中某个方法的名称。
  • 回顾方法 [DisBonjourALaDame] 的签名:

    public string DisBonjourALaDame(string nomDeLaDame) ;

该方法接受类型为 string 的参数,并返回类型为 string 的结果。 该页面允许我们调用方法 [DisBonjourALaDame]:在 [2] 中填入参数 nomDeLaDame 的值,并在 [3] 中请求执行该方法。 我们得到以下结果:

  • 在 [1] 中,请注意响应的 URI 与请求的 URI 不相同。它已经发生了变化。
  • 在 [2] 中,这是 Web 服务器的响应。请注意以下几点:
    • 这是一个 XML 响应,而非 HTML
    • 方法 [DisBonjourALaDame] 的结果被封装在代表其类型的 <string> 标签中。
    • <string>标签具有一个名为xmlnsXML命名空间)的属性,这是我们为Web服务指定的命名空间(见下文第1行)。

[WebService(Namespace = "http://st.istia.univ-angers.fr")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class Service : System.Web.Services.WebService

要了解网页浏览器是如何发出请求的,需要查看测试表单的 HTML 代码:

...
<span>
  <p class="intro">Click <a href="Service.asmx">here</a> for a complete list of operations.</p>
  <h2>DisBonjourALaDame</h2>
  <p class="intro"></p>

  <h3>Test</h3>

         To test the operation using the HTTP POST protocol, click the 'Invoke' button.

   <form action='http://localhost:1906/WsHello/Service.asmx/DisBonjourALaDame' method="POST">                                      
      <table>
              <tr>
                    <td>Parameter</td>
                    <td>Value</td>
                </tr>
                <tr>
               <td>nomDeLaDame:</td>
               <td><input type="text" size="50" name="nomDeLaDame"></td>
           </tr>
                <tr>
                   <td></td>
                  <td align="right"> <input type="submit" value="Invoke" class="button"></td>
           </tr>
      </table>
    </form>
<span>
...
  • 第 11 行:表单(form 标签)中的值将通过 POST 方法(method 属性)提交至 URL [ http://localhost:1906/WsHello/Service.asmx/DisBonjourALaDame](action 属性)。
  • 第19行:输入字段名为nomDeLaDamename属性)。

通过调用 Web 服务 [/Service.asmx],我们得以测试其方法,并对客户端与服务器的交互有了初步了解。

12.2.2. 客户端部分

可以使用基本的 TCP/IP 客户端来实现上述远程 Web 服务的客户端。以下是一个示例,展示了通过连接到远程 Web 服务(localhost,1906)的 putty 客户端实现的客户端/服务器对话:

POST /WsHello/Service.asmx/DisBonjourALaDame HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
Content-Length: 23

HTTP/1.1 100 Continue
Server: ASP.NET Development Server/9.0.0.0
Date: Sat, 10 May 2008 08:36:41 GMT
Content-Length: 0

nomDeLaDame=Carla+Bruni
HTTP/1.1 200 OK
Server: ASP.NET Development Server/9.0.0.0
Date: Sat, 10 May 2008 08:36:47 GMT
X-AspNet-Version: 2.0.50727
Cache-Control: private, max-age=0
Content-Type: text/xml; charset=utf-8
Content-Length: 119
Connection: Close

<?xml version="1.0" encoding="utf-8"?>
<string xmlns="http://st.istia.univ-angers.fr">Bonjour Mme Carla Bruni</string>
  • 第 1-5 行:由客户端 putty 发送的消息
  • 第 1 行:命令 POST
  • 第 6-10 行:服务器的响应。这意味着客户端可以发送 POST 的值。
  • 第11行:以param1=val1&param2=val2&形式提交的值……某些字符必须是URL中允许的字符。这就是之前所说的编码URL。此处表单仅有一个名为nomDeLaDame的参数。 提交的值总共包含23个字符。该长度需在第4行的HTTP头中声明。
  • 第12-22行:服务器的响应
  • 第22行:Web方法[DisBonjourALaDame]的结果。

使用 Visual C# 时,可以通过向导生成远程 Web 服务的客户端。接下来我们将演示这一过程。

上文提到的 [1] 层由一个名为 ClientWsHello 的 Visual Studio C# Windows 应用程序项目实现:

  • 在 [1] 中,Visual C# 中的 ClientWsHello 项目
  • 在 [2] 中,项目的默认命名空间为 Client(右键单击项目 / 属性 / 应用程序)。该命名空间将用于构建即将生成的客户端的命名空间。
  • 在 [3] 中,右键单击项目以添加对远程 Web 服务的引用
  • 在 [4] 中,输入之前构建的 Web 服务的 URI
  • 在 [4b] 中,将 Visual C# 连接到由 [4] 指定的 Web 服务。Visual C# 将检索 Web 服务的描述,并利用该描述生成一个客户端。
  • 在 [5] 中,一旦获取了 Web 服务的描述,Visual C# 即可显示其公共方法
  • 在 [6] 中,为即将生成的客户端指定一个命名空间。该命名空间将被添加到 [2] 中定义的命名空间中。因此,客户端的命名空间将为 Client.WsHello
  • 在 [6b] 中确认向导。
  • 在 [7] 中,项目中将显示对 Web 服务 WsHello 的引用。此外,已创建配置文件 [app.config]。
  • 在 [8] 中,查看项目中的所有文件。
  • 在 [9] 中,对 Web 服务 WsHello 的引用包含各种文件,我们在此不作详细说明。不过,我们将查看文件 [Reference.cs],该文件是生成的客户端 C# 代码:

namespace Client.WsHello {
...
    public partial class ServiceSoapClient : System.ServiceModel.ClientBase<Client.WsHello.ServiceSoap>, Client.WsHello.ServiceSoap {
        
        public ServiceSoapClient() {
        }
...        
        public string DisBonjourALaDame(string nomDeLaDame) {
            Client.WsHello.DisBonjourALaDameRequest inValue = new Client.WsHello.DisBonjourALaDameRequest();
            inValue.Body = new Client.WsHello.DisBonjourALaDameRequestBody();
            inValue.Body.nomDeLaDame = nomDeLaDame;
            Client.WsHello.DisBonjourALaDameResponse retVal = ((Client.WsHello.ServiceSoap)(this)).DisBonjourALaDame(inValue);
            return retVal.Body.DisBonjourALaDameResult;
        }
    }
}
  • 第 1 行:生成的客户端命名空间为 Client.WsHello。若需更改此命名空间,请在此处进行修改。
  • 第 3 行:类 ServiceSoapClient 是生成的客户端类。这是一个代理类,其作用是向 Windows 应用程序隐藏了远程 Web 服务的使用事实。 Windows 应用程序将通过本地类 Client.WsHello.ServiceSoapClient 调用远程类 WsHello。要创建客户端实例,需使用第 5 行中的构造函数:
Client.WsHello.ServiceSoapClient client=new Client.WsHello.ServiceSoapClient();
  • 第 8 行:方法 DisBonjourALaDame 是 Web 服务方法 DisBonjourALaDame 的客户端对应方法。 Windows 应用程序将通过本地方法 Client.WsHello.ServiceSoapClient.DisBonjourALaDame 调用远程方法 DisBonjourALaDame,形式如下:
string bonjour=client.DisBonjourALaDame("Carla Bruni");

生成的 [app.config] 文件如下:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.serviceModel>
        <bindings>
         ....
        </bindings>
        <client>
            <endpoint address="http://localhost:1906/WsHello/Service.asmx"... />
        </client>
    </system.serviceModel>
</configuration>

从该文件中,我们仅保留第 8 行,其中包含 Web 服务的 URI。如果该服务的 URI 发生变化,Windows 客户端无需重新构建。只需更改文件 [app.config] 中的 URI 即可。

让我们回到我们要构建的Windows应用程序架构:

我们已经构建了Web服务的[client]层。接下来将构建[ui]层:

编号
类型
名称
角色
1
TextBox
textBoxNomDame
女士姓名
2
Button
buttonSalutations
用于连接远程 Web 服务 WsHello 并调用方法 DisBonjourALaDame
3
标签
labelBonjour
Web 服务返回的结果

表单代码 [Form1.cs] 如下:


using System;
using System.Windows.Forms;
using Client.WsHello;

namespace ClientSalutations {
    public partial class Form1 : Form {
        public Form1() {
            InitializeComponent();
        }

        private void buttonSalutations_Click(object sender, EventArgs e) {
            // 沙漏
            Cursor=Cursors.WaitCursor;
            // Web服务查询
            labelBonjour.Text = new ServiceSoapClient().DisBonjourALaDame(textBoxNomDame.Text.Trim());
            // 普通光标
            Cursor = Cursors.Arrow;
        }
    }
}
  • 第 15 行:Web 服务客户端被实例化。其类型为 Client.WsHello.ServiceSoapClient。 第 3 行声明了命名空间 Client.WsHello。调用了本地方法 ServiceSoapClient().DisBonjourALaDame。已知该方法会调用 Web 服务中同名的远程方法。

12.3. 一个算术运算 Web 服务

我们将构建第二个客户端/服务器应用程序,其架构再次采用以下简化形式:

前一个 Web 服务仅提供一个方法。现在我们考虑一个提供四则运算的 Web 服务:

  1. add(a,b),返回 a+b
  2. 减法(a,b),返回 a-b
  3. 乘法(a,b),返回 a*b
  4. 除法(a,b),返回 a/b

并将通过以下图形界面进行调用:

  • 在 [1] 中,待执行的操作
  • 在 [2,3] 中:操作数
  • 在 [4] 中,调用 Web 服务的按钮
  • 在 [5] 中,Web 服务返回的结果

12.3.1. 服务器端

我们使用 Visual Web Developer 构建一个 Web 服务类型的项目:

  • 在 [1] 中,生成的 Web 应用程序 WsOperations
  • 在 [2] 中,对 Web 应用程序 WsOperations 进行了如下调整:
  • 网页 [Service.asmx] 已重命名为 [Operations.asmx]
  • 类 [Service.cs] 已重命名为 [Operations.cs]
  • 文件 [web.config] 已被删除,以示其并非必需。

网页 [Service.asmx] 包含以下内容:


<%@ WebService Language="C#" CodeBehind="~/App_Code/Operations.cs" Class="Operations" %>

该 Web 服务由以下类 [Operations.cs] 提供:


using System.Web.Services;

[WebService(Namespace = "http://st.istia.univ-angers.fr/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class Operations : System.Web.Services.WebService
{
    [WebMethod]
    public double Ajouter(double a, double b)
    {
        return a + b;
    }

    [WebMethod]
    public double Soustraire(double a, double b)
    {
        return a - b;
    }

    [WebMethod]
    public double Multiplier(double a, double b)
    {
        return a * b;
    }

    [WebMethod]
    public double Diviser(double a, double b)
    {
        return a / b;
    }

}

要将 Web 服务部署上线,我们按照 [3] 中的说明进行操作。随后,我们将获得 Web 服务 WsOperations 的 4 个方法的测试页面:

Image

请读者测试这 4 个方法。

12.3.2. 客户端部分

使用 Visual C# 创建一个 Windows 应用程序 ClientWsOperations

  • 在 [1] 中,Visual C# 中的 ClientWsOperations 项目
  • 在 [2] 中,项目的默认命名空间为 Client(右键单击项目 / 属性 / 应用程序)。该命名空间将用于构建即将生成的客户端命名空间。
  • 在 [3] 中,右键单击项目以向其添加对现有 Web 服务的引用
  • 在 [4] 中,输入之前构建的 Web 服务的 URI。为此,需查看显示 Web 服务测试页面的浏览器地址栏中的内容。
  • 在 [4b] 中,将 Visual C# 连接到由 [4] 指定的 Web 服务。Visual C# 将获取该 Web 服务的描述,并利用该描述生成一个客户端。
  • 在 [5] 中,获取 Web 服务描述后,Visual C# 可显示其公共方法
  • 在 [6] 中,为即将生成的客户端指定一个命名空间。该命名空间将被添加到 [2] 中定义的命名空间中。因此,客户端的命名空间将为 Client.WsOperations
  • 在 [6b] 中确认向导。
  • 在 [7] 中,对 Web 服务 WsOperations 的引用已出现在项目中。此外,已创建了一个配置文件 [app.config]。

需要提醒的是,生成的客户端类型为 Client.WsOperations.OperationsSoapClient,其中

  • Client.WsOperations 是 Web 服务客户端的命名空间
  • Operations 是远程 Web 服务的类。

尽管存在一种逻辑构建此名称的方法,但通常更简单的方式是直接在默认情况下为隐藏文件的 [Reference.cs] 中查找。其内容如下:


namespace Client.WsOperations {
 ...
    public partial class OperationsSoapClient : System.ServiceModel.ClientBase<Client.WsOperations.OperationsSoap>, Client.WsOperations.OperationsSoap {
        
        public OperationsSoapClient() {
        }
...
        public double Ajouter(double a, double b) {
            ...
        }
        
        public double Soustraire(double a, double b) {
            ...
        }
        
        public double Multiplier(double a, double b) {
            ...
        }
        
        public double Diviser(double a, double b) {
            ...
        }
    }
}

远程 Web 服务的 AjouterSoustraireMultiplier、Diviser将通过客户端(类型为Client.WsOperations.OperationsSoapClient,第3行)中同名的代理方法(第8、12、16、20行)进行访问。

接下来我们需要构建图形用户界面:

编号
类型
名称
角色
1
ComboBox
comboBoxOperations
算术运算列表
2
TextBox
textBoxA
数字 a
3
TextBox
textBoxB
编号 b
4
按钮
buttonExécuter
查询远程Web服务
5
标签
labelRésultat
操作结果

[Form1.cs] 的代码如下:


using System;
using System.Windows.Forms;
using Client.WsOperations;

namespace ClientWsOperations {
    public partial class Form1 : Form {
        // 运算表
        private string[] opérations = { "Ajouter", "Soustraire", "Multiplier", "Diviser" };
        // 待联系的Web服务
        private OperationsSoapClient opérateur = new OperationsSoapClient();

        // 生成器
        public Form1() {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e) {
            // 运算下拉列表填充
            comboBoxOperations.Items.AddRange(opérations);
            comboBoxOperations.SelectedIndex = 0;
        }

        private void buttonExécuter_Click(object sender, EventArgs e) {
            // 运算参数 a 和 b 的验证
            textBoxMessage.Text = "";
            bool erreur = false;
            Double a = 0;
            if (!Double.TryParse(textBoxA.Text, out a)) {
                textBoxMessage.Text += "Nombre a erroné...";
            }
            Double b = 0;
            if (!Double.TryParse(textBoxB.Text, out b)) {
                textBoxMessage.Text += String.Format("{0}Nombre b erroné...", Environment.NewLine);
            }
            if (erreur) {
                return;
            }
            // 执行操作
            Double c=0;
            try {
                switch (comboBoxOperations.SelectedItem.ToString()) {
                    case "Ajouter":
                        c=opérateur.Ajouter(a, b);
                        break;
                    case "Soustraire":
                        c=opérateur.Soustraire(a, b);
                        break;
                    case "Multiplier":
                        c=opérateur.Multiplier(a, b);
                        break;
                    case "Diviser":
                        c=opérateur.Diviser(a, b);
                        break;
                }
                // 显示结果
                labelRésultat.Text = c.ToString();
            } catch (Exception ex) {
                textBoxMessage.Text = ex.Message;
            }
        }
    }
}
  • 第 3 行:远程 Web 服务客户端的命名空间
  • 第 10 行:远程 Web 服务客户端与表单同时实例化
  • 第 17-21 行:操作下拉列表在表单初始加载时被填充
  • 第 23 行:执行用户请求的操作
  • 第25-37行:验证输入值a和b是否为实数
  • 第 41-54 行:使用 switch 语句来执行用户请求的远程操作
  • 第 43、46、49、52 行:查询的是本地客户端。该客户端会以透明的方式向远程 Web 服务发起查询。

12.4. 一个税费计算Web服务

我们重新审视这个如今已广为人知的税费计算应用程序。上次我们开发它时,将其构建为一个可通过互联网调用的远程TCP服务器。现在,我们将它改造成一个Web服务。

第 8 版的架构如下:

第 9 版的架构将与此类似:

该架构与第11.9.1节中探讨的第8版架构相似,但TCP服务器和客户端已被Web服务及其代理客户端所取代。 我们将完整沿用第8版中的[ui]、[metier]和[dao]这三个层。

12.4.1. 服务器部分

我们使用 Visual Web Developer 构建一个 Web 服务类型的项目:

  • 在 [1] 中,生成的 Web 应用程序 WsImpot
  • 为 [2],Web 应用程序 WsImpot 已按以下方式重新设计:
    • 网页 [Service.asmx] 已重命名为 [ServiceImpot.asmx]
    • 类 [Service.cs] 已重命名为 [ServiceImpot .cs]

网页 [ServiceImpot.asmx] 包含以下内容:


<%@ WebService Language="C#" CodeBehind="~/App_Code/ServiceImpot.cs" Class="ServiceImpot" %>

Web 服务由以下类 [ServiceImpot.cs] 提供:


using System.Web.Services;

[WebService(Namespace = "http://st.istia.univ-angers.fr/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class ServiceImpot : System.Web.Services.WebService
{

    [WebMethod]
    public int CalculerImpot(bool marié, int nbEnfants, int salaire)
    {
        return 0;
    }

}

Web 服务仅会暴露第 9 行中的 CalculerImpot 方法。

让我们回到第 8 版的客户端/服务器架构:

服务器 [1] 的 Visual Studio 项目如下:

  • 在 [1] 项目中,包含以下组件:
    • [ServeurImpot.cs]:以控制台应用程序形式存在的税务计算TCP/IP服务器。
    • [dbimpots.sdf]:第9.8.5节所述的SQL Server 7.0紧凑型数据库。
    • [App.config]:应用程序的配置文件。
  • 在 [2] 中,[lib] 文件夹包含项目所需的 DLL:
    • [ImpotsV7-dao]:第 7 版的 [dao] 层
    • [ImpotsV7-metier]:第 7 版的 [metier] 层
    • [antlr.runtime, CommonLogging, Spring.Core](用于 Spring
  • 在 [3] 中,项目引用

该版本的 [metier] 和 [dao] 层已存在:它们是版本 7 和 8 中使用的层。它们以 DLL 的形式存在,我们将它们按以下方式集成到项目中:

  • 将版本 8 服务器上的 [lib] 文件夹复制到版本 9 Web 服务项目中,生成 [1]。
  • 在 [2] 中,修改页面属性,将 [lib] 文件夹中的 DLL 和 [4] 添加到 [3] 项目的引用中。

完成此操作后,我们获得了服务器 [1] 所需的全部图层,如下所示:

如果服务器 [1]、[serveur]、[metier]、[dao]、[entites]、 [spring] 这些元素均已存在于 Visual Studio 项目中,但我们缺少在 Web 应用程序启动时对其进行实例化的组件。 在第 8 版中,一个拥有静态方法 [Main] 的主类负责借助 Spring 实例化各层。在 Web 应用程序中,能够执行类似工作的类是与文件 [Global.asax] 关联的类

  • 在 [1] 中,向 Web 项目添加了一个新元素
  • 在 [2] 中,选择“全局应用程序类”类型
  • 在 [3] 中,该元素的默认建议名称
  • 在 [4] 中,确认添加
  • 在 [5] 中,新元素已集成到项目中

让我们查看文件 [Global.asax] 的内容:


<%@ Application Language="C#" %>

<script runat="server">

    void Application_Start(object sender, EventArgs e) 
    {
        // 应用程序启动时运行的代码
    }
    
    void Application_End(object sender, EventArgs e) 
    {
        //  应用程序关闭时运行的代码
    }
        
    void Application_Error(object sender, EventArgs e) 
    { 
        // 发生未处理错误时运行的代码
    }

    void Session_Start(object sender, EventArgs e) 
    {
        // 新会话开始时运行的代码
    }

    void Session_End(object sender, EventArgs e) 
    {
        // 会话结束时运行的代码 
    }
       
</script>

该文件混合了发往 Web 服务器的标签(第 1、3、30 行)和 C# 代码。这种方法曾是 ASP(即 ASP.NET 的前身)的唯一实现方式,而 ASP.NET 是微软当前的 Web 编程技术。 在 ASP.NET 中,该方法仍然可用,但并非默认方法。默认方法是所谓的“CodeBehind”方法,我们在 Web 服务页面中曾遇到过,例如此处的 [ServiceImpot.asmx]:


<%@ WebService Language="C#" CodeBehind="~/App_Code/ServiceImpot.cs" Class="ServiceImpot" %>

CodeBehind 属性指定了页面 [ServiceImpot.asmx] 的源代码所在位置。 如果没有这个属性,源代码将位于页面 [ServiceImpot.asmx] 中,其语法与 [Global.asax] 中发现的类似。我们不会保留生成的 [Global.asax] 文件,但其代码让我们能够理解它的用途:

  • Global.asax 关联的类在应用程序启动时被实例化。其生命周期与整个应用程序一致。具体来说,它仅在 Web 服务器停止运行时才会消失。
  • 随后将执行 Application_Start 方法。这是该方法唯一一次被调用。因此,我们利用它来实例化所有用户共用的对象。这些对象被放置在:
    • 或者在与Global.asax关联的类的静态字段中。由于该类是永久存在的,因此任何用户的查询均可从中读取信息。
    • 或者是在 Application 容器中。该容器同样在应用程序启动时创建,其生命周期与应用程序一致。
      • 要将数据放入该容器,应编写:Application["clé"]=值;
      • 要读取该数据,则写入 T 值=(T)Application["clé"]; 其中 T 是 valeur 的类型。
  • 每当有新用户发起请求时,都会执行 Session_Start 方法。如何识别新用户?每个用户(通常是浏览器)在首次请求完成后,会收到一个会话令牌,该令牌是针对每个用户唯一的字符串。 随后,用户在每次发出新请求时都会返回收到的会话令牌。这使得Web服务器能够识别该用户。随着同一用户发出不同请求,其专属数据可存储在Session容器中
    • 要将数据放入该容器,应写为 Session["clé"]=值;
    • 要检索该数据,则写入 T 值=(T)Session["clé"]; 其中 T 是 valeur 的类型。

会话的生命周期默认限制为用户闲置 20 分钟(即 c.a.d,表示用户 20 分钟内未返回会话令牌)。

  • 当 Web 应用程序未处理的异常上报至 Web 服务器时,将执行方法 Application_Error
  • 其他方法则较少使用。

在介绍了这些概述之后,Global.asax 能为我们提供什么帮助?我们将使用其方法 Application_Start 来初始化 [metier]、 [metier] 和 [entites] 层,这些层包含在 DLL 和 [ImpotsV7-metier, ImpotsV7-dao] 中。 我们将使用 Spring 进行实例化。随后,将把这样创建的图层引用存储在与 Global.asax 关联的类的静态字段中

第一步,我们将 Global.asax 中的 C# 代码移至一个单独的类中。项目结构如下所示:

在 [1] 中,文件 [Global.asax] 将与类 [Global.cs] [2] 关联,并包含以下唯一一行代码:


<%@ Application Language="C#" Inherits="WsImpot.Global"%>

属性 Inherits="WsImpot.Global" 表示与 Global.asax 关联的类继承自类 WsImpot.Global。该类在 [Global.cs] 中定义如下:


using System;
using Metier;
using Spring.Context.Support;
namespace WsImpot
{
    public class Global : System.Web.HttpApplication
    {
        // 业务层
        public static IImpotMetier Metier;

        // 应用程序启动时执行的方法
        private void Application_Start(object sender, EventArgs e)
        {
            // [metier] 和 [dao] 层的实例化
            Metier = ContextRegistry.GetContext().GetObject("metier") as IImpotMetier;
        }
    }
}
  • 第 4 行:类的命名空间
  • 第 6 行:类 Global。可以为其命名任意名称。关键在于它必须继承自类 System.Web.HttpApplication
  • 第 9 行:一个公共静态字段,将包含对 [metier] 层的引用。
  • 第 12 行:Application_Start 方法,该方法将在应用程序启动时执行。
  • 第 15 行:使用 Spring 处理 [web.config] 文件,从中查找待实例化的对象,以创建 [metier] 和 [dao] 层。 在 Windows 应用程序中使用 Spring 与 [App.config],与在 Web 应用程序中使用 Spring 与 [web.config] 之间没有任何区别。 此外,[web.config] 和 [App.config] 具有相同的结构。第 15 行将 [metier] 层的引用存储在第 9 行的静态字段中,以便该引用可供所有用户的所有请求使用。

文件 [web.config] 的内容如下:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>

  <configSections>
    <sectionGroup name="spring">
      <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
      <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
    </sectionGroup>
  </configSections>

  <spring>
    <context>
      <resource uri="config://spring/objects" />
    </context>
    <objects xmlns="http://www.springframework.net">
      <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
        <constructor-arg index="0" value="MySql.Data.MySqlClient"/>
        <constructor-arg index="1" value="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;"/>
        <constructor-arg index="2" value="select limite, coeffr, coeffn from tranches"/>
      </object>
      <object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
        <constructor-arg index="0" ref="dao"/>
      </object>
    </objects>
  </spring>
</configuration>

这是应用程序第 7 版中使用的 [App.config] 文件,并在第 9.8.4 节中进行了探讨。

  • 第16-20行:定义了一个名为[dao]的图层,该图层与数据库MySQL5关联。该数据库已在第9.8.1节中描述。
  • 第21-23行:定义了[metier]层

让我们回到服务器这个难题:

应用程序启动时,已实例化 [metier] 和 [dao] 层。这些层的生命周期与应用程序本身一致。Web 服务何时被实例化?实际上是在每次收到请求时。 请求结束时,处理该请求的对象会被销毁。因此,Web 服务乍看之下是无状态的。它无法在两个请求之间将其所属字段中的信息保存下来。但它可以在用户的会话中保存信息。为此,它所暴露的方法必须标记一个特殊属性:


    [WebMethod(EnableSession=true)]
    public int CalculerImpot(bool marié, int nbEnfants, int salaire)
....

上文第 1 行允许方法 CalculerImpot 访问我们之前提到的容器 Session。 在我们的应用程序中无需使用该属性。因此,Web 服务 WsImpot 将在每次请求时被实例化,且为无状态。

现在我们可以编写实现该 Web 服务的 [ServiceImpot.cs] 代码:


using System.Web.Services;
using WsImpot;

[WebService(Namespace = "http://st.istia.univ-angers.fr/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class ServiceImpot : System.Web.Services.WebService
{

    [WebMethod]
    public int CalculerImpot(bool marié, int nbEnfants, int salaire)
    {
        return Global.Metier.CalculerImpot(marié, nbEnfants, salaire);
    }

}
  • 第 10 行:Web 服务的唯一方法
  • 第12行:使用[metier]层中的CalculerImpot方法。 该层的引用位于类 Global 的静态字段 Metier 中。该类属于命名空间 WsImpot(第 2 行)。

现在我们可以启动 Web 服务了。在此之前,必须先启动 SGBD 和 MySQL5,以便访问 bdimpots 数据库。完成上述操作后,我们启动 [1] Web 服务:

浏览器随后显示页面 [2]。我们点击链接:

CalculerImpot 方法的三个参数分别赋值,并调用该方法。我们得到以下正确结果:

Image

12.4.2. 用于远程 Web 服务的 Windows 图形客户端

现在 Web 服务已经编写完成,我们转而开发客户端。让我们回顾一下客户端/服务器应用程序的架构:

我们需要编写客户端 [2]。图形界面将与第 8 版完全一致:

为了编写第9版的[client]部分,我们将以第8版的[client]部分为基础,然后进行必要的修改。 我们将复制第 11.9.4.1 节中研究的 Visual Studio 项目,将其重命名为 ClientWsImpot,并加载到 Visual Studio 中:

第 8 版的 Visual Studio 解决方案由 2 个项目组成:

  • [metier] 项目[1] 项目,该项目是税务计算 TCP 服务器的 TCP 客户端
  • 图形用户界面的项目 [ui] [2]。

需要进行的修改如下:

  • 项目 [metier] 现应作为 Web 服务的客户端
  • 项目 [ui] 需引用新层 [metier] 中的 DLL
  • [App.config]中[metier]层的配置需进行调整。

12.4.2.1. 新层 [metier]

  • 在 [1] 中,IImpotMetier 是 [metier] 层的接口,而 ImpotMetierTcp 是通过 TCP 客户端对其的实现
  • 在 [2] 中,我们删除了 ImpotMetierTcp 的实现。我们需要创建另一个 IImpotMetier 接口的实现,该实现将作为 Web 服务的客户端。
  • 在 [3] 中,我们将 Client 命名为项目 [metier] 的默认命名空间。 生成的 DLL 将命名为 [ImpotsV9-metier.dll]。
  • 在 [4] 中,我们创建了对 Web 服务 WsImpot 的引用。
  • 在 [5] 中,我们对其进行配置并验证。
  • 在 [6] 中,已创建对 Web 服务 WsImpot 的引用,并生成了文件 [app.config]。

在隐藏文件 [Reference.cs] 中:

  • 命名空间为 Client.WsImpot
  • 客户端类名为 ServiceImpotSoapClient
  • 它有一个唯一的签名方法:

        public int CalculerImpot(bool marié, int nbEnfants, int salaire) ;

接下来我们需要实现接口 IImpotMetier


namespace Metier {
    public interface IImpotMetier {
        int CalculerImpot(bool marié, int nbEnfants, int salaire);
    }
}

我们使用以下 ImpotMetierWs 类来实现它:


using System.Net.Sockets;
using System.IO;
using Client.WsImpot;

namespace Metier {
    public class ImpotMetierWs : IImpotMetier {

        // 远程 Web 服务客户端
        private ServiceImpotSoapClient client = new ServiceImpotSoapClient();

        // 税款计算
        public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
            return client.CalculerImpot(marié, nbEnfants, salaire);
        }

    }
}
  • 第 6 行:类 ImpotMetierWs 实现了接口 IImpotMetier
  • 第 9 行:在创建 ImpotMetierWs 实例时,字段 client 被初始化为一个税费计算 Web 服务客户端的实例。
  • 第 12 行:IImpotMetier 接口中需要实现的唯一方法。
  • 第13行:使用远程税务计算Web服务客户端的CalculerImpot方法。最终,将调用远程Web服务的CalculerImpot方法。

可以生成该项目的 DLL:

  • 生成 [1],项目 [client] 在最终状态下
  • 生成 [2],生成该项目的 DLL
  • 生成 [3],DLL 和 ImpotsV9-metier.dll 位于项目的 /bin/Release 文件夹中。

12.4.2.2. 新的 [ui] 层

客户端的 [client] 层已编写完成。接下来我们需要编写 [ui] 层。回到 Visual Studio 项目:

  • 在 [1] 中,源自第 8 版的 [ui] 项目
  • 转换为 [2], 旧层 [metier] 中的 DLL ImpotsV8-metier 被新层中的 DLL ImpotsV9-metier 所取代
  • ,[3],DLL和ImpotsV9-metier已添加至项目引用中。

第二个变更涉及 [App.config]。需注意,Spring 会使用该文件来实例化 [metier] 层。由于该层已发生变更,因此 [App.config] 的配置也需相应调整。 此外,[App.config] 必须具备连接远程税务计算 Web 服务的配置。该配置是在项目 [metier] 的文件 [app.config] 中生成的,当时向该文件中添加了对远程 Web 服务的引用。

因此,文件 [App.config] 内容如下:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <object name="metier" type="Metier.ImpotMetierWs, ImpotsV9-metier">
            </object>
        </objects>
    </spring>

    <!-- Web 服务 -->
    <system.serviceModel>
        <bindings>
            <basicHttpBinding>
                <binding name="ServiceImpotSoap" closeTimeout="00:01:00" openTimeout="00:01:00"
                        receiveTimeout="00:10:00" sendTimeout="00:01:00" allowCookies="false"
                        bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard"
                        maxBufferSize="65536" maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
                        messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered"
                        useDefaultWebProxy="true">
                    <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
                            maxBytesPerRead="4096" maxNameTableCharCount="16384" />
                    <security mode="None">
                        <transport clientCredentialType="None" proxyCredentialType="None"
                                realm="" />
                        <message clientCredentialType="UserName" algorithmSuite="Default" />
                    </security>
                </binding>
            </basicHttpBinding>
        </bindings>
        <client>
            <endpoint address="http://localhost:2172/WsImpot/ServiceImpot.asmx"
                    binding="basicHttpBinding" bindingConfiguration="ServiceImpotSoap"
                    contract="WsImpot.ServiceImpotSoap" name="ServiceImpotSoap" />
        </client>
    </system.serviceModel>
</configuration>
  • 第 15-18 行:Spring 仅实例化了一个对象,即 [metier] 层
  • 第 16 行:[metier] 层由位于 DLL 中的 [Metier.ImpotMetierWs] 类实例化。
  • 第 22-46 行:远程 Web 服务客户端的配置。这是从项目 [metier] 中复制并粘贴文件 [app.config] 的内容。

准备就绪。按 Ctrl-F5 运行应用程序(需确保 Web 服务已启动,SGBD 和 MySQL5 已启动,且上述第 42 行中的端口设置正确):

  

12.5. 用于税务计算 Web 服务的 Web 客户端

让我们回顾一下刚刚编写的客户端/服务器应用程序的架构:

上文中的 [ui] 层是由 Windows 图形客户端实现的。现在我们将其通过 Web 界面实现:

 

这对用户而言是一项重大变化。目前,我们的客户端/服务器应用程序(第 9 版)可以同时服务多个客户端。这相较于第 8 版(当时每次只能服务一个客户端)是一项改进。 目前的限制是,希望使用税务计算网络服务的用户必须在其计算机上安装我们编写的Windows客户端。在即将推出的新版本(暂称为第10版)中,用户将能够通过浏览器直接访问税务计算网络服务。

在上述架构中:

  • 服务器端保持不变,与第9版完全一致。
  • 客户端方面,[client du service web]层保持不变。它已被封装在DLL和[ImpotsV9-metier]中。我们将复用该DLL。
  • 最终,唯一的改动就是将 Windows 图形界面替换为 Web 界面。

我们将探讨服务器端 Web 编程的一些新概念。由于本文档的目的并非教授 Web 编程,我们将尝试解释即将采取的方法,但不会深入细节。因此,本节内容会略带一些“魔术”色彩。不过,我们认为采取这种方法很有意义,因为它展示了一个多层架构的新示例,其中其中一层发生了变化。

因此,第 10 版的架构如下:

我们已经拥有所有层,唯独缺少 [web] 这一层。为了更好地理解后续操作,我们需要更详细地说明客户端的架构。其架构如下:

  • Web用户在浏览器中有一个Web表单
  • 该表单被提交至 Web 服务器 1,由其通过 [web] 层进行处理
  • [web] 层将需要远程 Web 服务客户端的服务,该服务封装在 [ImpotsV9-metier.dll] 中。
  • 远程 Web 服务客户端将与托管该远程 Web 服务的 Web 服务器 2 进行通信。
  • 远程 Web 服务的响应将回传至客户端的 Web 层,该层将响应格式化为页面并发送给用户。

因此,我们的工作是:

  • 构建用户在浏览器中看到的网页表单
  • 编写Web应用程序,用于处理用户的请求,并以新网页的形式向用户发送响应。该网页实际上与表单相同,只是我们在其中添加了应缴税额
  • 编写使所有组件协同工作的“粘合剂”

所有这些工作都将借助使用 Visual Web Developer 创建的新网站来完成:

  • [1]:选择“文件”/“新建网站”
  • [2]:选择“网站”类型的应用程序
  • [3]:选择开发语言:C#
  • [4]:指定创建项目的文件夹
  • [5]:在 Visual Web Developer 中创建的项目
    • [Default.aspx] 是一个被称为“默认页面”的网页。当访问 http://.../ClientAspImpot 且未指定具体文档时,系统将返回该页面。用户在浏览器中看到的税款计算表单即包含于此页面中。
    • [Default.aspx.cs] 是与该页面关联的类,它将生成发送给用户的表单,并在用户填写并提交后处理该表单。
    • [web.config] 是应用程序的配置文件。与之前不同,这次我们将保留它。

若回顾我们需要构建的架构:

  • [1] 将由 [Default.aspx] 实现
  • [2] 将由 [Default.aspx.cs] 实现
  • [3] 将由 DLL 和 [ImpotV9-metier] 实现

首先实现 [3] 层。该过程包括以下几个步骤:

  • 在 [1] 中,将 Windows 9 版图形客户端的 [lib] 文件夹复制到 Web 项目 [ClientAspWsImpot] 的文件夹中。 此操作需通过Windows资源管理器完成。要使该文件夹在Web Developer解决方案中显示,需点击[2]按钮刷新解决方案。
  • 随后将其添加到项目 [3,4,5] 的引用中。所引用的 DLL 文件会自动复制到项目 [6] 的 /bin 文件夹中。

现在我们已拥有 Spring 运行所需的 DLL,远程 Web 服务的客户端层也已实现。虽然其代码已存在,但仍需进行配置。在第 9 版中,它是通过以下 [App.config] 文件进行配置的:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <object name="metier" type="Metier.ImpotMetierWs, ImpotsV9-metier">
            </object>
        </objects>
    </spring>

    <!-- Web 服务 -->
    <system.serviceModel>
        <bindings>
            <basicHttpBinding>
...
            </basicHttpBinding>
        </bindings>
        <client>
            <endpoint address="http://localhost:2172/WsImpot/ServiceImpot.asmx"
                    binding="basicHttpBinding" bindingConfiguration="ServiceImpotSoap"
                    contract="WsImpot.ServiceImpotSoap" name="ServiceImpotSoap" />
        </client>
    </system.serviceModel>
</configuration>

我们将此配置原样保留,并按以下方式将其集成到文件 [web.config] 中:


<?xml version="1.0"?>
<configuration>


    <configSections>
      <sectionGroup name="system.web.extensions"...>
...
      </sectionGroup>
      <!-- Spring 部分开始 -->
        <sectionGroup name="spring">
          <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
          <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
      <!-- Spring 部分结束 -->
    </configSections>

  <!-- Spring 配置开始 -->
  <spring>
    <context>
      <resource uri="config://spring/objects" />
    </context>
    <objects xmlns="http://www.springframework.net">
      <object name="metier" type="Metier.ImpotMetierWs, ImpotsV9-metier">
      </object>
    </objects>
  </spring>
  <!-- Spring 配置结束 -->

  <!-- 远程 Web 服务客户端配置开始 -->
  <system.serviceModel>
    <bindings>
      <basicHttpBinding>
...
      </basicHttpBinding>
    </bindings>
    <client>
      <endpoint address="http://localhost:2172/WsImpot/ServiceImpot.asmx"
                    binding="basicHttpBinding" bindingConfiguration="ServiceImpotSoap"
                    contract="WsImpot.ServiceImpotSoap" name="ServiceImpotSoap" />
    </client>
  </system.serviceModel>
  <!-- 远程 Web 服务客户端配置结束 -->

   <!-- 生成的 web.config 中已存在的其他配置 -->
...
</configuration>

请注意,第37行引用了远程Web服务的端口。该端口可能会发生变化,因为Visual Developer会在随机端口上启动Web服务。

让我们回到需要构建的 Web 客户端架构:

  • [1] 将由 [Default.aspx] 实现
  • [2] 将由 [Default.aspx.cs] 实现
  • [3] 已由 DLL 实现 [ImpotV9-metier]

我们刚刚实现了 [3] 层。 我们转到由页面 [Default.aspx] 实现的 Web 界面 [1]。双击页面 [Default.aspx] 进入设计模式。

构建网页有两种方式:

  • 图形化方式,如 [2] 所示。此时需在 [1] 中选择 [Design] 模式。该按钮栏位于网页编辑器的状态栏底部。
  • 使用标记语言,如 [3] 所示。此时需在 [Source] 和 [1] 之间选择模式。

[Design] 和 [Source] 模式是双向的:在 [Design] 模式下进行的修改会转化为 [Source] 模式下的修改,反之亦然。 请注意,要在浏览器中显示的网页表单如下:

  • 在 [1] 中,浏览器中显示的表单
  • 在 [2] 中,用于构建表单的组件
  • 在 [3] 中,表单的设计页面。它包含以下元素:
    • A,两个名为RadioButtonOui和RadioButtonNon的单选按钮
    • B行,一个名为TextBoxEnfants的输入框和一个名为LabelErreurEnfants的标签
    • C 行:一个名为 TextBoxSalaire 的输入框和一个名为 LabelErreurSalaire 的标签
    • D 行,一个名为 LabelImpot 的标签
    • E行,两个按钮,名称分别为ButtonCalculerButtonEffacer

将组件放置在设计区域后,即可访问其属性:

  • 在 [1] 中,访问某个组件的属性
  • 在 [2] 中,可查看组件 [LabelErreurEnfants ] 的属性表
  • 在 [3] 中,(ID) 是组件的名称
  • 在 [4] 中,我们将标签文字设置为红色。

仅仅将组件放置在表单上并设置其属性是不够的。还必须组织它们的布局。在 Windows 图形界面中,这种布局是绝对的。只需将组件拖动到想要的位置即可。而在网页中,情况则不同,虽然更复杂,但功能也更强大。本文将不涉及这一方面。

该设计生成的源代码 [Default.aspx] 如下:


<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title>Calculer votre impôt</title>
</head>
<body bgcolor="#ffff99">
  <h2>
    Calculer votre impôt</h2>
  <form id="form1" runat="server">
  <asp:ScriptManager ID="ScriptManager2" runat="server" EnablePartialRendering="true" />
  <asp:UpdatePanel runat="server" ID="UpdatePanelPam">
    <ContentTemplate>
      <div>
      </div>
      <table>
        <tr>
          <td>
            Etes-vous marié(e)
          </td>
          <td>
            <asp:RadioButton ID="RadioButtonOui" runat="server" GroupName="statut" Text="Oui" />
            <asp:RadioButton ID="RadioButtonNon" runat="server" GroupName="statut" Text="Non"
              Checked="True" />
          </td>
        </tr>
        <tr>
          <td>
            Nombre d&#39;子节点
          </td>
          <td>
            <asp:TextBox ID="TextBoxEnfants" runat="server" Columns="3"></asp:TextBox>
          </td>
          <td>
            <asp:Label ID="LabelErreurEnfants" runat="server" ForeColor="#FF3300"></asp:Label>
          </td>
        </tr>
        <tr>
          <td>
            Salaire annuel
          </td>
          <td>
            <asp:TextBox ID="TextBoxSalaire" runat="server" Columns="8"></asp:TextBox>
          </td>
          <td>
            <asp:Label ID="LabelErreurSalaire" runat="server" ForeColor="#FF3300"></asp:Label>
          </td>
        </tr>
        <tr>
          <td>
            Impôt à payer
          </td>
          <td>
            <asp:Label ID="LabelImpot" runat="server" BackColor="#99CCFF"></asp:Label>
          </td>
        </tr>
      </table>
      <br />
      <table>
        <tr>
          <td>
            <asp:Button ID="ButtonCalculer" runat="server" Text="Calculer" OnClick="ButtonCalculer_Click" />
          </td>
          <td>
            <asp:Button ID="ButtonEffacer" runat="server" Text="Effacer" OnClick="ButtonEffacer_Click" />
          </td>
          <td>
            &nbsp;
          </td>
        </tr>
      </table>
      </div>
    </ContentTemplate>
  </asp:UpdatePanel>
  </form>
</body>
</html>

表单组件位于第 23、24、33、36、44、47、55、63 和 66 行。其余部分主要是格式设置。

让我们回到需要构建的架构:

  • [1] 已由 [Default.aspx] 实现
  • [2] 将由 [Default.aspx.cs] 实现
  • [3] 已由 DLL [ImpotV9-metier] 实现

[1] 和 [3] 层现已实现。 接下来我们需要编写 [2] 层,该层负责生成表单、将其发送给用户、处理用户填写后发回的表单、使用 [3] 层进行税款计算,并生成回复用户的网页后将其发回。 [Default.aspx.cs] 代码负责完成所有这些工作:


using System;
using WsImpot;

public partial class _Default : System.Web.UI.Page
{
    protected void ButtonCalculer_Click(object sender, EventArgs e)
    {
  ...
    }
    protected void ButtonEffacer_Click(object sender, EventArgs e)
    {
...
    }
}

该代码与经典的 Windows 表单代码非常相似。这正是 ASP.NET 技术的主要优势:Windows 编程模型与 ASP.NET 网页编程模型之间不存在断层。只需始终牢记以下流程:

当用户在[1]中点击[Calculer]按钮时,[Default.aspx.cs]第6行中的ButtonCalculer_Click过程将被执行。但在此期间:

  • 已填写表单的值将通过HTTP协议从浏览器传输至Web服务器
  • 服务器ASP.NET将分析该请求并将其转发至页面[Default.aspx]
  • [Default.aspx]页面将被实例化。
  • 其组件(RadioButtonOuiRadioButtonNonTextBoxEnfantsTextBoxSalaireLabelErreurEnfantsLabelErreurSalaireLabelImpot)将通过名为“ViewState”的机制,初始化为表单最初发送至浏览器时的值。
  • 提交的值将被赋给相应的组件(RadioButtonOuiRadioButtonNonTextBoxEnfantsTextBoxSalaire)。 因此,如果用户将子女数量设为2,则会得到TextBoxEnfants.Text="2"
  • 如果页面 [Default.aspx] 包含方法 [Page_Load],则该方法将被执行
  • 如果点击的是按钮 [Calculer],则将执行第 6 行中的方法 [ButtonCalculer_Click]
  • 如果点击的是按钮 [Effacer],则将执行第 10 行中的方法 [ButtonEffacer_Click]

从用户在浏览器中触发事件到该事件在 [Default.aspx.cs] 中被处理,其间存在高度的复杂性。这种复杂性是被隐藏的,因此在编写网页事件处理程序时,我们可以将其视为不存在。 但我们绝不能忘记,事件与其处理程序之间存在网络传输,因此绝不能处理诸如 Mouse_Move 之类的鼠标事件——这类事件会引发耗费资源的客户端/服务器往返通信……

按钮 [Calculer] 和 [Effacer] 的点击处理程序代码,与为传统 Windows 应用程序编写的代码相同:


protected void ButtonCalculer_Click(object sender, EventArgs e)
    {
        // 数据验证
        int nbEnfants;
        bool erreur = false;
        if (!int.TryParse(TextBoxEnfants.Text.Trim(), out nbEnfants) || nbEnfants < 0)
        {
            LabelErreurEnfants.Text = "Valeur incorrecte...";
            erreur = true;
        }
        int salaire;
        if (!int.TryParse(TextBoxSalaire.Text.Trim(), out salaire) || salaire < 0)
        {
            LabelErreurSalaire.Text = "Valeur incorrecte...";
            erreur = true;
        }
        // 错误?
        if (erreur) return;
        // 清除可能的错误
        LabelErreurEnfants.Text = "";
        LabelErreurSalaire.Text = "";
        // 婚姻状况
        bool marié = RadioButtonOui.Checked;
        // 计算税款
        try
        {
            LabelImpot.Text = String.Format("{0} euros",Global.Metier.CalculerImpot(marié, nbEnfants, salaire));
        }
        catch (Exception ex)
        {
            LabelImpot.Text = ex.Message;
        }
    }
  • 要理解这段代码,必须知道
    • 在代码执行之初,表单 [Default.aspx] 的状态即为用户填写后的状态。 因此,字段(RadioButtonOuiRadioButtonNonTextBoxEnfantsTextBoxSalaire)将包含用户输入的值。
    • 执行完成后,系统将自动向用户返回相同的页面 [Default.aspx]。

因此,过程 ButtonCalculer_Click 必须根据字段的当前值(RadioButtonOuiRadioButtonNonTextBoxEnfantsTextBoxSalaire)来确定所有字段(RadioButtonOuiRadioButtonNonTextBoxEnfantsTextBoxSalaire, LabelErreurEnfants, LabelErreurSalaire, LabelImpot)的值,该新页面[Default.aspx]将返回给用户。

这段代码没有特别的难点。只有第27行值得说明。它使用了Global.Metier字段的CalculerImpot方法,而该字段尚未出现。我们稍后会再回到这一点。

方法 ButtonEffacer_Click 如下:


    protected void ButtonEffacer_Click(object sender, EventArgs e)
    {
        // 清空表单
        TextBoxEnfants.Text = "";
        TextBoxSalaire.Text = "";
        LabelImpot.Text = "";
        LabelErreurEnfants.Text = "";
        LabelErreurSalaire.Text = "";
}

让我们回到需要构建的架构:

  • [1] 由 [Default.aspx] 实现
  • [2] 由 [Default.aspx.cs] 实现
  • [3] 已由 DLL 实现 [ImpotV9-metier]

接下来我们需要为这三个层构建“粘合层”。这主要涉及:

  • 在应用程序启动时实例化 [3] 层
  • 在适当位置放置一个引用,以便每次实例化网页 [Default.aspx.cs] 并要求其计算税款时,该网页都能从中获取该引用。

这并非新问题。在构建远程Web服务时已遇到过,并在第12.4.1节中进行了探讨。我们知道解决方案包括:

  • 创建一个与类 [Global.cs] 关联的文件 [Global.asax]
  • 在 [Global.cs] 的 Application_Start 方法中实例化 [3] 层
  • 将 [3] 层的引用放入 [Global.cs] 类的静态字段中,因为该类的生命周期与应用程序相同。

因此,我们的 Web 项目将按以下方式进行调整:

  • 演变为 [1],文件 [Global.asax]
  • 变为 [2],关联的代码为 [Global.cs]。包含该文件的 [App_Code] 文件夹默认不在 Web 解决方案中。请使用 [3] 创建该文件夹。

文件 Global.asax 内容如下:


<%@ Application Language="C#" Inherits="WsImpot.Global"%>

代码 [Global.cs] 如下:


using System;
using Metier;
using Spring.Context.Support;
namespace WsImpot
{
    public class Global : System.Web.HttpApplication
    {
        // 业务层
        public static IImpotMetier Metier;

        // 应用程序启动时执行的方法
        private void Application_Start(object sender, EventArgs e)
        {
            // [metier] 和 [dao] 层的实例化
            Metier = ContextRegistry.GetContext().GetObject("metier") as IImpotMetier;
        }
    }
}
  • 第 6 行:该类名为 Global,属于命名空间 WsImpot(第 4 行)。 因此其全名为 WsImpot.Global,需将此名称填入 Global.asaxInherits 属性中。
  • 第 6 行:已知与 Global.asax 关联的类必须继承自 System.Web.HttpApplication 类。
  • 第 12 行:在 Web 应用程序启动时执行的方法 Application_Start
  • 第 15 行:使用 Spring 并基于 [web.config] 中的以下配置,实例化 [metier] 层(正在构建的应用程序中的 [3] 层):

  <!-- Spring 对象 -->
  <spring>
    <context>
      <resource uri="config://spring/objects" />
    </context>
    <objects xmlns="http://www.springframework.net">
      <object name="metier" type="Metier.ImpotMetierWS, ImpotsV9-metier">
      </object>
    </objects>
</spring>

上文 (g) 行中的类 [Metier.ImpotMetierWS] 位于 [ImpotsV9-metier.dll] 中。

所创建的 [metier] 层引用被放入第 9 行的静态字段中。ButtonCalculer_Click 过程的第 27 行中使用的正是该字段:


LabelImpot.Text = String.Format("{0} euros",Global.Metier.CalculerImpot(marié, nbEnfants, salaire));

现在可以进行测试了。需要启动远程 Web 服务 SGBD MySQL5,并记录其运行的端口:

  

完成上述操作后,需检查Web客户端的[web.config]文件中,远程Web服务的端口是否正确:

完成上述操作后,可通过 Ctrl-F5 启动远程 Web 服务的 Web 客户端:

  

12.6. 用于税费计算 Web 服务的 Java 控制台客户端

为了演示Web服务可通过任何语言编写的客户端访问,我们将编写一个基本的Java控制台客户端。客户端/服务器应用程序的架构如下:

  • 客户端 [1] 将使用 Java 编写
  • 服务器 [2] 采用 C# 编写

首先,我们将修改税务计算Web服务中的一个细节。其在[ServiceImpot.cs]中的当前定义如下:


...
public class ServiceImpot : System.Web.Services.WebService
{

    [WebMethod]
    public int CalculerImpot(bool marié, int nbEnfants, int salaire)
    {
        return Global.Metier.CalculerImpot(marié, nbEnfants, salaire);
    }

}

测试表明,第 6 行和第 8 行中 marié 参数的重音符号可能会导致 Java 与 C# 之间的互操作性问题。我们将采用以下新定义:


...
public class ServiceImpot : System.Web.Services.WebService
{

    [WebMethod]
    public int CalculerImpot(bool marie, int nbEnfants, int salaire)
    {
        return Global.Metier.CalculerImpot(marie, nbEnfants, salaire);
    }

}

该服务将放置在一个名为 WsImpotsSansAccents 的新 Web Developer 项目中。该 Web 服务的 URL 将变为 [/WsImpotSansAccents/ServiceImpot.asmx]。

Image

编写 Java 客户端时,我们将使用 NetBeans IDE 和 [http://www.netbeans.org/]:

  • 在 [1] 中,创建一个新项目
  • 在 [2,3] 中,选择 Java 应用程序类型的项目 Java
  • 在 [4] 中,进入下一步
  • 在 [5] 中,为项目命名
  • 在 [6] 中,指定将创建名为该项目名称的子文件夹的目录
  • 在 [7] 中,为将包含在应用程序启动时执行的 main 方法的类命名
  • 在 [8] 中,完成向导
  • 在 [9] 中:生成的 Java 项目
  • 为 [10]:右键单击该项目以生成税款计算 Web 服务的客户端
  • 生成 [11],描述税费计算 Web 服务的文件 URL:

http://localhost:1089/WsImpotSansAccents/ServiceImpot.asmx?WSDL

该 URL 属于服务 [ServiceImpot.asmx],并在其后添加了参数 ?WSDL。位于该 URL 下的文档使用 XML 语言描述了服务 [15] 的功能。这是 Web 服务的标准组成部分。

  • 在 [12] 中,指定用于放置将要生成的类的包(相当于 C# 中的命名空间)
  • 在 [13] 中,保留默认值
  • 在 [14] 中,完成向导
  • 在 [16] 中,导入的 Web 服务已集成到 Java 项目中。它支持两种通信协议:Soap Soap12
  • 在 [17] 中,我们将使用生成的客户端的 [Main] 类
  • 在 [18] 中,我们将向方法 [main] 中插入代码。将光标置于需要插入代码的位置,右键单击并选择选项 [19]
  • 在 [20] 中,指定要生成远程税务计算服务中 CalculerImpot 函数的调用代码,然后点击“确定”。

在 [Main] 中生成的代码如下:

public class Main {

    public static void main(String[] args) {
         // TODO 应用程序逻辑代码在此
      try { // 调用 Web 服务操作
        wsimpot.ServiceImpot service = new wsimpot.ServiceImpot();
        wsimpot.ServiceImpotSoap port = service.getServiceImpotSoap();
         // TODO 在此初始化 WS 操作参数
        boolean marie = false;
        int nbEnfants = 0;
        int salaire = 0;
         // TODO 此处处理结果
        int result = port.calculerImpot(marie, nbEnfants, salaire);
        System.out.println("Result = "+result);
      } catch (Exception ex) {
         // TODO 在此处处理自定义异常
      }
    }
}

生成的代码展示了如何调用远程税务计算服务中的 CalculerImpot 函数。若与 C# 中的内容进行类比,第 7 行中的变量 port 相当于 C# 中使用的客户端。我们不再对该代码进行进一步说明。 我们将代码重构如下:

import wsimpot.ServiceImpot;
public class Main {
    public static void main(String[] args) {
      try {
         // 调用 Web 服务的 CalculerImpot 函数
        System.out.println(String.format("Montant à payer : %d euros", new ServiceImpot().getServiceImpotSoap().calculerImpot(true, 2, 60000)));
      } catch (Exception ex) {
        System.out.println(String.format("L'erreur suivante s'est produite %s",ex.getMessage()));
      }
    }
}
  • 第 1 行:导入类 ServiceImpot,该类代表向导生成的客户端。
  • 第 6 行:我们按照 main 中生成的代码中指定的步骤,调用远程方法 CalculerImpot

运行时(F6)在控制台中显示的结果如下:

init:
deps-jar:
wsimport-init:
wsimport-client-check-ServiceImpot.asmx:
wsimport-client-ServiceImpot.asmx:
wsimport-client-generate:
wsimport-client-compile:
Compiling 1 source file to C:\data\2007-2008\netbeans\ClientNetbeansPourServiceImpotDotNet\build\classes
compile:
run:
Montant à payer : 4282 euros
BUILD SUCCESSFUL (total time: 7 seconds)