тупик при использовании дуплексного опроса WCF с Silverlight - PullRequest
5 голосов
/ 10 января 2011

Я следил за демонстрацией Томека Янчука на 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 ]

2 голосов
/ 10 января 2011

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

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

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

Ура,

Коби

2 голосов
/ 10 января 2011

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

1 голос
/ 15 февраля 2014

Лучший способ решить проблему # 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);
...