Реализация чата на ASP.NET с использованием Long Polling

Вячеслав Гринин, March 13, 2010

Бродя по просторам интернета, на одном из сайтов я увидел простой чат. Просмотрев страницу в FireBug я понял, что никакими апплетами там и не пахнет, а значит чат реализован на простом JavaScript.

Я подключился к нему из нескольких браузеров и удивился, что второй браузер реагирует на сообщения первого практически мгновенно, что напрочь исключало возможность работы этого чата при помощи периодического опроса сервера скриптом на клиентской странице. Ну, или частота этого опроса должна быть настолько высокой, что любой сервер бы просто захлебнулся в этих запросах.

И встал передо мной вопрос – как они это сделали? Как они обеспечили такую высокую скорость отдачи свежих данных? Ведь веб-сервер на то и сервер, что может лишь отвечать на запросы пользователя, но уж никак не самостоятельно инициировать запросы.

Непродолжительный поиск по интернетам дал мне ответ. И ответ этот “Long Polling“, а встречается еще и другое название технологии – Comet.

Итак, как же это работает?

По сути, это все тот же старый добрый AJAX, и использование объекта XmlHttpRequest. С одной большой разницей, заключенной на серверной стороне. Разница состоит вот в чем. Клиент посылает XHR-запрос и длительное время ожидает ответа сервера. А “длительное время” отклика обеспечивает сам сервер, который не сразу отдает данные клиенту, а лишь только тогда, когда у него в очереди появляются свежие данные, то есть когда ему действительно есть что сказать.

Клиент, дождавшись ответа, обрабатывает его (например, отображает сообщение на странице), а затем, без промедления, снова запрашивает сервер. И снова ждет.

Если же подав запрос клиент так и не дождался ответа из-за таймаута, то он снова запрашивает сервер. А откуда этот таймаут? А просто серверу нам нечего ответить, вот он и не отдает клиенту результат запроса. Ждет. Но не дождавшись, разрывает соединение по таймауту. Такая вот несложная схема.

Это и называется Long Polling, то есть – длительный опрос.

Но от слов к делу. Сейчас мы изготовим серверную и клиентскую часть простого интернет-чата, построенного на описанной технологии. Мы подробно рассмотрим работу всего серверного и клиентского кода и даже в конце статьи получим ссылку на вполне работоспособный продукт. Сразу скажу, у него есть некоторые ограничения. Вы, например, не сможете его использовать в режиме сильной нагрузки, то есть наличии одновременно большого количества клиентов. В нем нет схемы очистки “потерянных” соединений. Это лишь работоспособный учебный пример. Все недостающие аспекты кода предлагаю вам на самостоятельную доработку.

Итак… Без особых размышлений приведу здесь листинг кода и объясню принцип его действия.

using System;
using System.Collections.Generic;

// Класс описывающий одно сообщение от клиента и
// метод его сериализации
public class CometMessage
{
 public string UserName;
 public string Message;
 public string Serialize()
 {
 return "{'user': '" + UserName + "', 'message': '" + Message + "'}";
 }
}

// Собственно, серверная часть
public static class CometServer
{
 // вспомогательный объект для блокировки ресурсов
 // многопоточного приложения
 private static Object _lock = new Object();

 // Список, хранящий состояние всех подключенных клиентов
 private static List<CometAsyncState> _clientStateList =
   new List<CometAsyncState>();

 // Возвращаем сообщение каждому подключенному клиенту
 public static void PushMessage(CometMessage message)
 {
 lock (_lock)
 {
 // Пробегаем по списку всех подключенных клиентов
 foreach (CometAsyncState clientState in _clientStateList)
 {
 if (clientState.CurrentContext.Session != null)
 {
 // И пишем в выходной поток текущее сообщение
 clientState.CurrentContext.Response.Write(message.Serialize());
 // После чего завершаем запрос - вот именно после этого результаты
 // запроса пойдут ко всем подключенным клиентам
 clientState.CompleteRequest();
 }
 }
 }
 }

 // Срабатывает кажды раз при запуске клиентом запроса Long poll
 // так как при этом HttpContext клиента изменяется, то надо обновить
 // все изменившиеся данные клиента в списке, идентифицируемом по
 // гуиду, который у клиента в течение работы остается постоянным
 public static void UpdateClient(CometAsyncState state, String guid)
 {
 lock (_lock)
 {
 // ищем клиента в списке по его гуиду
 CometAsyncState clientState = _clientStateList.Find(s => s.ClientGuid
   == guid);
 if (clientState != null)
 {
 // и если он нашелся, то обновляем все его параметры
 clientState.CurrentContext = state.CurrentContext;
 clientState.ExtraData = state.ExtraData;
 clientState.AsyncCallback = state.AsyncCallback;
 }
 }
 }

 // Регистрация клиента
 public static void RegicterClient(CometAsyncState state)
 {
 lock (_lock)
 {
 // Присваиваем гуид и добавляем в список
 state.ClientGuid = Guid.NewGuid().ToString("N");
 _clientStateList.Add(state);
 }
 }

