【问题标题】:Why is my WCF callback timing out?为什么我的 WCF 回调超时?
【发布时间】:2015-03-03 16:57:09
【问题描述】:

我有以下服务和回调合同(删节):

服务合同:

[ServiceContract(CallbackContract = typeof(ISchedulerServiceCallback))]
public interface ISchedulerService
{
    [OperationContract]
    void Stop();

    [OperationContract]
    void SubscribeStatusUpdate();
}

回调合约:

public interface ISchedulerServiceCallback
{
    [OperationContract(IsOneWay = true)] 
    void StatusUpdate(SchedulerStatus status);
}

服务实施:

[CallbackBehavior(UseSynchronizationContext = false, ConcurrencyMode = ConcurrencyMode.Multiple)] // Tried Reentrant as well.
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)] // Single due to a timer in the service that must keep time across calls.
public class SchedulerService : ISchedulerService
{
    private static Action<SchedulerStatus> statusUpdate = delegate { };

    public void Stop()
    {
        Status = SchedulerStatus.Stopped;
        statusUpdate(Status);
    }

    private SchedulerStatus Status { get; set; }

    public void SubscribeStatusUpdate()
    {
        ISchedulerServiceCallback sub = OperationContext.Current.GetCallbackChannel<ISchedulerServiceCallback>();
        statusUpdate += sub.StatusUpdate;
    }
}

服务消费者:

public class SchedulerViewModel : ViewModelBase,  ISchedulerServiceCallback
{
    private SchedulerServiceClient proxy;

    public SchedulerViewModel()
    {
        StopScheduler = new DelegateCommand(ExecuteStopSchedulerCommand, CanExecuteStopSchedulerCommand);
    }

    public void SubScribeStatusCallback()
    {
        ISchedulerServiceCallback call = this;
        InstanceContext ctx = new InstanceContext(call);
        proxy = new SchedulerServiceClient(ctx);
        proxy.SubscribeStatusUpdate();
    }

    private SchedulerStatus _status;
    private SchedulerStatus Status
    {
        get
        {
            return _status;
        }
        set
        {
            _status = value;
            OnPropertyChanged();
        }
    }

    public void StatusUpdate(SchedulerStatus newStatus)
    {
        Status = newStatus;
        Console.WriteLine("Status: " + newStatus);
    }

    public DelegateCommand StopScheduler { get; private set; }

    bool CanExecuteStopSchedulerCommand()
    {
        return true;
    }

    public void ExecuteStopSchedulerCommand()
    {
        proxy.Stop();
    }
}

SchedulerViewModel 通过其Status 和StopScheduler 属性绑定到一个带有文本框和按钮的简单窗口。 WCF 由一个简单的 Console 应用托管,用于调试:解决方案设置为先启动服务主机(控制台应用),然后再启动 WCF 应用。

当我单击主应用程序窗口上的按钮时,我希望调用该命令,即调用proxy.Stop();。这应该改变服务状态的状态并调用回调。我认为确实如此,但回调超时。调试器挂在proxy.Stop();线上,最终得到错误信息:

这个请求操作发送到 http://localhost:8089/TestService/SchedulerService/ 没有收到 在配置的超时 (00:00:59.9990000) 内回复。时间 分配给该操作的可能是较长时间的一部分 超时。这可能是因为服务仍在处理 操作或因为服务无法发送回复消息。 请考虑增加操作超时(通过强制转换 通道/代理到 IContextChannel 并设置 OperationTimeout 属性)并确保服务能够连接到 客户。

当我在控制台应用程序中使用SchedulerViewModel 时,回调工作正常,并且视图模型在控制台窗口中打印Status: Stopped。一旦我涉及其他线程,回调就不再起作用。 其他线程是 viewmodel 提升 OnPropertyChanged 以更新绑定的文本框,我不知道是否有更多线程参与启用/禁用命令。

调用的服务方法中的任何时间都不应超过毫秒,我相信我正朝着正确的方向前进,相信这是一个线程和/或 UI 挂起问题,因为我在进行研究时看到了类似的问题。大多数是完全不同的场景和深入的技术解决方案。

