.NET

индекс
121,03

Comet для ASP.NET своими руками

Не так давно в рамках разработки крупного ASP.NET проекта возникла следующая подзадача: реализовать визуальное отображение табличных данных, обновляющихся в режиме real-time. Схема обновления довольно проста, а именно: на сервер посредством QueryString присылаются данные, которые должны как можно быстрее заменить собой устаревшие данные на странице, причем без необходимости страницу эту рефрешить. Первым решением, которое сразу же пришло мне в голову, было использовать ставшую уже общепринятой технику AJAX-запросов по таймеру, скажем, каждые 5 секунд. Однако сразу же выявились очевидные недостатки применения такого подхода: во-первых, довольно внушительное число потенциальных клиентов, дергающих сервер каждые 5 секунд с созданием каждый раз нового соединения, а во-вторых, это все-таки довольно грубая эмуляция real-time'а, ведь данные на сервер гипотетически могут приходить даже по несоколько раз в секунду (а могут не приходить и по несколько минут, и это уже больше относится к «во-первых»).

Идея решения пришла довольно неожиданно от коллеги по работе, поделившегося линком на статью на Хабре, описывающую реализацию технологии Comet на Perl в целях создания веб-чата. "Comet — это то что нужно!", — подумали мы, и я начал разбираться, как же эту штуку можно прикрутить к ASP.NET. О чем, собственно, и пойдет речь под катом.



Прежде всего, разберемся, что же такое Comet. Вот что говорит нам по этому поводу Википедия:

Comet (в веб-разработке) — неологизм, описывающий модель работы веб-приложения, при которой постоянное HTTP-соединение позволяет веб-серверу отправлять (push) данные браузеру, без дополнительного запроса со стороны браузера. Comet — это гипероним, используемый для обозначения множества техник, позволяющих достичь такого взаимодействия. Общее у этих методов то, что они основаны на технологиях, непосредственно поддерживаемых браузером, таких как Javascript, а не на проприетарных плагинах. Теоретически подход Comet отличается от изначальной концепции всемирной паутины, при которой браузер запрашивает страницу полностью или частично для того, чтобы обновить страницу. Однако на практике приложения Comet обычно используют Ajax c long polling для проверки наличия новой информации на сервере.

Итак, ключевые слова, которые мы можем вынести для себя из этого определения, — это «Ajax с long polling». Что же это такое, и с чем его едят? При использовании технологии «long polling» клиент посылает на сервер запрос и… ждет. Ждет, когда на сервере появятся новые данные. Как только данные пришли, сервер отправляет их клиенту, после чего оный посылает новый запрос и снова ждет. Альтернативная технология «бесконечного запроса», реализуемая, например, через т.н. «forever iframe» (немного подробнее можно почитать здесь), далеко не всегда применима, т.к. такую штуку, как таймаут пока никто не отменял.

Что ж, задача предельно ясна — нужно реализовать подручными средствами (AJAX + ASP.NET) вышеупомянутый long polling. Отсюда вытекает и первая проблема: как законсервировать входящие реквесты и не выдавать респонсы, пока на сервере нет свежих данных, которые можно было бы отдать клиентам (а клиент у нас, очевидно, не один). И здесь на помощь нам приходит асинхронный HTTP Handler.

public interface IHttpAsyncHandler : IHttpHandler
{
 IAsyncResult BeginProcessRequest(HttpContext ctx,
                  AsyncCallback cb,
                  object obj);
 void EndProcessRequest(IAsyncResult ar);
}


* This source code was highlighted with Source Code Highlighter.


Заметьте, что наш класс мы будем наследовать не от интерфейса IHttpHandler, а от IHttpAsyncHandler, который привнесет в нашу реализацию наряду со знакомым методом ProcessRequest два новых: BeginProcessRequest и EndProcessRequest. Интересовать нас, в частности, будет первый из них, т.к. иммено при начале обработки реквеста нам этот реквест нужно схватить за руку и не отпускать до тех пор, пока не придет час икс. Как видим, BeginProcessRequest возвращает объект, реализующий интерфейс IAsyncResult.

