现在的位置: 首页 > 综合 > 正文

WCF基础

2013年01月24日 ⁄ 综合 ⁄ 共 20839字 ⁄ 字号 评论关闭

第1章

WCF基础

本章主要介绍WCF的基本概念、构建模块以及WCF体系架构,以指导读者构建一个简单的WCF服务。从本章的内容中,我们可以了解到WCF的基本术语,包括地址(Address)、绑定(Binding)、契约(Contract)和终结点(Endpoint);了解如何托管服务,如何编写客户端代码;了解WCF的相关主题,诸如进程内托管(In-Proc Hosting)以及可靠性的实现。即使你已经熟知WCF的基本概念,仍然建议你快速浏览本章的内容,它不仅能够巩固你的已有知识,而且本章介绍的一些辅助类与技术术语也将有助于你阅读全书。

什么是WCF

Windows通信基础(Windows Communication Foundation,WCF)是基于Windows平台下开发和部署服务的软件开发包(Software Development Kit,SDK)。WCF为服务提供了运行时环境(Runtime Environment),使得开发者能够将CLR类型公开为服务,又能够以CLR类型的方式使用服务。理论上讲,创建服务并不一定需要WCF,但实际上,使用WCF却可以使得创建服务的任务事半功倍。WCF是微软对一系列产业标准定义的实现,包括服务交互、类型转换、封送(Marshaling)以及各种协议的管理。正因为如此,WCF才能够提供服务之间的互操作性。WCF还为开发者提供了大多数应用程序都需要的基础功能模块,提高了开发者的效率。WCF的第一个版本为服务开发提供了许多有用的功能,包括托管(Hosting)、服务实例管理(Service Instance Management)、异步调用、可靠性、事务管理、离线队列调用(Disconnected Queued Call)以及安全性。同时,WCF还提供了设计优雅的可扩展模型,使开发人员能够丰富它的基础功能。事实上,WCF自身的实现正是利用了这样一种可扩展模型。本书的其余章节会专注于介绍这诸多方面的内容与特征。WCF的大部分功能都包含在一个单独的程序集System.ServiceModel.dll中,命名空间为System.ServiceModel。 

WCF是.NET 3.0的一部分,同时需要.NET 2.0的支持,因此它只能运行在支持它的操作系统上。目前,这些操作系统包括Windows Vista(客户端和服务器)、Windows XP SP2和Windows Server 2003 SP1以及更新的版本。

服务

服务(Services)是公开的一组功能的集合。从软件设计的角度考虑,软件设计思想经历了从函数发展到对象,从对象发展到组件,再从组件发展到服务的几次变迁。在这样一个漫长的发展旅程中,最后发展到服务的一步可以说是最具革新意义的一次飞跃。面向服务(Service-Orientation,SO)是一组原则的抽象,是创建面向服务应用程序的最佳实践。如果你不熟悉面向服务的原则,可以参见附录A,它介绍了使用面向服务的概况与目的。本书假定你对这些原则已经了然于胸。一个面向服务应用程序(SOA)将众多服务聚集到单个逻辑的应用程序中,这就类似于面向组件的应用程序聚合组件,或者面向对象的应用程序聚合对象,如图1-1所示。 

图1-1:面向服务应用程序

服务可以是本地的,也可以是远程的,可以由多个参与方使用任意技术进行开发。服务与版本无关,甚至可以在不同的时区同时执行。服务内部包含了诸如语言、技术、平台、版本与框架等诸多概念,而服务之间的交互,则只允许指定的通信模式。

服务的客户端只是使用服务功能的一方。理论上讲,客户端可以是任意的Windows窗体类、ASP.NET页面或其他服务。

客户端与服务通过消息的发送与接收进行交互。消息可以直接在客户端与服务之间进行传递,也可以通过中间方进行传递。WCF中的所有消息均为SOAP消息。注意WCF的消息与传输协议无关,这与Web服务不同。因此,WCF服务可以在不同的协议之间传输,而不仅限于HTTP。WCF客户端可以与非WCF服务完成互操作,而WCF服务也可以与非WCF客户端交互。不过,如果需要同时开发客户端与服务,则创建的应用程序两端都要求支持WCF,这样才能利用WCF的特定优势。

因为服务的创建对于外界而言是不透明的,所以WCF服务通常通过公开元数据(Metadata)的方式描述可用的功能以及服务可能采用的通信方式。元数据的发布可以预先定义,它与具体的技术无关(Technology-Neutral),例如采用基于HTTP-GET方式的WSDL,或者符合元数据交换的行业标准。一个非WCF客户端可以将元数据作为本地类型导入到本地环境中。相似的,WCF客户端也可以导入非WCF服务的元数据,然后以本地CLR类与接口的方式进行调用。

服务的执行边界

WCF不允许客户端直接与服务交互,即使它调用的是本地机器内存中的服务。相反,客户端总是使用代理(Proxy)将调用转发给服务。代理公开的操作与服务相同,同时还增加了一些管理代理的方法。

