与Silverlight一起使用WCF双工轮询时出现死锁

Kob*_*ari 5 silverlight wcf pollingduplexhttpbinding

我跟随Tomek Janczuk在silverlight tv上的演示,创建了一个使用WCF Duplex Polling Web服务的聊天程序.客户端订阅服务器,然后服务器向所有连接的客户端发起通知以发布事件.

想法很简单,在客户端上,有一个允许客户端连接的按钮.客户端可以编写消息并将其发布的文本框,以及显示从服务器接收的所有通知的更大文本框.

我连接了3个客户端(在不同的浏览器中 - IE,Firefox和Chrome),它们都运行良好.他们发送消息并顺利接收.当我关闭其中一个浏览器时,问题就出现了.一个客户一出门,其他客户就会卡住.他们停止收到通知.

我猜测服务器中通过所有客户端并向他们发送通知的循环卡在现在丢失的客户端上.我尝试捕获异常并将其从客户端列表中删除(请参阅代码)但它仍然没有帮助.

有任何想法吗?

服务器代码如下:

    using System;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.Collections.Generic;
using System.Runtime.Remoting.Channels;

namespace ChatDemo.Web
{
    [ServiceContract]
    public interface IChatNotification 
    {
        // this will be used as a callback method, therefore it must be one way
        [OperationContract(IsOneWay=true)]
        void Notify(string message);

        [OperationContract(IsOneWay = true)]
        void Subscribed();
    }

