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

时间:2011-01-10 14:32:25

标签: 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);
            }
        }
    }
}

客户端代码如下:

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";

    }
}

网页配置如下:

  <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>

3 个答案:

答案 0 :(得分:2)

尝试设置inactivityTimeout。之前有同样的问题。为我工作了。 pollingDuplex inactivityTimeout = “02:00:00” serverPollTimeout =“00:05:00”maxPendingMessagesPerSession =“2147483647”maxPendingSessions =“2147483647” duplexMode = “SingleMessagePerPoll”

答案 1 :(得分:2)

好的,我终于找到了解决方案。它是一种肮脏的补丁,但它有效且稳定,所以我会使用它。

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

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

问题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);
                }
            }

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

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

请注意,有一个线程可以处理每个客户端通知。如果客户端无法发送通知,该线程也会丢弃该客户端。

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

令人惊讶的是,这个问题只发生在探险家身上......我无法解释它,但在谷歌做了一些研究之后我发现我不是唯一一个注意到它的人,却找不到任何干净的解决方案除了显而易见的 - “只需每隔9秒ping一次服务器”。这正是我所做的。

所以我扩展了契约接口以包含一个服务器Ping方法,它立即调用客户端的Pong方法:

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

客户端的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();
}

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

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

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

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

这就是全部。如果有人知道解决这个问题的更简单的方法,我将非常乐意听到。

干杯,

了Kobi

答案 2 :(得分:1)

解决问题#1的更好方法是使用异步模式设置回调:

    [OperationContract(IsOneWay = true, AsyncPattern = true)]
    IAsyncResult BeginNotification(string message, AsyncCallback callback, object state);
    void EndNotification(IAsyncResult result);

当服务器通知剩余的客户端时,它会发出上半部分:

    channel.BeginNotification(message, NotificationCompletedAsyncCallback, channel);

这样,剩下的客户端就会收到通知,而不必等待已经下线的客户端超时。

现在将静态完成的方法设置为

    private static void NotificationCompleted(IAsyncResult result)

在这个完成的方法中,调用剩余的一半调用:

    IChatNotification channel = (IChatNotification)(result.AsyncState);
    channel.EndNotification(result);