public interface IAsyncResult
{
 public object   AsyncState            { get; }
 public bool    CompletedSynchronously { get; }
 public bool    IsCompleted            { get; }
 public WaitHandle AsyncWaitHandle     { get; }
}


* This source code was highlighted with Source Code Highlighter.


Создадим новый класс, который будет реализовывать указанный интерфейс, а также послужит хранилищем для передаваемых в BeginProcessRequest данных запроса и нашего собственного параметра clientGuid, который мы будем использовать в дальнейшем в качестве уникального идентификатора приконнектившегося к серверу клиента, дабы как-то идентифицировать его запросы.

public class CometAsyncRequestState : IAsyncResult
{
  private HttpContext _currentContext;
  private AsyncCallback _asyncCallback;
  private Object _extraData;

  private Boolean _isCompleted;
  private Guid _clientGuid;
  private ManualResetEvent _callCompleteEvent = null;

  public CometAsyncRequestState(HttpContext currentContext, AsyncCallback asyncCallback, Object extraData)
  {
    _currentContext = currentContext;
    _asyncCallback = asyncCallback;
    _extraData = extraData;

    _isCompleted = false;
  }

  public void CompleteRequest()
  {
    _isCompleted = true;

    lock (this)
    {
      if (_callCompleteEvent != null)
        _callCompleteEvent.Set();
    }

    if (_asyncCallback != null)
    {
      _asyncCallback(this);
    }
  }

  public HttpContext CurrentContext
  {
    get
    {
      return _currentContext;
    }
    set
    {
      _currentContext = value;
    }
  }

  public AsyncCallback AsyncCallback
  {
    get
    {
      return _asyncCallback;
    }
    set
    {
      _asyncCallback = value;
    }
  }

  public Object ExtraData
  {
    get
    {
      return _extraData;
    }
    set
    {
      _extraData = value;
    }
  }

  public Guid ClientGuid
  {
    get
    {
      return _clientGuid;
    }
    set
    {
      _clientGuid = value;
    }
  }

  // IAsyncResult implementations
  public Boolean CompletedSynchronously
  {
    get
    {
      return false;
    }
  }

  public Boolean IsCompleted
  {
    get
    {
      return _isCompleted;
    }
  }

  public Object AsyncState
  {
    get
    {
      return _extraData;
    }
  }

  public WaitHandle AsyncWaitHandle
  {
    get
    {
      lock (this)
      {
        if (_callCompleteEvent == null)
          _callCompleteEvent = new ManualResetEvent(false);

        return _callCompleteEvent;
      }
    }
  }
}


* This source code was highlighted with Source Code Highlighter.


Как видим, пока мы сами не вызовем функцию CompleteRequest, запрос не будет считаться завершенным. Замечательно — то, что нам и надо. Осталось только где-то эти входящие реквесты хранить. Для этой функции, а также для функции обработки запросов создадим статический класс CometClientProcessor:

public static class CometClientProcessor
{
  private static Object _lockObj;
  private static List<CometAsyncRequestState> _clientStateList;

  static CometClientProcessor()
  {
    _lockObj = new Object();
    _clientStateList = new List<CometAsyncRequestState>();
  }

  public static void PushData(String pushedData)
  {
    List<CometAsyncRequestState> currentStateList = new List<CometAsyncRequestState>();

    lock (_lockObj)
    {
      foreach (CometAsyncRequestState clientState in _clientStateList)
      {
        currentStateList.Add(clientState);
      }
    }

    foreach (CometAsyncRequestState clientState in currentStateList)
    {
      if (clientState.CurrentContext.Session != null)
      {
        clientState.CurrentContext.Response.Write(pushedData);
        clientState.CompleteRequest();
      }
    }
  }