 // Разрегистрация клиента
 public static void UnregisterClient(CometAsyncState state)
 {
 lock (_lock)
 {
 // Просто удаляем его из списка
 _clientStateList.Remove(state);
 }
 }
}

Итак:

6-14 строки – инкапсулируют класс сообщения от пользователя, как видим, там еще есть метод, преобразующий объект в JSON-строку. Вообще, по-хорошему, здесь надо пользоваться нормальным JSON-сериализатором, а не городить строку вручную.

Далее идет описание класса CometServer в котором и живет вся серверная логика LongPolling-технологии.

Заметим, что в переменной _clientStateList (строка 24) хранятся все подключенные на данный момент к серверу клиенты.

Метод PushMessage (строка 24) пробегает по всему списку клиентов и пишет в выходной поток каждого переданное ему в качестве аргумента сообщение, естественно, предварительно сериализовав его. После чего для каждого клиента в обязательном порядке вызывается метод CompleteRequest(), который завершает асинхронный запрос, передавая его результаты клиенту.

Метод UpdateClient (строка 51) по полученному им гуиду клиента, находит клиента в списке clientStateList и обновляет все его параметры, такие как HttpContext-например. Этим мы обеспечиваем всегда актуальное состояние списка клиентов, после каждого их реконнекта.

Метод RegicterClient (строка 69) /опечатка вышла в его имени, сами исправите при необходимости:)/ добавляет нового клиента в очередь.

Метод UnregisterClient (строка 80) соответственно – удаляет клиента из очереди. Что вполне законно. Зачем уведомлять клиента о новых сообщениях в чате, если он давно покинул чат?

Дальше идет описание класса CometAsyncState. Зачем мы его создали? Да чтобы хранить такие параметры клиента, как CurrentContext, AsyncCallback и ClientGuid. Ну то есть, чтобы отдельный поток, порожденный в пуле потоков и отвечающий за одного клиента, мог собственно функционировать и взаимодействовать с клиентом. Этот класс, по большому счету, всего лишь набор заглушек для методов интерфейса IAsyncResult плюс метод CompleteRequest(), который вызывает callback-функцию при завершении потока.

using System;
using System.Threading;
using System.Web;

public class CometAsyncState : IAsyncResult
{
    // Чтобы было где хранить все это и был создан
    // класс-наследник от IAsyncResult
    public HttpContext CurrentContext;
    public AsyncCallback AsyncCallback;
    public object ExtraData;
    public string ClientGuid;
    private Boolean _isCompleted;

    // Конструктор
    public CometAsyncState(HttpContext context,
      AsyncCallback callback, object data)
    {
        CurrentContext = context;
        AsyncCallback = callback;
        ExtraData = data;
        _isCompleted = false;
    }

    // Завершим запрос
    public void CompleteRequest()
    {
        // При завершении запроса просто выставим флаг
        // что он завершен
        // и вызовем callback
        _isCompleted = true;
        if (AsyncCallback != null)
        {
            AsyncCallback(this);
        }
    }

    #region IAsyncResult Members
    // И снова видим набор заглушек для интерфейса IAsyncResult

    public Boolean CompletedSynchronously
    {
        get
        {
            return false;
        }
    }

    public bool IsCompleted
    {
        get
        {
            return _isCompleted;
        }
    }

    public object AsyncState
    {
        get
        {
            return ExtraData;
        }
    }

    public WaitHandle AsyncWaitHandle
    {
        get
        {
            return new ManualResetEvent(false);
        }
    }
    #endregion
}

Осталась малость – клиентский JavaScript. Ниже я приведу его с моими подробными комментариями в самом коде и не буду его так детально разжевывать. Потому что весь он разжеван в начале статьи, где я описывал принцип работы LongPolling.

var clientGuid

$(document).ready(function() {
 // Подключаемся после загрузки страницы,
 // запускаем первый long polling
 Connect();
});

$(window).unload(function() {
 // При выгрузке страницы - запрашиваем сервер об отключении
 // клиента для экономии ресурсов
 Disconnect();
});

// Посылает lonp poll - запрос серверу
function SendRequest() {
 var url = './comet.ashx?guid=' + clientGuid;
 $.ajax({
 type: "POST",
 url: url,
 // Если запрос завершился успехом, значит сервер сообщил
 // о новых событиях - обрабатываем их
 success: ProcessResponse,
 // При ошибке (например таймауте), снова рекурсивно
 // посылаем запрос обеспечивая тем самым непрерывный
 // процесс прослушки серверных событий
 error: SendRequest
 });
}

// Регистрируемся на сервере
function Connect() {
 var url = './comet.ashx?cmd=register';
 $.ajax({
 type: "POST",
 url: url,
 success: OnConnected,
 error: ConnectionRefused
 });
}

// Разрегистрируемся на сервере
function Disconnect() {
 var url = './comet.ashx?cmd=unregister';
 $.ajax({
 type: "POST",
 url: url
 });
}