    // define this as a callback contract - to allow push
    [ServiceContract(Namespace="", CallbackContract=typeof(IChatNotification))]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    [ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
    public class ChatService
    {
        SynchronizedCollection<IChatNotification> clients = new SynchronizedCollection<IChatNotification>();

        [OperationContract(IsOneWay=true)]
        public void Subscribe()
        {
            IChatNotification cli = OperationContext.Current.GetCallbackChannel<IChatNotification>();
            this.clients.Add(cli);
            // inform the client it is now subscribed
            cli.Subscribed();

            Publish("New Client Connected: " + cli.GetHashCode());

        }

        [OperationContract(IsOneWay = true)]
        public void Publish(string message)
        {
            SynchronizedCollection<IChatNotification> toRemove = new SynchronizedCollection<IChatNotification>();

            foreach (IChatNotification channel in this.clients)
            {
                try
                {
                    channel.Notify(message);
                }
                catch
                {
                    toRemove.Add(channel);
                }
            }

            // now remove all the dead channels
            foreach (IChatNotification chnl in toRemove)
            {
                this.clients.Remove(chnl);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

客户端代码如下:

void client_NotifyReceived(object sender, ChatServiceProxy.NotifyReceivedEventArgs e)
{
    this.Messages.Text += string.Format("{0}\n\n", e.Error != null ? e.Error.ToString() : e.message);
}

private void MyMessage_KeyDown(object sender, KeyEventArgs e)
{
    if (e.Key == Key.Enter)
    {
        this.client.PublishAsync(this.MyMessage.Text);
        this.MyMessage.Text = "";
    }
}

private void Button_Click(object sender, RoutedEventArgs e)
{
    this.client = new ChatServiceProxy.ChatServiceClient(new PollingDuplexHttpBinding { DuplexMode = PollingDuplexMode.MultipleMessagesPerPoll }, new EndpointAddress("../ChatService.svc"));

    // listen for server events
    this.client.NotifyReceived += new EventHandler<ChatServiceProxy.NotifyReceivedEventArgs>(client_NotifyReceived);

    this.client.SubscribedReceived += new EventHandler<System.ComponentModel.AsyncCompletedEventArgs>(client_SubscribedReceived);

    // subscribe for the server events
    this.client.SubscribeAsync();

}

void client_SubscribedReceived(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
{
    try
    {
        Messages.Text += "Connected!\n\n";
        gsConnect.Color = Colors.Green;
    }
    catch
    {
        Messages.Text += "Failed to Connect!\n\n";

    }
}
Run Code Online (Sandbox Code Playgroud)

并且web配置如下:

  <system.serviceModel>
    <extensions>
      <bindingExtensions>
        <add name="pollingDuplex" type="System.ServiceModel.Configuration.PollingDuplexHttpBindingCollectionElement, System.ServiceModel.PollingDuplex, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
      </bindingExtensions>
    </extensions>
    <behaviors>
      <serviceBehaviors>
        <behavior name="">
          <serviceMetadata httpGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="false"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <bindings>
      <pollingDuplex>        
        <binding name="myPollingDuplex" duplexMode="MultipleMessagesPerPoll"/>
      </pollingDuplex>
    </bindings>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true"/>
    <services>
      <service name="ChatDemo.Web.ChatService">
        <endpoint address="" binding="pollingDuplex" bindingConfiguration="myPollingDuplex" contract="ChatDemo.Web.ChatService"/>
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
      </service>
    </services>
  </system.serviceModel>
Run Code Online (Sandbox Code Playgroud)

Kob*_*ari 2

好吧,我终于找到了解决办法。它是一个肮脏的补丁,但它有效且稳定,所以这就是我将使用的。

首先,我想澄清一下情况本身。我以为这是一个僵局,但事实并非如此。实际上,这是两个不同问题的组合,让我认为客户端都在等待,而服务器却卡在了某些东西上。服务器并没有卡住,它只是处于一个非常漫长的过程中。问题是,IE 客户端有自己的问题,这使得它看起来像是永远等待。

我最终设法隔离了这两个问题,然后为每个问题提供了自己的解决方案。

问题 1:服务器在尝试向已断开连接的客户端发送通知时长时间挂起。

由于这是在循环中完成的,其他客户端也必须等待:

 foreach (IChatNotification channel in this.clients)
            {
                try
                {
                    channel.Notify(message); // if this channel is dead, the next iteration will be delayed
                }
                catch
                {
                    toRemove.Add(channel);
                }
            }
Run Code Online (Sandbox Code Playgroud)

因此,为了解决这个问题,我让循环为每个客户端启动一个不同的线程,这样给客户端的通知就变得独立了。这是最终的代码:

[OperationContract(IsOneWay = true)]
public void Publish(string message)
{
    lock (this.clients)
    {
        foreach (IChatNotification channel in this.clients)
        {
            Thread t = new Thread(new ParameterizedThreadStart(this.notifyClient));
            t.Start(new Notification{ Client = channel, Message = message });
        }
    }

}

public void notifyClient(Object n)
{
    Notification notif = (Notification)n;
    try
    {
        notif.Client.Notify(notif.Message);
    }
    catch
    {
        lock (this.clients)
        {
            this.clients.Remove(notif.Client);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,有一个线程来处理每个客户端通知。如果客户端未能发送通知,该线程还会丢弃该客户端。

问题 2:客户端在 10 秒空闲后终止连接。

令人惊讶的是,这个问题只发生在资源管理器中......我无法真正解释它,但在谷歌做了一些研究后,我发现我不是唯一注意到它的人,但除了明显的解决方案之外找不到任何干净的解决方案- “只需每 9 秒 ping 一次服务器”。这正是我所做的。

因此,我扩展了合约接口以包含服务器 Ping 方法,该方法立即调用客户端的 Pong 方法:

[OperationContract(IsOneWay = true)]
public void Ping()
{
    IChatNotification cli = OperationContext.Current.GetCallbackChannel<IChatNotification>();
    cli.Pong();
}
Run Code Online (Sandbox Code Playgroud)

客户端的 Pong 事件处理程序创建一个休眠 9 秒的线程,然后再次调用 ping 方法:

void client_PongReceived(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
{
    // create a thread that will send ping in 9 seconds
    Thread t = new Thread(new ThreadStart(this.sendPing));
    t.Start();
}

void sendPing()
{
    Thread.Sleep(9000);
    this.client.PingAsync();
}
Run Code Online (Sandbox Code Playgroud)

就是这样。我用多个客户端测试了它,通过关闭浏览器删除了一些客户端,然后重新启动它们,一切都有效。而丢失的客户端最终被服务器清理掉。

还有一点需要注意 - 由于客户端连接被证明是不可靠的,我用 try - catch 异常包围它,这样我就可以响应连接自发终止的情况:

        try
        {
            this.client.PublishAsync(this.MyMessage.Text);
            this.MyMessage.Text = "";
        }
        catch
        {
            this.Messages.Text += "Was disconnected!";
            this.client = null;
        }
Run Code Online (Sandbox Code Playgroud)

当然,这没有帮助,因为“PublishAsync”立即成功返回,而自动生成的代码(在 Reference.cs 中)则在另一个线程中执行将消息发送到服务器的实际工作。我能想到的捕获此异常的唯一方法是更新自动生成的代理...这是一个非常糟糕的主意...但我找不到任何其他方法。(想法将不胜感激)。

就这样。如果有人知道解决此问题的更简单方法,我将非常高兴听到。

干杯,

科比