  public static void AddClient(CometAsyncRequestState state)
  {
    Guid newGuid;

    lock (_lockObj)
    {
      while (true)
      {
        newGuid = Guid.NewGuid();
        if (_clientStateList.Find(s => s.ClientGuid == newGuid) == null)
        {
          state.ClientGuid = newGuid;
          break;
        }
      }

      _clientStateList.Add(state);
    }
  }

  public static void UpdateClient(CometAsyncRequestState state, String clientGuidKey)
  {
    Guid clientGuid = new Guid(clientGuidKey);

    lock (_lockObj)
    {
      CometAsyncRequestState foundState = _clientStateList.Find(s => s.ClientGuid == clientGuid);

      if (foundState != null)
      {
        foundState.CurrentContext = state.CurrentContext;
        foundState.ExtraData = state.ExtraData;
        foundState.AsyncCallback = state.AsyncCallback;
      }
    }
  }

  public static void RemoveClient(CometAsyncRequestState state)
  {
    lock (_lockObj)
    {
      _clientStateList.Remove(state);
    }
  }
}


* This source code was highlighted with Source Code Highlighter.


CometClientProcessor содержит в себе список удерживаемых на данный момент реквестов, функции AddClient для добавления реквестов (при коннекте нового клиента), UpdateClient для обновления реквестов (когда уже приконнектившийся клиент послает новый запрос) и RemoveClient для удаления реквестов (когда клиент дисконнектится), а также основной метод PushData. «Пушить» для наглядности мы будем простейшие данные, а именно строку, которая приходит на сервер через параметр в URL. Как видим, все предельно просто — пробегаем по текущим удерживаемым реквестам, записываем в респонс данные, пришедшие с сервера, и вызываем функцию CompleteRequest, которая освобождает запрос, и отправляет клиенту ответ. Вызов PushData осуществляется в данном примере из функции Page_Load нашей единственной страницы:

protected void Page_Load(object sender, EventArgs e)
  {
    if (!IsPostBack)
    {
      if (Request.QueryString["x"] != null)
      {
        CometClientProcessor.PushData(Request.QueryString["x"].ToString());
      }
    }
  }


* This source code was highlighted with Source Code Highlighter.


Как уже говорилось выше, данные к нам приходят через параметр в URL, в данном случае для наглядности он носит имя «x». В серверной части осталось только реализовать, собственно, сам асинхронный хэндлер. Но сперва обратимся к части клиентской и напишем (не без помощи библиотеки jQuery) несколько довольно банальных Javascript-овых функций:

var clientGuid

$(document).ready(function() {
  var str = window.location.href;
  if (str.indexOf("?") < 0)
    Connect();
});

$(window).unload(function() {
  var str = window.location.href;
  if (str.indexOf("?") < 0)
    Disconnect();
});

function SendRequest() {
  var url = './CometAsyncHandler.ashx?cid=' + clientGuid;
  $.ajax({
    type: "POST",
    url: url,
    success: ProcessResponse,
    error: SendRequest
  });
}

function Connect() {
  var url = './CometAsyncHandler.ashx?cpsp=CONNECT';
  $.ajax({
    type: "POST",
    url: url,
    success: OnConnected,
    error: ConnectionRefused
  });
}

function Disconnect() {
  var url = './CometAsyncHandler.ashx?cpsp=DISCONNECT';
  $.ajax({
    type: "POST",
    url: url
  });
}

function ProcessResponse(transport) {
  $("#contentWrapper").html(transport);
  SendRequest();
}

function OnConnected(transport) {
  clientGuid = transport;
  SendRequest();
}

function ConnectionRefused() {
  $("#contentWrapper").html("Unable to connect to Comet server. Reconnecting in 5 seconds...");
  setTimeout(Connect(), 5000);
}


* This source code was highlighted with Source Code Highlighter.