// Обработка сообщений, принятых с сервера
function ProcessResponse(transport) {
 eval('var d=' + transport + ';');
 document.getElementById("content").innerHTML +=
    ' <strong>' + d.user + '</strong> : "' + d.message + '"<br/>';
 // После отображения результатов запроса -
 // снова циклично делаем запрос.
 SendRequest();
}

// После регистрации на сервере сохраняем наш guid и
// посылаем первый long poll запрос на сервер
function OnConnected(transport) {
 clientGuid = transport;
 SendRequest();
}

// Если подключиться не удалось, то ждем три мекунды
// и опять пробуем подключиться
function ConnectionRefused() {
 $("#content").html("не удалось подключиться к серверу.
    Попробуем через 3 секунды...");
 setTimeout(Connect(), 3000);
}

// Отправка сообщения на сервер
function clickSendMessage() {
 var userName = document.getElementById("userName").value;
 var message = document.getElementById("message").value;
 var url = './comet.ashx?cmd=send&message=' + message + '&user=' + userName;
 $.ajax({
 type: "POST",
 url: url
 });
}

Ну и разумеется, код серверного хэндлера comet.ashx, который обрабатывает команды клиентского скрипта. Скажу сразу, хэндлер этот асинхронный, а потому унаследован не от IHttpHandler, а от IHttpAsyncHandler

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

using System;
using System.Web;
using System.Threading;

public class CometAsyncHandler : IHttpAsyncHandler,
  System.Web.SessionState.IRequiresSessionState
{
    #region IHttpAsyncHandler Members

    public IAsyncResult BeginProcessRequest(HttpContext ctx,
      AsyncCallback cb, Object obj)
    {
        // Готовим объект для передачи его в QueueUserWorkItem
        CometAsyncState currentAsyncState =
          new CometAsyncState(ctx, cb, obj);

        // Добавляем в тредпул новый ждущий поток
        ThreadPool.QueueUserWorkItem(new WaitCallback(RequestWorker),
           currentAsyncState);

        return currentAsyncState;
    }

    public void EndProcessRequest(IAsyncResult ar)
    {
    }

    #endregion

    #region IHttpHandler Members
    // IHttpHandler Members - просто пустые заглушки,
    // так как нам не требуется реализация синхронных методов

    public bool IsReusable
    {
        get
        {
            return true;
        }
    }

    public void ProcessRequest(HttpContext context)
    {
    }

    #endregion

    // Основная функция рабочего потока
    private void RequestWorker(Object obj)
    {
        // obj - второй параметр
        // при вызове ThreadPool.QueueUserWorkItem()
        CometAsyncState state = obj as CometAsyncState;

        string command =
          state.CurrentContext.Request.QueryString["cmd"];
        string guid =
          state.CurrentContext.Request.QueryString["guid"];

        switch (command)
        {
            case "register":
                // Регистрируем клиента в очереди сообщений
                CometServer.RegicterClient(state);
                state.CurrentContext.Response.Write(
                  state.ClientGuid.ToString());
                state.CompleteRequest();
                break;
            case "unregister":
                // Удаляем клиента из очереди сообщений
                CometServer.UnregisterClient(state);
                state.CompleteRequest();
                break;
            case "send":
                // Отсылка сообщения
                string message =
                  state.CurrentContext.Request.QueryString["message"];
                string userName =
                  state.CurrentContext.Request.QueryString["user"];
                CometServer.PushMessage(new CometMessage() {
                  Message = message, UserName = userName });
                state.CompleteRequest();
                break;
            default:
                // При реконнекте клиента
                if (guid != null)
                {
                    CometServer.UpdateClient(state, guid);
                }
                break;

        }
    }
}

Основная логика программы кроется в методе RequestWorker (строки 51-95), который в зависимости от полученной с клиента команды, выполняет либо регистрацию/разрегистрацию клиента, либо отправку сообщения. Либо обновление данных о клиенте при его реконнекте.

Здесь вы можете скачать готовый работающий проект чата. Для того, чтобы его протестировать, просто запустите его в двух страницах браузера и попробуйте в одном окошке отправить сообщение. Наблюдайте при этом за вторым окошком.

Вот и все. Надеюсь, что статья оказалось полезной для вас.

Upd:
Оказалось, что при существовании какой-либо информации в сессии LongPolling-чат перестает корректно работать, обрывая соединения, и не обеспечивая доставку сообщений конечным пользователям. Решение проблемы описано в статье HTTPHandler : IRequiresSessionState halts execution of pages, а если кратко – следует наследовать CometAsyncHandler от System.Web.SessionState.IReadOnlySessionState, а не от System.Web.SessionState.IRequiresSessionState. Спасибо пользвателю Алеша за предоставленную информацию.

В тему:

52комментария

Спасибо за интереснейший материал. Обязательно использую Long Polling в своем следующем проекте. Хотя давно пора внедрять в браузеры push-технологии (т.е. чтобы сервер мог по своей инициативе устанавливать соединение с браузером).

Максим, July 7, 2010 10:48 am Reply

Пожалуйста.
Когда надумаете использовать этот код, сообщите мне. Мне очень интересно, как это работает в реальных условиях, так как я этот код тестировал только на своем локальном сервере.
Интересно, как он будет работать в IIS7 ?

admin, July 7, 2010 8:31 pm Reply

Постараюсь не забыть, честно :)

