WCF开发指南之构建服务
一. 引言
Windows通讯基础(简称为WCF)是一种SDK,用于让你使用典型的CLR编程结构(例如用于发布和消费服务的类和接口等)来构建Windows面向服务的应用程序。WCF的编程模型是声明性的并且大部分是属性驱动的。
WCF为通讯服务提供了一种运行时刻环境,使你能够把CLR类型暴露为服务并且以CLR类型来消费服务。
尽管在理论上你可以不用WCF来构建服务;但是,在实践中,WCF能够显著地简化这一任务。WCF是微软的一组工业标准的实现,该标准定义了服务交互、类型转化、编排和多种协议的管理。因此,WCF提供了服务间的互操作性并且提高了开发效率(包括几乎任何应用程序所要求实现的基本的常规的繁重任务)。本文将描述WCF块及其架构的基本概念和构建,从而使你能够构建简单的服务。
二. 什么是WCF服务?
一个服务是一个暴露给外界的功能单元。从编程模型的发展历史来说,它经历了从函数到对象再到组件最后到服务的过程;而WCF服务正代表了下一代的革命性的Windows编程模型。面向服务(SO)是一组原则的抽象集和针对于构建SO应用程序的最好实践,但这其中的一大部分已经超出了本文的范围。
一个面向服务的应用程序(SOA)把服务聚合成单个逻辑的应用程序(见图1),这类似于一个面向组件的应用程序聚合组件或一个面向对象的应用程序聚合对象的方式。服务可以是本地的也可以是远程的,可以由多种团队使用任何技术开发而成,它们可以被独立地进行版本化管理,甚至可以在不同的时间进度上执行。在一个服务内部,你可以使用例如语言,技术,平台,版本和框架等概念;然而,在服务之间,只允许使用规定的通讯模式。
图1:一个面向服务的应用程序的框架。 |
客户端通过发送和接收消息与服务进行交互。消息可以从客户端直接或经中介传输到服务。在WCF中,所有的消息都是SOAP消息。注意,这些消息独立于传输协议——不象Web服务,WCF服务可以通过多种传输协议进行通讯,而不仅是HTTP。
在WCF中,客户端从不直接与服务进行交互,即使在当处理一个本地的内存中服务时。而是,客户端总是使用一个代理来把该调用转发给服务。WCF允许客户端跨越所有执行边界与服务进行通讯。在同一台计算机上(见图2),客户端可以跨越同一进程中的应用程序域或进程来消费同一个应用程序域中的服务。通过跨越计算机边界(图3),客户端能够在企业内网或跨越因特网与服务进行交互。
图3.跨机器通讯:这里是一个跨机器使用WCF通讯的例子。 图2.使用WCF在同一台机器上通讯。 |
因为所有的交互是经由一个代理实现的,所以对于本地和远程情况下,WCF保持相同的编程模型,这样以来不仅能够使你进行位置切换而不影响客户端,而且显著地简化应用程序编程模型。大多数WCF功能被包括到位于System.ServiceModel命名空间的单个的程序集 System.ServiceModel.dll中。
三. 服务地址
在WCF中,每一个服务都与唯一一个地址相联系。该地址提供了两个重要的元素:服务的位置和用于与服务进行通讯的传输协议。地址的位置部分指出目标计算机名,站点或网络,一个通讯端口,管道或队列,还有一个可选的特定的路径或URI。至于传输,WCF 1.0支持下列:
- HTTP
- TCP
- 端对端网络
- IPC(通过命名管道进行的进程间通讯)
- MSMQ
地址总是使用如下格式:
[base address]/[optional URI] |
其中,基地址总是使用如下格式:
[transport]://[machine or domain][:optional port] |
下面是一些可能的服务地址:
http://localhost:8001
http://localhost:8001/MyService
net.tcp://localhost:8002/MyService
net.pipe://localhost/MyPipe
net.msmq://localhost/private/MyService
四. 服务合同
在WCF中,所有的服务都暴露合同。合同是一种描述服务所实现功能的平台中立的标准的方式。WCF定义了四种类型的合同:
- 服务合同描述你可以在服务上执行哪些操作。
- 数据合同定义哪些数据类型被传入和传出服务。WCF为内置类型定义隐式合同,例如int和string,但是你可以容易地为定制类型定义显式的选入式数据合同。
- 错误合同定义哪些错误将被该服务所激发,以及该服务怎样处理错误信息和把如何把它们传播到客户端。
- 消息合同允许服务直接与消息进行交互。消息合同可以被类型化或非类型化,并且有点类似于CLR中的迟绑定调用。不过,消息合同很少为SOA开发者所用。
在这4种类型的合同中,本文将集中讨论服务合同。
你可以使用ServiceContractAttribute来定义一个服务合同,并且你可以把该属性应用于一个接口或一个类,如列表1(见本文相应下载源码)所示。
服务合同独立于接口或类可见性-公共或内部可见性是一个CLR概念,而不是WCF概念。在一个内部接口上应用 ServiceContractAttribute将把该接口暴露为一个公共服务合同(可以跨越服务边界进行消费)。没有 ServiceContractAttribute的话,该接口对WCF客户端是不可见的,这与面向服务的宗旨一致(服务边界是显式的)。为了强制实现这一点,所有的合同必须是严格选入的。
OperationContractAttribute仅能被应用到方法(而不是属性,索引器或事件,这都是一些CLR概念)中。OperationContractAttribute把一个合同方法暴露为在服务合同上执行的一种逻辑操作。该接口上的其它不具有OperationContractAttribute属性的方法不会成为合同的一部分。这可以强制实现显式的服务边界,并且,对于操作本身来说,保持一种选入模型。注意,合同操作独立于方法可见性。列表1展示了通过定义一个合同接口把服务合同与其实现分离开来的最好应用。
另外,你还可以直接把ServiceContractAttribute和OperationContractAttribute应用于类,在这种情况下,WCF使用OperationContractAttribute从类中推断出一个服务合同和方法。这是一种应该尽量避免使用的技术:
//尽量避免使用 [ServiceContract] class MyService { [OperationContract] //可见性并不要紧 string MyMethod(string text) { return "Hello " + text; } public string MyOtherMethod(string text) { return "Cannot call this method over WCF"; } } |
这个ServiceContractAttribute把CLR接口(或推断的接口)映射到一个技术中立的WCF合同上。通过派生和实现多个带有 ServiceContractAttribute的接口,单个类可以支持多个合同。类能够通过隐式或显式方式实现这个接口,因为该方法可见性对WCF没有任何影响。然而,存在许多实现约束:避免使用参数化的构造器,因为WCF仅使用默认的构造器。尽管该类能够使用内部属性,索引器和静态成员,但是没有 WCF客户端能够存取它们。
五. 宿主
每个WCF服务必须宿主在一个Windows进程中(称为宿主进程)。单个宿主进程可以宿主多个服务,而相同的服务类型可以宿主在多个进程中。WCF并不要求是否该宿主进程也是客户端进程。
显然,应该有一个独立的进程支持错误和安全的隔离。另外,谁提供进程或调用哪种类型的进程都不是实质性的问题。这个宿主可以由IIS或Windows Vista中的Widows活动服务(WAS)或由开发者作为应用程序的一部分来提供。
六. IIS宿主
在IIS中宿主一个服务的主要优点是,在发生客户端请求时宿主进程会被自动启动,并且你可以依靠IIS来管理宿主进程的生命周期。IIS宿主的主要不利在于,你仅仅可以在IIS5和IIS6上使用HTTP传输数据;而且当使用IIS5时,你仅可以使用80端口。在IIS上宿主非常类似于宿主一个典型的 ASMX Web服务。你需要在IIS下创建一个虚拟的目录并且提供一个.svc文件。这个.svc文件的功能就象一个被用来标识服务的code-behind文件和类的.asmx文件一样。
<%@ ServiceHost Language = "C#" Debug = "true" CodeBehind = "~/App_Code/MyService.cs" Service = "MyService" %> |
你甚至可以把服务代码以内联方式注入到.svc文件中,但是不建议这样用(就象对于ASMX的情形一样)。一旦你准备好了.svc文件,你就可以使用一个浏览器来观看它。如果一切顺利,那么你将得到一个确认页面。
Visual Studio 2005能够为你生成一个新的IIS宿主的服务。这只要从File菜单下选择"New Website",然后从"New Web Site"对话框中选择WinFX服务。这使得Visual Studio 2005创建一个新的Web站点,服务代码和匹配的.svc文件。另外,Web站点配置文件必须列举出你想要暴露的服务类型。你需要使用完全限定类型名(包括程序集名),如果类型来自于一个未引用的程序集的话。
<system.serviceModel> <services> <service name="MyNamespace.MyService"> ... </service> </services> </system.serviceModel> |
七. 自宿主
自宿主是当开发者负责提供和管理宿主进程的生命周期时使用的技术名词。自宿主被应用在位于客户端和服务之间的一个进程(或计算机)边界环境中,以及当使用进程中服务的情况下(也就是说,与客户端处于相同的进程中)。你需要提供的进程可能是任何Windows进程,例如,一个Windows表单应用程序,一个控制台应用程序或一个Windows NT服务。注意,该进程必须在客户端调用服务之前先运行起来;典型情况下,这意味着,你必须预先启动它。对于NT进程中服务来说这并不是一个问题。
类似于IIS宿主,宿主应用程序配置文件必须列出你想宿主的服务的类型并且暴露给外界。而且,该宿主进程必须在运行时刻显式地注册服务类型并且打开该宿主以便于客户端调用。典型地,这是在Main()方法中使用如下定义的助理类ServiceHost实现的:
public interface ICommunicationObject : IDisposable { void Open(); void Close(); //更多成员 } public abstract class CommunicationObject : ICommunicationObject {...} public class ServiceHostBase : CommunicationObject,... {...} public class ServiceHost : ServiceHostBase,... { public ServiceHost(Type serviceType, params Uri[]baseAddresses); //更多成员 } |
提供给ServiceHost的构造函数的信息有:服务类型和(可选)默认的基地址。该基地址集可以是一个空集(以后,你可以配置不同的基地址)。拥有一组基地址能够使服务接受在多个地址和协议上的调用。注意,每个ServiceHost实例都关联与一个特定的服务类型,并且如果宿主进程需要宿主多个类型的服务的话,你需要一些匹配的ServiceHost实例。通过调用宿主中的ServiceHost.Open()方法,你允许调入(call- in);并且通过调用ServiceHost.Close()方法,你可以体面地退出宿主实例并完成到当前客户端的数据发送,并且还要拒绝未来的客户端调用-即使宿主进程仍在运行中。典型地,关闭操作是在宿主进程关闭时实现的。例如,为了把这个服务宿主在一个Windows表单应用程序中:
[ServiceContract] interface IMyContract {...} class MyService : IMyContract {...} 你可以编写: public static void Main() { Uri baseAddress = new Uri("http://localhost:8000/"); ServiceHost serviceHost; serviceHost = new ServiceHost(typeof(MyService),baseAddress); serviceHost.Open(); //能够拦截调用: Application.Run(new MyForm()); serviceHost.Close(); } |
注意,你可以在调用ServiceHost.Open()之后拦截调用,因为该宿主接收在工作者线程上的所有调用。对ServiceHost.Open()的调用将加载WCF运行时刻并且支持接收客户端调用。该宿主能注册多个基地址,只要它们至少在传输方面存在不同:
Uri tcpBaseAddress = new Uri("net.tcp://localhost:8001/"); Uri httpBaseAddress = new Uri("http://localhost:8002/"); ServiceHost serviceHost = new ServiceHost(typeof(MyService),tcpBaseAddress,httpBaseAddress); |
通过从"Add New Item"对话框中选择WCF服务,Visual Studio 2005允许你把一个WCF服务添加到任何应用程序工程。以此方式添加的服务当然是进程中服务(相对于宿主进程来说),但是也可以由外部客户端存取。
八. WAS宿主
Windows活动服务(WAS)是可用于Windows Vista中的一种系统服务。WAS是IIS7的一部分,但是可以被独立地配置。为了使用WAS来宿主通讯WCF服务,你需要提供一个.svc文件。该 WAS提供与IIS和自宿主相比更多的优点,包括空闲时间管理、标识管理、应用程序池、隔离等等,并且是选择的宿主进程(在可用的情况下)。而且,这种自宿主的进程为进程内宿主提供独特的优点:处理未知的客户环境、依赖于TCP或IPC(当只有IIS可用时)、利用HTTP上的多个端口(当只有IIS 6可用时)以及通过编程方式存取一些高级宿主特征。
九. 绑定
与任何给定的服务进行通讯都存在多方面的问题。首先,存在许多可能的通讯模式:消息可能是同步请求/响应或异步式的"激活-忘记"模式(fire-and-forget);消息也可能是双向的;消息能够被立即传输或队列化操作而该队列可能是持久性的或易破坏性的。
还存在许多可能的消息传输协议:例如HTTP(或HTTPS),TCP,P2P(端对端网络),IPC(命名管道)或MSMQ。也存在一些可能的消息编码选项:你可以选择普通文本以支持互操作性,二进制编码以便于优化性能,或MTOM(消息传输优化机制)以便处理巨大载荷。
也存在一些消息保护选项:你可以选择不对之进行保护,你也可以使用它们来仅提供传输级安全或提供消息级隐私和安全,并且当然,也存在很多种方式用于对客户端实现认证和授权。消息传输在跨越中间媒体和中断连接时可能是不可靠的或可靠的端到端式,并且消息可能是以其发送的方式传输的或是以其接收方式传输的。
通讯服务可能需要与其它服务或仅能够使用基本Web服务协议的客户端互操作,或者它们能够使用WS-*现代协议的核心(例如WS-安全和WS-原子事务)。通讯服务可能需要与旧式的客户端通过原始的MSMQ消息进行互操作,或你可能想限制通讯服务以便仅与另一个WCF服务或客户端互操作。
简言之,通讯存在许多方面的内容,包括大量的参数和决策点。其中,一些选择可能是互斥的,而另一些选择可能要求必须使用另外的相应选择。很明显,客户端和服务必须在所有这些选项上相吻合,以达到正确交流的目的。为了简化并使其更具可管理性,WCF小组共同在绑定中提供了一个这样的通讯方面集合。
个绑定仅仅是对于相协调的传输协议、消息编码、通讯模式、可靠性、安全性、事务传播和互操作性的预封装。理想情况下,你能够从通讯服务代码中"提取"所有的这些繁重的任务方面并且允许它专注于实现业务逻辑。这样做可以使你在相当不同的繁重任务方面使用相同的服务逻辑,而绑定正好使你能够实现这一目的。
你可以使用WCF提供的绑定,也就是说,你或者可以"浓缩"它们的属性,或者是从头编写通讯自己的定制绑定。一个服务在它的元数据中出版它的绑定选择,这使得客户端能够查询这种类型和绑定的特定属性,因为客户端必须使用与服务完全一样的绑定。单个服务能够支持在独立的地址上的多个绑定。
通常,服务并不指定关于绑定本身。WCF定义了列举于表格1中的共9种标准绑定。基于文本的编码使一个WCF服务(或客户端)能够通过HTTP与任何其它服务(或客户端)进行交流而不考虑它的技术;然而,通过TCP或IPC的二进制编码传输能够产生最优的性能,但是却以失去极广泛的互操作性为代价(因为它必须使用WCF到WCF的通讯)。
表格1:WCF标准绑定
名称 | 传输 | 编码 | Interop |
BasicHttpBinding | HTTP/HTTPS | Text | + |
NetTcpBinding | TCP | Binary | - |
NetPeerTcpBinding | P2P | Binary | - |
NetNamedPipeBinding | IPC | Binary | - |
WSHttpBinding | HTTP/HTTPS | Text,MTOM | + |
WSFederationBinding | HTTP/HTTPS | Text,MTOM | + |
WSDualHttpBinding | HTTP | Text,MTOM | + |
NetMsmqBinding | MSMQ | Binary | - |
MsmqIntegrationBinding | MSMQ | Binary | + |
为一个传输协议选择MSMQ能够强制实现WCF到WCF或WCF到MSQM的通讯,但是,这仅是针对非连接的离线工作情况提供的。典型情况下,为通讯服务选择一个绑定应该遵循如图4所示的策略活动图。
图4.策略活动图:该图展示了选择一个绑定的过程。 |
你应该问自己的第一个问题是,是否通讯服务需要与非WCF客户进行交互。如果回答"是",并且如果客户端是一个旧的MSMQ客户端,那么应该选择 NetMsmqBinding-它可以使通讯服务与这样的一个客户端通过MSMQ进行互操作。如果你需要与一非WCF客户端进行互操作并且该客户端期望使用基本的Web服务协议(ASMX Web服务),那么,你可以选择BasicHttpBinding-它能够把通讯WCF服务暴露到外界,就好象它是一个ASMX Web服务一样。
缺点是你不能利用任何现代WS-*协议。然而,如果非WCF客户端能理解这些标准,那么,你可以选择WS绑定之一,例如WSHttpBinding, WSFederationBinding或WSDualHttpBinding。如果你可以假定客户端是一个WCF客户端,但它要求离线或非连接性交互,那么你可以选择使用MSMQ的NetMsmqBinding来传输消息。如果客户端需要连接的通讯但能够跨越计算机边界被调用,那么你可以选择通过TCP 进行通讯的NetTcpBinding。
如果客户端位于与服务同一台计算机上,那么你可以选择使用命名管道的 NetNamedPipeBinding来(IPC)最优化性能。注意,一个使用NetNamedPipeBinding的服务不能接受除它自己以外的来自任何其它计算机的调用,并且这样也会更为安全。你可以基于其它标准(例如,回调需要(WSDualHttpBinding),端对端网络 (NetPeerTcpBinding)或联盟安全(WSFederationBinding))来详细地调整绑定选择。
十. 端点
每一个服务都关联于一个定义了该服务所在位置的地址,一个定义了如何与服务进行通讯的绑定和一个定义了该服务所实现功能的合同。
事实上,WCF用端点的形式来形式化描述这种关系。该端点是地址、合同和绑定的一个结合(见图5)。每一个服务必须具有三个端点,而且由服务暴露该端点。从逻辑上讲,端点是服务的接口,并且类似于一个CLR或COM接口。
图5.该端点是地址、合同和绑定的结合 |
每一个服务必须暴露至少一个业务端点,并且每一个端点都具有一个相同的合同。在一个服务上的所有的端点都具有唯一的地址,而单个服务可以暴露多个端点。这些端点能够使用相同的或不同的绑定并能暴露相同的或不同的合同。你可以使用一个配置文件来以管理方式配置端点或以编程方式来实现端点配置。
十一. 管理端点配置
请考虑下列服务定义:
namespace MyNamespace { [ServiceContract] interface IMyContract {...} Class MyService : IMyContract {...} } |
列表2(见本文相应下载源码)展示了在宿主进程配置文件中要求的入口。管理配置是在大多数情况下的配置,因为它提供灵活性来实现改变服务地址、绑定甚至暴露合同而不必重新构建和重新发布服务。
源码中的列表3展示了一个配置文件-它定义暴露多个端点的单个服务。注意,这些端点必须提供一个与绑定相一致的基地址(例如,对于HTTP使用 WSHttpBinding绑定)。每一个不匹配都会导致在服务加载时刻抛出一个异常。只要URI是不同的,那么你可以使用相同的基地址来配置多个端点:
<service name="MyNamespace.MyService"> <endpoint Address = "net.tcp://localhost:8001/Service1/" ... /> <endpoint address="net.tcp://localhost:8001/Service2/" ... /> </service> |
还可以省略地址-在这种情况下,该服务使用与宿主一起注册的基地址(宿主必须提供一个匹配的基地址):
<endpoint binding="wsHttpBinding" contract="MyNamespace.IMyContract" /> |
可以仅提供一个URI-在这种情况下,地址是在基地址下的相对地址(并且宿主必须提供一个匹配的基地址):
<endpoint address="SubAddress" ... /> |
当提供一个基地址时,该端点覆盖宿主所提供的任何基地址:
<endpoint address="http://localhost:8000/MyService/" ... /> |
注意,当使用IIS进行宿主时,服务必须使用IIS基地址(在HTTP中使用计算机名+虚拟目录)。
十二. 端点配置编程
以编程方式实现端点配置完全等价于管理配置;然而,它不必依赖于一个配置文件而是可以通过编程调用来把端点添加到ServiceHost实例。再次强调的是,这些调用总是位于服务代码的范围之外。ServiceHost提供AddServiceEndpoint()方法的重载版本:
public class ServiceHost : ServiceHostBase { public ServiceEndpoint AddServiceEndpoint(Type implementedContract, Binding binding,String address); //另外的成员 } |
列表4展示了与在列表3中的端点一样的可编程的配置。为了依赖宿主基地址,只需要提供如地址一样的URI即可:
Uri tcpBaseAddress = new Uri("http://localhost:8000/"); ServiceHost serviceHost = new ServiceHost(typeof(MyService),tcpBaseAddress); Binding tcpBinding = new NetTcpBinding(); //使用基地址作为地址 serviceHost.AddServiceEndpoint(typeof(IMyContract) ,tcpBinding,""); //添加相对地址 serviceHost.AddServiceEndpoint(typeof(IMyContract), tcpBinding,"MyService"); //忽略基地址 serviceHost.AddServiceEndpoint(typeof(IMyContract), tcpBinding, "net.tcp://localhost:8001/MyService"); serviceHost.Open(); |
十三. 小结
在本篇中,我们全面介绍了构建一个WCF服务所需要的基本概念,有关完整的WCF服务的例子请参考本文相应源码。在下篇中,我们将给出一个使用WCF进行Windows开发的客户端案例分析。