一、回调操作概述
WCF支持服务将调用返回给它的客户端。在回调期间,许多方面都将颠倒过来:服务将成为客户端,客户端将编程服务。回调操作可以用在各种场景和应用程序中,但在涉及事件或者服务发生时间需要通知客户端时,显得特别有用。
回调操作通常被认为是双向操作。并非所有的绑定都支持回调操作,只有在具有了双向能力的绑定时,才支持回调操作。比如,HTTP协议本质上是与与连接无关的,所以他不能用于回调,所以,不能基于BasicHttpBingding绑定或WsHttpBingding绑定使用回调。为了让HTTP协议支持回调,WCF提供了WSDualHttpBingding绑定,它实际上设置了两个HTTP通道:一个用于从客户端到服务的调用,另一个则是服务到客户端的调用。WCF也为NetTcpBingding和NetNamePipeBingding绑定提供了对回调操作的支持。所以,TCP和IPC协议均支持双向通信。
双工回调并不是标准的操作,因为没有对于的行业标准来规定客户端如何传递客户端地址给服务,或者服务如何实现发布回调契约。双工回调会损害WCF的性能。很少用到WSDualHttpBingding,因为它无法穿越客户端和服务端的重重阻碍。这种连接线问题由Windows Azure AppFabric Service Bus解决了,在云上它支持双工对调,使用NetTcpRelayBingding绑定。
二、回调操作5步骤
1.创建标准契约和回调契约
/// <summary>/// 回调契约/// </summary>public interface IMyContractCallback{[OperationContract]void OnCallback(string name);}[ServiceContract(CallbackContract = typeof(IMyContractCallback))]public interface IMyContract{[OperationContract]void DoSomething(string name);}2.在服务端实现标准契约
[ServiceBehavior(ConcurrencyMode=ConcurrencyMode.Reentrant)]public class MyContract : IMyContract{private IMyContractCallback callback;public void DoSomething(string name){callback = OperationContext.Current.GetCallbackChannel<IMyContractCallback>();callback.OnCallback(name + "回来了");}}3.创建服务端承载
App.configclass Program{static void Main(string[] args){ServiceHost host = new ServiceHost(typeof(WCF.CallbackOperation.Service.MyContract));host.Open();Console.WriteLine("服务启动成功......");int i = 0;foreach (ServiceEndpoint endpoint in host.Description.Endpoints){i++;Console.WriteLine("终结点序号:{0},终结点名称:{1},终结点地址:{2},终结点绑定:{3}{4}", i, endpoint.Name, endpoint.Address, endpoint.Binding, Environment.NewLine);}Console.Read();}}<system.serviceModel><services><service name="WCF.CallbackOperation.Service.MyContract" behaviorConfiguration="CallbackTcpBehavior"><endpoint name="tcp_IMyContract" address="MyContract" contract="WCF.CallbackOperation.Service.IMyContract" binding="netTcpBinding"></endpoint><endpoint address="MyContract/mex" binding="mexTcpBinding" contract="IMetadataExchange"></endpoint><host><baseAddresses><add baseAddress="net.tcp://127.0.0.1:9000"/></baseAddresses></host></service></services><behaviors><serviceBehaviors><behavior name="CallbackTcpBehavior"><serviceMetadata httpGetEnabled="false" httpsGetEnabled="false"/><serviceDebug includeExceptionDetailInFaults="true"/></behavior></serviceBehaviors></behaviors><diagnostics performanceCounters="All"/></system.serviceModel>4.实现客户端代理
使用Visual Studio的SvcUtil工获取
5.在客户端实现并调用
class Program{static void Main(string[] args){MyCallback callback = new MyCallback();callback.CallService();callback.Dispose();Console.Read();}class MyCallback : IMyContractCallback, IDisposable{private MyContractClient m_Proxy;public void CallService(){InstanceContext context = new InstanceContext(this);m_Proxy = new MyContractClient(context);m_Proxy.DoSomething("zxj");}public void OnCallback(string name){Console.WriteLine(name);}public void Dispose(){m_Proxy.Close();}}}实例代码下载:下载
三、回调契约
回调操作是服务契约的一部分,它取决于服务契约对回调契约的定义。一个服务契约最多只能包含一个回调契约。一旦定义了回调契约,就需要客户端支持回调,并在每次调用中提供执行服务的回调终结点。若要定义回调契约,则可以使用ServiceContract特性提供的Type类型的属性CallbackContract。
public sealed class ServiceContractAttribute : Attribute {public Type CallbackContract { get; set; } }在定义包含回调契约的服务契约时,需要为ServiceContract提供回调契约的类型,以及回调契约的定义,如下所示:
public interface ISomeCallbackContract{[OperationContract]void OnCallback();}[ServiceContract(CallbackContract=typeof(ISomeCallbackContract))]public interface IMyContract{[OperationContract]void DoSomething();}注意,回调契约必须标记ServiceContract特性。因为只要类型定义为回调契约,就意味着它具有ServiceContract特性,并且在服务元数据中也将包含该特性。当然,我们仍然腰围所有的回调接口方法标记OperationContract特性。
当客户端导入的回调接口与原来的服务端定义的名称不同时,它会被修改为服务契约的接口名后将后缀Callback。例如,如果客户端导入上个例子的定义,则客户端会获得以下的定义:
/// <summary>/// 回调契约/// </summary>public interface IMyContractCallback{[OperationContract]void OnCallback(string name);}[ServiceContract(CallbackContract = typeof(IMyContractCallback))]public interface IMyContract{[OperationContract]void DoSomething(string name);}所以,建议在服务端使用命名规范,即在回调契约命名为服务契约接口后将后缀Callback。
四、客户端回调设置
客户端负责托管回调对象以及公开回调终结点。服务实例最内层的执行范围是实例上下文。InstanceContext类定义的构造函数能够将服务实例传递给宿主。
public sealed class InstanceContext : CommunicationObject, IExtensibleObject<InstanceContext> {// 实现服务实例的对象。public InstanceContext(object implementation);// 为实例上下文返回服务的实例。public object GetServiceInstance();...... }为了托管一个回调对象,客户端需要实例化回调对象,然后通过它创建一个上下文对象:
private MyContractClient m_Proxy;public void CallService() {InstanceContext context = new InstanceContext(this);m_Proxy = new MyContractClient(context);m_Proxy.DoSomething("zxj"); }值得一提的是,虽然回调方法是在客户端,但它们仍然属于WCF操作,因此他们都是一个操作调用上下文,通过OperationContext.Current访问。
双向代理
无论何时,只要服务终结点的契约定义了一个回调契约,客户端在与它进行交互时,都必须使用代理创建双向通信,并将回调终结点的引用传递给服务。因此没客户端使用的代理必须继承一个专门的代理类DuplexClientBase<T>,如下所示:
public abstract class DuplexClientBase<TChannel> : ClientBase<TChannel> where TChannel : class {protected DuplexClientBase(InstanceContext callbackInstance);protected DuplexClientBase(object callbackInstance);protected DuplexClientBase(InstanceContext callbackInstance, ServiceEndpoint endpoint);protected DuplexClientBase(object callbackInstance, ServiceEndpoint endpoint);protected DuplexClientBase(InstanceContext callbackInstance, Binding binding, EndpointAddress remoteAddress);protected DuplexClientBase(object callbackInstance, Binding binding, EndpointAddress remoteAddress);// 获取内部双工通道。public IDuplexContextChannel InnerDuplexChannel { get; }...... }客户端需要提供实例上下文绑定给托管宿主的DuplexClientBase<T>构造函数。代理会根据回调上下文构建终结点,从服务终结点配置信息里推断回调终结点的信息:回调终结点契约是通过服务契约回调类型定义的。回调终结点会使用与外发调用相同的绑定。对于地址,WCF会使用客户端机器名。只是简单地传递实例上下文给双向代理,并使用代理来调用服务暴露的终结点。为了简化这个过程,DuplexClientBase<T>提供了可以接受回调对象的构函数,并将其包装在上下文里。无论出于什么原因,当客户端需要访问上下文时,DuplexClientBase<T>都会另外提供IDuplexContextChannel类型的InnerDuplexChannel属性。它提供了通过InnerDuplexChannel属性访问上下文的方式。
使用Visual Studio能够为包含了回调契约的目标服务生成代理类,生成的类派生自DuplexClientBase<T>,如下所示:
public partial class MyContractClient : System.ServiceModel.DuplexClientBase<IMyContract>, IMyContract {public MyContractClient(System.ServiceModel.InstanceContext callbackInstance) : base(callbackInstance){}public MyContractClient(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName) : base(callbackInstance, endpointConfigurationName){}public MyContractClient(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName, string remoteAddress) : base(callbackInstance, endpointConfigurationName, remoteAddress){}public MyContractClient(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) : base(callbackInstance, endpointConfigurationName, remoteAddress){}public MyContractClient(System.ServiceModel.InstanceContext callbackInstance, System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) : base(callbackInstance, binding, remoteAddress){}public void DoSomething(){base.Channel.DoSomething();}public System.Threading.Tasks.Task DoSomethingAsync(){return base.Channel.DoSomethingAsync();} }客户端可以使用派生的代理类创建回调实例,并将上下文作为它的宿主,创建代理、调用代理服务,这样就可以传递回调终结点的引用。
class MyCallback : IMyContractCallback, IDisposable{private MyContractClient m_Proxy;public void CallService(){InstanceContext context = new InstanceContext(this);m_Proxy = new MyContractClient(context);m_Proxy.DoSomething("zxj");}public void OnCallback(string name){Console.WriteLine(name);} }注意,只要客户端正在等待回调,就不能关闭代理。如果关闭回调终结点,当服务试图将调用返回时,就会导致服务端产生错误。
最常见的实现方式是由客户端自身实现回调契约,此时,客户端通常可以将代理定义为成员变量,并在释放客户端时关闭它,如下所示:
class MyCallback : IMyContractCallback,IDisposable {private MyContractClient m_Proxy;public void CallService(){InstanceContext context = new InstanceContext(this);m_Proxy = new MyContractClient(context);m_Proxy.DoSomething();}public void OnCallback(){throw new NotImplementedException();}public void Dispose(){m_Proxy.Close();} }
五、服务端的回调调用
随着服务端的每一次调用,客户端的回调终结点引用都会被传递到服务,组成传入消息的一部分。OperationContext类为服务提供了方便访问回调引用的途径,即调用泛型方法GetCallbackChannel<T>();
public sealed class OperationContext : IExtensibleObject<OperationContext> {// 获取调用当前操作的客户端实例的通道。public T GetCallbackChannel<T>(); }服务如何处理回调引用及何时决定使用它,完全由服务选择,这一点毋庸置疑。服务能够从操作上下文中提取出回调引用,然后将它保存起来以备日后使用;或者可以在服务运行期间使用它,将调用返回客户端。如下所示:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] public class MyContract : IMyContract {private IMyContractCallback callback;public void DoSomething(string name){callback = OperationContext.Current.GetCallbackChannel<IMyContractCallback>();callback.OnCallback(name + "回来了");} }回调重入
服务可能需要调用传入的回调引用。然而,这样的调用在默认情况下是禁止的,因为它受默认的服务并发管理的限制。在默认情况下,服务类被配置为单线程访问:服务实例上下文与锁关联,在任何时刻都只能有一个线程拥有锁,也只能有一个个线程能够访问上下文中的服务实例。在操作调用期间,在客户端发布的调用需要阻塞服务线程,并调用回调。问题是一旦回调返回它需要的重入的相同上下文及获取同一个锁的所有权,处理从相同通道的客户端返回的应答消息时就会导致死锁。注意,服务仍然可能调用到其它客户端的回调,或者调用其他服务,这属于正在调用的会导致死锁的客户端的回调。
如果单线程的服务实例试图调用返回给他的客户端,为了避免死锁,WCF会抛出InvilidOperationException异常。对于这种情况,有三种可能的解决方案:
第一种方案是配置服务,允许多线程访问。由于服务实例与锁无关,因此允许正在调用的客户端回调。但是这种情况也可能增加服务开发者的负担,因为它需要为服务提供同步。
第二种方案是将服务配置为重入。一旦配置为重入,服务实例就与锁管理,同时只允许单线程访问。如果服务正在回调它的客户端,WCF就会先释放锁。就目前而言,如果服务需要回调它的客户端,则可以使用ServiceBehavior特性的ConcurrencyMode属性,将服务的并发行为配置为多线程或者重入。
[ServiceBehavior(ConcurrencyMode=ConcurrencyMode.Reentrant)] public class MyContract : IMyContract {private IMyContractCallback callback;public void DoSomething(string name){callback = OperationContext.Current.GetCallbackChannel<IMyContractCallback>();callback.OnCallback(name + "回来了");} }第三种方案是将回调契约操作配置为单向操作,这样服务就能够安全地将调用返回给客户端。因为没有任何应答消息会竞用锁,即使并发模式设置为单线程,服务也能够支持回调。注意,实例代码中使用第二种方案,如使用第三种方案,请重新生成客户端代理类。
public interface IMyContractCallback {[OperationContract(IsOneWay = true)]void OnCallback(string name); }[ServiceContract(CallbackContract = typeof(IMyContractCallback))] public interface IMyContract {[OperationContract]void DoSomething(string name); }public class MyContract : IMyContract {private IMyContractCallback callback;public void DoSomething(string name){callback = OperationContext.Current.GetCallbackChannel<IMyContractCallback>();callback.OnCallback(name + "回来了");} }
六、回调连接管理
如果客户端保持打开状态,则服务只能将调用返回给客户端。通常情况下,这建立在代理没有关闭的情况下。保证代理处于打开状态同样可以避免回调对象被垃圾回收期回收。如果服务维持了一个在对调终极点上的引用,而且客户端代理是关闭的,或者客户端应用程序已经推出,那么当服务调用回调时,就会从服务通道处获取一个ObjectDisposedException异常。因此,对于客户端而言,当它不再需要接收回调会客户端应用已经关闭,最好能够通知服务无。为此,可以在服务契约中显式添加一个Disconnect()方法。如果每个放哪广发调用都担忧回调引用,那么服务就能够在Disconnect()方法中将回调从内部存储结构中移除。
当然,也建议开发者同时在服务契约中也显式提供一个Connect()方法。定义Connect()方法,能够是客户端重复地连接或断开,同时还提供一个明确的时间点,以判断何时需要一个回调,因为回调只能够发生在调用Connect()方法之后。
[ServiceContract(CallbackContract = typeof(IMyContractCallback))]public interface IMyContract{[OperationContract]void DoSomething(string name);[OperationContract]void Connect();[OperationContract]void DisConnect();}[ServiceBehavior(ConcurrencyMode=ConcurrencyMode.Reentrant)]public class MyContract : IMyContract{private IMyContractCallback callback;public void DoSomething(string name){callback = OperationContext.Current.GetCallbackChannel<IMyContractCallback>();callback.OnCallback(name + "回来了");}public void Connect(){//可以实现为回调列表,将回调引用添加到列表中 }public void DisConnect(){//可以实现为回调列表,将回调引用从列表中移除 }}连接管理与实例操作
单向服务
只有在操作调用回调引用对象自身或者将它存储在某些全局变量(如静态变量)时,单调服务才能够使用 回调引用。既然服务可能使用的存储在引用对象中的任何实例状态在操作返回时都会丢失,那么单调服务就必须使用某些静态变量用以存储回调引用。因而,单调服务特别需要类似于Disconnect()方法。如果没有该放哪广发,共享存储就会随着时间的推移,而出现大量冗余的回调引用。
单例服务
当然,单例服务也存在类似的情况。由与单例对象的生命周期不会结束,因而它会无限累计回调引用,随着时间的推移,回调的客户端不服存在,大多数回调引用也随之失效。定义DisConnect()方法可以保证单例服务只连接到对应的活动客户端。
会话服务
会话服务即使没有DisConnect()方法也能获得引用,只要它将回调引用保存在某个实例成员的变量中。原因在于当会话结束时,服务实例会被自动释放,整个会话不存在保持引用的危险。这就能够保证回调引用总是有效的。但是,如果会话服务为了让其它宿主段或跨会话访问而将它的回调引用保存在某个全局变量中,必须添加DisConnect()方法,已达到移除回调引用的目的,这就是因为在调用Dispose()期间,不能使用回调引用。
我们也可以为会话服务添加Connect()和DisConnect()方法,因为它允许客户端在会话期间决定何时启动或停止接受回调消息。
七、回调契约层级
设计回调契约时,需要注意设计上的约束。如果一个服务契约的基契约可回调接口,则服务契约定义的回调结构必须是它的契约定义的所有回调接口的子接口。如下,以下回调契约是无效的:
interface ICallbackContract1 {......}interface ICallbackContract2 {......}[ServiceContract(CallbackContract = typeof(ICallbackContract1))] interface IMyBaseContract {......}//无效 [ServiceContract(CallbackContract = typeof(ICallbackContract2))] interface IMySubContract {......}IMySubContract不能指定ICanllbackContract2为它的回调契约,因为ICallbackContract2不能ICallbackContract1的子类,而IMyBaseContract(IMySubContract的父接口)定义了ICallbackContract1作为它的回调接口。
满足约束的最直接方法实在回调契约层级反映服务契约的层次:
interface ICallbackContract1 {......}interface ICallbackContract2:ICallbackContract1 {......}[ServiceContract(CallbackContract = typeof(ICallbackContract1))] interface IMyBaseContract {......}[ServiceContract(CallbackContract = typeof(ICallbackContract2))] interface IMySubContract {......}