Максим, July 7, 2010 9:25 pm Reply

Столкнулся с непонятной ситуацией.. При добавление Asp.Net контролов в comet.aspx перестает работать отправка сообщений. Не сталкивались?))

Никита, July 21, 2010 3:45 pm Reply

Проверьте HTML-разметку получившейся после вставки контрола страницы. Думаю, что когда Вы вставляете asp.net контрол в эту страничку, VisualStudio некорректно генерит разметку и создает дополнительную форму FORM, которая в дальнешем и сабмитится, вместо исходной

admin, July 24, 2010 12:03 pm Reply

Очень интересно. Только начал осваивать этот паттерн.
Вопрос: “Как сие счастье можно прикрутить в Master page?”

Если использовать обычные aspx-страницы, то всё работает замечательно. Но есть помещать этот чат в мастер-страницу, то сообщение появляется только на таймаут comet-запроса.

P.S. Спасибо за ответ.

Артур, August 27, 2010 10:07 am Reply

Здравствуйте.

А вы всю форму целиком помещаете в мастер-страницу? То есть проблема, как я понимаю, состоит в том, что при нажатии на кнопку “Послать” у вас ничего не посылается? То есть не выполняется JS-функция clickSendMessage(). Может Вы забыли скрипт comet.js или jquery.min.js к мастер-странице привязать?

admin, August 27, 2010 12:54 pm Reply

Спасибо.

Буду разбираться и пробовать;)

Андрей, September 10, 2010 9:40 pm Reply

// Список, хранящий состояние всех подключенных клиентов
private static List _clientStateList = new List();

Здравствуйте. Такой вопрос (я как бы не профи в пограммировании):

как так получается, что список один и тот же для всех пользователей? Просто читаю книгу и нарвался на главу УПРАВЛЕНИЕ СОСТОЯНИЕМ и там разные варианты… А с каким работаем мы в этом примере – понять не могу((

У нас ведь должно быть что-то вроде глобальной перемнной для всего приложения. Но объявляются такие переменные, как я понял, в global.asax… А у нас не так.

Андрей, September 23, 2010 5:01 pm Reply

Вы все верно говорите. Да, для хранения списка клиентов нужна “глобальная” переменная. А _clientStateList и есть та самая глобальная переменная, потому что она объявлена с модификатором static, и является статической, то есть существует не как член _экземпляра_, а как член _класса_ CometServer. Ее конструктор new List() вызывается один раз в начале создания домена приложения, который в свою очередь создается при загрузке IIS-ом веб-приложения. Или, по другому, создание списка _clientStateList происходит не при каждом запросе клиента к серверу, а единожды при создании веб-приложения.
А где описывать этот список – в global.asax или еще где – не важно, Уже сам факт того, что член класса – статический, накладывает на веб-приложение обязанность создать его сразу при загрузке приложения. В global.asax надо прописывать обработчики событий жизненного цикла приложения, вот здесь http://msdn.microsoft.com/ru-ru/library/ms178473.aspx подробно про этот цикл и про global.asax написано.

admin, September 24, 2010 9:32 am Reply

На самом деле, этот код будет работать некорректно в реальных условиях нагруженного веб-сервиса. Потому что реальные веб-сервисы работают обычно на ферме веб-серверов, предназначенной для балансировки нагрузки. А это значит, что на каждом веб-сервере в ферме будет свой домен приложения со своим собственным списком клиентов, а потому сообщения в чате клиента №1 будут рассылаться по одному списку, а сообщения клиента №2, который имел “счастье” при балансировке нагрузки подключиться к другому серверу в ферме, будут рассылаться по совсем другому списку клиентов, и, к сожалению, эти списки №1 и №2 будут не пересекающимися. Более того, даже в рамках одного веб-сервера может быть создано несколько доменов приложения, а это тоже – разные списки. Так что в качестве домашнего задания :) могу предложить реализовать хранение списка клиентов в некоем единственном для фермы хранилище, это может быть и SQL-сервер и, например, MSMQ (Microsoft Windows Message Queuing), или например в общем файле (самое плохое решение, по причине его нетривиальной блокировки в многопоточной и очень агрессивной среде реального веб-приложения). На мой взгляд, удобнее всего будет использовать именно MSMQ, потому что она уже является реализацией очереди, то есть по сути списком.

admin, September 24, 2010 9:42 am Reply

Спасибо за ответ. Действительно, переменная одна единственная создаётся. Только вот непонятно))) Хотя нет, всё понятно)

