基于WCF定时器的服务不会回调客户端

时间:2012-06-20 22:13:19

标签: c# .net multithreading wcf callback

我想创建一个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),它甚至可以与计时器一起使用(三秒钟),所以我的猜测就是也许在subscribe方法完成后,回调对象的通道会被关闭。对使用 OperationContext.Current.GetCallbackChannel(); 而不是方法范围变量检索的回调对象使用类范围变量没有帮助。

之前我尝试在计时器服务的计时器的已用事件处理程序中创建新的线程,以使其更快。抛出了 ObjectDisposedException ,并显示消息:“无法访问已处置的对象。对象名称:'System.ServiceModel.Channels.ServiceChannel”。然后我尝试简化我的服务,发现即使只使用 Timer 也会导致问题,但我想这个异常表明某个地方与客户端回调对象的连接丢失了。奇怪的是,如果我不在 Timer 线程中创建新线程,则没有例外。不会调用回调方法。

1 个答案:

答案 0 :(得分:1)

在双工绑定中,两个通道的寿命是相互关联的。如果TimerService的通道关闭,那么CallbackTarget的回调通道也会关闭。如果您尝试使用已关闭的通道,则可以获取ObjectDisposedExcpetion。在你的情况下,这是不好的,因为你不想让Subscribe()频道保持打开只是为了接收OnTimeElasped()调用......而我假设你想订阅一段无限长的时间。

双工频道正试图让您的生活更轻松,但不符合您的需求。在幕后,双工通道实际上是为CallbackTarget创建第二个WCF服务主机。如果您手动创建客户端的服务主机以接收回调,则可以独立于Subscribe()通道管理其生命周期。

下面是一个功能齐全的命令行程序,它演示了这个想法:

  1. 创建TimerService
  2. 创建一个TimerClient来接收通知
  3. 将TimerClient的端点地址作为订阅调用的一部分传递给TimerService
  4. TimerService使用从Subscribe()获取的地址向TimerClient发送通知。
  5. 请注意,没有任何频道可以打开超过一次通话所需的时间。

    标准免责声明 :这是为了说明如何创建“双面打字”行为。缺乏错误处理和其他捷径。

    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();
                }
            }
        }
    }