WCF服务编程 读书笔记——第1章 WCF基础(2)
续: 第1章 WCF基础(1)
元数据交换
服务有两种方案可以发布自己的元数据。一种是基于HTTP-GET协议提供元数据, 另一种则是后面将要讨论的使用专门的终结点的方式。WCF能够为服务自动提供基于HTTPGET的元数据,但需要显式地添加服务行为( Behavior)以支持这一功能。本书后面的章节会介绍行为的相关知识。现在,我们只需要知道行为属于服务的本地特性,例如是否需要基于HTTP-GET交换元数据, 就是一种服务行为。我们可以通过编程方式或管理方式添加行为。在例 1 - 10 演示的宿主应用程序的配置文件中,所有引用了定制<behavior> 配置节的托管服务都支持基于 HTTP-GET 协议实现元数据交换。为了使
用 HTTP-GET,客户端使用的地址需要注册服务的 HTTP 基地址。我们也可以在行为中指定一个外部 URL 以达到同样的目的。
例 1-10:使用配制文件启用元数据交换行为
<system.serviceModel> <services> <service name = "MyService" behaviorConfiguration = "MEXGET"> <host> <baseAddresses> <add baseAddress = "http://localhost:8000/"/> </baseAddresses> </host> ... </service> <service name = "MyOtherService" behaviorConfiguration = "MEXGET"> <host> <baseAddresses> <add baseAddress = "http://localhost:8001/"/> </baseAddresses> </host> ... </service> </services> <behaviors> <serviceBehaviors> <behavior name = "MEXGET"> <serviceMetadata httpGetEnabled = "true"/> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel>
一旦启用了基于 HTTP-GET 的元数据交换,在浏览器中就可以通过 HTTP 基地址(如果存在)进行访问。如果一切正确,就会获得一个确认页面,如图 1-6 所示,告知开发者已经成功托管了服务。确认页面与 IIS 托管无关,即使使用自托管,我们也可以使用浏览器定位服务地址。
编程方式启用元数据交换
若要以编程方式启用基于 HTTP-GET 的元数据交换,首先需要将行为添加到行为集合中,该行为集合是宿主针对服务类型而维持的。ServiceHostBase类定义了Description属性,类型为 ServiceDescription:
public abstract class ServiceHostBase : ... { public ServiceDescription Description {get;} // 更多成员 }
顾名思义, ServiceDescription 就是对服务各个方面与行为的描述。 ServiceDescription类定义了类型为KeyedByTypeCollection<T>的属性Behaviors。其中,类型 KeyedByTypeCollection<T> 的泛型参数类型为 IServiceBehavior 接口:
public class KeyedByTypeCollection<T> : KeyedCollection<Type, T> { public T Find<T>(); public T Remove<T>(); // 更多成员 } public class ServiceDescription { public KeyedByTypeCollection<IServiceBehavior> Behaviors { get; } }
所有行为的类与特性均实现了IServiceBehavior接口。KeyedByTypeCollection<T>定义了泛型方法 Find<T>(),它能够返回包含在集合中的请求行为,如果在集合中没有找到,则返回 null。查询集合时,最多只能有一个返回的符合条件的行为类型。例1-11 演示了如何通过编程方式启用行为。
例 1-11:编程方式启用元数据交换行为
ServiceHost host = new ServiceHost(typeof(MyService)); ServiceMetadataBehavior metadataBehavior; metadataBehavior = host.Description.Behaviors.Find<ServiceMetadataBehavior>(); if (metadataBehavior == null) { metadataBehavior = new ServiceMetadataBehavior(); metadataBehavior.HttpGetEnabled = true; host.Description.Behaviors.Add(metadataBehavior); } host.Open();
首先,托管代码调用 KeyedByTypeCollection<T> 的 Find<T>()方法,它负责判断配置文件是否提供 MEX 终结点行为。 Find<T> 方法的类型参数为 ServiceMetadata-Behavior 类型。 ServiceMetadataBehavior 类定义在 System.ServiceModel.Description命名空间下:
public class ServiceMetadataBehavior : IServiceBehavior { public bool HttpGetEnabled {get;set;} // 更多成员 }
如果返回的行为为 null,托管代码就会创建一个新的 ServiceMetadataBehavior对象,并将 HttpGetEnabled属性值设为 true,然后将它添加到服务描述的 behaviors属性中。
元数据交换终结点
元数据交换终结点是一种特殊的终结点, 有时候又被称为MEX终结点。 服务能够根据元数据交换终结点发布自己的元数据。图 1-7 展示了一个具有业务终结点和元数据交换终结点的服务。不过,在通常情况下并不需要在设计图中显示元数据交换终结点。
元数据交换终结点支持元数据交换的行业标准,在WCF中表现为IMetadataExchange接口:
[ServiceContract(...)] public interface IMetadataExchange { [OperationContract(...)] Message Get(Message request); // 更多成员 }
IMetadataExchange 接口定义的细节并不合理。它与多数行业标准相似,都存在难以实现的问题。所幸, WCF 自动地为服务宿主提供了IMetadataExchange 接口的实现,公开元数据交换终结点。我们只需要指定使用的地址和绑定,以及添加服务元数据行为。对于绑定, WCF 提供了专门的基于 HTTP、 HTTPS、 TCP 和 IPC 协议的绑定传输元素。对于地址,我们可以提供完整的地址,或者使用任意一个注册了的基地址。没有必要启用 HTTP-GET 选项,但是即使启用了也不会造成影响。例 1-12 演示的服务公开了三个MEX 终结点,分别基于 HTTP、 TCP 和 IPC。出于演示的目的, TCP 和 IPC 的 MEX 终结点使用了相对地址, HTTP 则使用了绝对地址。
例 1-12:添加 MEX 终结点
<services> <service name = "MyService" behaviorConfiguration = "MEX"> <host> <baseAddresses> <add baseAddress = "net.tcp://localhost:8001/"/> <add baseAddress = "net.pipe://localhost/"/> </baseAddresses> </host> <endpoint address = "MEX" binding = "mexTcpBinding" contract = "IMetadataExchange" /> <endpoint address = "MEX" binding = "mexNamedPipeBinding" contract = "IMetadataExchange" /> <endpoint address = "http://localhost:8000/MEX" binding = "mexHttpBinding" contract = "IMetadataExchange" /> </service> </services> <behaviors> <serviceBehaviors> <behavior name = "MEX"> <serviceMetadata/> </behavior> </serviceBehaviors> </behaviors>
编程方式添加 MEX 终结点
与其他终结点相似,我们只能在打开宿主之前通过编码方式添加元数据交换终结点。WCF并没有为元数据交换终结点提供专门的绑定类型。为此,我们需要创建定制绑定。定制绑定使用了与之匹配的传输绑定元素,然后将绑定元素对象作为构造函数的参数,传递给定制绑定实例。最后,调用宿主的 AddServiceEndpoint()方法,参数值分别为地址、定制绑定与 IMetadataExchange契约类型。例 1-13 的代码添加了基于 TCP 的 MEX 终结点。注意,在添加终结点之前,必须校验元数据行为是否存在。例 1-13:编程方式添加 TCP MEX 终结点
BindingElement bindingElement = new TcpTransportBindingElement(); CustomBinding binding = new CustomBinding(bindingElement); Uri tcpBaseAddress = new Uri("net.tcp://localhost:9000/"); ServiceHost host = new ServiceHost(typeof(MyService), tcpBaseAddress); ServiceMetadataBehavior metadataBehavior; metadataBehavior = host.Description.Behaviors.Find<ServiceMetadataBehavior>(); if (metadataBehavior == null) { metadataBehavior = new ServiceMetadataBehavior(); host.Description.Behaviors.Add(metadataBehavior); } host.AddServiceEndpoint(typeof(IMetadataExchange), binding, "MEX"); host.Open();
简化 ServiceHost<T> 类
我们可以扩展 ServiceHost < T > 类,从而自动实现例 1-11 和例 1-13 中的代码。
ServiceHost<T>定义了Boolean型属性EnableMetadataExchange,通过调用该属性添加 HTTP-GET 元数据行为和 MEX 终结点:
public class ServiceHost<T> : ServiceHost { public bool EnableMetadataExchange { get; set; } public bool HasMexEndpoint { get; } public void AddAllMexEndPoints(); // 更多成员 }
如果 EnableMetadataExchange属性设置为true,就会添加元数据交换行为。如果没有可用的 MEX终结点,它就会为每个已注册的基地址样式添加一个 MEX 终结点。使用ServiceHost<T>,例 1-11 和例 1-13 就可以简化为:
ServiceHost<MyService> host = new ServiceHost<MyService>(); host.EnableMetadataExchange = true; host.Open();
ServiceHost<T>还定义了 Boolean 属性 HasMexEndpoint。如果服务包含了任意一个MEX 终结点(与传输协议无关),则返回 true 。 ServiceHost < T > 定义的AddAllMexEndPoints()方法可以为每个已注册的基地址添加一个 MEX终结点,这些基地址的样式类型包括 HTTP、 TCP 或 IPC。例 1-14 介绍了这些方法的实现。
例 1-14:实现 EnableMetadataExchange 以及它支持的方法
public class ServiceHost<T> : ServiceHost { public bool EnableMetadataExchange { set { if(State == CommunicationState.Opened) { throw new InvalidOperationException("Host is already opened"); } ServiceMetadataBehavior metadataBehavior; metadataBehavior = Description.Behaviors.Find<ServiceMetadataBehavior>(); if(metadataBehavior == null) { metadataBehavior = new ServiceMetadataBehavior(); metadataBehavior.HttpGetEnabled = value; Description.Behaviors.Add(metadataBehavior); } if(value == true) { if(HasMexEndpoint == false) { AddAllMexEndPoints(); } } } get { ServiceMetadataBehavior metadataBehavior; metadataBehavior = Description.Behaviors.Find<ServiceMetadataBehavior>(); if(metadataBehavior == null) { return false; } return metadataBehavior.HttpGetEnabled; } } public bool HasMexEndpoint { get { Predicate<ServiceEndpoint>(译注8) mexEndPoint= delegate(ServiceEndpoint endpoint) {return endpoint.Contract.ContractType == typeof(IMetadataExchange); }; return Collection.Exists(Description.Endpoints,mexEndPoint); } } public void AddAllMexEndPoints() { Debug.Assert(HasMexEndpoint == false); foreach(Uri baseAddress in BaseAddresses) { BindingElement bindingElement = null; switch(baseAddress.Scheme) { case "net.tcp": { bindingElement = new TcpTransportBindingElement(); break; } case "net.pipe": {...} case "http": {...} case "https": {...} } if(bindingElement != null) { Binding binding = new CustomBinding(bindingElement); AddServiceEndpoint(typeof(IMetadataExchange),binding,"MEX"); } } } }
EnableMetadataExchange通过判断CommunicationObject基类的State属性值,确保宿主没有被打开。如果在配置文件中没有找到元数据行为,EnableMetadataExchange不会重写配置文件中的配置值,而只是将 value 赋给新建的元数据行为对象 metadata behavior 的 HttpGetEnabled属性。读取 EnableMetadataExchange 的值时,属性首先会检查值是否已经配置。如果没有配置元数据行为,则返回 false;否则返回它的HttpGetEnabled值。 HasMexEndpoint属性将匿名方法赋给 Predicate 泛型委托。匿名方法负责检查给定终结点的契约是否属于 IMetadataExchange 类型。然后,调用 Collection静态类的Exists()方法,方法的参数值为服务宿主中可用的终结点集合。 Exists() 方法将遍历集合中的每个元素并调用 Predicate 泛型委托对象mexEndPoint,如果集合中的任意一个元素符合 Predicate 指定的比较条件(也就是说,
如果匿名方法的调用返回 true),则返回 true,否则返回 false。 AddAllMexEndPoints()方法会遍历 BaseAddresses集合。根据基地址的样式,创建匹配的 MEX 传输绑定元素,然后再创建一个定制绑定,并将它传入到AddServiceEndpoint()中,就像例 1-13 那样添加终结点。
元数据浏览器
元数据交换终结点提供的元数据不仅描述了契约与操作,还包括关于数据契约、安全性、事务性、可靠性以及错误的信息。为了可视化表示正在运行的服务的元数据,我们开发了元数据浏览器工具,它的实现包含在本书附带的源代码中。图 1-8 显示了使用元数据浏览器获得的例 1-7 定义的终结点。若要使用元数据浏览器,只需要提供 HTTP-GET 地址或者正在运行的服务的元数据交换终结点,就能获取返回的元数据。
客户端编程
若要调用服务的操作,客户端首先需要导入服务契约到客户端的本地描述( Native Representation)中。如果客户端使用了 WCF,调用操作的常见做法是使用代理。代理是一个 CLR 类,它公开了一个单独的 CLR 接口用以表示服务契约。注意,如果服务支持多个契约(至少是多个终结点),客户端则需要一个代理对应每个契约类型。代理不仅提供了与服务契约相同的操作,同时还包括管理代理对象生命周期的方法,以及管理服务连接的方法。代理完全封装了服务的每个方面:服务的位置、实现技术、运行时平台以及通信传输。
生成代理
我们可以使用 Visual Studio 2010 导入服务的元数据,然后生成代理。如果服务是自托管的,则首先需要启动服务,然后从客户端项目的快捷菜单中选择“ Add Service Reference...”。如果服务托管在 IIS 或 WAS 中,则无需预先启动服务。值得注意的是,如果服务同时又作为客户端项目自托管在相同解决方案的另一个项目中,则可以在Visual Studio 2010 中启动宿主,并添加引用。不同于大多数项目的设置,这一选项在
调试状态下并没有被禁用(参见图 1-9)。
上述操作会弹出 Add Service Reference对话框,然后开发者需要提供服务的基地址(或者基地址加上一个 MEX 的 URI)以及包含了代理的命名空间。如果不使用Visual Studio 2010,也可以使用命令行工具SvcUtil.exe。我们需要为SvcUtil工具提供HTTP-GET地址或者元数据交换终结点地址,而代理文件名则作为可选项。默认的代理文件名为 output.cs,但是我们也可以使用 /out 开关指定不同的名字。例如,如果服务 MyService托管在 IIS 或 WAS 上,同时拥有可用的基于 HTTP-GET 的元数据共享,则只需要运行下列命令行:
SvcUtil http://localhost/MyService/MyService.svc /out:Proxy.cs
如果服务的宿主为IIS,并且选择的端口号不是 80(例如端口号 81),则必须将端口号提供给基地址:
SvcUtil http://localhost:81/MyService/MyService.svc /out:Proxy.cs
如果是自托管服务, 假定它启用了基于HTTP-GET的元数据发布方式, 则可以注册这些基地址,然后公开包含了一个 MEX 相对地址的与之匹配的元数据交换终结点:
http://localhost:8002/
net.tcp://localhost:8003
net.pipe://localhost/MyPipe
启动宿主后,可以使用如下命令生成代理:
SvcUtil http://localhost:8002/MEX /out:Proxy.cs
SvcUtil http://localhost:8002/ /out:Proxy.cs
SvcUtil net.tcp://localhost:8003/MEX /out:Proxy.cs
SvcUtil net.pipe://localhost/MyPipe/MEX /out:Proxy.cs
注意: SvcUtil 工具优于 Visual Studio 2010 之处,在于它提供了大量的选项,通过开关控制生成的代理,正如我们在本书后面将要看到的那样。
针对服务的定义如下:
[ServiceContract(Namespace = "MyNamespace")] interface IMyContract { [OperationContract] void MyMethod(); } class MyService : IMyContract { public void MyMethod() {...} }
SvcUtil 生成的代理如例 1-15 所示。在大多数情况下,我们完全可以删除 Action 和ReplyAction 的设置,因为默认使用方法名的设置已经足够。
例 1-15:客户端代理文件
[ServiceContract(Namespace = "MyNamespace")] public interface IMyContract { [OperationContract(Action = "MyNamespace/IMyContract/MyMethod", ReplyAction = "MyNamespace/IMyContract/MyMethodResponse")] void MyMethod(); } public partial class MyContractClient : ClientBase<IMyContract>, IMyContract { public MyContractClient() { } public MyContractClient(string endpointName) : base(endpointName) { } public MyContractClient(Binding binding, EndpointAddress remoteAddress) : base(binding, remoteAddress) { } /* 其他构造函数 */ public void MyMethod() { Channel.MyMethod(); } }
代理类的闪光之处在于它可以只包含服务公开的契约,而不需要添加对服务实现类的引用。我们可以通过提供地址和绑定的客户端配置文件使用代理,也可以不通过配置文件直接使用。注意,每个代理的实例都确切地指向了一个终结点。创建代理时需要与终结点交互。正如前文提及,如果服务端契约没有提供命名空间,则默认的命名空间为 http://tempuri.org。
管理方式配置客户端
客户端需要知道服务的位置,需要使用与服务相同的绑定,当然,客户端还要导入服务契约的定义。本质上讲,它的信息与从服务的终结点获取的信息完全相同。为了体现这些信息,客户端配置文件需要包含目标终结点的信息,甚至使用与宿主完全相同的终结点配置样式。
例1-16 演示了与一个服务交互时必需的客户端配置文件, 其中,服务宿主的配置参见例1-6。
例 1-16:客户端配置文件
<system.serviceModel> <client> <endpoint name = "MyEndpoint" address = "http://localhost:8000/MyService/" binding = "wsHttpBinding" contract = "IMyContract" /> </client> </system.serviceModel>
客户端配置文件可以列出同样多的对应服务支持的终结点,客户端能够使用这些终结点中的任意一个。例 1-17 展示的客户端配置文件,与例 1-7 中的宿主配置文件相匹配。注意,客户端配置文件中的每个终结点都有一个唯一的名称。
例 1-17:包含了多个目标终结点的客户端配置文件
<system.serviceModel> <client> <endpoint name = "FirstEndpoint" address = "http://localhost:8000/MyService/" binding = "wsHttpBinding" contract = "IMyContract" /> <endpoint name = "SecondEndpoint" address = "net.tcp://localhost:8001/MyService/" binding = "netTcpBinding" contract = "IMyContract" /> <endpoint name = "ThirdEndpoint" address = "net.tcp://localhost:8002/MyService/" binding = "netTcpBinding" contract = "IMyOtherContract" /> </client> </system.serviceModel>
绑定配置
我们可以使用与服务配置相同的风格定制匹配服务绑定的客户端标准绑定,如例1-18所示。
例 1-18:客户端绑定配置
<system.serviceModel> <client> <endpoint name = "MyEndpoint" address = "net.tcp://localhost:8000/MyService/" bindingConfiguration = "TransactionalTCP" binding = "netTcpBinding" contract = "IMyContract" /> </client> <bindings> <netTcpBinding> <binding name = "TransactionalTCP" transactionFlow = "true" /> </netTcpBinding> </bindings> </system.serviceModel>
生成客户端配置文件
在默认情况下, SvcUtil也可以自动生成客户端配置文件 output.config。同时,可以使用/config 开关指定配置文件名:
SvcUtil http://localhost:8002/MyService/ /out:Proxy.cs /config:App.Config
也可以使用 /noconfig 开关生成精简的配置文件:
SvcUtil http://localhost:8002/MyService/ /out:Proxy.cs /noconfig
建议永远不要使用 SvcUtil 工具生成配置文件。原因在于它会自动地为关键的绑定节设置默认值,反而导致了整个配置文件的混乱。
进程内托管配置
对于进程内托管,客户端配置文件就是服务宿主的配置文件。同一个文件既包含了服务配置入口,也包含了客户端的配置入口,如例 1-19 所示。
例 1-19:进程内托管的配置文件
<system.serviceModel> <services> <service name = "MyService"> <endpoint address = "net.pipe://localhost/MyPipe" binding = "netNamedPipeBinding" contract = "IMyContract" /> </service> </services> <client> <endpoint name = "MyEndpoint" address = "net.pipe://localhost/MyPipe" binding = "netNamedPipeBinding" contract = "IMyContract" /> </client> </system.serviceModel>
注意,进程内宿主使用了命名管道绑定。
SvcConfigEditor 编辑器
WCF提供了配置文件的编辑器 SvcConfigEditor.exe, 它既能编辑宿主配置文件,又能编辑客户端配置文件(参见图 1-10)。启动编辑器的方法是在 Visual Studio 中,右键单击配置文件(客户端和宿主文件),然后选择“ Edit WCF Configuration”。
对于使用SvcConfigEditor,优劣参半。一方面,它可以帮助开发者轻松快捷地编辑配置文件,从而节约了掌握配置样式的时间。另一方面,它却不利于开发者对 WCF 配置的整体理解。多数情况下,采用手工方式对配置文件进行细微的修改,要比使用 Visual Studio 2010 更加快速。
创建和使用代理
代理类派生自 ClientBase<T> 类,定义如下:
public abstract class ClientBase<T> : ICommunicationObject, IDisposable { protected ClientBase(string endpointName); protected ClientBase(Binding binding, EndpointAddress remoteAddress); public void Open(); public void Close(); protected T Channel { get; } // 其他成员 }
ClientBase<T> 类通过泛型类型参数识别代理封装的服务契约。 ClientBase<T> 的Channel 属性类型就是泛型参数的类型。 ClientBase<T> 的子类通过 Channel 调用它指向的服务契约的方法(参见例 1-15)。若要使用代理,客户端首先需要实例化代理对象,并为构造函数提供终结点信息,即配
置文件中的终结点节名。如果没有使用配置文件,则为终结点地址和绑定对象。然后,客户端使用代理类的方法调用服务。一旦客户端调用完毕,就会关闭代理实例。以例1-15 和例 1-16的定义为例,客户端创建代理,然后通过配置文件识别使用的终结点,再调用代理的方法,最后关闭代理:
MyContractClient proxy = new MyContractClient("MyEndpoint"); proxy.MyMethod(); proxy.Close();
如果在客户端配置文件中,只为代理正在使用的契约类型定义了一个终结点,则客户端
可以省略构造函数中的终结点名:
MyContractClient proxy = new MyContractClient(); proxy.MyMethod(); proxy.Close();
然而,如果相同的契约类型包含了多个可用的终结点,则代理会抛出异常。
关闭代理
最佳的做法是在客户端调用代理完毕之后要关闭代理。第 4 章会详细解释为何在正确情况下客户端需要关闭代理,因为关闭代理会终止与服务的会话,关闭连接。使用代理的 Dispose()方法同样可以关闭代理。这种方式的优势在于它支持 using 语句的使用,即使出现异常,仍然能够调用:
using(MyContractClient proxy = new MyContractClient()) { proxy.MyMethod(); }
如果客户端直接声明了契约,而不是具体的代理类,则客户端可以首先判断代理对象是否实现了 IDisposable 接口:
IMyContract proxy = new MyContractClient()); proxy.MyMethod(); IDisposable disposable = proxy as IDisposable; if(disposable != null) { disposable.Dispose(); }
或者使用 using 语句,省略对类型的判断:
IMyContract proxy = new MyContractClient(); using(proxy as IDisposable) { proxy.MyMethod(); }
调用超时
WCF客户端的每次调用都必须在配置的超时值内完成。 无论何种原因,一旦调用时间超出该时限,调用就会被取消,客户端会收到一个 TimeoutException异常。绑定的一个属性用于设定超时的确切值,默认的超时值为1min。若要设置不同的超时值,可以设置Binding 抽象基类的 SendTimeout 属性:
public abstract class Binding : ... { public TimeSpan SendTimeout {get;set;} // 更多成员 }
例如,使用 WSHttpBinding 时:
<client> <endpoint ...="" binding = "wsHttpBinding" bindingConfiguration = "LongTimeout" ...="" /> </client> <bindings> <wsHttpBinding> <binding name = "LongTimeout" sendTimeout = "00:05:00"/> </wsHttpBinding> </bindings>
编程方式配置客户端
如果不借助于配置文件,客户端也可以通过编程方式创建匹配服务终结点的地址与绑定对象,并将它们传递给代理类的构造函数。既然代理的泛型类型参数就是契约,因此不必为构造函数提供契约。为了表示地址,客户端需要实例化 EndpointAddress类,定义如下:
public class EndpointAddress { public EndpointAddress(string uri); // 更多成员 }
例 1-20 演示了编程方式配置客户端的技术,所示代码的功能与例 1-16 等价,它们使用的目标服务则为例 1-9 的定义。
例 1-20:编程方式配置客户端
Binding wsBinding = new WSHttpBinding(); EndpointAddress endpointAddress = new EndpointAddress("http://localhost:8000/MyService/"); MyContractClient proxy = new MyContractClient(wsBinding,endpointAddress); proxy.MyMethod(); proxy.Close();
与在配置文件中使用绑定节的方法相似,客户端可以通过编程方式配置绑定属性:
WSHttpBinding wsBinding = new WSHttpBinding(); wsBinding.SendTimeout = TimeSpan.FromMinutes(5); wsBinding.TransactionFlow = true; EndpointAddress endpointAddress = new EndpointAddress("http://localhost:8000/MyService/"); MyContractClient proxy = new MyContractClient(wsBinding,endpointAddress); proxy.MyMethod(); proxy.Close();
注意,使用 Binding类的具体子类,是为了访问与绑定相关的属性,例如事务流。
编程方式配置与管理方式配置
目前介绍的配置客户端与服务的两种技术各有所长,相辅相成。管理配置方式允许开发者在部署服务之后,修改服务与客户端的主要特性,而不需要重新编译或重新部署。主要缺陷则是不具备类型安全,只有在运行时才能发现配置的错误。如果配置的决策完全是动态的,那么编程配置方式就体现了它的价值,它可以在运行时基于当前的输入或条件对服务的配置进行处理。如果判断条件是静态的,而且是恒定不变的,就可以采取硬编码方式。例如,如果我们只关注于进程内托管的调用,就可以采取硬编码方式,使用 NetNamePipeBinding以及它的配置(译注11)。不过,大体而言,大多数客户端和服务都会使用配置文件。
WCF 体系架构
本章内容全面地介绍了建立和使用简单的 WCF 服务所需要的知识。然而,正如本书其余章节将要描述的那样, WCF 提供了对可靠性、事务性、并发管理、安全性以及实例激活等技术的有力支持,它们均依赖于基于拦截机制的WCF体系架构 ( WCF Architecture)。通过代理与客户端的交互意味着WCF总是处于服务与客户端之间,拦截所有的调用,执行调用前和调用后的处理。当代理将调用栈帧( Stack Frame)序列化到消息中,并将消息通过通道链向下传递时, WCF就开始执行拦截。通道相当于一个拦截器,目的在于执行一个特定的任务。每个客户端通道都会执行消息的调用前处理。链的组成与结构主要依赖于绑定。例如,一个通道对消息编码(二进制格式、文本格式或者 MTOM),另一个通道传递安全的调用上下文;还有一个通道传播客户端的事务,一个通道管理可靠会话,另一个通道对消息正文( Message Body)加密(如果进行了配置),诸如此类。客户端的最后一个通道是传输通道,根据配置的传输方式发送消息给宿主。在宿主端,消息同样通过通道链进行传输,它会对消息执行宿主端的调用前处理。宿主端的第一个通道是传输通道,接收传输过来的消息。随后的通道执行不同的任务,例如消息正文的解密、消息的解码、参与传播事务、设置安全准则、管理会话、激活服务实例。宿主端的最后一个通道负责将消息传递给分发器( Dispatcher)。分发器将消息转换到一个栈帧,并调用服务实例。执行顺序如图 1-11 所示。
服务并不知道它是否被本地客户端调用。事实上,服务会被本地客户端 —— 分发器调用。客户端与服务端的拦截器确保了它们能够获得运行时环境,以便于它们执行正确的操作。服务实例会执行调用,然后将控制权( Control)返回给分发器。分发器负责将返回值以及错误信息(如果存在)转换为一条返回消息。分发器获得控制权,执行的过程则刚好相反:分发器通过宿主端通道传递消息,执行调用后的处理,例如管理事务、停用实例、回复消息的编码与加密等。为了执行客户端调用后的处理,包括解密、解码、提交或取消事务等任务,传输通道会将返回消息发送到客户端通道。最后一个通道将消息传递给代理。代理将返回消息转化到栈帧,然后将控制权返回给客户端。特别值得注意的是,体系架构中的所有要点均与可扩展性息息相关。我们可以为专有交互定制通道,为实例管理定制行为,以及定制安全行为等。事实上, WCF提供的标准功能都能够通过相同的可扩展模式实现。本书介绍了许多针对可扩展性的实例与应用。
宿主体系架构
如何将与技术无关的面向服务交互转换为CLR接口与类,对这一技术的探索无疑充满了趣味。宿主消除了两者之间的鸿沟,搭建了相互之间转换的桥梁。每个 .NET 宿主进程都包含了多个应用程序域。每个应用程序域则包含了零到多个宿主实例。每个服务宿主实例专门对应于一个特殊的服务类型。创建一个宿主实例,实际上就是为对应于基地址的宿主机器的类型,注册一个包含了所有的终结点的服务宿主实例。每个服务宿主实例拥有零到多个上下文( Context)。上下文是服务实例最核心的执行范围。一个上下文最多只能与一个服务实例关联,这意味着上下文可能为空,不包含任何服务实例。体系架
构如图 1-12 所示。
注意: WCF 上下文的概念与企业服务上下文( Enterprise Services Context)或者 .NET 上下文绑定对象( Context-Bound Object)的上下文概念相似。WCF 上下文将服务宿主与公开本地 CLR 类型为服务的上下文组合在一起。当消息经由通道进行传递时,宿主会将消息映射到新的或者已有的上下文(内部包含对象实例),然后通过上下文处理调用。
使用通道
我们可以直接使用通道调用服务的操作,而无须借助于代理类。 ChannelFactory<T>类(以及它所支持的类型)有助于我们轻松地创建代理,如例1-21 所示。
例 1-21: ChannelFactory<T> 类
public class ContractDescription { public Type ContractType {get;set;} //更多成员 } public class ServiceEndpoint { public ServiceEndpoint(ContractDescription contract,Binding binding,EndpointAddress address); public EndpointAddress Address {get;set;} public Binding Binding {get;set;} public ContractDescription Contract {get;} //更多成员 } public abstract class ChannelFactory : ... { public ServiceEndpoint Endpoint {get;} //更多成员 } public class ChannelFactory<T> : ChannelFactory,... { public ChannelFactory(ServiceEndpoint endpoint); public ChannelFactory(string configurationName); public ChannelFactory(Binding binding,EndpointAddress endpointAddress); public static T CreateChannel(Binding binding,EndpointAddress endpointAddress); public T CreateChannel(); //更多成员 }
我们需要向 ChannelFactory<T>类的构造函数传递一个终结点对象,终结点名称可以从客户端配置文件中获取;或者传递绑定与地址对象,或者传递 ServiceEndpoint对象。接着,调用 CreateChannel()方法获得代理的引用,然后使用代理的方法。最后,通过将代理强制转换为 IDisposable类型,调用 Dispose()方法关闭代理。当然,也可以将代理强制转换为ICommunicationObject类型,通过调用 Close()方法关闭代理:
ChannelFactory<IMyContract> factory = new ChannelFactory<IMyContract>(); IMyContract proxy1 = factory.CreateChannel(); using(proxy1 as IDisposable) { proxy1.MyMethod(); } IMyContract proxy2 = factory.CreateChannel(); proxy2.MyMethod(); ICommunicationObject channel = proxy2 as ICommunicationObject; Debug.Assert(channel != null); channel.Close();
我们还可以直接调用CreateChannel()静态方法,根据给定的绑定和地址值创建代理,这样就不需要创建 ChannelFactory<T> 类的实例了。
Binding binding = new NetTcpBinding(); EndpointAddress address = new EndpointAddress("net.tcp://localhost:8000"); IMyContract proxy = ChannelFactory<IMyContract>.CreateChannel(binding,address); using(proxy as IDisposable) { proxy1.MyMethod(); }
InProcFactory 类
为了演示 ChannelFactory<T> 类的强大功能,考虑静态辅助类 InProcFactory,定义如下:
public static class InProcFactory { public static I CreateInstance<S, I>() where I : class where S : I; public static void CloseProxy<I>(I instance) where I : class; // 更多成员 }
设计 InProcFactory类的目的在于简化并自动实现进程内托管。 CreateInstance()方法定义了两个泛型类型参数:服务类型S以及服务支持的契约类型I。CreateInstance()要求 S 必须派生自 I。可以直接使用 InProcFactory 类:
IMyContract proxy = InProcFactory.CreateInstance<MyService,IMyContract>();
proxy.MyMethod();
InProcFactory.CloseProxy(proxy);
InProcFactory能够接收一个服务类,并将它升级为 WCF 服务。这就好比我们可以在WCF 中调用旧的 Win32 函数 LoadLibrary()。
InProcFactory<T> 的实现
所有的进程内调用都应该使用命名管道,同时还应该传递所有事务。我们可以使用编程配置自动地配置客户端和服务,同时使用 ChannelFactory<T>,以避免对代理对象的使用。例 1-22 演示了 InProcFactory的实现。为简便起见,省略了一部分与此无关的代码。
例 1-22: InProcFactory 类
public static class InProcFactory { struct HostRecord { public HostRecord(ServiceHost host, string address) { Host = host; Address = address; } public readonly ServiceHost Host; public readonly string Address; } static readonly Uri BaseAddress = new Uri("net.pipe://localhost/" + Guid.NewGuid().ToString()); static readonly Binding NamedPipeBinding; static Dictionary<Type, HostRecord> m_Hosts = new Dictionary<Type, HostRecord>(); static InProcFactory() { NetNamedPipeBinding binding = new NetNamedPipeBinding(); binding.TransactionFlow = true; NamedPipeBinding = binding; AppDomain.CurrentDomain.ProcessExit += delegate { foreach (KeyValuePair<Type, HostRecord> pair in m_Hosts) { pair.Value.Host.Close(); } }; } public static I CreateInstance<S, I>() where I : class where S : I { HostRecord hostRecord = GetHostRecord<S, I>(); return ChannelFactory<I>.CreateChannel(NamedPipeBinding, new EndpointAddress(hostRecord.Address)); } static HostRecord GetHostRecord<S, I>() where I : class where S : I { HostRecord hostRecord; if (m_Hosts.ContainsKey(typeof(S))) { hostRecord = m_Hosts[typeof(S)]; } else { ServiceHost host = new ServiceHost(typeof(S), BaseAddress); string address = BaseAddress.ToString() + Guid.NewGuid().ToString(); hostRecord = new HostRecord(host, address); m_Hosts.Add(typeof(S), hostRecord); host.AddServiceEndpoint(typeof(I), NamedPipeBinding, address); host.Open(); } return hostRecord; } public static void CloseProxy<I>(I instance) where I : class { ICommunicationObject proxy = instance as ICommunicationObject; Debug.Assert(proxy != null); proxy.Close(); } }
实现 InProcFactory所面临的最大挑战,就是如何实现 CreateInstance()方法,使得它能够为每个类型创建服务实例。由于每个服务类型都必须有一个对应的宿主( ServiceHost 的一个实例),如果为每次调用都分配一个宿主实例,算不上是一个好的办法。问题在于,如果需要为相同的类型实例化第二个对象, CreateInstance()方法应该怎么做:
IMyContract proxy1 = InProcFactory.CreateInstance<MyService,IMyContract>();
IMyContract proxy2 = InProcFactory.CreateInstance<MyService,IMyContract>();
解决办法是在内部管理一个字典( Dictionary)集合,以建立服务类型与特定的宿主实例之间的映射。调用 CreateInstance()方法创建指定类型的实例时,通过调用辅助方法 GetHostRecord()查找字典集合,只有不存在该服务类型,才会创建宿主实例。如果需要创建宿主, GetHostRecord()方法会以编程方式为宿主添加一个终结点,并生成 GUID 作为唯一标识的管道名。 GetHostRecord()方法会返回一个 HostRecord 对象。 HostRecord 是一个结构类型,定义了 ServiceHost 实例与地址值。然后,CreateInstance()根据HostRecord对象获得终结点地址,调用ChannelFactory<T>类的方法创建代理。 在InProcFactory类的静态构造函数(静态构造函数会在初始化类时调用)中, InProcFactory 订阅了 ProcessExit 事件,使用了匿名方法在停止进程时关闭所有宿主。最后,为了方便客户端关闭代理,InProcFactory定义了CloseProxy()方法,它将代理强制转换为 ICommunicationObject类型以关闭代理。
可靠性
WCF 与其他面向服务技术之间最大的区别在于传输可靠性( Transport Reliability)与消息可靠性( Message Reliability)。传输可靠性(例如通过 TCP 传输)在网络数据包层提供了点对点保证传递( Point-to-Point Guaranteed Delivery),以确保数据包的顺序无误。传输可靠性不会受到网络连接的中断或其他通信问题的影响。顾名思义,消息可靠性负责处理消息层的可靠性,它与传递消息的数据包数量无关。消息可靠性提供了端对端保证传递( End-to-End Guaranteed Delivery),确保消息的顺序无误。消息可靠性与引入的中间方的数量无关,与网络跳数( Network Hops)的数量也
没有关联。消息可靠性基于一个行业标准。该行业标准为可靠的基于消息的通信维持了一个在传输层的会话。如果传输失败,例如无线连接中断,消息可靠性就会提供重试 ( Retries)功能。它还能够自动处理网络阻塞 ( Congestion)、消息缓存 ( Message Buffering)以及流控制( Flow Control),根据具体情况适时调整发送的消息数。消息可靠性还能够通过对连接的验证管理连接自身,并在不需要连接时清除它们。
绑定与可靠性
WCF 的可靠性是在绑定中控制与配置的。一个特定的绑定可以支持可靠消息传输( Reliable Messaging),也可以不支持它。如果支持,也可以通过设置为启用或禁用。何种绑定支持何种可靠性值,要根据绑定的目标场景而定。表 1-2总结了绑定、 可靠性、有序传递( Ordered Delivery)以及它们各自的默认值之间的关系。
BasicHttpBinding、 NetPeerTcpBinding以及两种MSMQ绑定( NetMsmqBinding和 MsmqIntegrationBinding)不支持可靠性。因为 BasicHttpBinding 面向旧的ASMX Web服务,是不具有可靠性的。NetPeerTcpBinding则为广播场景设计。MSMQ绑定针对断开调用,在任何情况下都不存在传输会话。WSDualHttpBinding 总是支持可靠性的,它能够保持回调通道,确保基于 HTTP 协议的客户端存在。NetTcpBinding绑定以及各种 WS绑定,默认情况下并不支持可靠性,但是允许启用对它的支持。由于NetNamedPipeBinding绑定总是拥有一个确定的从客户端到服务的跳
数,因而它的可靠性是绑定固有的。
有序消息
消息可靠性确保了消息的有序传递,允许消息按照发送顺序而非接收顺序执行。此外,它保证了消息只会被传递一次。WCF允许开发者只启用可靠性,而不启用有序传递。此时,消息按照接收它们的顺序进行传递。如果同时启用了可靠性与有序传递,则所有绑定的默认值均支持可靠性。
配置可靠性
通过编程方式或管理方式都可以配置可靠性(以及有序传递)。如果我们启用了可靠性,则客户端与服务宿主端必须保持一致,否则客户端无法与服务通信。我们可以只对支持它的绑定配置可靠性。例 1-23所示的服务端配置文件,使用了绑定配置节,启用了 TCP绑定的可靠性。
例 1-23:启用 TCP 绑定的可靠性
<system.serviceModel> <services> <service name = "MyService"> <endpoint address = "net.tcp://localhost:8000/MyService" binding = "netTcpBinding" bindingConfiguration = "ReliableTCP" contract = "IMyContract" /> </service> </services> <bindings> <netTcpBinding> <binding name = "ReliableTCP"> <reliableSession enabled = "true"/> </binding> </netTcpBinding> </bindings> </system.serviceModel>
至于编程配置方式, TCP 绑定和 WS 绑定提供了略微不同的属性来配置可靠性。例如,NetTcpBinding绑定接受一个 Boolean 型的构造函数参数,用来启动可靠性:
public class NetTcpBinding : Binding,... { public NetTcpBinding(...,bool reliableSessionEnabled); // 更多成员 }
我们只能在对象的构造期间启用可靠性。如果通过编程方式设置可靠性,需要创建支持可靠性的绑定对象:
Binding reliableTcpBinding = new NetTcpBinding(...,true);
NetTcpBinding定义了只读的 ReliableSession类,通过它获取可靠性的状态:
public class ReliableSession { public TimeSpan InactivityTimeout { get; set; } public bool Ordered { get; set; } // 更多成员 } public class OptionalReliableSession : ReliableSession { public bool Enabled { get; set; } // 更多成员 } public class NetTcpBinding : Binding,... { public OptionalReliableSession ReliableSession { get; } // 更多成员 }
必备有序传递
理论上,服务代码和契约定义应该与它使用的绑定及属性无关。服务不应该考虑绑定,在服务代码中也不应该包含它所使用的绑定。不管配置的绑定是哪一种,服务都应该能够正常工作。然而实际上,服务的实现或者契约本身都会依赖于消息的有序传递( Ordered Delivery)。为了帮助契约或服务的开发者能够约束支持的绑定, WCF定义了DeliveryRequirementsAttribute 特性:
[AttributeUsage(AttributeTargets.Class|AttributeTargets.InterfaceAllowMultiple = true)] public sealed class DeliveryRequirementsAttribute : Attribute,... { public Type TargetContract {get;set;} public bool RequireOrderedDelivery {get;set;} // 更多成员 }
DeliveryRequirements特性可以应用到服务一级,对服务的所有终结点施加影响,或者只对公开了特定契约的终结点施加影响;如果应用到服务一级,则意味着选用有序传递是根据具体实现作出的决策。DeliveryRequirements特性也可以应用到契约一级,它会对所有支持该契约的服务施加影响。这样一种在契约一级的应用,体现了对有序传递的要求是根据设计作出的决策。这一约束会在装载服务时得到执行与验证。如果一个终结点包含的绑定并不支持可靠性;或者支持可靠性,却被禁用了;或者虽然启用了可靠性,但却禁用了有序传递,那么装载服务就会失败,抛出InvalidOperationException
异常。
注意: 命名管道绑定符合有序传递的约束。
举例来说,如果不考虑契约,要求服务的所有终结点都启用有序传递,则可以将DeliveryRequirements特性直接应用到服务类上:
[DeliveryRequirements(RequireOrderedDelivery = true)] class MyService : IMyContract,IMyOtherContract {...}
通过设置TargetContract属性,只有支持目标契约的服务终结点才需要遵循可靠的有序传递的约束:
[DeliveryRequirements(TargetContract = typeof(IMyContract), RequireOrderedDelivery = true)] class MyService : IMyContract,IMyOtherContract {...}
如果将 DeliveryRequirements 特性应用到契约接口上,则支持该契约的所有服务都必须遵循这一约束:
[DeliveryRequirements(RequireOrderedDelivery = true)] [ServiceContract] interface IMyContract {...} class MyService : IMyContract {...} class MyOtherService : IMyContract {...}
RequireOrderedDelivery的默认值为false,如果只应用了DeliveryRequirements特性,没有设置 RequireOrderedDelivery的值,则是无效的。例如,如下语句是等效的:
[ServiceContract] interface IMyContract {...}
[DeliveryRequirements] [ServiceContract] interface IMyContract {...}
[DeliveryRequirements(RequireOrderedDelivery = false)] [ServiceContract] interface IMyContract {...}
摘自:《WCF服务编程》Juval Louml著 张逸 徐宁 译
转载请注明出处: