【问题标题】:WCF Timer based service not calling client back基于 WCF 计时器的服务未回调客户端
【发布时间】:2012-06-23 03:28:39
【问题描述】:

我想创建一个 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();        
  }
}

如果我在没有 Timer 的情况下在 subscribe 方法中使用不同的线程,它可以工作:

  ThreadPool.QueueUserWorkItem(p =>
  {
    listener.OnTimeElapsed(999);
  });

如果我将 Thread.Sleep(3000) 放在订阅方法的末尾,它甚至可以与 Timer(三秒)一起使用,所以我的猜测是订阅方法完成后,回调对象的通道可能会关闭。对使用 OperationContext.Current.GetCallbackChannel(); 检索的回调对象使用类范围变量而不是方法范围变量没有帮助。

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

【问题讨论】:

  • 您可能需要设置IsOneWay = false,因为它是一个双工系统,而不是一劳永逸。

标签: c# .net multithreading wcf callback


【解决方案1】:

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

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

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

  1. 创建一个 TimerService
  2. 创建 TimerClient 以接收通知
  3. 将 TimerClient 的端点地址作为订阅调用的一部分传递给 TimerService
  4. 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();
            }
        }
    }
}

【讨论】:

  • 感谢您的详细回答。有用。现在唯一的问题是,当我在服务的 elapsed 处理程序中使用 ThreadPool 线程来执行回调时,会出现 ServerTooBusyException(仅当我在执行期间中止客户端时),但我想这是另一回事。
  • 当您关闭客户端时,它应该取消订阅,以便服务停止调用。如果服务处理程序正在调用不存在的客户端,则某种异常是正确的行为...更好的问题是为什么 您在使用计时器时会看到异常.
  • 计时器文档说“在 .NET Framework 2.0 版和更早版本中,计时器组件捕获并抑制由事件处理程序为 Elapsed 事件引发的所有异常。”我没有看到任何提及 .NET 4.0 的变化。这可能是你的问题吗? msdn.microsoft.com/en-us/library/system.timers.timer.aspx
  • 很好的信息,谢谢。它解释了不同的行为(计时器不会导致服务器崩溃)。因此,如果我想让服务器更健壮(防止意外的客户端“消失”),我可以像 Timer 可能隐含的那样捕获 ServerTooBusyException。
  • 您好,我使用 Visual Studio 2013,当我尝试测试您的代码时抛出此异常 System.ServiceModel.AddressAccessDeniedException HTTP 无法注册 URL http: 8080 请参阅 stackoverflow.com/questions/8727293/…
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-27
  • 1970-01-01
  • 2013-06-23
  • 1970-01-01
  • 2011-03-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多