Но непонятно с web-фермами и доменами приложения) В реальных условиях простенькия сайт располагается грубо говоря на одном компе в одном домене, ведь так? Ну на моём сайте так вроде)) Наверное) А вообще на этой недели постараюсь интегрировать Ваш чат к себе на сайт связав её с имеющейся аутентификацией.

Только проблема ещё вот такая: ajax классический плохо знаю. Но знаю метод, когда вызовы посылаются веб-службам. Там и свой специальный клиентский код упрощённый очень. То етсь вызывать буду службу, а там уже перенаправлять на хэндлер. Должно получится по идее)))

Можно было бы оставить всё как есть, но тут что-то типа дело принципа) Ведь, походу, Майкрософт специально внедрила такую возможность, чтобы избавить программера серверной стороны кодить на javaScript.

Андрей, September 24, 2010 3:01 pm Reply

Если у вас сервер – единственный, ситуация становится радостнее. Я точно не помню, как регулируется политика создания рабочих процессов в IIS 7, пожалуй я все таки сгустил краски, описывая множество доменов приложения в рамках одного веб-сервера, а домен приложения будет всего один и вы не получите всего того ужаса, который связан с несколькими веб-серверами.
А теперь насчет Ajax. Его действительно неудобно и совершенно не нужно использовать “с нуля”, ну то есть никто не заставляет писать свой собственный скрипт для ajax-обмена. Достаточно использовать готовый JS-фреймворк. Поддержка аякса есть сейчас во всех основных JS-фрейиворках: prototype, jQuery, ExtJS. Есть она и в веб-службах microsoft. Я работал со семи этими фреймворками и могу сказать, что в каждом из них раюота с аяксом легка и удобна. Но вот размеры фреймворков – различные. Майкрософтовский мне не понравился именно своим объемом – грузился долго, загружал много бесполезных для меня вещей. Единственное отличие его от остальных конкурентов – он сразу генерирует удобные обертки для вызова всех методов веб-сервиса, используемого для ajax-обмена. И там действительно вызов серверной функции превращается в вызов одноименной скриптовой функции с такой же сигнатурой.
В данной статье я предложил применять поддержку ajax взятую из jQuery. В принципе можно применять то, что вам больше нравится или то что вам лучше знакомо. Мне например все равно на каком языке писать: на C# или на JavaScript, у меня есть веб-проекты, где бОльшую часть кода составляет именно JavaScript. Так что изучайте все что вам пригодится в работе, если вас интересует веб-программирование.

admin, September 24, 2010 4:23 pm Reply

Честно сказать, я не знаю, как вы будете перенаправлять вызовы из веб-сервиса на веб-хэндлер.
Я вспомнил еще один минус майкрософтовского аякса: для сериализации сообщений он использует SOAP, в пакетах которого присутствует огромное количество служебной информации, таким образом, чтобы отправить простую строчку Hello world, вашему браузеру потребуется отправить несколько килобайт текста, эдакий большущий XML-файл, в котором среди прочего будет и ваше сообщение.
Мммммм…. кстати, я не помню, а мой код нормально работает с русскими символами?

admin, September 24, 2010 4:34 pm Reply

ну веб-сервис можно организовать, насколько я правильно понял, и описать обычной функцией даже на страничке в файлу aspx.cs (то е тсь на странице чата, например). И в этой функции вызывать редирект на хэндлер (он же типа как страница, вроде=)) Навреное получится.. Хотя чего ходить вокруг да около))) Пробовать нужно)

Ну вообщем буду пробовать… хотя вот с сериализацией Вы меня расстроили))) Пробью этот момент точно…

И в любом случае, чего-нибудь да попробую… Переделать не проблема потом чуть что.

Ещё раз спасибо за инфу и советы;)

Андрей, September 24, 2010 11:33 pm Reply

Ну что ж, пробуйте, расскажите потом о подводных камнях. Хотя я бы посоветовал не париться и не усложнять ситуацию только лишь оттого, что не хочется программировать на JS. Уверяю вас, JS – хороший язык, если писать на нем ровно (а не криво).

admin, September 27, 2010 11:52 am Reply