WCF允许客户端跨越执行边界与服务通信。在同一台机器中(参见图1-2),客户端可以调用同一个应用程序域中的服务,也可以在同一进程中跨应用程序域调用,甚至跨进程调用。

图1-2:使用WCF实现相同机器通信

图1-3则展示了跨机器边界的通信方式,客户端可以跨越Intranet或Internet的边界与服务交互。

图1-3:使用WCF实现不同机器通信

WCF与位置透明度

过去,诸如DCOM或.NET Remoting等分布式计算技术,不管对象是本地还是远程,都期望为客户端提供相同的编程模型。本地调用时,客户端使用直接引用;处理远程对象时,则使用代理。因为位置的不同而采用两种不同的编程模型会导致一个问题,就是远程调用远比本地调用复杂。复杂度体现在生命周期管理、可靠性、状态管理、可伸缩性(scalability)以及安全性等诸多方面。由于远程对象并不具备本地对象的特征,而编程模型却力图让它成为本地对象,反而使得远程编程模型过于复杂。WCF同样要求客户端保持一致的编程模型,而不用考虑服务的位置。但它的实现途径却大相径庭:即使对象是本地的,WCF仍然使用远程编程模型的实例化方式,并使用代理。由于所有的交互操作都经由代理完成,要求相同的配置与托管方式,因而对于本地和远程方式而言,WCF都只需要维持相同的编程模型。这就使得开发者不会因为服务位置的改变影响客户端,同时还大大地简化了应用程序的编程模型。

地址

WCF的每一个服务都具有一个唯一的地址(Addresses)。地址包含两个重要元素:服务位置与传输协议(Transport Protocol),或者是用于服务通信的传输样式(Transport Schema)。服务位置包括目标机器名、站点或网络、通信端口、管道或队列,以及一个可选的特定路径或者URI。URI即统一资源标识(Universal Resource Identifier),它可以是任意的唯一标识的字符串,例如服务名称或GUID。

WCF 1.0支持下列传输样式:

·HTTP

·TCP

·Peer network(对等网)

·IPC(基于命名管道的内部进程通信)

·MSMQ

地址通常采用如下格式:[基地址]/[可选的URI]

基地址(Base Address)通常的格式如下:[传输协议]://[机器名或域名][:可选端口]

下面是一些地址的示例:

http://localhost:8001

http://localhost:8001/MyService

net.tcp://localhost:8002/MyService

net.pipe://localhost/MyPipe

net.msmq://localhost/private/MyService

net.msmq://localhost/MyService

可以将地址http://localhost:8001读作:“采用HTTP协议访问localhost机器,并在8001端口等待用户的调用。”

如果URI为http://localhost:8001/MyService,则读作:“采用HTTP协议访问localhost机器,MyService服务在8001端口处等待用户的调用。”

TCP地址

TCP地址采用net.tcp协议进行传输,通常它还包括端口号,例如:

net.tcp://localhost:8002/MyService

如果没有指定端口号,则TCP地址的默认端口号为808:

net.tcp://localhost/MyService

两个TCP地址(来自于相同的宿主,具体内容将在本章后面介绍)可以共享一个端口:

net.tcp://localhost:8002/MyService

net.tcp://localhost:8002/MyOtherService

本书广泛地使用了基于TCP协议的地址。

注意:我们可以将不同宿主的TCP地址配置为共享一个端口。

HTTP地址

HTTP地址使用http协议进行传输,也可以利用https进行安全传输。HTTP地址通常会被用作对外的基于Internet的服务,并为其指定端口号,例如:

http://localhost:8001

如果没有指定端口号,则默认为80。与TCP地址相似,两个相同宿主的HTTP地址可以共享一个端口,甚至相同的机器。

本书广泛地使用了基于HTTP协议的地址。

IPC地址

IPC地址使用net.pipe进行传输,这意味着它将使用Windows的命名管道机制。在WCF中,使用命名管道的服务只能接收来自同一台机器的调用。因此,在使用时必须指定明确的本地机器名或者直接命名为localhost,为管道名提供一个唯一的标识字符串:

net.pipe://localhost/MyPipe

每台机器只能打开一个命名管道,因此,两个命名管道地址在同一台机器上不能共享一个管道名。

本书广泛地使用了基于IPC的地址。

MSMQ地址

MSMQ地址使用net.msmq进行传输,即使用了微软消息队列(Microsoft Message Queue,MSMQ)机制。使用时必须为MSMQ地址指定队列名。如果是处理私有队列,则必须指定队列类型,但对于公有队列而言,队列类型可以省略:

net.msmq://localhost/private/MyService

net.msmq://localhost/MyService

本书第9章将专门介绍队列调用。

对等网地址

对等网地址(Peer Network Address) 使用net.p2p进行传输,它使用了Windows的对等网传输机制。如果没有使用解析器(Resolver),我们就必须为对等网地址指定对等网名、唯一的路径以及端口。对等网的使用与配置超出了本书范围,但在本书的后续章节中会简略地介绍对等网。

契约

WCF的所有服务都会公开为契约(Contract)。契约与平台无关,是描述服务功能的标准方式。WCF定义了四种类型的契约。

