WCF 双工回调示例失败

Posted

技术标签:

【中文标题】WCF 双工回调示例失败【英文标题】:WCF Duplex Callback Sample failing 【发布时间】:2011-03-24 10:57:15 【问题描述】:

为了完善一些示例服务以用作我们内部场景的参考,我创建了这个 WCF 双工通道示例,汇集了多年来发现的几个示例。

双工部分不工作,我希望我们能一起解决这个问题。我讨厌发布这么多代码,但我觉得我已经将它缩减到尽可能短的 WCF,同时整合了我希望社区审查的所有部分。这里可能有一些非常糟糕的想法,我并不是说它是正确的,这只是我目前所得到的。

分为三个部分。通道、服务器和客户端。三个项目,这里是三个代码文件。没有XML配置,一切都是编码进去的。后面是代码输出。

Channel.proj / Channel.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;

namespace Channel

    public interface IDuplexSyncCallback
    
        [OperationContract]
        string CallbackSync(string message, DateTimeOffset timestamp);
    

    [ServiceContract(CallbackContract = typeof(IDuplexSyncCallback))]
    public interface IDuplexSyncContract
    
        [OperationContract]
        void Ping();

        [OperationContract]
        void Enroll();

        [OperationContract]
        void Unenroll();
    

Server.proj / Server.cs,引用Channel、System.Runtime.Serialization、System.ServiceModel

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Timers;
using Channel;
using System.Diagnostics;
using System.Net.Security;

namespace Server

    class Program
    
        // All of this just starts up the service with these hardcoded configurations
        static void Main(string[] args)
        
            ServiceImplementation implementation = new ServiceImplementation();
            ServiceHost service = new ServiceHost(implementation);

            NetTcpBinding binding = new NetTcpBinding(SecurityMode.Transport);
            binding.Security.Message.ClientCredentialType = MessageCredentialType.Windows;
            binding.Security.Mode = SecurityMode.Transport;
            binding.Security.Transport.ClientCredentialType = TcpClientCredentialType.Windows;
            binding.Security.Transport.ProtectionLevel = ProtectionLevel.EncryptAndSign;
            binding.ListenBacklog = 1000;
            binding.MaxConnections = 30;
            binding.MaxReceivedMessageSize = 2147483647;
            binding.ReaderQuotas.MaxStringContentLength = 2147483647;
            binding.ReaderQuotas.MaxArrayLength = 2147483647;
            binding.SendTimeout = TimeSpan.FromSeconds(2);
            binding.ReceiveTimeout = TimeSpan.FromSeconds(10 * 60); // 10 minutes is the default if not specified
            binding.ReliableSession.Enabled = true;
            binding.ReliableSession.Ordered = true;

            service.AddServiceEndpoint(typeof(IDuplexSyncContract), binding, new Uri("net.tcp://localhost:3828"));

            service.Open();

            Console.WriteLine("Server Running ... Press any key to quit");
            Console.ReadKey(true);

            service.Abort();
            service.Close();
            implementation = null;
            service = null;
        
    

    /// <summary>
    /// ServiceImplementation of IDuplexSyncContract
    /// </summary>
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single,
        MaxItemsInObjectGraph = 2147483647,
        IncludeExceptionDetailInFaults = true,
        ConcurrencyMode = ConcurrencyMode.Multiple,
        UseSynchronizationContext = false)]
    class ServiceImplementation : IDuplexSyncContract
    
        Timer announcementTimer = new Timer(5000); // Every 5 seconds
        int messageNumber = 0; // message number incrementer - not threadsafe, just for debugging.

        public ServiceImplementation()
        
            announcementTimer.Elapsed += new ElapsedEventHandler(announcementTimer_Elapsed);
            announcementTimer.AutoReset = true;
            announcementTimer.Enabled = true;
        

        void announcementTimer_Elapsed(object sender, ElapsedEventArgs e)
        
            AnnounceSync(string.Format("HELLO? (#0)", messageNumber++));
        

        #region IDuplexSyncContract Members
        List<IDuplexSyncCallback> syncCallbacks = new List<IDuplexSyncCallback>();

        /// <summary>
        /// Simple Ping liveness
        /// </summary>
        [OperationBehavior]
        public void Ping()  return; 

        /// <summary>
        /// Add channel to subscribers
        /// </summary>
        [OperationBehavior]
        void IDuplexSyncContract.Enroll()
        
            IDuplexSyncCallback current = System.ServiceModel.OperationContext.Current.GetCallbackChannel<IDuplexSyncCallback>();

            lock (syncCallbacks)
            
                syncCallbacks.Add(current);

                Trace.WriteLine("Enrollment Complete");
            
        

        /// <summary>
        /// Remove channel from subscribers
        /// </summary>
        [OperationBehavior]
        void IDuplexSyncContract.Unenroll()
        
            IDuplexSyncCallback current = System.ServiceModel.OperationContext.Current.GetCallbackChannel<IDuplexSyncCallback>();

            lock (syncCallbacks)
            
                syncCallbacks.Remove(current);

                Trace.WriteLine("Unenrollment Complete");
            
        

        /// <summary>
        /// Callback to clients over enrolled channels
        /// </summary>
        /// <param name="message"></param>
        void AnnounceSync(string message)
        
            var now = DateTimeOffset.Now;

            if (message.Length > 2000) message = message.Substring(0, 2000 - "[TRUNCATED]".Length) + "[TRUNCATED]";
            Trace.WriteLine(string.Format("0: 1", now.ToString("mm:ss.fff"), message));

            lock (syncCallbacks)
            
                foreach (var callback in syncCallbacks.ToArray())
                
                    Console.WriteLine("Sending \"0\" synchronously ...", message);

                    CommunicationState state = ((ICommunicationObject)callback).State;

                    switch (state)
                    
                        case CommunicationState.Opened:
                            try
                            
                                Console.WriteLine("Client said '0'", callback.CallbackSync(message, now));
                            
                            catch (Exception ex)
                            
                                // Timeout Error happens here
                                syncCallbacks.Remove(callback);
                                Console.WriteLine("Removed client");
                            
                            break;
                        case CommunicationState.Created:
                        case CommunicationState.Opening:
                            break;
                        case CommunicationState.Faulted:
                        case CommunicationState.Closed:
                        case CommunicationState.Closing:
                        default:
                            syncCallbacks.Remove(callback);
                            Console.WriteLine("Removed client");
                            break;
                    
                
            
        
        #endregion
    

