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 双工回调示例失败的主要内容,如果未能解决你的问题,请参考以下文章