WCF - Callback
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="This operation would deadlock because the reply cannot be received until the current Message completes processing. If you want to allow out-of-order message processing, specify ConcurrencyMode of Reentrant or Multiple on ServiceBehaviorAttribute."
Source="mscorlib"
StackTrace:
Server stack trace:
在 System.ServiceModel.Channels.ServiceChannel.PrepareCall(ProxyOperationRuntime operation, Boolean oneway, ProxyRpc& rpc)
死锁(deadlock)?没错,你找到了让本文继续下去的理由。
在缺省情况下,服务实例是 single-threaded (ConcurrencyMode=ConcurrencyMode.Single) 的。当服务实例调用客户端回调服务时,方法被阻塞,直到客户端回调服务消息返回(reply message)。而问题是,Single模式不允许重入调用(reentrant calls),即便系统获得返回消息,也会因为无法重入调用方法解除阻塞而造成死锁。了解了这些背景,要解决这个死锁也很简单。
方法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);
}
}
无论使用哪种方法,修改后再次运行,我们得到了应有的结果。
回调机制可以用来实现类似事件通知等功能,但对于多数情况下,并不推荐使用。服务器要连接客户端实现回调,可能面临很多问题,诸如内部网络地址、防火墙等等,而且回调机制也让服务实例的生存期管理变得更复杂。
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/zengjibing/archive/2009/01/17/3813719.aspx