将 WCF 双工轮询与 Silverlight 结合使用时出现死锁

发布于 2024-10-11 09:30:11 字数 5303 浏览 5 评论 0原文

我按照 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";

    }
}

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>

I have followed Tomek Janczuk's demonstration on silverlight tv to create a chat program that uses WCF Duplex Polling web service. The client subscribes to the server, and then the server initiates notifications to all connected clients to publish events.

The Idea is simple, on the client, there is a button that allows the client to connect. A text box where the client can write a message and publish it, and a bigger text box that presents all the notifications received from the server.

I connected 3 clients (in different browsers - IE, Firefox and Chrome) and it all works nicely. They send messages and receive them smoothly. The problem starts when I close one of the browsers. As soon as one client is out, the other clients get stuck. They stop getting notifications.

I am guessing that the loop in the server that goes through all the clients and sends them the notifications is stuck on the client that is now missing. I tried catching the exception and removing it from the clients list (see code) but it still does not help.

any ideas?

The server code is as follows:

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

The client code is as follows:

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

    }
}

And the web config is as follows:

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

未蓝澄海的烟 2024-10-18 09:30:11

尝试设置inactivityTimeout。以前也有同样的问题。对我来说很有效。
pollingDuplex inactivityTimeout="02:00:00" serverPollTimeout="00:05:00" maxPendingMessagesPerSession="2147483647" maxPendingSessions="2147483647"
duplexMode =“SingleMessagePerPoll”

Try to set inactivityTimeout. Had the same problem before. Worked out for me.
pollingDuplex inactivityTimeout="02:00:00" serverPollTimeout="00:05:00" maxPendingMessagesPerSession="2147483647" maxPendingSessions="2147483647"
duplexMode="SingleMessagePerPoll"

清浅ˋ旧时光 2024-10-18 09:30:11

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

首先,我想澄清一下情况本身。我以为这是一个僵局,但事实并非如此。实际上,这是两个不同问题的组合,让我认为客户端都在等待,而服务器却卡在了某些东西上。服务器并没有卡住,它只是处于一个非常漫长的过程中。问题是,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);
                }
            }

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

[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 秒空闲后终止连接。

令人惊讶的是,这个问题只发生在资源管理器中......我无法真正解释它,但在 Google 做了一些研究后我发现我不是唯一一个注意到这一点的人,但除了明显的“只需每 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 中)在另一个线程中完成将消息发送到服务器的实际工作。我能想到的捕获此异常的唯一方法是更新自动生成的代理...这是一个非常糟糕的主意...但我找不到任何其他方法。 (想法将不胜感激)。

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

干杯,

科比

OK, I finally found a solution. Its kind of a dirty patch, but it works and its stable, so that's what I'll use.

First, I want to clarify the situation itself. I thought this was a deadlock, but it wasn't. It was actually a combination of 2 different problems that made me think that the clients are all waiting while the server is stuck on something. The server was not stuck, it was just in the middle of a very lengthy process. The thing is, that the IE client had a problem of its own, which made it seem like it was waiting forever.

I eventually managed to isolate the 2 problems and then gave each problem its own solution.

Problem number 1: The server hangs for a long time while trying to send a notification to a client that was disconnected.

Since this was done in a loop, other clients had to wait as well:

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

So, to solve this problem, I made the loop start a distinct thread for each client, so the notifications to the clients become independent. Here is the final code:

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

Note that there is one thread to handle each client notification. The thread also discards the client, if it failed to send the notification.

Problem number 2: The client kills the connection after 10 idle seconds.

This problem, surprisingly, only happened in explorer... I can't really explain it, but after doing some research in Google I found that I was not the only one to notice it, but could not find any clean solution except the obvious - "just ping the server every 9 seconds". Which is exactly what I did.

So I extended the contract interface to include a server Ping method, which instantly calls a client's Pong method:

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

the client's Pong event handler creates a thread that sleeps for 9 seconds and then calls the ping method again:

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

And that was it. I tested it with multiple clients, removed some clients by closing their browsers, then re launched them, it all worked. And the lost clients were eventually cleaned by the server.

One more note - Since the client connection proved to be unreliable, I surrounded it with a try - catch exception so I can respond to cases where the connection spontaneously dies:

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

This, of course, does not help, since the "PublishAsync" returns instantly, and successfully, while the code that was automatically generated (in Reference.cs) does the actual work of sending the message to the server, in another thread. The only way I could think of to catch this exception is by updating the automatically generated proxy... which is a very bad idea... but I could not find any other way. (Ideas will be appreciated).

That's all. If anybody knows of an easier way to work around this issue, I will be more than happy to hear.

Cheers,

Kobi

纵情客 2024-10-18 09:30:11

解决问题 #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);

A better way to solve problem #1 is to set up the callback using the async pattern:

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

When the server notifies the remaining clients it issues the first half:

    channel.BeginNotification(message, NotificationCompletedAsyncCallback, channel);

This way the remaining clients get notified without having to wait for the time-out on the client that has dropped off.

Now set up the static completed method as

    private static void NotificationCompleted(IAsyncResult result)

In this completed method call the remaining half of the call like this:

    IChatNotification channel = (IChatNotification)(result.AsyncState);
    channel.EndNotification(result);
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文