WCF双工回调示例失败

时间:2010-08-02 21:57:51

标签: wcf duplex

为了努力磨练一些示例服务作为我们内部场景的参考,我创建了这个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

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

相反,ping工作正常,但第一次尝试时Callback出现错误,在重新连接时转到客户端,但没有返回到服务器,并且一切都断开连接。

我看到的唯一例外情况与之前出现故障的频道有关,因此无法使用,但实际故障尚未导致频道达到该状态。

我和[OperationalBehavior(IsOneWay= true)]使用类似的代码很多次。奇怪的是,这个看似更常见的案例给了我这样的悲痛。

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

5 个答案:

答案 0 :(得分:3)

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

答案 1 :(得分:1)

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

[ServiceContract]
public interface IDuplexSyncCallback
{
    [OperationContract(IsOneWay = true)
    string CallbackSync(string message, DateTimeOffset timestamp);
}

答案 2 :(得分:1)

这是非常愚蠢/恶化,但似乎ProtectionLevel.EncryptAndSign是问题所在。我发现Google上的错误消息很少与绑定和Windows身份验证相关。引导我猜测,由于与绑定加密相关的某些东西,上游通信可能无法正常工作......或者其他东西。但是将其设置为ProtectionLevel.None会突然允许双工通道用于双向方法(将值返回给服务器的方法)

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

答案 3 :(得分:0)

只需0.02美元;下载WCF&amp; WF样本打包并使用Duplex样本。 http://www.microsoft.com/downloads/details.aspx?FamilyID=35ec8682-d5fd-4bc3-a51a-d8ad115a8792&displaylang=en

答案 4 :(得分:0)

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