Как только документ загружается, мы проверяем URL на наличие параметров в нем (параметризованный URL, еще раз позволю себе напомнить — это передача данных серверу для «пушинга») и вызываем функцию Connect. Та, в свою очередь, уже начинает общаться с нашим хэндлером. Служебные слова, определяющие действие (CONNECT/DISCONNECT), как видим, для простоты, передаются через параметр cpsp. Соответственно, Connect должен инициировать на сервере вызов AddClient, а Disconnect — RemoveClient. Когда соединение установлено и клиент получил свой clientGuid, вызывается функция SendRequest, которая и будет «лонгполлить» сервер, пока клиент не решит от него отсоединиться. Каждый вызов SendRequest будет инициировать выполнение на сервере функции UpdateClient, которая для данного клиента обновит контекст и точку возврата (callback).

Что-ж, практически все готово, настало время для реализации ядра всего вышепредставленного механизма — асинхронного хэндлера.

public enum ConnectionCommand
{
  CONNECT,
  DISCONNECT
}

public static class ConnectionProtocol
{
  public static String PROTOCOL_GET_PARAMETER_NAME = "cpsp";
  public static String CLIENT_GUID_PARAMETER_NAME = "cid";
}


* This source code was highlighted with Source Code Highlighter.


<%@ WebHandler Language="C#" Class="CometAsyncHandler" %>

using System;
using System.Web;

using DevelopMentor;

public class CometAsyncHandler : IHttpAsyncHandler, System.Web.SessionState.IRequiresSessionState
{
  static private ThreadPool _threadPool;

  static CometAsyncHandler()
  {
    _threadPool = new ThreadPool(2, 50, "Comet Pool");
    _threadPool.PropogateCallContext = true;
    _threadPool.PropogateThreadPrincipal = true;
    _threadPool.PropogateHttpContext = true;
    _threadPool.Start();
  }

  public IAsyncResult BeginProcessRequest(HttpContext ctx, AsyncCallback cb, Object obj)
  {
    CometAsyncRequestState currentAsyncRequestState = new CometAsyncRequestState(ctx, cb, obj);
    _threadPool.PostRequest(new WorkRequestDelegate(ProcessServiceRequest), currentAsyncRequestState);

    return currentAsyncRequestState;
  }

  private void ProcessServiceRequest(Object state, DateTime requestTime)
  {
    CometAsyncRequestState currentAsyncRequestState = state as CometAsyncRequestState;

    if (currentAsyncRequestState.CurrentContext.Request.QueryString[ConnectionProtocol.PROTOCOL_GET_PARAMETER_NAME] ==
      ConnectionCommand.CONNECT.ToString())
    {
      CometClientProcessor.AddClient(currentAsyncRequestState);
      currentAsyncRequestState.CurrentContext.Response.Write(currentAsyncRequestState.ClientGuid.ToString());
      currentAsyncRequestState.CompleteRequest();
    }
    else if (currentAsyncRequestState.CurrentContext.Request.QueryString[ConnectionProtocol.PROTOCOL_GET_PARAMETER_NAME] ==
      ConnectionCommand.DISCONNECT.ToString())
    {
      CometClientProcessor.RemoveClient(currentAsyncRequestState);
      currentAsyncRequestState.CompleteRequest();
    }
    else
    {
      if (currentAsyncRequestState.CurrentContext.Request.QueryString[ConnectionProtocol.CLIENT_GUID_PARAMETER_NAME] != null)
      {
        CometClientProcessor.UpdateClient(currentAsyncRequestState,
          currentAsyncRequestState.CurrentContext.Request.QueryString[ConnectionProtocol.CLIENT_GUID_PARAMETER_NAME].ToString());
      }
    }
  }

  public void EndProcessRequest(IAsyncResult ar)
  {
  }

  public void ProcessRequest(HttpContext context)
  {
  }

  public bool IsReusable
  {
    get
    {
      return true;
    }
  }
}

* This source code was highlighted with Source Code Highlighter.