Client.proj / Client.cs,引用 Channel、System.Runtime.Serialization、System.ServiceModel

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Timers;
using System.Diagnostics;
using Channel;
using System.Net;

namespace Client

    class Program
    
        static void Main(string[] args)
        
            using (var callbackSyncProxy = new CallbackSyncProxy(new Uri("net.tcp://localhost:3828"), CredentialCache.DefaultNetworkCredentials))
            
                callbackSyncProxy.Faulted += (s, e) => Console.WriteLine("CallbackSyncProxy Faulted.");
                callbackSyncProxy.ConnectionUnavailable += (s, e) => Console.WriteLine("CallbackSyncProxy ConnectionUnavailable.");
                callbackSyncProxy.ConnectionRecovered += (s, e) => Console.WriteLine("CallbackSyncProxy ConnectionRecovered.");

                callbackSyncProxy.Ping();
                callbackSyncProxy.Ping();
                callbackSyncProxy.Ping();

                Console.WriteLine("Pings completed.  Enrolling ...");

                callbackSyncProxy.AnnouncementSyncHandler = AnnouncementHandler;

                Console.WriteLine("Enrolled and waiting.  Press any key to quit ...");
                Console.ReadKey(true); // Wait for quit
            
        

        /// <summary>
        /// Called by the server through DuplexChannel
        /// </summary>
        /// <param name="message"></param>
        /// <param name="timeStamp"></param>
        /// <returns></returns>
        static string AnnouncementHandler(string message, DateTimeOffset timeStamp)
        
            Console.WriteLine("0: 1", timeStamp, message);

            return string.Format("Dear Server, thanks for that message at 0.", timeStamp);
        
    

    /// <summary>
    /// Encapsulates the client-side WCF setup logic.
    /// 
    /// There are 3 events Faulted, ConnectionUnavailable, ConnectionRecovered that might be of interest to the consumer
    /// Enroll and Unenroll of the ServiceContract are called when setting an AnnouncementSyncHandler
    /// Ping, when set correctly against the server's send/receive timeouts, will keep the connection alive
    /// </summary>
    public class CallbackSyncProxy : IDisposable
    
        Uri listen;
        NetworkCredential credentials;
        NetTcpBinding binding;
        EndpointAddress serverEndpoint;
        ChannelFactory<IDuplexSyncContract> channelFactory;
        DisposableChannel<IDuplexSyncContract> channel;

        readonly DuplexSyncCallback callback = new DuplexSyncCallback();

        object sync = new object();
        bool enrolled;
        Timer pingTimer = new Timer();
        bool quit = false; // set during dispose

        // Events of interest to consumer
        public event EventHandler Faulted;
        public event EventHandler ConnectionUnavailable;
        public event EventHandler ConnectionRecovered;

        // AnnouncementSyncHandler property.  When set to non-null delegate, Enrolls client with server.
        // passes through to the DuplexSyncCallback callback.AnnouncementSyncHandler
        public Func<string, DateTimeOffset, string> AnnouncementSyncHandler
        
            get
            
                Func<string, DateTimeOffset, string> temp = null;

                lock (sync)
                
                    temp = callback.AnnouncementSyncHandler;
                
                return temp;
            
            set
            
                lock (sync)
                
                    if (callback.AnnouncementSyncHandler == null && value != null)
                    
                        callback.AnnouncementSyncHandler = value;

                        Enroll();
                    
                    else if (callback.AnnouncementSyncHandler != null && value == null)
                    
                        Unenroll();

                        callback.AnnouncementSyncHandler = null;
                    
                    else // null to null or function to function, just update it
                    
                        callback.AnnouncementSyncHandler = value;
                    
                
            
        

        /// <summary>
        /// using (var proxy = new CallbackSyncProxy(listen, CredentialCache.DefaultNetworkCredentials)  ... 
        /// </summary>
        public CallbackSyncProxy(Uri listen, NetworkCredential credentials)
        
            this.listen = listen;
            this.credentials = credentials;

            binding = new NetTcpBinding(SecurityMode.Transport);
            binding.Security.Message.ClientCredentialType = MessageCredentialType.Windows;
            binding.Security.Mode = SecurityMode.Transport;
            binding.Security.Transport.ClientCredentialType = TcpClientCredentialType.Windows;
            binding.MaxReceivedMessageSize = 2147483647;
            binding.ReaderQuotas.MaxArrayLength = 2147483647;
            binding.ReaderQuotas.MaxBytesPerRead = 2147483647;
            binding.ReaderQuotas.MaxDepth = 2147483647;
            binding.ReaderQuotas.MaxStringContentLength = 2147483647;
            binding.ReliableSession.Enabled = true;
            binding.ReliableSession.Ordered = true;
            serverEndpoint = new EndpointAddress(listen);

            pingTimer.AutoReset = true;
            pingTimer.Elapsed += pingTimer_Elapsed;
            pingTimer.Interval = 20000;
        

        /// <summary>
        /// Keep the connection alive by pinging at some set minimum interval
        /// </summary>
        void pingTimer_Elapsed(object sender, ElapsedEventArgs e)
        
            bool locked = false;

            try
            
                locked = System.Threading.Monitor.TryEnter(sync, 100);
                if (!locked)
                
                    Console.WriteLine("Unable to ping because synchronization lock could not be aquired in a timely fashion");
                    return;
                
                Debug.Assert(channel != null, "CallbackSyncProxy.channel is unexpectedly null");

                try
                
                    channel.Service.Ping();
                
                catch
                
                    Console.WriteLine("Unable to ping");
                
            
            finally
            
                if (locked) System.Threading.Monitor.Exit(sync);
            
        

        /// <summary>
        /// Ping is a keep-alive, but can also be called by the consuming code
        /// </summary>
        public void Ping()
        
            lock (sync)
            
                if (channel != null)
                
                    channel.Service.Ping();
                
                else
                
                    using (var c = new DisposableChannel<IDuplexSyncContract>(GetChannelFactory().CreateChannel()))
                    
                        c.Service.Ping();
                    
                
            
        

        /// <summary>
        /// Enrollment - called when AnnouncementSyncHandler is assigned
        /// </summary>
        void Enroll()
        
            lock (sync)
            
                if (!enrolled)
                
                    Debug.Assert(channel == null, "CallbackSyncProxy.channel is unexpectedly not null");

                    var c = new DisposableChannel<IDuplexSyncContract>(GetChannelFactory().CreateChannel());

                    ((ICommunicationObject)c.Service).Open();

                    ((ICommunicationObject)c.Service).Faulted += new EventHandler(CallbackChannel_Faulted);

                    c.Service.Enroll();

                    channel = c;

                    Debug.Assert(!pingTimer.Enabled, "CallbackSyncProxy.pingTimer unexpectedly Enabled");

                    pingTimer.Start();

                    enrolled = true;
                
            
        

        /// <summary>
        /// Unenrollment - called when AnnouncementSyncHandler is set to null
        /// </summary>
        void Unenroll()
        
            lock (sync)
            
                if (callback.AnnouncementSyncHandler != null)
                
                    Debug.Assert(channel != null, "CallbackSyncProxy.channel is unexpectedly null");

                    channel.Service.Unenroll();

                    Debug.Assert(!pingTimer.Enabled, "CallbackSyncProxy.pingTimer unexpectedly Disabled");

                    pingTimer.Stop();

                    enrolled = false;
                
            
        

        /// <summary>
        /// Used during enrollment to establish a channel.
        /// </summary>
        /// <returns></returns>
        ChannelFactory<IDuplexSyncContract> GetChannelFactory()
        
            lock (sync)
            
                if (channelFactory != null &&
                    channelFactory.State != CommunicationState.Opened)
                
                    ResetChannel();
                

                if (channelFactory == null)
                
                    channelFactory = new DuplexChannelFactory<IDuplexSyncContract>(callback, binding, serverEndpoint);

                    channelFactory.Credentials.Windows.ClientCredential = credentials;

                    foreach (var op in channelFactory.Endpoint.Contract.Operations)
                    
                        var b = op.Behaviors[typeof(System.ServiceModel.Description.DataContractSerializerOperationBehavior)] as System.ServiceModel.Description.DataContractSerializerOperationBehavior;

                        if (b != null)
                            b.MaxItemsInObjectGraph = 2147483647;
                    
                
            

            return channelFactory;
        

        /// <summary>
        /// Channel Fault handler, set during Enrollment
        /// </summary>
        void CallbackChannel_Faulted(object sender, EventArgs e)
        
            lock (sync)
            
                if (Faulted != null)
                
                    Faulted(this, new EventArgs());
                

                ResetChannel();

                pingTimer.Stop();
                enrolled = false;

                if (callback.AnnouncementSyncHandler != null)
                
                    while (!quit) // set during Dispose
                    
                        System.Threading.Thread.Sleep(500);

                        try
                        
                            Enroll();

                            if (ConnectionRecovered != null)
                            
                                ConnectionRecovered(this, new EventArgs());

                                break;
                            
                        
                        catch
                        
                            if (ConnectionUnavailable != null)
                            
                                ConnectionUnavailable(this, new EventArgs());
                            
                        
                    
                
            
        

        /// <summary>
        /// Reset the Channel & ChannelFactory if they are faulted and during dispose
        /// </summary>
        void ResetChannel()
        
            lock (sync)
            
                if (channel != null)
                
                    channel.Dispose();
                    channel = null;
                

                if (channelFactory != null)
                
                    if (channelFactory.State == CommunicationState.Faulted)
                        channelFactory.Abort();
                    else
                        try
                        
                            channelFactory.Close();
                        
                        catch
                        
                            channelFactory.Abort();
                        

                    channelFactory = null;
                
            
        

        // Disposing of me implies disposing of disposable members
        #region IDisposable Members
        bool disposed;
        void IDisposable.Dispose()
        
            if (!disposed)
            
                Dispose(true);
            

            GC.SuppressFinalize(this);
        

        void Dispose(bool disposing)
        
            if (disposing)
            
                quit = true;

                ResetChannel();

                pingTimer.Stop();

                enrolled = false;

                callback.AnnouncementSyncHandler = null;
            

            disposed = true;
        
        #endregion
    

    /// <summary>
    /// IDuplexSyncCallback implementation, instantiated through the CallbackSyncProxy
    /// </summary>
    [CallbackBehavior(UseSynchronizationContext = false, 
    ConcurrencyMode = ConcurrencyMode.Multiple, 
    IncludeExceptionDetailInFaults = true)]
    class DuplexSyncCallback : IDuplexSyncCallback
    
        // Passthrough handler delegates from the CallbackSyncProxy
        #region AnnouncementSyncHandler passthrough property
        Func<string, DateTimeOffset, string> announcementSyncHandler;
        public Func<string, DateTimeOffset, string> AnnouncementSyncHandler
        
            get
            
                return announcementSyncHandler;
            
            set
            
                announcementSyncHandler = value;
            
        
        #endregion

        /// <summary>
        /// IDuplexSyncCallback.CallbackSync
        /// </summary>
        [OperationBehavior]
        public string CallbackSync(string message, DateTimeOffset timestamp)
        
            if (announcementSyncHandler != null)
            
                return announcementSyncHandler(message, timestamp);
            
            else
            
                return "Sorry, nobody was home";
            
        
    

    // This class wraps an ICommunicationObject so that it can be either Closed or Aborted properly with a using statement
    // This was chosen over alternatives of elaborate try-catch-finally blocks in every calling method, or implementing a
    // new Channel type that overrides Disposable with similar new behavior
    sealed class DisposableChannel<T> : IDisposable
    
        T proxy;
        bool disposed;

        public DisposableChannel(T proxy)
        
            if (!(proxy is ICommunicationObject)) throw new ArgumentException("object of type ICommunicationObject expected", "proxy");

            this.proxy = proxy;
        

        public T Service
        
            get
            
                if (disposed) throw new ObjectDisposedException("DisposableProxy");

                return proxy;
            
        

        public void Dispose()
        
            if (!disposed)
            
                Dispose(true);
            

            GC.SuppressFinalize(this);
        

        void Dispose(bool disposing)
        
            if (disposing)
            
                if (proxy != null)
                
                    ICommunicationObject ico = null;

                    if (proxy is ICommunicationObject)
                        ico = (ICommunicationObject)proxy;

                    // This state may change after the test and there's no known way to synchronize
                    // so that's why we just give it our best shot
                    if (ico.State == CommunicationState.Faulted)
                        ico.Abort(); // Known to be faulted
                    else
                        try
                        
                            ico.Close(); // Attempt to close, this is the nice way and we ought to be nice
                        
                        catch
                        
                            ico.Abort(); // Sometimes being nice isn't an option
                        

                    proxy = default(T);
                
            

            disposed = true;
        
    