服务契约(Service Contract)

服务契约描述了客户端能够执行的服务操作。服务契约是下一章的主题内容,但书中的每一章都会广泛使用服务契约。

数据契约(Data Contract)

数据契约定义了与服务交互的数据类型。WCF为内建类型如int和string隐式地定义了契约;我们也可以非常便捷地将定制类型定义为数据契约。本书第3章专门介绍了数据契约的定义与使用,在后续章节中也会根据需要使用数据契约。

错误契约(Fault Contract)

错误契约定义了服务抛出的错误,以及服务处理错误和传递错误到客户端的方式。第6章专门介绍了错误契约的定义与使用。

消息契约(Message Contract)

消息契约允许服务直接与消息交互。消息契约可以是类型化的,也可以是非类型化的。如果系统要求互操作性,或者遵循已有消息格式,那么消息契约会非常有用。由于WCF开发者极少使用消息契约,因此本书不会介绍它。

服务契约

ServiceContractAttribute的定义如下:

[AttributeUsage(AttributeTargets.Interface|AttributeTargets.Class,

                Inherited = false)]

public sealed class ServiceContractAttribute : Attribute

{

   public string Name

   {get;set;}

   public string Namespace

   {get;set;}

   //更多成员

}

这个特性允许开发者定义一个服务契约。我们可以将该特性应用到接口或者类类型上,如例1-1所示。 

例1-1:定义和实现服务契约

[ServiceContract]

interface IMyContract

{

   [OperationContract]

   string MyMethod(string text);

   

   //不会成为契约的一部分

   string MyOtherMethod(string text);

}

class MyService : IMyContract

{

   public string MyMethod(string text)

   {

      return "Hello " + text;

   }

   public string MyOtherMethod(string text)

   {

      return "Cannot call this method over WCF";

   }

}

ServiceContract特性可以将一个CLR接口(或者通过推断获得的接口,后面将详细介绍)映射为与技术无关的服务契约。ServiceContract特性公开了CLR接口(或者类)作为WCF契约。WCF契约与类型的访问限定无关,因为类型的访问限定属于CLR的概念。即使将ServiceContract特性应用在内部(Internal)接口上,该接口同样会公开为公有服务契约,以便于跨越服务边界实现服务的调用。如果接口没有标记ServiceContract特性,WCF客户端则无法访问它(即使接口是公有的)。这一特点遵循了面向服务的一个原则,即明确的服务边界。为满足这一原则,所有契约必须明确要求:只有接口(或者类)可以被标记为ServiceContract特性,从而被定义为WCF服务,其他类型都不允许。

即使应用了ServiceContract特性,类型的所有成员也不一定就是契约中的一部分。我们必须使用OperationContractAttribute特性显式地标明哪些方法需要暴露为WCF契约中的一部分。OperationContractAttribute的定义如下: 

[AttributeUsage(AttributeTargets.Method)]

public sealed class OperationContractAttribute : Attribute

{

   public string Name

   {get;set;}

   //更多成员 

}

WCF只允许将OperationContract特性应用到方法上,而不允许应用到同样属于CLR概念的属性、索引器和事件上。WCF只能识别作为逻辑功能的操作(Operation)。通过应用OperationContract特性,可以将契约方法暴露为逻辑操作,使其成为服务契约的一部分。接口(或类)中的其他方法如果没有应用OperationContract特性,则与契约无关。这有利于确保明确的服务边界,为操作自身维护一个明确参与(Opt-In)的模型。此外,契约操作不能使用引用对象作为参数,只允许使用基本类型或数据契约。

应用ServiceContract特性

WCF允许将ServiceContract特性应用到接口或类上。当接口应用了Service-Contract特性后,需要定义类实现该接口。总的来讲,我们可以使用C#或VB去实现接口,服务类的代码无需修改,自然而然成为一个WCF服务:

[ServiceContract]

interface IMyContract

{

   [OperationContract]

   string MyMethod();

}

class MyService : IMyContract

{

   public string MyMethod()

   {

      return "Hello WCF";

   }

}

我们可以隐式或显式实现接口:

class MyService : IMyContract

{

   string IMyContract.MyMethod()

   {

      return "Hello WCF";

   }

}

一个单独的类通过继承和实现多个标记了ServiceContract特性的接口,可以支持多个契约。

[ServiceContract]

interface IMyContract

{

   [OperationContract]

   string MyMethod();

}

[ServiceContract]

interface IMyOtherContract

{

   [OperationContract]

   void MyOtherMethod();

}

class MyService : IMyContract,IMyOtherContract

{

   public string MyMethod()

   {...}

   public void MyOtherMethod()

   {...}

}

然而,服务类还有一些实现上的约束。我们要避免使用带参构造函数,因为WCF只能使用默认构造函数。同样,虽然类可以使用内部(internal)的属性、索引器以及静态成员,但WCF客户端却无法访问它们。

WCF允许我们直接将ServiceContract特性应用到服务类上,而不需要首先定义一个单独的契约:

//避免

[ServiceContract]

class MyService

{

