我想创建一个 WCF 定时服务,客户端可以在其中注册,以便在经过一定时间后从服务中回调。问题是客户端没有被回调。不会引发异常。
回调接口为:
[ServiceContract]
public interface ITimerCallbackTarget
{
[OperationContract(IsOneWay = true)]
void OnTimeElapsed(int someInfo);
}
该服务如下所示:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single,
ConcurrencyMode = ConcurrencyMode.Single)]
public class TimerService : ITimerService
private readonly Timer _timer = new Timer(2000); //System.Timers.Timer
public void Subscribe()
{
ITimerCallbackTarget listener =
OperationContext.Current.GetCallbackChannel<ITimerCallbackTarget>();
_timer.Elapsed += (p1, p2) =>
{
listener.OnTimeElapsed(999);
};
_timer.Start();
}
客户端使用的回调方法是:
private class TimerCallbackTarget : ITimerCallbackTarget
{
public void OnTimeElapsed(int someInfo)
{
Console.WriteLine(someInfo);
}
}
客户端注册方式如下:
private static void TestTimerService()
{
InstanceContext callbackInstance = new InstanceContext(new TimerCallbackTarget());
using (DuplexChannelFactory<ITimerService> dcf =
new DuplexChannelFactory<ITimerService>(callbackInstance,
"TimerService_SecureTcpEndpoint"))
{
ITimerService timerProxy = dcf.CreateChannel();
timerProxy.Subscribe();
}
}
如果我在没有计时器的情况下在订阅方法中使用不同的线程,它可以工作:
ThreadPool.QueueUserWorkItem(p =>
{
listener.OnTimeElapsed(999);
});
如果我在订阅方法的末尾放置一个 Thread.Sleep(3000),它甚至可以与计时器一起使用(三秒钟),所以我的猜测是,在订阅方法完成后,回调对象的通道可能会关闭。对使用 OperationContext.Current.GetCallbackChannel() 检索的回调对象使用类范围变量而不是方法范围变量没有帮助。
以前,我尝试在定时服务的计时器的已用事件处理程序中创建新的线程,以使其更快。抛出了一个 ObjectDisposedException,并显示消息:"无法访问已释放的对象。对象名称:"System.ServiceModel.Channels.ServiceChannel"。然后我尝试简化我的服务,发现即使只使用 Timer 也会导致所描述的问题,但我想异常表明与客户端回调对象的连接在某处丢失。奇怪的是,如果我不在计时器线程中创建新线程,则没有例外。只是不调用回调方法。
在双工绑定中,两个通道的生存期是链接的。如果到计时器服务的通道关闭,则到回调目标的回调通道也会关闭。如果您尝试使用已关闭的通道,则可以获取 ObjectDisposedExcpetion。在您的情况下,这很糟糕,因为您不想保持 Subscribe() 通道打开只是为了接收 OnTimeElasped() 调用......我假设你想订阅无限长的时间。
双工通道试图让您的生活更轻松,但不符合您的需求。在后台,双工通道实际上是在为 CallbackTarget 创建第二个 WCF 服务主机。如果手动创建客户端的服务主机以接收回调,则可以独立于 Subscribe() 通道管理其生存期。
下面是一个功能齐全的命令行程序,演示了这个想法:
- 创建计时器服务
- 创建计时器客户端以接收通知
- 将计时器客户端的终结点地址作为订阅调用的一部分传递给计时器服务
- TimerService 使用它从 Subscribe() 获得的地址向 TimerClient 发送通知。
请注意,任何通道的打开时间都不会超过进行单个呼叫所需的时间。
标准免责声明 :这旨在展示如何创建"类似双工"的行为。缺乏错误处理和其他捷径。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Timers;
using System.ServiceModel.Description;
namespace WcfConsoleApplication
{
[ServiceContract]
public interface ITimerCallbackTarget
{
[OperationContract(IsOneWay = true)]
void OnTimeElapsed(int someInfo);
}
[ServiceContract]
public interface ITimerService
{
[OperationContract(IsOneWay = true)]
void Subscribe(string address);
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single,
ConcurrencyMode = ConcurrencyMode.Single)]
public class TimerService : ITimerService
{
private readonly Timer _timer = new Timer(2000);
private ChannelFactory<ITimerCallbackTarget> _channelFac;
private int _dataToSend = 99;
public void Subscribe(string address)
{
// note: You can also load a configured endpoint by name from app.config here,
// and still change the address at runtime in code.
_channelFac = new ChannelFactory<ITimerCallbackTarget>(new BasicHttpBinding(), address);
_timer.Elapsed += (p1, p2) =>
{
ITimerCallbackTarget callback = _channelFac.CreateChannel();
callback.OnTimeElapsed(_dataToSend++);
((ICommunicationObject)callback).Close();
// By not keeping the channel open any longer than needed to make a single call
// there's no risk of timeouts, disposed objects, etc.
// Caching the channel factory is not required, but gives a measurable performance gain.
};
_timer.Start();
}
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single,
ConcurrencyMode = ConcurrencyMode.Single)]
public class TimerClient : ITimerCallbackTarget
{
public void OnTimeElapsed(int someInfo)
{
Console.WriteLine("Got Info: " + someInfo);
}
}
class Program
{
static void Main(string[] args)
{
ServiceHost hostTimerService = new ServiceHost(typeof(TimerService), new Uri("http://localhost:8080/TimerService"));
ServiceHost hostTimerClient = new ServiceHost(typeof(TimerClient), new Uri("http://localhost:8080/TimerClient"));
ChannelFactory<ITimerService> proxyFactory = null;
try
{
// start the services
hostTimerService.Open();
hostTimerClient.Open();
// subscribe to ITimerService
proxyFactory = new ChannelFactory<ITimerService>(new BasicHttpBinding(), "http://localhost:8080/TimerService");
ITimerService timerService = proxyFactory.CreateChannel();
timerService.Subscribe("http://localhost:8080/TimerClient");
((ICommunicationObject)timerService).Close();
// wait for call backs...
Console.WriteLine("Wait for Elapsed updates. Press enter to exit.");
Console.ReadLine();
}
finally
{
hostTimerService.Close();
hostTimerClient.Close();
proxyFactory.Close();
}
}
}
}