整理输出:

>> Server Running ... Press any key to quit
                           Pings completed.  Enrolling ... <<
          Enrolled and waiting.  Press any key to quit ... <<
>> Sending "HELLO? (#0)" synchronously ...
                                CallbackSyncProxy Faulted. <<
                    CallbackSyncProxy ConnectionRecovered. <<
>> Removed client
>> Sending "HELLO? (#2)" synchronously ...
                   8/2/2010 2:47:32 PM -07:00: HELLO? (#2) <<
>> Removed client

正如 Andrew 所指出的,问题并不是那么不言自明。这种“整理输出”不是所需的输出。相反,我希望服务器正在运行,Ping 和注册成功,然后每隔 5 秒,服务器会“发送”HELLO? (#m)“同步”,客户端立即转换并返回,服务器接收并打印出来。

相反,ping 可以工作,但回调在第一次尝试时出错,在重新连接时到达客户端但没有返回到服务器,并且一切都断开连接。

我看到的唯一异常与通道先前发生故障并因此无法使用有关,但还没有关于导致通道达到该状态的实际故障。

我已经多次使用[OperationalBehavior(IsOneWay= true)] 的类似代码。奇怪的是,这个看似更常见的案例却让我如此悲痛。

在服务器端捕获的异常,我不明白,是: System.TimeoutException:“发送到 schemas.microsoft.com/2005/12/ServiceModel/Addressing/Anonymous 的此请求操作未在配置的超时 (00:00:00) 内收到回复。分配给此操作的时间可能有是较长超时的一部分。这可能是因为服务仍在处理操作或因为服务无法发送回复消息。请考虑增加操作超时(通过将通道/代理转换为 IContextChannel 并设置 OperationTimeout属性)并确保服务能够连接到客户端。”

【问题讨论】:

你应该说双工部分如何“不工作”。否则,任何人阅读代码的动机都是零。 好点。我在底部添加了 cmets。这样更好吗? 也许您应该将可构建和可运行的项目放在网络上某个位置的 zip 文件中,以便理想情况下任何有时间的人都可以构建、xcopy 部署并在 5 分钟内运行,然后在调试器中观察正在发生的事情和有 VS assitence 来挖掘代码。顺便说一句,StackOverfkow 需要更改他们的 CSS 以开始使用紧凑的代码间距 :-) 【参考方案1】:

在 AnnounceSync 方法中的服务器上添加 FaultException 处理,您会被告知没有来自服务器(在您的情况下是客户端)的响应,这意味着没有收到回调。

由于超时,正如您所建议的那样。 所以改变

binding.SendTimeout = TimeSpan.FromSeconds(3);

它会按预期工作。

try

    Console.WriteLine("Client said '0'",callback.CallbackSync(message, now) );

catch (FaultException fex)

    syncCallbacks.Remove(callback);
    Console.WriteLine("Failed to call Client because" + fex.Reason);
    Console.WriteLine(fex.Message);

【讨论】:

你的自信很诱人,但我无法让它发挥作用。我已将此 catch 块添加到 server.cs 并将超时从 2 秒更改为 3 秒。行为没有变化。抛出的异常是 TimeoutException,而不是 FaultException。【参考方案2】:

这可能无法完全解决您的问题,但查看您的代码,IDuplexSyncCallback 绝对是一个嫌疑犯。它的部分服务实现已经到位,但它也应该用ServiceContractAttribute 装饰。在进行回调时,它也必须被指定为单向。以下是我过去为回调合约所做的示例,也可能对您有所帮助。

[ServiceContract]
public interface IDuplexSyncCallback

    [OperationContract(IsOneWay = true)
    string CallbackSync(string message, DateTimeOffset timestamp);

【讨论】:

即使在 larsw 引用的 Samples 中,Callback 接口也没有用 [ServiceContract] 标记。我在别处读到它也不是必需的,但我再也找不到那个网址了。无论如何,我只是为了不固执而尝试它,这没有任何区别。 IsOneWay = true 的第二期不适合这种情况——我需要回应!我还没有读到回调需要 IsOneWay = true (它们不能返回值) OneWay 不是必需的,这是正确的。回调通常是异步的,这就是为什么您可能会以这种方式找到示例。对不起,没有帮助。【参考方案3】:

这很愚蠢/令人恼火,但似乎ProtectionLevel.EncryptAndSign 是问题所在。我发现 Google 上的错误消息很少与绑定和 Windows 身份验证相关。让我猜测可能是由于与绑定加密有关的某些原因,上游通信无法正常工作……或其他原因。但是将其设置为 ProtectionLevel.None 反而突然允许双工通道适用于双向方法(将值返回到服务器的方法)

我并不是说关闭保护级别是一个好主意,但至少这是一个重要的领先优势。如果您需要 EncryptAndSign 的好处,可以从那里进一步调查。

【讨论】:

【参考方案4】:

只要我的 0.02 美元;下载 WCF 和 WF 示例包并改用 Duplex 示例。 http://www.microsoft.com/downloads/details.aspx?FamilyID=35ec8682-d5fd-4bc3-a51a-d8ad115a8792&displaylang=en

【讨论】:

我已经检查了这个示例,但它特别是 [OperationContract(IsOneWay = true)],这对于这种情况是不可接受的。【参考方案5】:

不幸的是,单向操作是双工通道的先决条件。

【讨论】:

以上是关于WCF 双工回调示例失败的主要内容,如果未能解决你的问题,请参考以下文章

在 WCF 双工合同中检测客户端死亡

如何在双工回调中读取 WCF 消息头?

防止 WCF 双工回调服务出现死锁问题

Wcf 回调网络 tcp 双工仅 1 路故障

响应式 WCF 客户端的双工回调或客户端线程

不活动后出现错误的 WCF 双工回调 - 保持活动长时间运行的推送通知