Проблема)))))))) Втупую копирую файлы вашего проета в свой… Запуская.. Запускается, регитсрируется клиент… Но сообщения не шлются…(( Скажите, нужно ли какие-то параметры в web.config менять?

Функция JS, обрабатывающая клик срабатывает, но запрос никакой, как поучается, не отправляет… Брейкпоинт не срабатывает, в месте где кэйс определяет тип команды((((

Андрей, September 30, 2010 2:54 pm Reply

Загляните в скрипт Scripts/comet.js, там есть несколько строк, которые начинаются вот так:
var url = ‘./comet.ashx?
Попробуйте поставить полный путь типа:
var url = ‘http://localhost/comet.ashx?
или наоборот путь от корневой директории, вот так:
var url = ‘/comet.ashx?
Если не поможет, то скачайте firebug для mozilla firefox и проведите отладку в ней – главное увидеть, куда пытается обратиться скрипт.

admin, October 1, 2010 10:12 am Reply

уууууух… Вся фигня в том, что регистрация проходит успешно. То есть запрос посылается на хэндлер как нужно. В url везде всё одинаковое… Во всех функциях. В плане начало “./”

При этом при нажатии виснет сервер… То есть после отправки сообщения (которое ничего и не отправляет – ни один из брейкпоинтов в comet.ashx не срабатывает) нельзя даже обновить страничку чата… Ничего не происходит. По крайней мере быстро ничего не происходит. Быстро в смысле 5 минут))

firebuge пытался посмотреть… Ну а в каком месте смотреть – так и не понял(( Ставлю брейкпоинт на обработчике клика, url подсвечивает с “./” начинается который. Потом, видимо, начинается выполняться код низкоуровневый, который jQuery там мутит у себя. Вот там уследить ничего не могу…

Ну а вообще почему психую сижу (а я именно это и делаю))). Регистрация потому что проходит))) А вот сообщения не посылаются(((

Андрей, October 8, 2010 11:13 am Reply

при постом / ничгео не поисходит, даже регитсрации… При (внимание!) полном пути снова… РЕГИСТРИРУЕТСЯ но НЕ принимаются сообщения сервером((

Напомню, что я сделал: просто скопировал все файлы к себе в приложение. Проект в виде беспроектного решения. То есть самый постой вариант.((

Когда просто запускаю ваш проект – всё работает…((( Я с ума сойду скоро(((

Андрей, October 8, 2010 11:18 am Reply

И ещё вопрос… Почему мы используем при отправке сообщения POST-запрос, когда сообщение передаём всё равно в строке запроса и из неё же потом на сервере (state.CurrentContext.Request.QueryString["message"];) выдираем это сообщение.

И вообще тут же вопрос, может подскажите… Параметров ведь много в POST запросе может быть… Как к этим параметрам обращаться на сервере?

Андрей, October 8, 2010 12:09 pm Reply

Расскажу про GET и POST. Как они работают.
При отсылке данных браузер чаще использует две команды: GET и POST, их вообще несколько больше, но обычно используются только эти. Браузер отсылает эти команды приблизительно так:
1) POST-запрос
POST /comet.ashx?par1=var1&par2=var2
Заголовки запроса


post1=postvar1&post2=postvar2

2) GET-запрос
GET /comet.ashx?par1=var1&par2=var2
Заголовки запроса

Как видно, и в том и в другом случае можно передавать GET-параметры (а также читать их на сервере вот так CurrentContext.Request.QueryString["par1"]). Но в POST-режиме появляется еще и тело запроса (в моем примере это post1=postvar1&post2=postvar2), в теле запроса обычно присутствуют POST-параметры, которые форматируются также точно как и GET-параметры (читаются POST-параметры на стороне сервера вот так state.CurrentContext.Request.Form["post1"]).
Размер POST-параметров почти ничем не ограничен, это и дает нам возможность заливать на сервера файлы, например. Размер же GET-запроса ограничен длиной 255.
Есть еще одно существенное различие между POST и GET, которое очень ярко можно отследить на примере LongPolling-чата(попробуйте заменить все POST-запросы на GET и ваш чат зациклится после отправки первого же сообщения, оно снова и снова будет вставляться в окно сообщений и браузер повиснет, к счастью, сервер при этом совсем не будет испытывать никакой нагрузки). Так вот отличие это состоит в том, что абсолютно одинаковые GET-запросы (ну, то есть те , у которых URL-ы идентичны) бразуер отсылает на сервер ровно один раз, во второй раз браузер просто достанет прошлый ответ сервера из собственного кеша и подсунет его скрипту. Сервер даже не узнает, что к нему обращались.
Иногда, когда позарез нужно использовать именно GET-запрос, и при этом, обращаясь к одному и тому же URL-у, получать разные данные, используется такой трюк. В конце URL-а скриптом дописывается еще один параметр, который каждый раз принимает рандомные значения. В нашем случае это выглядело бы как-то так /comet.ashx?cmd=send&random=321654864. Таким образом браузер каждый раз будет получать новый URL, и отсылать запрос на сервер, не находя такого урла у себя в кеше.

Таким образом, у меня возникает ощущение, что у вас вместо POST-запросов используются GET-запросы. В данном случае симптомы, описанные вами, полностью удовлетворяют моему предположению, я даже специально проверил. :)

Что-то я не припомню, чтобы браузеры по собственному усмотрению подменяли POST на GET. Вы сами изменили это поведение скрипта?

Чтобы проверить гипотезу, вставьте в самое начало JS-функции SendRequest() оператор alert(‘Hello’) И увидите, что после отправки сообщения ваш браузер будет постоянно выбрасывать одно и то же всплывающее окошко “Hello”.

admin, October 8, 2010 12:53 pm Reply

У вас проект, в который вы копируете мои файлы, уже наполнен чем-то? Спрашиваю, потому что мне хотелось бы посмотреть своими глазами на этот проект. Если можно – вышлите мне исходники. Если нельзя, тогда сделайте следующее: создайте пустой проект, как вы делали раньше, скопируйте в него мои файлы, проверьте будет ли работать, если не будет работать – вышлите мне этот пустой проект с моими файлами, я посмотрю, что там может работать не так. Потому что я сейчас создал пустой веб-сайт, добавил в него мои файлы (кроме web.config конечно) и запустил. Он работает. И поставьте в comet.js в конце первой строки точку с запятой, она там отсутствует.

admin, October 8, 2010 12:20 pm Reply

попробую всё это проделать. А засовываю в свой уже существующий проект.

Посмотрю, что да как и отпишусь.

Только вопрос по поводу POST, если не трудно, разъясните, пожалуйста.

Андрей, October 8, 2010 12:46 pm Reply

в пустой закидываю – всё работает)))))) жесть)

попробую свой проект разобрать на мелкие части… поудалять их, эти части… оставив минимум… подрублю Ваш чат и понаблюдаю… О! А ещё на ноуте попробую подрубить. Может какой-то глюк?

В любом случае, спасибо за помощь Вам;)

Андрей, October 8, 2010 1:03 pm Reply

ой… почитал только сейчас Ваш совет последний… Сейчас убегаю по делам… Как попробую – сообщу!

Ещё раз спасибо;)

Андрей, October 8, 2010 1:04 pm Reply

А как скачать весь Ваш проект?
Я ссылки нигде не вижу. Сообщите, пожалуйста.

Дмитрий, February 20, 2011 2:44 pm Reply

Как скачать?!

Дмитрий, February 20, 2011 2:45 pm Reply

Скачать проект можно кликнув по ссылке “здесь” в начале предпоследнего абзаца.

Вячеслав Гринин, February 20, 2011 9:05 pm Reply

Здравствуйте!

Решил попробовать затестить ваш чат сказал проект запустил, все в порядке, запросы отсылаются, функционал работает. Но, когда создал еще asp страницу и с нее попытался редиректом перейти на чат,но отправка сообщения и обновление страницы происходит очень долго (затягивается на пару минут).
C данной технологией раньше не работал. Хотелось бы узнать ваши предположения, о причине возникновения данной проблемы.

Алеша, February 21, 2011 12:23 am Reply

Заметил, что такая долгая передача сообщений возникает, если передать что-либо в Session. В чем может быть проблема с сессией???

Алеша, February 21, 2011 1:16 pm Reply

Я предлагаю Вам для начала посмотреть в режиме отладки на чем именно висит чат. И написать сюда.

Вячеслав Гринин, February 21, 2011 3:22 pm Reply

В FireBug поставил точку останова в clickSendMessage. По f10 она благополучно завершилась, но сообщение не появилось.Оно появляется потом, минут через 5

Алеша, February 21, 2011 4:46 pm Reply

Висит на функции var onreadystatechange = xhr.onreadystatechange = function( isTimeout ) при отладке через dragonfly.

Алеша, February 21, 2011 5:23 pm Reply

Я бы с большим интересом попробовал поставить точки останова внутри серверного кода. Тот факт, что сообщение появляется лишь через несколько минут, говорит о том, что серверный обработчик сразу не отдает результат, то есть не завершает асинхронное выполнение. А вместо этого соединение рвется по таймауту, и вот именно тогда происходит получение скриптом результата. Но вот отчего выполнение серверного обработчика не завершается при получении сообщения – я пока не понимаю. Не могли бы вы мне дать исходник той страницы с которой у вас происходит редирект на страницу с чатом. Или опишите подробно, как именно происходит ее вызов.

Вячеслав Гринин, February 21, 2011 6:27 pm Reply

на странице есть кнопка, по ней в сессию заносится ин-фа и редирект на страницу с чатом
вот код
protected void bt_enter_Click(object sender, EventArgs e)
{
Session["q"] = “qwe”;
Response.Redirect(“./comet.aspx”);
}

Алеша, February 21, 2011 6:48 pm Reply

Вынужден признать, что баг действительно есть. Но причину его возникновения я не знаю. Действительно, как только появляется информация в сессии – запросы к comet-ядру подвисают до таймаута. Обещаю, что я еще буду разбираться с этим вопросом. Если у вас появятся какие-нибудь идеи о причинах такого поведения, расскажите мне.

Вячеслав Гринин, February 23, 2011 7:41 pm Reply

Думаю, мне удалось решить эту проблему(помогло это http://stackoverflow.com/questions/3312711/httphandler-irequiressessionstate-halts-execution-of-pages):класс CometAsyncHandler нужно наследовать от System.Web.SessionState.IReadOnlySessionState, а не от System.Web.SessionState.IRequiresSessionState. По крайней мере, сообщения приходят сразу. Но правда хэндлер теперь не сможет писать ничего в сессию.

Алеша, February 23, 2011 10:06 pm Reply

Спасибо за решение проблемы. Код, выложенный в архиве, я обновил.

Вячеслав Гринин, February 24, 2011 10:08 am Reply

Вячеслав, добрый вечер. Заинтересовался вашим чатом. Я в этом деле новичок, и у меня возник такой вопрос, нельзя ли по подробнее объяснить как решить проблему с хранением пользователей в единой переменной (вы писали что это можно сделать с помощью SQL) при появлении нескольких доменов приложения.

Ильшат, February 15, 2012 7:28 pm Reply

Я обязательно отвечу на Ваш вопрос немного позже.

Вячеслав Гринин, February 15, 2012 8:12 pm Reply

спасибо за старания и отличный разжёванный пример, я как раз не знал, с какого боку к си шарпу подойти)

еще один pull request вам: :)

setTimeout(Connect(), 3000);
=>
setTimeout(Connect, 3000);

в первом случае на медленном компьютере чуть ли не интерфейс вешается и память кушать начинает, так как создания XHR идут без пауз в три секунды.

dagen, March 4, 2012 5:03 am Reply

У меня возникла проблема… данный чат успешно работает в визуал студии а если публиковать его на IIS Сервер то он ругается на ashx файл… почему то не видит классы CometAsyncState, CometServer … в браузере при попытке открыть напрямую этот файл выдает ошибку в строке 16 этого хендлера.. пишет что CometAsyncState отсутствует в текущем контексте… перепробовал всё. не как не могу понять в чём проблема…

Visual Studio 2010
IIS 6.0

Антон, March 21, 2012 6:56 am Reply

данный чат успешно собрался и работает на сервере в visual studio … при переносе на IIS 6.0 начинает ругаться в ashx файле в 16-той строке… не находит классы из cs файлов… неймспейсы везде одинаковые. на локалке в визуал студии всё прекрасно работает -)

