2012-06-20 65 views
0

我想做一個WCF定時器服務,客戶端可以註冊,以便在經過一定時間後從服務中回調。問題是客戶端不會被回叫。不引發異常。基於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」。然後,我嘗試簡化我的服務,發現即使只使用計時器也會導致上述問題,但我猜這個例外表明與客戶端的回調對象的連接丟失。奇怪的是,如果我沒有在定時器線程中創建新線程,那麼沒有excepiton。回調方法只是不被調用。

+0

您可能需要設置爲「IsOneWay = false」,因爲它是一個雙工系統,不會發生遺忘。 – oleksii

回答

1

在雙工綁定中,兩個通道的壽命是相關聯的。如果通往TimerService的通道關閉,則CallbackTarget的回調通道也會關閉。如果您嘗試使用已關閉的頻道,則可以獲得ObjectDisposedExcpetion。在你的情況下,這是不好的,因爲你不想保持訂閱()通道打開只是爲了接收OnTimeElasped()調用......並且我假設你想訂閱無限長的時間。

雙工通道試圖讓您的生活更輕鬆,但不符合您的需求。在後臺,雙工通道實際上是爲CallbackTarget創建第二個WCF服務主機。如果您手動創建客戶端的服務主機以接收回調,則可以獨立於Subscribe()通道管理其生存期。

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

  1. 創建TimerService
  2. 創建TimerClient接收notificatioins
  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(); 
      } 
     } 
    } 
} 
+0

謝謝你的詳細解答。有用。現在唯一的問題是,當我在服務的過期處理程序中使用ThreadPool線程來執行回調時,出現了ServerTooBusyException(僅當我在執行期間中止客戶端時),但我想這是一個不同的錯誤。 – user764754

+0

當您關閉客戶端時,它應該執行取消訂閱,以便服務停止呼叫。如果服務處理程序正在調用不在那裏的客戶端,那麼某種異常是正確的行爲......更好的問題是爲什麼*不會*在使用計時器時看到異常。 – ErnieL

+1

計時器文檔中提到「在.NET Framework 2.0及更早版本中,Timer組件捕獲並抑制事件處理程序爲Elapsed事件拋出的所有異常。」我沒有看到任何提及.NET 4.0的變化。這可能是你的問題嗎? http://msdn.microsoft.com/en-us/library/system.timers.timer.aspx – ErnieL

相關問題