   [OperationContract]

   string MyMethod()

   {

      return "Hello WCF";

   }

}

通过服务类的定义,WCF能够推断出契约的定义。至于OperationContract特性,则可以应用到类的任何一个方法上,不管它是私有方法,还是公有方法。

警告:应尽量避免将ServiceContract特性直接应用到服务类上,而应该定义一个单独的契约,这有利于在不同场景下使用契约。

名称与命名空间

可以为契约定义命名空间。契约的命名空间具有与.NET编程相同的目的:确定契约的类型范围,以降低类型的冲突几率。可以使用ServiceContract类型的Namespace属性设置命名空间:

[ServiceContract(Namespace = "MyNamespace")]

interface IMyContract

{...} 

若非特别指定,契约的默认命名空间为http://tempuri.org。对外服务的命名空间通常使用公司的URL;至于企业网(Intranet)内部服务的命名空间,则可以定义有意义的唯一名称,例如MyApplication。

在默认情况下,契约公开的名称就是接口名。但是也可以使用ServiceContract特性的Name属性为契约定义别名,从而在客户端的元数据(Metadata)中公开不同的名称:

[ServiceContract(Name = "IMyContract")]

interface IMyOtherContract

{...}

相似的,操作公开的名称默认为方法名,但我们同样可以使用OperationContract特性的Name属性设置别名,从而公开不同的操作名:

[ServiceContract]

interface IMyContract

{

   [OperationContract(Name = "SomeOperation")]

   void MyMethod(string text);

}

我们将在下一章介绍这些属性的使用。

托管

WCF服务类不能凭空存在。每个WCF服务都必须托管(Hosting) 在Windows进程中,该进程被称为宿主进程(Host Process)。单个宿主进程可以托管多个服务,而相同的服务类型也能够托管在多个宿主进程中。WCF没有要求宿主进程是否同时又是客户端进程。显然,一个独立的进程有利于错误与安全的隔离。谁提供进程或是提供何种类型的进程并不重要。宿主可以由IIS提供,也可以由Windows Vista的Windows激活服务(Windows Activation Service,WAS)提供,或者开发者直接将它作为应用程序的一部分。

注意:一种特殊的托管方式称为进程内托管(In-Process Hosting),简称in-proc。服务与客户端驻留在相同的进程中。通过定义,开发者能够提供进程内托管。

IIS托管

在微软的Internet信息服务器(Internet Information Server,IIS)中托管服务,主要的优势是宿主进程可以在客户端提交第一次请求的时候自动启动,还可以借助IIS管理宿主进程的生命周期。IIS托管的主要缺点在于只能使用HTTP协议。如果是IIS 5,还要受端口限制,要求所有服务必须使用相同的端口号。

在IIS中托管服务与经典的ASMX Web服务托管相似,需要在IIS下创建虚拟目录,并提供一个.svc文件。.svc文件的功能与.asmx文件相似,主要用于识别隐藏在文件和类后面的服务代码。例1-2展示了.svc文件的语法结构。

例1-2:.svc文件

<%@ ServiceHost

       Language= "C#" 

       Debug = "true"

       CodeBehind = "~/App_Code/MyService.cs" 

       Service = "MyService" 

%>

注意:我们甚至可以将服务代码注入到.svc文件中,但这样的做法并不明智。这与ASMX Web服务的要求相同。

使用IIS托管,服务的基地址必需与.svc文件的地址保持一致。

使用Visual Studio 2005

使用Visual Studio 2005,可以生成IIS托管服务的模版文件。选择File菜单的New Website菜单项,然后从New Web Site对话框中选择WCF Service。通过这种方式可以让Visual Studio 2005创建一个新的Web站点,以及服务代码和对应的.svc文件。之后,我们还可以通过Add New Item对话框添加另外的服务。

Web.Config文件

Web站点的配置文件(Web.Config)必须列出需要公开为服务的类型。类型使用类型全名,如果服务类型来自于一个没有被引用的程序集,则还要包括程序集名:

<system.serviceModel>

   <services>

      <service name = "MyNamespace.MyService">

         ...

      </service>

   </services>

</system.serviceModel>

自托管

所谓自托管(Self-Hosting) ,就是由开发者提供和管理宿主进程的生命周期。自托管方式适用于如下场景:需要确定客户端与服务之间的进程(或机器)边界时;使用进程内托管,即服务与客户端处于相同的进程中时。进程可以是任意的Windows进程,例如Windows窗体应用程序、控制台应用程序或Windows NT服务。注意,进程必须在客户端调用服务之前运行,这意味着通常必须预先启动进程。但NT服务或进程内托管不受此限制。宿主程序的实现只需要简单的几行代码,就能够实现IIS托管的一部分特性。 

与IIS托管相似,托管应用程序的配置文件(App.Config)必须列出所有希望托管和公开的服务类型:

<system.serviceModel>

   <services>

      <service name = "MyNamespace.MyService">

         ...

      </service>

   </services>

</system.serviceModel> 