После всего вышесказанного, единственный вопрос, который может возникнуть у внимательного читателя — «зачем использовать кастом тредпул»? Ответ довольно прост, хоть и не совсем очевиден: чтобы как можно быстрее «отпустить» рабочий поток тредпула ASP.NET, дабы он мог продолжить обрабатывать входящие реквесты, а непосредственную обработку запроса передать «внутреннему» потоку. Если этого не сделать, то при достаточно большом количестве входящих реквестов, может произойти банальный «затык» по довольно смешной на первый взгляд причине: «у ASP.NET кончились рабочие потоки». По этой же причине не получится использовать ни асинхронный делегат, возбуждаемый методом BeginInvoke, ни метод стандартного тредпула ThreadPool.QueueUserWorkItem, т.к. в обоих этих случая поток будет изыматься из того же тредпула ASP.NET, что приводит нас к ситуации «шило на мыло». В данном примере используется кастом тредпул, реализованный Майком Вудрингом (Mike Woodring); эту и многие другие из его наработок можно посмотреть здесь.

Вот, в общем-то и все. Не так уж и сложно, как казалось вначале. Клиенты коннектятся к нашему Comet-серверу посредством вызова Default.aspx, а данные мы пушим передавая той же странице GET-параметр аля Default.aspx?x=Happy_New_Year. К сожелению провести массирование тестирование масштабируемости такого подхода пока не было возможности, однако если у кого-то есть идеи по этому поводу — пишите, не стесняйтесь.

Спасибо за внимание.

UPD Добавляю ссылку на архив с сэмпловым проектом (~30 KB). Как посмотреть: в VS ставим стартовой страницей CometPage.aspx, запускаем, открываем в браузере/браузерах несколько вкладок с тем же URL (только помним про ограничение в оных браузерах количества одновременных подключений), затем в одном из табов к URL дописываем параметр ?x=[любой_текст] и наблюдаем, как значение параметра появится во всех открытых вкладках.
+37
26 января 2010, 10:10
75

комментарии (40)

0
atd #
Спасибо за ссылку на threadpool. Сейчас то же самое используем, и пока что боролись с ограничением на потоки с помощью разных «подпорок».
0
Cromathaar #
Рад, что чем-то смог Вам помочь.
0
plsc_Rover #
А чем не подошли WCF сервисы? msdn.microsoft.com/en-us/library/bb398998.aspx
+5
Cromathaar #
Понимаете, суть технологии Comet заключается в передаче данных клиентам по инициативе сервера, а не самих клиентов. В идеале это постоянное соединение между клиентом и сервером, и именно сервер, когда считает нужным (по возникновению какого-то серверного события, например), выдает клиенту данные. В том же примере из MSDN, который вы привели, отнюдь не сервер решает, когда данные должны быть отправлены клиенту, это решает клиент путем асинхронного вызова метода веб-сервиса. Таким образом, чтобы обеспечить обновление данных на клиенте в режиме близком к режиму реального времени, вам придется постоянно вызывать данный метод (в частности, по таймеру), проверяя, есть ли серверу чем поделиться. Недостатки такого подхода уже описаны в первом абзаце статьи.
+2
Akos #
Возможно, имелись ввиду дуплексные сервисы, но там нет поддержки JS-клиентов, только Silverlight.
+1
vktechno #
В WCF есть поддержка дуплексных биндингов (через http (wsDualHttpBinding), tcp (netTcpBinding и netPeerTcpBinding) и именованные каналы), с помощью которых сервер может делать запросы к клиенту. Другой вопрос, есть в каком-нибудь js-фреймворке реализация требуемой для этого клиентской части.
+3
Zhendalf #
Спасибо большое, хороший материал. Вот только разобраться с сорсом было бы гораздо проще, нежели читать здесь.
Если у вас остался этот проектик, не могли бы выложить архив?
+3
Cromathaar #
Добавил в конец статьи ссылку на архив с исходниками и кратенький мануал.
0
el777 #
Скажите, это у вас в интранете или ориентировано на широкий круг пользователей?
0
Cromathaar #
Ориентировано на широкий (в перспективе даже очень широкий) круг пользователей, однако, как я уже писал, масштабный стресс-тест такого подхода провести пока не было возможности (честно говоря, мы пока не нашли подходящий способ, поэтому я в конце статьи просил поделиться наработками, если кто-то сталкивался с подобным). Лично я проверял на 25 клиентах (и сервер, и клиенты были на одной машине) при пушинге частотой до десятка раз в секунду, при этом нагрузка на CPU была настолько мизерной, что стандартным Task Manager'ом ее даже невозможно было разглядеть.
0
hansi #
Спасибо за полезную и интересную статью, а также за ссылку на сайт Майка Вудринга. Есть чему поучиться)
+1
vikingkom #
Спасибо, за статью!
Некоторое время назад я проделал подобную работу (задача, правда, немного отличалась) и столкнулся с проблемой, что, не получается определить подсоединен ли еще клиент или уже закрыл страницу.
Может оказаться, что вы обслуживаете уже давно «мертвых» клиентов.