为什么会发生这种情况,我有什么办法可以使用相当标准的 WPF 和 WCF 基础结构和函数来启用此回调?我可悲的替代方案是让服务将状态写入文件,并让视图模型观察文件。这是一个肮脏的解决方法?

【问题讨论】:

    标签: .net wpf wcf events wcf-callbacks


    【解决方案1】:

    不幸的是,您正在 WPF 中创建死锁。

    1. 当您同步调用 Stop 时,您会阻塞您的 UI 线程。
    2. 服务器处理Stop 请求并在返回给客户端之前处理所有回调。
    3. 来自服务器的回调是同步处理的,因此它会阻止从 Stop 返回,直到 WPF 中的回调处理程序处理 StatusUpdate 回调。但是 StatusUpdate 处理程序无法启动,因为它需要 UI 线程 - 并且 UI 线程仍在等待对 Stop 的原始请求完成。

    如果您使用的是 NET 4.5,解决方案很简单。您的“点击”处理程序将被标记为async,您在客户端中调用await client.StopAsync()。

    var ssc = new SchedulerServiceClient(new InstanceContext(callback));
    try
    {
        ssc.SubscribeStatusUpdate();
        await ssc.StopAsync();
    }
    finally
    {
        ssc.Close();
    }
    

    如果您使用的是 NET 4.0,则需要以其他方式异步调用 Stop。最有可能通过 TPL。

    您的控制台客户端没有这个问题,因为它只是在不同的线程上触发回调。

    我创建了一个非常简单的解决方案,显示了GitHub 上的 WPF 和控制台应用程序之间的区别。在 WPF 客户端中,您会发现 3 个按钮 - 显示 2 种如何异步触发 Stop 的方法和 1 个会导致死锁的同步调用。

    此外,在我看来,您根本不处理 unsubscribe - 因此,一旦您的客户端断开连接,服务器将尝试调用死回调 - 这可能而且很可能也会导致调用从其他客户端到Stop 超时。因此,在您的服务类中实现类似:

    public void SubscribeStatusUpdate()
    {
        var sub = OperationContext.Current.GetCallbackChannel<ISchedulerServiceCallback>();
    
        EventHandler channelClosed =null;
        channelClosed=new EventHandler(delegate
        {
            statusUpdate -= sub.StatusUpdate;
        });
        OperationContext.Current.Channel.Closed += channelClosed;
        OperationContext.Current.Channel.Faulted += channelClosed;
        statusUpdate += sub.StatusUpdate;
    }
    

    【讨论】:

    • 感谢您提供如此全面的答案。我无法立即对其进行测试,但我认为无论如何都值得接受。我刚刚从 WCF 中删除了所有“传出消息”,并且只向其主机引发事件以发出异常信号。状态更新由调用 WCF 的仅计时器模型处理,并更新视图模型。从 UI 中仅调用一个“长时间运行”的 WCF 方法,并使用await 调用。我想使用async 重新访问回调,但在我至少提供一个 alpha 版本之前,我无法进行更多更改。
    • 别担心,祝你好运!一旦您能够对其进行测试,只需使用答案所附的示例项目 - 它显示了回调处理的不同方法。
    • 自从提出这个问题以来,我已经做出了一些战术上的改变,现在只是从一个完全不同的角度尝试你的答案,但你帮助我让回调工作,尽管不同。但是,我仍然有一些问题,也许是另一个问题,但是,在您在 GitHub 上的示例中,我看到您在每个处理程序中实例化并订阅了一个代理。与在我的视图模型 ctor 中这样做相比,这样做有什么原因吗?
    • 嗨@ProfK,不,您没有理由每次都订阅 - GitHub 示例只是为了演示死锁是如何发生的以及如何避免它。您可以在初始化 VM 时订阅它,然后仅在处置 VM 时断开连接。客户端应该保持通道打开,否则它可能会错过一些在服务器上触发的事件。
    • 是的,谢谢,@milanio。我不确定因为我终于让回调工作了,但是遇到了一些问题。一个是在托管 WCF 之前我无法订阅我的 VM;当它的轮询告诉它主机正在运行时,通过重新实例化和订阅虚拟机来修复。
    猜你喜欢
    • 2010-11-02
    • 1970-01-01
    • 2012-05-22
    • 2015-03-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-25
    • 2016-02-16
    相关资源
    最近更新 更多