65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 WCF 和自动客户端代理

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (19投票s)

2008年10月7日

CPOL

6分钟阅读

viewsIcon

91934

downloadIcon

580

以类似于 .NET Remoting 的方式使用 WCF 中自动生成的代理

引言

Microsoft 开发的 Windows Communication Foundation (WCF) 是其早期 .NET Web 服务和 Remoting 技术的自然后续。从文档上看,WCF 似乎只使用与 Web 服务相同的手动客户端代理生成方式。然而,WCF 同样能够以类似于 Remoting 的方式自动生成客户端代理。本文旨在纠正文档中的不足之处。

背景

Web 服务专门为需要公开接口到企业外部(此时共享接口代码不切实际)和/或技术不同的情况而设计(例如,Java 客户端使用 .NET Web 服务)。客户端通常通过从运行中的 Web 服务提取接口元数据、生成代理接口代码并将其构建到客户端应用程序中来创建。

Remoting 是为企业内部使用而设计的,通常在单个计算机或企业 LAN/WAN 上。客户端和服务端实际上是同一个整体应用程序或系统的一部分,共享接口程序集,客户端代理从这些程序集中在运行时自动生成。通常,客户端和服务端都作为同一解决方案的一部分或在同一个构建过程中构建,该构建过程可以在一次传递中完成。

从 Remoting 迁移过来的开发人员一定想知道如何像共享程序集一样自动生成 WCF 代理,因为 WCF 文档对此主题的解释并不充分。保留自动代理有充分的理由,包括更简单的构建过程以及避免因遗漏手动代理生成步骤而导致的接口不匹配错误。自动代理还可以在客户端和服务端之间提供通用类型,这对于想要在两者之间共享其他代码至关重要。

WCF 如何为我们提供自动客户端代理?这是一个简单的例子。

服务接口

首先,我们定义接口(契约),并使用常规的 WCF 服务和操作属性对其进行装饰。然后将其编译到共享程序集中,在本例中为 ICalculator.dll

// ICalculator.cs

using System.ServiceModel;

namespace Calculator
{
    [ServiceContract]
    public interface ICalculator : IDisposable
    {
        [OperationContract]
        int Add (int a, int b);
    }
}

WCF 文档对此主题有很好的涵盖,但值得指出的是 IDisposable 的使用,我稍后将解释其原因。

服务实现

这里的示例服务器实现了计算器服务,并提供了一个非常简单的控制台托管应用程序。

// CalculatorService.cs

using System;
using System.ServiceModel;

namespace Calculator
{

  // Implementation of CalculatorService

  public class CalculatorService : ICalculator
  {

    public CalculatorService ()
    {
      Console.WriteLine ("Constructing CalculatorService");
    }

    public int Add (int a, int b)
    {
      Console.WriteLine ("Call {0} Adding {1} and {2}", callNo++, a, b);
      return a+b;
    }

    public void Dispose ()
    {
      Console.WriteLine ("Disposing CalculatorService");
    }

    private int callNo = 1;

  }


  // Simple console service host

  public class TestProgram
  {
    public static void Main ()
    {
      ServiceHost svcHost = new ServiceHost (typeof (CalculatorService));
      svcHost.Open();
      Console.WriteLine ("Press a key to exit");
      Console.ReadKey();
      svcHost.Close();
    }
  }

}

CalculatorService 实现共享的 ICalculator 接口,并将操作记录到控制台。请注意,它还实现了 IDiposable,这为放置任何服务清理代码提供了逻辑位置。该服务还有一个私有计数器 callNo,说明了其保持状态信息的能力。

上面的非常简单的控制台服务托管应用程序只是启动 ServiceHost 并等待用户关闭它(请注意,这是 .NET 3.5 语法。 .NET 3.0 需要更多端点细节)。ServiceHost 在配置文件中定义的端点上监听连接。

<!--CalculatorService.config-->

<configuration>
  <system.serviceModel>

    <services>
      <service name="Calculator.CalculatorService">

        <endpoint address="net.tcp://:5000/Calc"
                  binding="netTcpBinding"
                  contract="Calculator.ICalculator" />

        <endpoint address="net.pipe:///Calc"
                  binding="netNamedPipeBinding"
                  contract="Calculator.ICalculator" />

        <endpoint address="https://:8081/Calc"
                  binding="wsHttpBinding"
                  contract="Calculator.ICalculator" />

        <endpoint address="https://:8080/Calc"
                  binding="basicHttpBinding"
                  contract="Calculator.ICalculator" />

      </service>
    </services>

  </system.serviceModel>

</configuration>

在这种情况下,我使用了四种端点暴露了相同的服务,这些端点对应于我将在下面进一步探讨的四种绑定。

客户端代理

由于本文是关于自动客户端代理的,这就是真正发挥作用的地方。首先,客户端程序必须引用共享接口程序集 ICalculator.dll,以便可以使用 ICalculator 类型。

创建代理不使用 new。有多种选择,但最接近常规构造的方式是

    ICalculator calc = new ChannelFactory<ICalculator>("TcpCalc").CreateChannel();

"TcpCalc" 是连接到的远程端点的名称,如客户端配置文件(下方)中所定义。

此语句在客户端创建了 ICalculator 类型的代理对象,并在服务器上创建了 CalculatorService 服务对象的实例。

创建代理后,客户端可以简单地调用远程服务对象的各种方法

    int sum1 = calc.Add (1,2);
    int sum2 = calc.Add (3,4);

这里存在一个问题。即使我们完成了客户端代理的使用,到服务器的通道(对于某些协议)仍可能保持打开状态,并且远程服务对象仍然存在,占用宝贵的网络和服务器资源。只有当客户端代理被垃圾回收时,系统才会进行清理。