此外,宿主进程必须在运行时显式地注册服务类型,同时为客户端的调用打开宿主,因此,我们才要求宿主进程必须在客户端调用到达之前运行。创建宿主的方法通常是在Main()方法中调用ServiceHost类。ServiceHost类的定义如例1-3所示。 

例1-3:ServiceHost类 

public interface ICommunicationObject

{

   void Open();

   void Close();

   //更多成员

}

public abstract class CommunicationObject : ICommunicationObject

{...}

public abstract class ServiceHostBase : CommunicationObject,IDisposable,...

{...}

public class ServiceHost : ServiceHostBase,...

{

   public ServiceHost(Type serviceType,params Uri[] baseAddresses);

   //更多成员

创建ServiceHost对象时,需要为ServiceHost的构造函数提供服务类型,至于默认的基地址则是可选的。可以将基地址集合设置为空。如果提供了多个基地址,也可以将服务配置为使用不同的基地址。ServiceHost拥有基地址集合可以使得服务能够接收来自于多个地址和协议的调用,同时只需要使用相对的URI。注意,每个SeriviceHost实例都与特定的服务类型相关,如果宿主进程需要运行多个服务类型,则必须创建与之匹配的多个ServiceHost实例。在宿主程序中,通过调用Open()方法,可以允许调用传入;通过调用Close()方法终结宿主实例,完成进程中的调用。此时,即使宿主进程还在运行,仍然会拒绝客户端的调用。而在通常情况下,执行关闭操作会停止宿主进程。例如,在Windows窗体应用程序中托管服务:

[ServiceContract]

interface IMyContract

{...}

class MyService : IMyContract

{...} 

我们可以编写如下的托管代码:

public static void Main()

{

   Uri baseAddress = new Uri("http://localhost:8000/");

  ServiceHost host = new ServiceHost(typeof(MyService),baseAddress); 

   host.Open();

   //可以执行用于阻塞的调用:

   Application.Run(new MyForm()); 

   host.Close();

}

打开宿主时,将装载WCF运行时(WCF runtime),启动工作线程监控传入的请求。由于引入了工作线程,因此可以在打开宿主之后执行阻塞(blocking)操作。通过显式控制宿主的打开与关闭,提供了IIS托管难以实现的特征,即能够创建定制的应用程序控制模块,管理者可以随意地打开和关闭宿主,而不用每次停止宿主的运行。

使用Visual Studio 2005

Visual Studio 2005允许开发者为任意的应用程序项目添加WCF服务,方法是在Add New Item对话框中选择WCF Service选项。当然,这种方式添加的服务,对于宿主进程而言属于进程内托管方式,但进程外的客户端仍然可以访问它。

自托管与基地址

启动服务宿主时,无需提供任何基地址:

public static void Main()

   ServiceHost host = new ServiceHost(typeof(MyService)); 

   host.Open();

   Application.Run(new MyForm());

host.Close();

}

警告:但是我们不能向空列表传递null值,这会导致抛出异常:

serviceHost host;

host = new ServiceHost(typeof(MyService),null); 

只要这些地址没有使用相同的传输样式(Transport Schema),我们也可以注册多个基地址,并以逗号作为地址之间的分隔符。代码实现如下所示(注意例1-3中params限定符的使用):

Uri tcpBaseAddress  = new Uri("net.tcp://localhost:8001/");

Uri httpBaseAddress = new Uri("http://localhost:8002/"); 

ServiceHost host = new ServiceHost(typeof(MyService), 

                                  tcpBaseAddress,httpBaseAddress); 

WCF也允许开发者在宿主配置文件中列出基地址内容:

<system.serviceModel>

   <services>

      <service name = "MyNamespace.MyService">

         <host>

            <baseAddresses>

               <add baseAddress = "net.tcp://localhost:8001/"/>

               <add baseAddress = "http://localhost:8002/"/>

            </baseAddresses>

         </host>

         ...

      </service>

   </services>

</system.serviceModel> 

创建宿主时,无论在配置文件中找到哪一个基地址,宿主都会使用它,同时还要加上以编程方式提供的基地址。需要特别注意,我们必须确保配置的基地址的样式不能与代码中的基地址的样式重叠。

我们甚至可以针对相同的类型注册多个宿主,只要这些宿主使用了不同的基地址:

Uri baseAddress1  = new Uri("net.tcp://localhost:8001/");

ServiceHost host1 = new ServiceHost(typeof(MyService),baseAddress1);

host1.Open(); 

Uri baseAddress2 = new Uri("net.tcp://localhost:8002/");

ServiceHost host2 = new ServiceHost(typeof(MyService),baseAddress2);

host2.Open(); 

然而,这并不包括第8章介绍的使用线程的情况,以这种方式打开多个宿主并无优势可言。此外,如果基地址是配置文件提供的,那么就需要使用ServiceHost的构造函数为相同的类型打开多个宿主。

托管的高级特性

ServiceHost实现的ICommunicationObject接口定义了一些高级特性,如例1-4所示。

例1-4:ICommunicationObject接口

public interface ICommunicationObject

{

   void Open();

   void Close();

   void Abort(); 

   event EventHandler Closed;

