2.1.2 如何指定地址
WCF的通信是完全基于终结点进行的:服务的提供者将服务通过若干终结点暴露给潜在的服务消费者,并通过HTTP-GET或MEX终结点的方式将元数据(Metadata)以WSDL的方式对外发布;服务的消费者通过访问WSDL,导入元数据生成服务代理相关的代码和配置。借助于这些自动生成的服务代理相关的代码和配置,创建相匹配的终结点对服务进行访问。
可见,无论是在服务端对服务进行寄宿,还是在客户端对服务进行调用,都须要创建终结点对象。作为终结点三要素之一的地址,对于终结点来说,当然是不可或缺的。
为服务指定地址
无论采用哪种类型的寄宿方式,都需要为基于该服务ServiceHost对象(或者其他继承自ServiceHostBase的自定义ServiceHost类型对象)添加一个或者多个终结点对象。一般地,地址的设定伴随着终结点的添加。
1.通过代码方式指定地址
当通过自我寄宿(self-host)的方式对某个服务进行寄宿的时候,可以通过ServiceHostBase或其子类ServiceHost的AddServiceEndpoint方法为添加的终结点指定相应的地址,具体的实现如下面的代码所示:
using (ServiceHost serviceHost = new ServiceHost(typeof(CalculatorService))) { serviceHost.AddServiceEndpoint(typeof(ICalculator),new WSHttpBinding(), "http://127.0.0.1:9999/CalculatorService"); serviceHost.Open(); //...... }
ServiceHostBase是一个抽象类,定义了一些最基本的用于服务寄宿的方法和相关属性,常用的ServiceHost类型就直接继承自ServiceHostBase。ServiceHostBase定义了如下4个重载AddServiceEndpoint的方法:
public abstract class ServiceHostBase { //其他成员 public ServiceEndpoint AddServiceEndpoint(string implementedContract, Binding binding, string address); public ServiceEndpoint AddServiceEndpoint(string implementedContract, Binding binding, Uri address); public ServiceEndpoint AddServiceEndpoint(string implementedContract, Binding binding, string address, Uri listenUri); public ServiceEndpoint AddServiceEndpoint(string implementedContract, Binding binding, Uri address, Uri listenUri); }
上面的4个重载AddServiceEndpoint的3个参数address、binding和implementedContract,分别对应终结点的三要素。implementedContract以字符串的形式表示服务契约类型的有效名称。比如下面一段对AddServiceEndpoint方法的调用代码中,Artech.EndpointAddressDemos. ICalculator代表CalculatorService的contract:ICalculator。
using (ServiceHost serviceHost = new ServiceHost(typeof(CalculatorService))) { serviceHost.AddServiceEndpoint("Artech.EndpointAddressDemos. ICalculator", new WSHttpBinding(), "http://127.0.0.1:9999/CalculatorService"); serviceHost.AddServiceEndpoint("Artech.EndpointAddressDemos. ICalculator", new NetTcpBinding(), "net.tcp://127.0.0.1:8888/CalculatorService"); serviceHost.Open(); //...... }
AddServiceEndpoint的另一个参数listenUri,代表服务的监听地址,也就是前面介绍的物理地址。在默认的情况下,监听地址与终结点地址是统一的,只有在逻辑地址和物理地址相互分离的情况下,才须要指定不同于终结点地址的监听地址。
除了直接继承ServiceHostBase这4个AddServiceEndpoint重载外,ServiceHost(System. ServiceModel.ServiceHost)还定义了4个额外的重载,这4个重载和ServiceHostBase一一相对,唯一不同的是将contract的类型从字符串变成Type类型。
public abstract class ServiceHost { //其他成员 public ServiceEndpoint AddServiceEndpoint(Type implementedContract, Binding binding, string address); public ServiceEndpoint AddServiceEndpoint(Type implementedContract, Binding binding, Uri address); public ServiceEndpoint AddServiceEndpoint(Type implementedContract, Binding binding, string address, Uri listenUri); public ServiceEndpoint AddServiceEndpoint(Type implementedContract, Binding binding, Uri address, Uri listenUri); }
2.通过配置指定地址
除了调用ServiceHostBase及其子类ServiceHost的AddServiceEndpoint为服务添加所需的终结点外,还可以通过配置的方式为某个服务添加终结点,并指定有效的EndpointAddress。在对应服务的配置节下,(<system.serviceModel>/<services>/<service>),可以添加相关的<endpoint>列表。而终结点地址通过address属性指定,具体设置如下面的配置所示:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.serviceModel> <services> <service name="Artech.EndpointAddressDemos.Services. CalculatorService"> <endpoint address="http://127.0.0.1:9999/CalculatorService" binding="wsHttpBinding" contract="Artech. EndpointAddressDemos.Contracts.ICalculator" /> <endpoint address="net.tcp://127.0.0.1:8888/ CalculatorService" binding="netTcpBinding" contract= "Artech.EndpointAddressDemos.Contracts.ICalculator" /> </service> </services> </system.serviceModel> </configuration>
3.IIS寄宿下对地址的指定
与自我寄宿不同的是,IIS寄宿的方式须要为服务创建一个.svc文件,并将该.svc文件部署到一个确定的IIS虚拟目录下,下面的代码片断简单演示了一个简单服务和对应的.svc文件的定义:
using Artech.WcfServices.Contracts; namespace Artech.WcfServices.Services { public class CalculatorService:ICalculator { public double Add(double x, double y) { return x + y; } } }
<%@ ServiceHost Service="Artech.WcfServices.Services.CalculatorService, Artech.WcfServices.Services"%>
由于服务的消费者通过访问.svc文件进行服务的调用,所以该.svc文件的地址就是服务的地址,所以无须再通过配置指定终结点的地址。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.serviceModel> <services> <service name="Artech.WcfServices.Services.CalculatorService"> <endpoint binding="basicHttpBinding" contract="Artech. WcfServices.Contracts.ICalculator" /> </service> </services> </system.serviceModel> </configuration>
基地址与相对地址
除了以绝对路径的方式指定某个服务的终结点地址外,还可以通过“基地址+相对地址”的方式对其进行设置。对于一个服务来说,可以指定一个或多个基地址,但是对于一种传输协议(Scheme)类型,只能具有一个唯一的基地址。服务的基地址和终结点的相对地址可以通过代码的方式,在创建ServiceHost对象时在构造函数中指定。ServiceHost的构造函数中有一个重载接受一个可选的URI数组参数,用于在进行ServiceHost初始化的时候设定一组基地址。
public class ServiceHost : ServiceHostBase { //其他成员 public ServiceHost(object singletonInstance, params Uri[] baseAddresses); public ServiceHost(Type serviceType, params Uri[] baseAddresses); }
下面的代码中,在寄宿CalculatorService服务的时候,添加了两个基地址,一个是基于HTTP的,另一个是针对net.tcp的。然后为CalculatorService添加了两个终结点,其中一个是采用基于HTTP的BasicHttpBinding,另一个则采用基于TCP的NetTcpBinding。添加的两个终结点均采用相对地址CalculatorService。
Uri[] baseAddresses = new Uri[2]; baseAddresses[0] = new Uri("http://127.0.0.1:9999/myservices"); baseAddresses[1] = new Uri("net.tcp://127.0.0.1:8888/myservices"); using (ServiceHost serviceHost = new ServiceHost(typeof(CalculatorService), baseAddresses)) { serviceHost.AddServiceEndpoint(typeof(ICalculator), new BasicHttpBinding(), "CalculatorService"); serviceHost.AddServiceEndpoint(typeof(ICalculator), new NetTcpBinding(), "CalculatorService"); serviceHost.Open(); }
由于在AddServiceEndpoint指定的是相对地址,所以WCF系统会根据绑定采用的传输协议(Scheme)在ServiceHost的基地址列表中寻找与之匹配的基地址,相对地址和基地址组合确定终结点的绝对地址。
对于上面的例子,添加的第一个终结点的地址是:http://127.0.0.1:9999/myservices/CalculatorService;第二个是:net.tcp://127.0.0.1:8888/myservices/ CalculatorService。由于基地址和相对地址的匹配关系是根据绑定对象采用的传输协议确定的,所以对于一个确定的传输协议,最多只能有一个基地址。在上面例子的基础上,倘若再为CalculatorService添加一个基于HTTP的基地址,当ServiceHost构造函数执行的时候,会抛出下一个如图2-1所示的ArgumentException异常,提示基地址集合中已经存在一个HTTP地址。
图2-1 指定重复协议的基地址
Uri[] baseAddresses = new Uri[2]; baseAddresses[0] = new Uri("http://127.0.0.1:9999/myservices"); baseAddresses[1] = new Uri("http://127.0.0.1:7777/myservices"); baseAddresses[2] = new Uri("net.tcp://127.0.0.1:8888/myservices"); using (ServiceHost serviceHost = new ServiceHost(typeof(CalculatorService),
baseAddresses)) { serviceHost.AddServiceEndpoint(typeof(ICalculator), new BasicHttpBinding(), "CalculatorService"); serviceHost.AddServiceEndpoint(typeof(ICalculator), new NetTcpBinding(), "CalculatorService"); serviceHost.Open(); }
服务的基地址和相对地址同样可以通过配置的方式进行设定,服务的基地址列表定义在<host>/<baseAddresses>配置节下。对于设置了基地址的服务,终结点的address属性则可以直接配置成相对地址。前面添加终结点的代码就完全可以用下面一段配置来代替。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.serviceModel> <services> <service name="Artech.WcfServices.Services.CalculatorService"> <endpoint address="CalculatorService" binding= "basicHttpBinding" contract="Artech.WcfServices.Contracts.ICalculator" /> <endpoint address="CalculatorService" binding="netTcpBinding" bindingConfiguration="" contract="Artech.WcfServices.Contracts.ICalculator" /> <host> <baseAddresses> <add baseAddress="http://127.0.0.1:9999/myservices" /> <add baseAddress="net.tcp://127.0.0.1:8888/myservices"/> </baseAddresses> </host> </service> </services> </system.serviceModel> </configuration>
注:如果采用了代码和配置的方式,两者都会生效,所以必须确保两者设置的内容不能相互冲突,比如一个基于HTTP的基地址通过配置和代码的方式加了两次,将会出现如图2-1所示的ArgumentException异常。
地址的跨终结点共享
一个终结点由三要素构成:地址、绑定和契约。对于基于同一个服务的若干终结点来讲,服务一般只实现唯一一个契约,所以所有终结点共享相同的服务契约,在这种情况下,各个终结点的地址不能共享,它们对应的地址必须是不同的。倘若一个服务实现了一个以上的服务契约,情况就有所不同了。如下面的代码所示,现在有一个称作InstrumentationService的服务,实现了两个不同的服务契约:IEventLogWriter,IPerformanceCounterWriter,它们分别是采用Event Log和PerformanceCounter的Instrumentation功能。
namespace Artech.EndpointAdressDemos.Services {
public class InstrumentationService:IEventLogWriter, IPerformanceCounterWriter { public void Increment(string categoryName, string counterName) { //...... } public void WriteEventLog(string source, string message, EventLogEntryType eventLogEntryType) { //...... } } }
namespace Artech.EndpointAdressDemos.Contracts { [ServiceContract] public interface IEventLogWriter { [OperationContract] void WriteEventLog(string source, string message, EventLogEntryType eventLogEntryType); } [ServiceContract] public interface IPerformanceCounterWriter { [OperationContract] void Increment(string categoryName, string counterName); } }
由于InstrumentationService实现了两个不同的契约,我们可以基于这两个不同的契约添加两个终结点,这两个终结点又可以共享相同的地址和绑定。相关的配置如下所示。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.serviceModel> <services> <service name="Artech.EndpointAdressDemos.Services. InstrumentationService"> <endpoint address= "http://127.0.0.1:9999/instrumentationservice" binding="wsHttpBinding" contract= "Artech.EndpointAdressDemos.Contracts.IEventLogWriter" /> <endpoint address= "http://127.0.0.1:9999/instrumentationservice" binding="wsHttpBinding" contract="Artech.EndpointAdressDemos. Contracts.IPerformanceCounterWriter" /> </service> </services> </system.serviceModel> </configuration>
InstrumentationService的这两个终结点的地址(http://127.0.0.1:9999/instrumentationservice)和绑定(WsHttpBinding)都是一样的。实际上对于这种情况,两个终结点使用的是同一个绑定对象。进一步地说,绑定对象的根本功能是提供基于通信的实现,WCF系统通过绑定对象构建一个信道栈(Channel Stack)创建了一座通信的桥梁。从这个意义上讲,通过配置指定的两个终结点使用的是同一个信道栈,我们可以通过下面的方式来证实这一点。倘若使用如下代码的方式来代替上面使用配置的方式。
using (ServiceHost serviceHost = new ServiceHost (typeof(InstrumentationService))) { serviceHost.AddServiceEndpoint(typeof(IEventLogWriter), new WSHttpBinding(), "http://127.0.0.1/instrumentationservice"); serviceHost.AddServiceEndpoint(typeof(IPerformanceCounterWriter), new WSHttpBinding(), "http://127.0.0.1/instrumentationservice"); serviceHost.Open(); }
由于添加的两个终结点使用的是两个不同的WsHttpBinding实例,当ServiceHost的Open方法被执行时,会抛出如图2-2所示的一个InvalidOperationException异常。
图2-2 添加重复地址的终结点
图2-2所示的InvalidOperationException异常错误信息已经明确指明了解决的办法:要么使用同一个绑定对象实例,要么采用上面使用的通过配置的方案。所以正确的编程方式如下所示。
using (ServiceHost serviceHost = new ServiceHost (typeof(InstrumentationService))) { WSHttpBinding binding = new WSHttpBinding(); serviceHost.AddServiceEndpoint(typeof(IEventLogWriter), binding "http://127.0.0.1/instrumentationservice"); serviceHost.AddServiceEndpoint(typeof(IPerformanceCounterWriter), binding, "http://127.0.0.1/instrumentationservice"); serviceHost.Open(); //...... }
在客户端指定地址
在客户端,有两种常见的服务调用方式:第一种通过代码生成器(比如SvcUtil.exe)或添加服务引用导入元数据生成服务代理类型;另一种则是通过ChannelFactory<T>或DuplexChannelFactory<T>直接创建服务代理对象。
1.为ClientBase<TChannel>指定地址
无论是直接借助于SvcUtil.exe,还是添加服务引用的方式,生成的核心类是继承自System.ServiceModel.ClientBase<TChannel>的子类,TChannel为服务契约型类型。比如对于下面的服务契约。
using System.ServiceModel; namespace Artech.WcfServices.Contracts { [ServiceContract] public interface ICalculator { [OperationContract] double Add(double x, double y); } }
上面的代码将会生成下面3个类:ICalculator、ICalculatorChannel、CalculatorClient。ICalculator是服务契约在客户端的表示,ICalculatorChannel是继承System.ServiceModel. IClientChannel,后者定义了客户端信道的基本行为,CalculatorClient是最终用于服务访问的服务代理类,继承了泛型基类ClientBase<ICalculator>,并实现了服务契约。
namespace Artech.WcfServices.Clients.ServiceReference { [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")] [System.ServiceModel.ServiceContractAttribute(ConfigurationName= "ServiceReference.ICalculator")] public interface ICalculator { [System.ServiceModel.OperationContractAttribute(Action= "http://tempuri.org/ICalculator/Add", ReplyAction= "http://tempuri.org/ICalculator/AddResponse")] double Add(double x, double y); } [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")] public interface ICalculatorChannel : Artech.WcfServices.Clients. ServiceReference.ICalculator, System.ServiceModel.IClientChannel { } [System.Diagnostics.DebuggerStepThroughAttribute()] [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")] public partial class CalculatorClient : System.ServiceModel. ClientBase<Artech.WcfServices.Clients.ServiceReference.ICalculator>, Artech.WcfServices.Clients.ServiceReference.ICalculator {
public CalculatorClient() { } public CalculatorClient(string endpointConfigurationName) : base(endpointConfigurationName) { } public CalculatorClient(string endpointConfigurationName, string remoteAddress) : base(endpointConfigurationName, remoteAddress) { } public CalculatorClient(string endpointConfigurationName, System. ServiceModel.EndpointAddress remoteAddress) : base(endpointConfigurationName, remoteAddress) { } public CalculatorClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) : base(binding, remoteAddress) { } public double Add(double x, double y) { return base.Channel.Add(x, y); } } }
对于继承自ClientBase<ICalculator>的CalculatorClient,地址的指定放在构造函数中。如下所示,在下面3个重载的构造函数中,分别以EndpointAddress对象和string的形式通过remoteAddress参数指定了调用的服务终结点的地址。
public CalculatorClient(string endpointConfigurationName, string remoteAddress): base(endpointConfigurationName, remoteAddress) { } public CalculatorClient(string endpointConfigurationName, System. ServiceModel.EndpointAddress remoteAddress): base(endpointConfigurationName, remoteAddress) { } public CalculatorClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress): base(binding, remoteAddress) { }
由于生成的客户端类型直接继承自泛型的System.ServiceModel.ClientBase<TChannel>类,可以简单看看ClientBase<TChannel>的定义。
public abstract class ClientBase<TChannel> : ICommunicationObject, IDisposable where TChannel: class { protected ClientBase();
protected ClientBase(InstanceContext callbackInstance); protected ClientBase(string endpointConfigurationName); protected ClientBase(Binding binding, EndpointAddress remoteAddress); protected ClientBase(InstanceContext callbackInstance, string endpointConfigurationName); protected ClientBase(string endpointConfigurationName, EndpointAddress remoteAddress); protected ClientBase(string endpointConfigurationName, string remoteAddress); protected ClientBase(InstanceContext callbackInstance, Binding binding, EndpointAddress remoteAddress); protected ClientBase(InstanceContext callbackInstance, string endpointConfigurationName, EndpointAddress remoteAddress); protected ClientBase(InstanceContext callbackInstance, string endpointConfigurationName, string remoteAddress); protected virtual TChannel CreateChannel(); protected TChannel Channel { get; } public ChannelFactory<TChannel> ChannelFactory { get; } }
ClientBase<TChannel>是客户端进行服务调用的服务代理基类。WCF通过相匹配的终结点实现了客户端和服务端的交互,ClientBase<TChannel>提供了一系列的重载构造函数,使开发者灵活地指定客户点终结点的三要素:地址、绑定和契约。对于双工通信(Duplex)的情况,通过InstanceContext包装回调实例实现从服务端对客户端操作的回调。
对于没有显式地在构造函数中指定终结点要素的情况,默认从配置中读取。客户端WCF配置核心为一个终结点列表,对于每个终结点,为其指定相应的地址、绑定、契约、终结点配置的名称。下面的配置,为实现同一个服务契约(Artech.WcfServices.Contracts.ICalculator)的服务添加了两个具有不同绑定类型(WsHttpBinding和NetTcpBinding)的终结点,它们的地址分别为:http://127.0.0.1:8888/CalculatorService和net.tcp://127.0.0.1:9999/CalculatorService。
<configuration> <system.serviceModel> <client> <endpoint name="CalculatorService_wsHttpBinding" address= "http://127.0.0.1:8888/CalculatorService" binding= "wsHttpBinding" contract="Artech.WcfServices.Contracts. ICalculator"> </endpoint> <endpoint name="CalculatorService_netTcpBinding" address= "net.tcp://127.0.0.1:9999/CalculatorService" binding= "netTcpBinding"contract="Artech.WcfServices.Contracts. ICalculator"> </endpoint> </client> </system.serviceModel> </configuration>
如果终结点所有配置项都通过配置的方式提供,就可以通过终结点配置名称创建继承于ClientBase<TChannel>的服务代理。如果基于服务契约的终结点在配置中是唯一的,设置还
可以不用指定任何参数。
double result; using (CalculatorClient calculator = new CalculatorClient ("CalculatorService_wsHttpBinding")) { result = calculator.Add(1, 2); }
double result; using (CalculatorClient calculator = new CalculatorClient()) { result = calculator.Add(1, 2); }
2.通过ChannelFactory<TChannel>指定地址
对于上面为ClientBase<TChannel>指定地址的介绍,细心的读者应该会注意到,当我们创建继承自ClientBase<TChannel>的服务代理对象,并调用某个服务操作的时候,实际上该方法操作是通过调用基类(ClientBase<TChannel>)的Channel属性相应的方法实现的。
public partial class CalculatorClient : System.ServiceModel.ClientBase<Artech. WcfServices.Clients.ICalculator>, Artech.WcfServices.Clients.ICalculator { //其他成员 public double Add(double x, double y) { return base.Channel.Add(x, y); } }
而对于定义在ClientBase<TChannel>中的Channel属性,实际上是通过WCF客户端框架的另一个重要的对象创建的,这个对象就是ChannelFactory<TChannel>, 其中TChannel一般是服务契约类型。ChannelFactory<TChannel>不仅仅是为ClientBase<TChannel>服务,而且可以供我们单独使用。可以通过ChannelFactory<TChannel>直接创建服务代理对象。下面是ChannelFactory<TChannel>的简单定义。
public class ChannelFactory<TChannel> : ChannelFactory, IChannelFactory<TChannel>, IChannelFactory, ICommunicationObject { //其他成员 public ChannelFactory(); public ChannelFactory(Binding binding); public ChannelFactory(ServiceEndpoint endpoint); public ChannelFactory(string endpointConfigurationName); public ChannelFactory(Binding binding, EndpointAddress remoteAddress); public ChannelFactory(Binding binding, string remoteAddress); public ChannelFactory(string endpointConfigurationName, EndpointAddress remoteAddress); public TChannel CreateChannel(); public TChannel CreateChannel(EndpointAddress address);
public virtual TChannel CreateChannel(EndpointAddress address, Uri via); public static TChannel CreateChannel(Binding binding, EndpointAddress endpointAddress); public static TChannel CreateChannel(Binding binding, EndpointAddress endpointAddress, Uri via); }
ChannelFactory<TChannel>具有与ClientBase<TChannel>类似的构造函数重载。ChannelFactory<TChannel>定义了两套CreateChannel方法用于服务代理对象的创建:实例方法和静态方法。对于实例方法CreateChannel,可以使用初始化ChannelFactory<TChannel>对象时指定的地址和绑定,也可以在CreateChannel方法中直接指定地址和绑定。如果我们把所有的终结点信息都放在配置文件中,那么可以通过下面的代码进行服务代理的创建和服务操作的调用。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.serviceModel> <client> <endpoint name="CalculatorService_wsHttpBinding" address= "http://127.0.0.1:8888/CalculatorService" binding= "wsHttpBinding" contract="Artech.WcfServices.Contracts. ICalculator"> </endpoint> </client> </system.serviceModel> </configuration>
double result; using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("CalculatorService")) { ICalculator calculator = channelFactory.CreateChannel(); using (calculator as IDisposable) { result = calculator.Add(1, 2); } }