Собственно, про long polling я узнал от своего коллеги Ильи Кубаря. Long polling весьма простое в реализации решение для осуществления реалтайм обновлений на клиентской стороне, например, в чатах или например в текстовых трансляциях футбольных матчей.
Суть метода состоит в следующем:
-
с клиента посылается на сервер AJAX-запрос;
-
на серверной части если данные обновились, то отправляется ответ, если нет – ответ не отправляется до тех пор пока нет обновлений;
- на клиенте и на сервере выставляется таймаут.
Long polling по сравнению с polling’ом имеет значительные плюсы:
- данные действительно обновляются моментально
- уменьшается кол-во запросов с клиента к серверу (не страшно, что они дольше выполняются)
Итак, рассмотрим в качестве простейшего примера прототип текстовой трансляции совместив на одной странице список сообщений и их добавление (сделаем прототип страницы управления так сказать), а также дополнительно посмотрим как сделать wcf-сервис так сказать RESTful-сервисом.
Создадим WCF Service Application и переименуем дефолтный интерфейс к ILongPollingService со следующим содержанием:
[ServiceContract(SessionMode = SessionMode.NotAllowed)] // сессия нам не нужна
public interface ILongPollingService
{
[OperationContract]
[WebGet(ResponseFormat = WebMessageFormat.Json)] //метод доступен при обращении по http
List<Message> PushData(string msg); //новое сообщение от ведущего трансляции
[OperationContract]
[WebGet(ResponseFormat = WebMessageFormat.Json)] //метод доступен при обращении по http
List<Message> GetData(string ID); // ID - это идентификатор последнего сообщения полученного клиентом
}
Далее опишем класс реализующий интерфейс ILongPollingService:
[AspNetCompatibilityRequirements
(RequirementsMode
= AspNetCompatibilityRequirementsMode
.Allowed)] //нужно дя поддержки REST'а
[ServiceBehavior
(ConcurrencyMode
= ConcurrencyMode
.Single, InstanceContextMode
= InstanceContextMode
.PerCall)]
public class Service
: ILongPollingService
{
static List
<Message
> list
= new List
<Message
>(); //здесь храним сообщения трансляции
DateTime end
; // время когда нужно выдать ответ
public List
<Message
> PushData
(string msg
) //метод добавляющий сообщения
{
string id
= Guid
.NewGuid().ToString(); //создаём идентификатор
list
.Add(new Message
{ ID
= id, MessageBody
= msg
}); // добавляем сообщение в список
return list
;
}
public List
<Message
> GetData
(string ID
) //метод, возвращающий текст трансляции
{
if (list
.Count > 0)
if (list
[list
.Count - 1].ID == ID
) //если у клиента то же сообщение, что и последнее в списке добавленных, ждём
{
end
= DateTime
.Now.AddMinutes(2); //через две минуты отдаём то, что есть, чтобы если что, очередь запросов двигалась
Wait
(ID
); // ждём
}
return list
;
}
private void Wait
(string ID
)
{
while (list
[list
.Count - 1].ID == ID
&& DateTime
.Now < end
)
{
System.Threading.Thread.Sleep(1);
};
}
}
Принцип я думаю понятен: на каждый запрос создаётся отдельный экземпляр сервиса обрабатывающий запрос и отдающий данные. Список сообщений доступен всем экземплярам поэтому все могут отследить изменения.
Ну, и напоследок, класс сообщений выглядит так:
[DataContract]
public class Message
{
[DataMember]
public string ID
{
get;
set;
}
[DataMember]
public string MessageBody
{
get;
set;
}
}
Для поддержки REST нужно внести кое-какие настройки в кофиг, например:
<system.serviceModel>
<services>
<service name="LongPollingService.Service" behaviorConfiguration="LongPollingService.ServiceBehavior">
<endpoint address="/json" binding="webHttpBinding" contract="LongPollingService.ILongPollingService" behaviorConfiguration="LongPollingServiceJsonBehavior"/>
</service>
</services>
<bindings>
</bindings>
<behaviors>
<endpointBehaviors>
<behavior name="LongPollingServiceJsonBehavior">
<webHttp/>
</behavior>
</endpointBehaviors>
<serviceBehaviors>
<behavior name="LongPollingService.ServiceBehavior">
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
</system.serviceModel>
И в разметку LongPollingService.svc добавить Factory:
<%@ ServiceHost Language="C#" Debug="false" Service="LongPollingService.Service" CodeBehind="LongPollingService.svc.cs" Factory="System.ServiceModel.Activation.WebScriptServiceHostFactory" %>
Этих простых шагов достаточно для добавления поддержки REST.
Ну. а код страницы базируется на паре js-функций и очень прост:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="LongPollingService.Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<script src="jquery-1.6.4.min.js" type="text/javascript"></script>
<script type="text/javascript">
var id = '';
$(document).ready(function () {
getmsgs();
});
function getmsgs() {
$.ajax({
async: true,
url: '/LongPollingService.svc/json/GetData',
data: 'ID=' + id,
type: 'GET',
dataType: 'json',
timeout: 130000, //ставим времени больше двух минут, а то будут Aborted-ответы, а нам нужны 200
success: function (data) {
var str = '';
for (var index = 0; index < data.length; index++) {
str += data[index].MessageBody + '<br/>';
id = data[index].ID;
}
$("#msgs").html(str);
},
complete: getmsgs
});
}
function send() {
$.ajax({
async: true,
url: '/LongPollingService.svc/json/PushData',
data: 'msg=' + $("#txt").val(),
type: 'GET',
dataType: 'json',
success: function (data) {
var str = '';
for (var index = 0; index < data.length; index++) {
str += data[index].MessageBody + '<br/>';
id = data[index].ID;
}
$("#msgs").html(str);
}
});
}
</script>
</head>
<body>
<form id="form1" runat="server">
<div>
<div id="msgs">
</div>
<input type="text" id="txt" />
<input type="button" value="send" onclick="send(); return false;" />
</div>
</form>
</body>
</html>
CodeBehind абсолютно пуст, поэтому его не привожу.
Исходный код примера можно скачать.
P.S.: возможно в WCF есть встроенная поддержка long polling, но замарачиваться на этот счёт я не стал.