   event EventHandler Closing;

   event EventHandler Faulted;

   event EventHandler Opened;

   event EventHandler Opening; 

   IAsyncResult BeginClose(AsyncCallback callback,object state);

   IAsyncResult BeginOpen(AsyncCallback callback,object state);

   void EndClose(IAsyncResult result);

  void EndOpen(IAsyncResult result);

   CommunicationState State

   {get;}

  //更多成员

}

public enum CommunicationState

{

   Created,

   Opening,

   Opened,

   Closing,

   Closed,

   Faulted

}

如果打开或关闭宿主的操作耗时较长,可以采用异步方式调用BeginOpen()和BeginClose()方法。我们可以订阅诸如状态改变或错误发生等宿主事件,通过调用State属性查询当前的宿主状态。ServiceHost类同样实现了Abort()方法。该方法提供强行退出功能,能够及时中断进程中的所有服务调用,然后关闭宿主。此时,活动的客户端会获得一个异常。

ServiceHost<T>类

ServiceHost<T>类能够改进WCF提供的ServiceHost类,它的定义如例1-5所示。

例1-5:ServiceHost<T>类

public class ServiceHost<T> : ServiceHost

{

   public ServiceHost() : base(typeof(T))

   {}

   public ServiceHost(params string[] baseAddresses) : 

                                   base(typeof(T),Convert(baseAddresses))

   {}

   public ServiceHost(params Uri[] baseAddresses) :

                                            base(typeof(T),baseAddresses)

   {}

   static Uri[] Convert(string[] baseAddresses)

   {

      Converter<string,Uri> convert =  delegate(string address)

                                       {

                                          return new Uri(address); 

                                       };

      return Array.ConvertAll(baseAddresses,convert);

   }

}

ServiceHost<T>简化了构造函数,它不需要传递服务类型作为构造函数的参数,还能够直接处理字符串而不是处理令人生厌的Uri值。在本书余下的内容中,对Service-Host<T>进行了扩展,增加了一些特性,提高了它的性能。

WAS托管

Windows激活服务(WAS)是一个系统服务,只适用于Windows Vista。WAS是IIS 7的一部分,但也可以独立地安装与配置。若要使用WAS托管WCF服务,必须提供一个.svc文件,这与IIS托管一样。IIS与WAS的主要区别在于WAS并不局限于使用HTTP,它支持所有可用的WCF传输协议、端口与队列。

WAS提供了大量基于自托管的强大功能,包括应用程序池、回收机制、空闲时间管理(Idle Time Management)、身份管理(Identity Management)以及隔离(Isolation);宿主进程可以根据情况选择使用这些功能。若需考虑可扩展性,就应该使用Vista服务器作为目标机器;如果只有少数客户端,则可以将Vista客户机作为服务器。

当然,自托管进程还提供了许多卓越特性,例如进程内宿主、匿名用户环境的处理,同时还为之前介绍的高级宿主特性提供了便捷地编程访问方式。

绑定

服务之间的通信方式是多种多样的,有多种可能的通信模式。包括:同步的请求/应答(Request/Reply)消息,或者异步的“即发即弃(Fire-and-Forget)”消息;双向(Bidirectional)消息;即时消息或队列消息;以及持久(Durable)队列或者可变(Volatile)队列。传递消息的传输协议包括:HTTP(或HTTPS)、TCP、P2P(对等网)、IPC(命名管道)以及MSMQ。消息编码格式包括:保证互操作性的纯文本编码格式;优化性能的二进制编码格式;提供有效负载的MTOM(消息传输优化机制,Message Transport Optimization Mechanism)编码格式。消息的安全保障也有多种策略,包括:不实施任何安全策略;只提供传输层的安全策略;消息层的隐私保护与安全策略。当然,WCF还包括多种对客户端认证与授权的安全策略。消息传递(Message Delivery)可能是不可靠的,也可能是可靠的端对端跨越中间方,然后断开连接的方式。消息传递可能按照发送消息的顺序处理,也可能按照接收消息的顺序处理。服务可能需要与其他服务或客户端交互,这些服务或客户端或者只支持基本的Web服务协议,或者使用了流行的WS-*协议,例如WS-Security或者WS-Atomic Transaction。服务可能会基于原来的MSMQ消息与旧的客户端(Legacy Client)交互,或者限制服务只能与其他的WCF服务或客户端交互。

若要计算所有可能的通信模式与交互方式之间的组合,数量可能达到上千万。在这些组合选项中,有的可能是互斥的,有的则彼此约束。显然,客户端与服务必须合理地组合这些选项,才能保证通信的顺畅。对于大多数应用程序而言,管理如此程度的复杂度并无业务价值。然而,一旦因此作出错误决定,就会影响系统的效率与质量,造成严重的后果。

为了简化这些选项,使它们易于管理,WCF引入了绑定(Binding)技术将这些通信特征组合在一起。一个绑定封装了诸如传输协议、消息编码、通信模式、可靠性、安全性、事务传播以及互操作性等相关选项的集合,使得它们保持一致。理想状态下,我们希望将所有繁杂的基础功能模块从服务代码中解放出来,允许服务只需要关注业务逻辑的实现。绑定使得开发者能够基于不同的基础功能模块使用相同的服务逻辑。