В моем случае, основное отличие было в том, что обновления приходили очень нерегулярно. В течении одной минуты их могло быть сотня, а потом в течении часа затишье.
0
Cromathaar #
Использование события unload для определения дисконнекта клиента является, наверное, самым очевидным решением для решения данной проблемы. Минус, правда, тоже очевиден — реконнект клиента при постбэках. Интересно узнать, как Вы решили эту проблему?
0
vikingkom #
Эта идея хорошо сработает для переходов на другие страницы, либо для правильного закрытия окна,
но совершенно не работает при разрыве связи или других аварийных ситуациях.

Как вариант, можно периодически писать что-то (проблемы, например) в поток OutputStream. По идее, если клиент отключился, то будет ошибка и св-во IsConnected обновится.
Я не проверял этот метод, может вам будет интересно проверить.
+1
Zhendalf #
Вы не подскажете, пока клиент ждет, можно ли отправлять другие аякс-запросы? Другими словами — сколько одновременных запросов можно слать?
0
Cromathaar #
Это то, что вы ищите?
0
Zhendalf #
По ссылке инфа только про IE. Нету ничего про другие браузеры.
0
el777 #
Вообще по стандарту рекомендуется открывать не более 2 соединений на домен.
В данном случае получится один гонит данные в одну сторону, другой в другую. Чтобы можно было передавать разные данные, нужно использовать мультиплексирование, например, Bayeux Protocol.
0
dummy #
Спасибо за статью! Интересная тема и написано хорошо. Возможно я невнимательный читатель, но у меня остался один вопрос. В начале вы описали задачу и все было затеяно (как я понял) с целью применть этот Comet for ASP.NET подход в своем проекте. Так вы его применили в конце концов или нет? Какие у вас были результаты? Проблемы?
0
Cromathaar #
Как раз вчера пришла спецификация, сегодня начинаем наращивать на этот Comet-скелет сухожилия и мышцы. Предварительные результаты описаны в этом комментарии. Как дойдет дело до масштабных испытаний и стрессовых нагрузок, отпишу дополнительно.
0
orcy #
Ну думали про использование готовых серверов/клиенских библиотек для организации Comet взаимодействия? Есть ведь много чего, в том числие модули для nginx и вспомогательные библиотеки для клиентской стороны. Еще интересно, как расчитывали сколько потребуется ресурсов на сервер для поддержки постоянно открытых соединений.

