WCF - Callback

来源:互联网 发布:宏观经济数据 知乎 编辑:程序博客网 时间:2024/06/06 08:34

Callback 机制又被称之为 "duplex call",说白了就是在原有基础上,为客户端也添加一个服务端点,让服务器能调用这个客户端"服务",从而实现所谓的回调机制。也正因为如此,通讯 binding就必须支持双向通讯能力(bidirectional-capable),通常我们会选择WSDualHttpBinding、NetTcpBinding 以及 NetNamedPipeBinding。

接下来,我们回忆一下 Callback 的开发步骤。

1. 创建服务契约。

[ServiceContract(SessionMode=SessionMode.Required)]
public interface IService
{
  [OperationContract]
  void Test();
}


2. 定义回调接口。

由于回调接口的实现类型在客户端执行,因此无需添加 ServiceContract。

public interface ICallBack
{
  [OperationContract]
  void Execute(DateTime d);
}


3. 实现服务契约。

我们可以使用 "OperationContext.Current.GetCallbackChannel<T>()" 来回调客户端 "Callback"。

public class MyService : IService, IDisposable
{
  public void Test()
  {
    Console.WriteLine("Service Invoke CallBack...");
    OperationContext.Current.GetCallbackChannel<ICallBack>().Execute(DateTime.Now);
  }

  public void Dispose()
  {
    Console.WriteLine("Dispose:{0}", DateTime.Now);
  }
}


4. 创建 ServiceHost。

注意发布 Metadata。

ServiceHost host = new ServiceHost(typeof(MyService), new Uri("http://localhost:8080/myservice"));
host.AddServiceEndpoint(typeof(IService), new WSDualHttpBinding(), "");

ServiceMetadataBehavior metadata = new ServiceMetadataBehavior();
metadata.HttpGetEnabled = true;
host.Description.Behaviors.Add(metadata);

host.Open();


5. 使用 Svcutil.exe 或 VS2005 创建客户端代理。

//------------------------------------------------------------------------------
// <auto-generated>
//   此代码由工具生成。
//   运行库版本:2.0.50727.42
//
//   对此文件的更改可能会导致不正确的行为,并且如果
//   重新生成代码,这些更改将会丢失。
// </auto-generated>
//------------------------------------------------------------------------------

namespace ConsoleApplication1.localhost
{
  [GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
  [ServiceContractAttribute(ConfigurationName= "ConsoleApplication1.localhost.IService", CallbackContract =typeof(IServiceCallback), SessionMode = SessionMode.Required)]
  public interface IService
  {
    [OperationContractAttribute(Action = "http://.../IService/Test", ReplyAction = "http://.../IService/TestResponse")]
    void Test();
  }

  [GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
  public interface IServiceCallback
  {
    [OperationContractAttribute(Action = "http://.../IService/Execute", ReplyAction = "http://.../IService/ExecuteResponse")]
    void Execute(System.DateTime d);
  }

  [GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
  public interface IServiceChannel : IService, IClientChannel
  {
  }

  [DebuggerStepThroughAttribute()]
  [GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
  public partial class ServiceClient : DuplexClientBase<IService>, IService
  {
    public ServiceClient(System.ServiceModel.InstanceContext callbackInstance)
      : base(callbackInstance)
    {
    }

    public void Test()
    {
      base.Channel.Test();
    }
  }
}


6. 创建客户端回调接口的实现类型。

class CallBack : IServiceCallback
{
  public void Execute(DateTime d)
  {
    Console.WriteLine("Client:{0}", d);
  }
}


7. 为客户端自动生成的配置文件添加 clientBaseAddress。

服务器通过这个地址调用客户端回调服务。在同一台机器上调试,注意使用一个不同的端口。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <bindings>
      <wsDualHttpBinding>
        <binding name="WSDualHttpBinding_IService" clientBaseAddress="http://localhost:8081/" >
        </binding>
      </wsDualHttpBinding>
    </bindings>
    <client>
      <endpoint address="http://localhost:8080/myservice" binding="wsDualHttpBinding"
        bindingConfiguration="WSDualHttpBinding_IService" contract="ConsoleApplication1.localhost.IService"
        name="WSDualHttpBinding_IService">
        <identity>
          <userPrincipalName value="HZH/Administrator" />
        </identity>
      </endpoint>
    </client>
  </system.serviceModel>
</configuration>


8. 创建客户端调用代码。

InstanceContext instance = new InstanceContext(new CallBack());
using (ServiceClient client = new ServiceClient(instance))
{
  client.Test();
}


运行,看看结果。你看到了什么?异常!!!

用户代码未处理 System.InvalidOperationException
  Message="Thisoperation would deadlock because the reply cannot be received until thecurrent Message completes processing. If you want to allow out-of-ordermessage processing, specify ConcurrencyMode of Reentrant or Multiple onServiceBehaviorAttribute."
  Source="mscorlib"
  StackTrace:
    Server stack trace:
      在System.ServiceModel.Channels.ServiceChannel.PrepareCall(ProxyOperationRuntimeoperation, Boolean oneway, ProxyRpc& rpc)


死锁(deadlock)?没错,你找到了让本文继续下去的理由。

在缺省情况下,服务实例是 single-threaded (ConcurrencyMode=ConcurrencyMode.Single)的。当服务实例调用客户端回调服务时,方法被阻塞,直到客户端回调服务消息返回(replymessage)。而问题是,Single模式不允许重入调用(reentrantcalls),即便系统获得返回消息,也会因为无法重入调用方法解除阻塞而造成死锁。了解了这些背景,要解决这个死锁也很简单。

方法1: 将回调接口方法设置为 "IsOneWay=true",这样就无需处理客户端返回消息。

为回调接口方法添加 IsOneWay。

public interface ICallBack
{
  [OperationContract(IsOneWay=true)]
  void Execute(DateTime d);
}


修改客户端代理文件,添加 IsOneWay,删除 ReplyAction。

[GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
public interface IServiceCallback
{
  [OperationContractAttribute(IsOneWay = true, Action = "http://.../IService/Execute")]
  void Execute(System.DateTime d);
}


方法2: 修改服务契约的并发模式,改为 "ConcurrencyMode.Reentrant" 或 "ConcurrencyMode.Multiple"。

和方法1不同,本方法无需对客户端做出修改。

[ServiceBehavior(ConcurrencyMode=ConcurrencyMode.Reentrant)]
public class MyService : IService, IDisposable
{
  public void Test()
  {
    Console.WriteLine("Service Invoke CallBack...");
    OperationContext.Current.GetCallbackChannel<ICallBack>().Execute(DateTime.Now);
  }

  public void Dispose()
  {
    Console.WriteLine("Dispose:{0}", DateTime.Now);
  }
}


无论使用哪种方法,修改后再次运行,我们得到了应有的结果。

回调机制可以用来实现类似事件通知等功能,但对于多数情况下,并不推荐使用。服务器要连接客户端实现回调,可能面临很多问题,诸如内部网络地址、防火墙等等,而且回调机制也让服务实例的生存期管理变得更复杂。

原创粉丝点击