5

我按照 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>
4

3 回答 3

2

尝试设置 inactivityTimeout。以前也有同样的问题。为我解决了。pollingDuplex inactivityTimeout = "02:00:00" serverPollTimeout="00:05:00" maxPendingMessagesPerSession="2147483647" maxPendingSessions="2147483647" duplexMode="SingleMessagePerPoll"

于 2011-01-10T16:45:37.277 回答
2

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

首先,我想澄清情况本身。我以为这是一个僵局,但事实并非如此。这实际上是 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);
                }
            }

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

[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 中)在另一个线程中执行将消息发送到服务器的实际工作。我能想到的捕捉这个异常的唯一方法是更新自动生成的代理......这是一个非常糟糕的主意......但我找不到任何其他方法。(想法将不胜感激)。

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

干杯,

科比

于 2011-01-10T16:48:33.460 回答
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);
于 2014-02-15T11:42:06.537 回答