Запросы раз в 5 секунд кстати вовсе не факт что будут создавать свое соединение, но это мелочи, для realtime действительно лучше comet или websockets (когда заработают).
0
Cromathaar #
О готовых решениях думал, читал. Однако Comet настолько заинтересовал, что дико захотелось реализовать самому. Кое-что из этого желания, как видим, все-таки вышло :) Что касается ресурсов, какие именно вы имеете в виду?
0
orcy #
Я встерчал например что на 10000 соединений нужно 450Мб памяти, по 45Кб на соединение. По крайней мере с этого началось и двигалось в сторону уменьшения в этой статье: aleccolocco.blogspot.com/2008/10/gazillion-user-comet-server-with.html. В этой статье рассматривался Linux/C++, интересно что покажет .NET.

Мне интересно какой например нужен VPS хостинг чтобы поддерживать Comet соединения.
0
Cromathaar #
Сегодня попробуем померять все это дело.
+1
self #
Скажите, а зачем вы делаете вот это:

newGuid = Guid.NewGuid();
if (_clientStateList.Find(s => s.ClientGuid == newGuid) == null)

Разве свежесгенерированный GUID не является уникальным во времени и пространстве? По крайней на одной и той же машине два последовательных значения GUID точно должны быть уникальны. Т.е. вторая строчка похожа на какую-то дикую перестраховку, т.к. заведомо дает true.
+1
LexL #
Это на всякий случай, вдруг (раз в 100 лет) программа сгенерирует Guid, который уже зарегистрирован в системе…

Как говорится — береженого…
0
self #
По-моему под вашим сервером быстрее вулкан проснется, чем гуид совпадет :)
0
LexL #
Под моим — возможно, но уважаемый ОП считает иначе :)
0
Cromathaar #
Вероятность того, что значение нового Guid будет содержать одни нули или окажется равным какому-либо другому Guid, предельно мала. ©MSDN

Предельно мала, но все-таки не равна нулю.
0
sse #
Глядя на ваш код C#, у меня осталось ощущение, что на вас давит тяжкое наследие Си. Так ли это?
Я думаю, что C#-код можно было бы сократить раза в 2, подняв читабельность
0
Cromathaar #
Скорее это пережитки не так давно покинутой предыдущей конторы с ее тоннами документации по оформлению кода. Если не сложно, укажите на конкретные места и лучшие решения — учиться, как говорится, никогда не поздно.
+1
oleg40a #
и лучшим тому подтверджением будет ссылка на архив с вашим компактным и читаемым вариантом
0
sse #
Договорились. Только вечером, когда домой приду.
0
sse #
Привет, а вот и читаемый вариант:
www.filefront.com/15440909/skeleton.zip

Введения:
— autoproperties
— in-place init
— выкинул locker object — он не нужен, у вас карго-культ имхо.
— прикрутил read-only везде, где можно.

В методических целях и проверка guid «на существование» не нужна.
0
Timmay #
Ссылка умерла, обновите, пожалуйста
0
hyborg #
Вот люди тоже пилят свой aspcomet. Релиза пока нет, и фишки с threadpool тоже нету. Но зато обещают Bayeux.
0
vikingkom #
В ASP.NET есть встроенная возможность для AJAX запросов UpdatePanel.По идее, можно совершенно без проблем прикрутить её к Long Polling Requests и сразу убьем двух зайцев.
При этом практически избавляемся от клиентского кода и используем стандартные механизмы ASP.NET.
При этом, конечно, можно незначительно потерять в быстродействии, если объемы большие, но о такой возможности, как минимум, стоит знать.
0
RomanNikitin #
Упаси вас прогресс вообще пользоваться UpdatePanel.
0
1000tour #
как по прошествии времени проявило себя Ваше решение?
0
Nigrimmist #
Спасибо за статью. При реализации появился странный баг. — при добавлении файла global.asax — второй запрос висел (первый не отпускал сервер), дело оказалось в методе Session_start. Начал копать дальше и выяснил, что не даёт освободить запрос — интерфейс System.Web.SessionState.IRequiresSessionState. Вместо его следует использовать: IReadOnlySessionState. Происходит это потому что для записи сессия доступна только одному потоку, а остальные ждут. В режиме же чтения проблема многопоточности разрешается — читать могут все.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.