幸运的是,碰巧 ChannelFactory 构建的代理对象始终实现 IDisposable 以及它们的泛型类型(ICalculator)。由于 ICalculator 也恰好实现了 IDisposable,我们可以简单地将其释放

    calc.Dispose();

这看起来就像一个代理调用对应的服务 Dispose() 操作,并且似乎具有此效果,但 Dispose() 未被定义为 [OperationContract],因此这不可能。实际上,客户端 Dispose() 会向服务发送“关闭”消息并关闭任何打开的连接。收到关闭消息后,服务会调用服务对象的 Dispose()

当然,最好使用 using 块(或使用 Dispose()try...finally

    using (ICalculator calc = new ChannelFactory<ICalculator>("TcpCalc").CreateChannel())
    {
      int sum1 = calc.Add (1,2);
      int sum2 = calc.Add (3,4);
      . . .
    }

在我看来,最后这个代码片段真正说明了当客户端和服务端都源自同一组织时 WCF 客户端应该是什么样子。

示例客户端

// CalculatorClient.cs

using System;
using System.ServiceModel;

namespace Calculator
{
  class TestClient
  {

    static void Main ()
    {

      // Create a proxy via TCP which is not closed until calc1 is garbage collected

      ICalculator calc1 = new ChannelFactory<ICalculator>("TcpCalc").CreateChannel();
      int sum1 = calc1.Add (1,2);
      int sum2 = calc1.Add (3,4);
      Console.WriteLine ("Sum1 = {0}  Sum2 = {1}", sum1, sum2);

      // Create a proxy via named pipe which explicitly closed by Dispose()

      ICalculator calc2 = new ChannelFactory<ICalculator>("PipeCalc").CreateChannel();
      sum1 = calc2.Add (5,6);
      sum2 = calc2.Add (7,8);
      calc2.Dispose();
      Console.WriteLine ("Sum1 = {0}  Sum2 = {1}", sum1, sum2);

      // Create a proxy via WS HTTP binding which closes at the end of the 'using' block

      using (ICalculator calc3 = new ChannelFactory<ICalculator>(
          "WsHttpCalc").CreateChannel())
      {
        sum1 = calc3.Add (9,10);
        sum2 = calc3.Add (11,12);
        Console.WriteLine ("Sum1 = {0}  Sum2 = {1}", sum1, sum2);
      }

      // Create a proxy via basic HTTP binding. Each call closes itself & 'using'
      // does nothing

      using (ICalculator calc4 = new ChannelFactory<ICalculator>(
          "HttpCalc").CreateChannel())
      {
        sum1 = calc4.Add (13,14);
        sum2 = calc4.Add (15,16);
        Console.WriteLine ("Sum1 = {0}  Sum2 = {1}", sum1, sum2);
      }

    }

  }
}

这里有几个有趣的要点

  • 对于 calc1,客户端代理没有被显式释放。服务端的日志输出显示 Dispose() 仅在程序执行后期,当 calc1 被垃圾回收时才被调用。
  • Calc2calc3 在使用后被正确释放,并且对应的服务端 Dispose() 立即被调用。
  • 对于使用 basic http 绑定的 calc4,对于每次调用 Add(),服务端对象都会被构造和释放。由于每次方法调用都有一个不同的服务对象,因此调用之间不保留任何状态。
  • 相比之下,通过 TCP、命名管道和 WS HTTP 绑定连接的服务会在方法调用之间保留状态,如 callNo 的递增值所示。考虑到行为因网络传输而异,使用状态和/或使用 basic HTTP 绑定存在风险。这由开发人员自行判断。

对应的客户端配置文件定义了四个命名服务终结点

<!--CalculatorClient.config-->

<configuration>
  <system.serviceModel>
    <client>

      <endpoint address="net.tcp://:5000/Calc"
                binding="netTcpBinding"
                contract="Calculator.ICalculator"
                name="TcpCalc" />

      <endpoint address="net.pipe:///Calc"
                binding="netNamedPipeBinding"
                contract="Calculator.ICalculator"
                name="PipeCalc" />

      <endpoint address="https://:8081/Calc"
                binding="wsHttpBinding"
                contract="Calculator.ICalculator"
                name="WsHttpCalc" />

      <endpoint address="https://:8080/Calc"
                binding="basicHttpBinding"
                contract="Calculator.ICalculator"
                name="HttpCalc" />

    </client>
  </system.serviceModel>
</configuration>

结论

根据我的经验,企业内部客户端和服务之间的通信比向第三方公开外部接口更常见。WCF 通过使用 ChannelFactory 自动生成代理,为这种情况提供了一个有效的机制,该机制更简单、代码更容易编写,并且不易出现手动构建过程错误。缺乏清晰的示例阻碍了其采用。

文档的缺乏也可能阻止了一些 Remoting 用户迁移到 WCF。虽然 WCF 自动代理并不能解决迁移的所有问题,但它们确实扫清了采用的一个障碍。在许多情况下,WCF 是 Remoting 的有效替代品,表现出可比的性能和相似的协议选择。

最后,一些读者可能认为共享类型违反了 SOA 的规则。事实并非如此。SOA 的第三个原则是“共享模式/契约,而非类”。共享的接口类型定义了契约——它们不是类(类代表实现)。我认为这种方法实际上比手动生成的代理**更**面向服务。SOA 的第一个原则是边界应该是明确的,而通过 ChannelFactory 自动创建客户端代理比仅仅使用 new 和手动生成的代理要明确得多。无论如何,没有什么能阻止相同的服务既可以通过内部使用自动代理访问,也可以通过外部使用手动代理访问。

历史

第一版,2008 年 10 月 6 日。

© . All rights reserved.