- WCF技術剖析(卷1)
- 蔣金楠
- 3997字
- 2018-12-27 11:32:38
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); } }