софт:
visual studio 2010
IIS 6.0

помогите разобраться в чем ошибка -(

Антон, March 21, 2012 8:32 am Reply

Столкнулся с проблемой, при размещение сайта на IIS 7.5 и обращение к странице 5-10 раз, (простым нажатием F5) сайт виснет! Кто натыкался на подобное?

gollandec, November 10, 2012 2:36 pm Reply

не знаю, возможно причина не совсем в вашей реализации, делал практически тоже самое, за исключением добавления кода для сохранения сообщений в MSSQl , + добавил удаление из коллекции состояний тех, к которым не приходил Реквест на update более минуты…(это на случай если юзер просто закроет окно браузера и т.д.) проверяю коллекцию состояния, и там действительно лишние удалились, смотрю память на IIS а она медленно но уверенно растет, в конце концов в течении дня IIS падает…

В чем может быть проблемма, моэжет быть необходимо перед удалением вызывать CallBAck функцию для состояния… Заранее спасибо.

Петр, December 10, 2012 9:47 pm Reply

Я этот код написал и выложил в чисто образовательных целях. Нигде в реальных проектах не использовал, поэтому мне сложно судить об утечках памяти. Может Вы что-нибудь расскажете, если удалось проблему решить?

Вячеслав Гринин, January 24, 2013 4:30 pm Reply

Нужна помощь в настройке. При запуске проекта в Visual Web Developer Express все работает отлично. Как только заливаю на хостинг ничего не работает. Просто открывается страница в браузере, есть все контролы. Ввожу имя, сообщение, жму отправить, но ничего не происходит. Сообщение не приходит в чат. Помогите плиз советом, в какую сторону копать.

Сергей, April 29, 2013 7:35 am Reply

Небольшие уточнения к предыдущему посту:
Проект собираю в Visual Web Developer 2010 Express
Версия IIS на хостинге 8.0.
Очень нужен совет. Уже облазил кучу форумов, никак не пойму в чем причина. Впервые столкнулся с подобной проблемой.
А и еще: сообщения со стороны клиента шлются(проверил перехватом HTTp пакетов). Поэтому проблема где-то на стороне сервера, он почему-то ответ не отдает(

Сергей, April 29, 2013 7:48 am Reply

Пробовали наследовать хэндлер от IReadOnlySessionState, как описано в конце статьи?

Вячеслав Гринин, April 30, 2013 9:32 pm Reply

У меня проблема с закрытием потоков – при переподключении поток не закрывается. т.е. общее количество потоков растет – и поэтому (как писал Петр) IIS падает. Как это можно исправить?

Руслан, September 25, 2013 11:39 am

Прошу прощения у моих читателей, но сейчас у меня совсем нет времени на развернутые и вдумчивые ответы в моем блоге. Представленный код я никогда не использовал в промышленных системах, и создавал его исключительно в образовательных целях. Сразу так навскидку не могу предположить где именно утекают ресурсы. Может, стоит попробовать WebSocket API (библиотека SignalR), он доступен начиная с IIS 8.

Вячеслав Гринин, September 25, 2013 10:14 pm Reply
Ваше имя
Ваш email*
Ваш сайт
Текст вашего комментария:

Поиск по блогу:
Подписаться:
Популярные:
Облако тегов:
Разное:
Счетчик: