Тупик при использовании дуплексного опроса WCF с Silverlight

Я следил за демонстрацией Томека Янчука на Silverlight TV, чтобы создать программу чата, которая использует веб-сервис WCF Duplex Polling. Клиент подписывается на сервер, а затем сервер отправляет уведомления всем подключенным клиентам для публикации событий.

Идея проста, на клиенте есть кнопка, которая позволяет клиенту подключиться. Текстовое поле, в котором клиент может написать сообщение и опубликовать его, а также текстовое поле большего размера, в котором представлены все уведомления, полученные с сервера.

Я подключил 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 ответа

Решение

ОК, я наконец нашел решение. Это своего рода грязное исправление, но оно работает и стабильно, так что это то, что я буду использовать.

Во-первых, я хочу прояснить саму ситуацию. Я думал, что это тупик, но это не так. Это было на самом деле сочетание двух разных проблем, которые заставили меня думать, что все клиенты ждут, пока сервер застрянет на чем-то. Сервер не застрял, он был просто в середине очень длительного процесса. Дело в том, что у клиента 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 секунд простоя.

Эта проблема, на удивление, произошла только в обозревателе... Я не могу ее объяснить, но, проведя некоторое исследование в Google, я обнаружил, что я не единственный, кто это заметил, но не смог найти ни одного чистого решения, кроме очевидного - "просто пингуйте сервер каждые 9 секунд". Именно это я и сделал.

Поэтому я расширил интерфейс контракта, включив в него метод 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) выполняет фактическую работу по отправке сообщения на сервер в другом потоке. Единственный способ поймать это исключение - обновить автоматически сгенерированный прокси... что очень плохая идея... но я не смог найти другого пути. (Идеи будут оценены).

Это все. Если кто-нибудь знает более простой способ обойти эту проблему, я буду более чем счастлив услышать.

Ура,

Коби

Попробуйте установить inactivityTimeout. Была такая же проблема раньше. Получилось для меня. pollingDuplex inactivityTimeout ="02:00:00" serverPollTimeout = "00:05:00" maxPendingMessagesPerSession = "2147483647" maxPendingSessions = "2147483647" duplexMode = "SingleMessagePerPoll"

Лучший способ решить проблему № 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);
Другие вопросы по тегам