Тупик при использовании дуплексного опроса 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);