在使用WCF提供的绑定时,可以调整绑定的属性,也可以从零开始定制自己的绑定。服务在元数据中发布绑定的选项,由于客户端使用的绑定必须与服务的绑定完全相同,因此客户端能够查询绑定的类型与特定属性。单个服务能够支持各个地址上的多个绑定。

标准绑定

WCF定义了9种标准绑定:

基本绑定(Basic Binding)

由BasicHttpBinding类提供。基本绑定能够将WCF服务公开为旧的ASMX Web服务,使得旧的客户端能够与新的服务协作。如果客户端使用了基本绑定,那么新的WCF客户端就能够与旧的ASMX服务协作。

TCP绑定

由NetTcpBinding类提供。TCP绑定使用TCP协议实现在Intranet中跨机器的通信。TCP绑定支持多种特性,包括可靠性、事务性、安全性以及WCF之间通信的优化。前提是,它要求客户端与服务都必须使用WCF。

对等网绑定

由NetPeerTcpBinding类提供。它使用对等网进行传输。对等网允许客户端与服务订阅相同的网格(Grid),实现广播消息。因为对等网需要网格拓扑(Grid Topology)与网状计算策略(Mesh Computing Strategies)方面的知识,故而不在本书讨论范围之内。

IPC绑定

由NetNamedPipeBinding类提供。它使用命名管道为同一机器的通信进行传输。这种绑定方式最安全,因为它不能接收来自机器外部的调用。IPC绑定支持的特性与TCP绑定相似。

Web服务(WS)绑定

由WSHttpBinding类提供。WS绑定使用HTTP或HTTPS进行传输,为基于Internet的通信提供了诸如可靠性、事务性与安全性等特性。

WS联邦绑定(Federated WS Binding)

由WSFederationHttpBinding类提供。WS联邦绑定是一种特殊的WS绑定,提供对联邦安全(Federated Security)的支持。联邦安全不在本书讨论范围之内。

WS双向绑定(Duplex WS Binding)

由WSDualHttpBinding类提供。WS双向绑定与WS绑定相似,但它还支持从服务到客户端的双向通信,相关内容在第5章介绍。

MSMQ绑定

由NetMsmqBinding类提供。它使用MSMQ进行传输,用以提供对断开的队列调用的支持。相关内容在第9章介绍。

MSMQ集成绑定(MSMQ Integration Binding)

由MsmqIntegrationBinding类提供。它实现了WCF消息与MSMQ消息之间的转换,用以支持与旧的MSMQ客户端之间的互操作。MSMQ集成绑定不在本书讨论范围之内。

格式与编码

每种标准绑定使用的传输协议与编码格式都不相同,如表1-1所示。

表1-1:标准绑定的传输协议与编码格式(默认的编码格式为黑体)

文本编码格式允许WCF服务(或客户端)能够通过HTTP协议与其他服务(或客户端)通信,而不用考虑它使用的技术。二进制编码格式通过TCP或IPC协议通信,它所获得的最佳性能是以牺牲互操作性为代价的,它只支持WCF到WCF的通信。

选择绑定

为服务选择绑定应该遵循图1-4所示的决策活动图表。

首先需要确认服务是否需要与非WCF的客户端交互。如果是,同时客户端又是旧的MSMQ客户端,选择MsmqIntegrationBinding绑定就能够使得服务通过MSMQ与该客户端实现互操作。如果服务需要与非WCF客户端交互,并且该客户端期望调用基本的Web服务协议(ASMX Web服务),那么选择BasicHttpBinding绑定就能够模拟

图1-4:选择绑定

ASMX Web服务(即WSI-Basic Profile)公开WCF服务。缺点是我们无法使用大多数最新的WS-*协议的优势。但是,如果非WCF客户端能够识别这些标准,就应该选择其中一种WS绑定,例如WSHttpBinding、WSFederationBinding或者WSDualHttpBinding。如果假定客户端为WCF客户端,同时需要支持脱机或断开状态下的交互,则可以选择NetMsmqBinding使用MSMQ传输消息。如果客户端需要联机通信,但是需要跨机器边界调用,则应该选择NetTcpBinding通过TCP协议进行通信。如果相同机器上的客户端同时又是服务,选择NetNamePipeBinding使用命名管道可以使性能达到最优化。如果基于额外的标准,例如回调(选择WSDualHttpBinding)或者联邦安全(选择WSFederationBinding),则应对选择的绑定进行微调。

注意:即使超出了使用的目标场景,大多数绑定工作仍然良好。例如,我们可以使用TCP绑定实现相同机器甚至进程内的通信;我们也可以使用基本绑定实现Intranet中WCF对WCF的通信。然而,我们还是应尽量按照图1-4选择绑定。

使用绑定

每种绑定都提供了多种可配置的属性。绑定有三种工作模式。如果内建绑定符合开发者的需求,就可以直接使用它们。我们也可以对绑定的某些属性如事务传播、可靠性和安全性进行调整与配置,还可以定制自己的绑定。最常见的情况是使用已有的绑定,然后只对绑定的几个方面进行配置。应用程序开发者几乎不需要编写定制绑定,但这却是框架开发者可能需要做的工作。

终结点

服务与地址、绑定以及契约有关。其中,地址定义了服务的位置,绑定定义了服务通信的方式,契约则定义了服务的内容。为便于记忆,我们可以将这种类似于“三权分立”一般管理服务的方式简称为服务的ABC。WCF用终结点表示这样一种组成关系。终结点就是地址、契约与绑定的混成品(参见图1-5)。

图1-5:终结点

每一个终结点都包含了三个元素,而宿主则负责公开终结点。从逻辑上讲,终结点相当于服务的接口,就像CLR或者COM接口一样。注意,图1-5使用了传统的“棒棒糖”形式展示了一个终结点的构成。

注意:从概念上讲,不管是C#还是VB,一个接口就相当于一个终结点:地址就是类型虚拟表的内存地址,绑定则是CLR的JIT(Just-In-Time)编译,而契约则代表接口本身。由于经典的.NET编程模式不需要处理地址或绑定,你可能认为它们是理所当然存在的。而WCF并未规定地址与绑定,因而必须对它们进行配置。

每个服务至少必须公开一个业务终结点,每个终结点有且只能拥有一个契约。服务上的所有终结点都包含了唯一的地址,而一个单独的服务则可以公开多个终结点。这些终结点可以使用相同或不同的绑定,公开相同或不同的契约。每个服务提供的不同终结点之间绝对没有任何关联。

重要的一点是,服务代码并没有包含它的终结点,它们通常放在服务代码之外。我们可以通过管理方式(Administratively)使用配置文件或者通过编程方式(Programmatically)配置终结点。

管理方式配置终结点

以管理方式配置一个终结点需要将终结点放到托管进程的配置文件中,如下的服务定义:

namespace MyNamespace

{

   [ServiceContract]

   interface IMyContract

   {...}

   class MyService : IMyContract

   {...}

}

例1-6演示了配置文件要求的配置入口。在每个服务类型下列出它的终结点。

例1-6:管理方式配置终结点

<system.serviceModel>

   <services>

      <service name = "MyNamespace.MyService">

         <endpoint

            address  = http://localhost:8000/MyService/

            binding  = "wsHttpBinding"

            contract = "MyNamespace.IMyContract" 

         />

      </service>

   </services>

</system.serviceModel>

当我们指定服务和契约类型时,必须使用类型全名。在本书的其余例子中,为简略起见,我省略了类型的命名空间,但在实际应用中,命名空间是必备的。注意,如果终结点已经提供了基地址,则地址的样式必须与绑定一致,例如HTTP对应WSHttpBinding。如果两者不匹配,就会在装载服务时导致异常。

例1-7的配置文件为一个单独的服务公开了多个终结点。多个终结点可以配置相同的基地址,前提是URI互不不同。

例1-7:相同服务的多个终结点

<service name = "MyService">

   <endpoint

    address  = "http://localhost:8000/MyService/" 

     binding  = "wsHttpBinding"

      contract = "IMyContract"

   />

   <endpoint

      address  = "net.tcp://localhost:8001/MyService/"

      binding  = "netTcpBinding"

      contract = "IMyContract"

   />

   <endpoint

      address  = "net.tcp://localhost:8002/MyService/"

      binding  = "netTcpBinding"

      contract = "IMyOtherContract"

   />

</service>

㏒  zhouyong

大多数情况下,我们的首选是管理的配置方式,因为它非常灵活,即使修改了服务的地址、绑定和契约,也不需要重新编译服务和重新部署服务。

使用基地址

例1-7中的每个终结点都提供了自己独有的基地址。如果我们提供了显式的基地址,它会重写宿主提供的所有基地址。

我们也可以让多个终结点使用相同的基地址,只要终结点地址中的URI不同:

<service name = "MyService"> 

   <endpoint

      address  = "net.tcp://localhost:8001/MyService/"

      binding  = "netTcpBinding"

      contract = "IMyContract"

   />

   <endpoint

      address  = "net.tcp://localhost:8001/MyOtherService/" 

      binding  = "netTcpBinding"

      contract = "IMyContract" 

   />

</service>

反之,如果宿主提供了与传输样式匹配的基地址,则可以省略地址项。此时,终结点地址与该基地址完全相同:

<endpoint

   binding  = "wsHttpBinding"

   contract = "IMyContract"

/>

如果宿主没有提供匹配的基地址,则在装载服务宿主时会抛出异常。

配置终结点地址时,可以为基地址添加相对URI:

<endpoint

   address  = "SubAddress"

   binding  = "wsHttpBinding"

   contract = "IMyContract"

/>

此时,终结点地址等于它所匹配的基地址加上URI。当然,前提是宿主必须提供匹配的基地址。

绑定配置

使用配置文件可以为终结点使用的绑定进行定

抱歉!评论已关闭.