Long Polling от А до Я своими руками

    Как реализовать long polling с помощью Nginx и Javascript в сети достаточно много материала. Но полного руководства я ещё не встречал. То возникают проблемы с компиляцией модуля под Nginx, то в браузере вертится иконка загрузки при long poll запросах. Под катом, полный материал как же все таки это сделать правильно.


    Компиляция Nginx модуля под linux


    Для поддержки long polling подключений в сервере Nginx, реализован замечательный модуль nginx-push-stream-module. Так как он не входит в официальную поставку, его нужно скачать, настроить и скомпилировать вместе с Nginx.

    Перед этим у вас должны быть установлены все необходимые пакеты
    apt-get install git
    apt-get install make
    apt-get install g++
    apt-get install libpcre3 libpcre3-dev libpcrecpp0 libssl-dev zlib1g-dev
    


    Далее нужно скачать сам модуль nginx-push-stream-module, nginx и скомпилировать их вместе.

    Клонируем проект из GIT
    git clone http://github.com/wandenberg/nginx-push-stream-module.git
    


    Скачиваем и распаковываем последний nginx
    NGINX_PUSH_STREAM_MODULE_PATH=$PWD/nginx-push-stream-module
    wget http://nginx.org/download/nginx-1.2.6.tar.gz
    tar xzvf nginx-1.2.6.tar.gz
    


    Настраиваем и компилируем nginx вместе с nginx-push-stream-module
    cd nginx-1.2.6
    ./configure --add-module=../nginx-push-stream-module
    make
    make install
    


    Если нет ошибок компиляции, все готово. Проверим, что мы установили именно тот nginx и то, что теперь в нем действительно есть модуль nginx-push-stream-module
    check: /usr/local/nginx/sbin/nginx -v
    test configuration: /usr/local/nginx/sbin/nginx -c $NGINX_PUSH_STREAM_MODULE_PATH/misc/nginx.conf -t
    


    После выполнения этих команд, вы должны увидеть такое:
    nginx version: nginx/1.2.6
    the configuration file $NGINX_PUSH_STREAM_MODULE_PATH/misc/nginx.conf syntax is ok
    configuration file $NGINX_PUSH_STREAM_MODULE_PATH/misc/nginx.conf test is successful
    


    Настройка Nginx для Long Polling подключений


    Для настройки поддержки long polling, в конфигурации Nginx, нужно прописать как минимум два контроллера. Первый для подписчиков (тех кто будет получать сообщения), второй для публикации сообщений (тех кто будет посылать сообщения).

    Опуская настройку остальных параметров сервера, конфигурационный файл /usr/local/nginx/nginx.conf должен выглядеть так:
    ...
    
    http {
        ...
    
        server {
            listen 80;
            server_name stream.example.com;
            charset utf-8;
    
            location /pub {
                push_stream_publisher admin;
                set $push_stream_channel_id             $arg_id;
                allow  1.1.1.1  # ip адрес сервера посылающего событие
            }
    
            location ~ /sub/(.*) {
                push_stream_subscriber                  long-polling;
                set $push_stream_channels_path          $1;
                push_stream_last_received_message_tag   $arg_tag;
                push_stream_last_received_message_time  $arg_time;
                push_stream_longpolling_connection_ttl  25s;
            }
        }
    }
    


    В данном примере /pub — адрес для публикации сообщений, его должен видеть только ваш сервер (1.1.1.1), от которого приходят события, /sub — адрес для подписчиков, тех кому будут пересылаться сообщения. Идентификатор, который будет идентифицировать подписчиков, передается после /sub, и принимается как параметр id в /pub.

    Об очень важных параметрах push_stream_last_received_message_tag и push_stream_last_received_message_time речь пойдет ниже, когда коснемся javascript.

    Пример для понимания работы:
    Можно создать несколько подписчиков, вызвав: stream.example.com/sub/1, stream.example.com/sub/2, stream.example.com/sub/3. Каждый из них будет «висеть» на Nginx сервере в течении 25 секунд (push_stream_longpolling_connection_ttl). Если мы вызовем POST запрос на stream.example.com/pub?id=2 и передадим в теле сообщение «Hello», то подписчик «висящий» на /sub/2, получит ответ «Hello». Удобно проверять это в плагине Poster для FireFox.

    Создание подписчиков в Javascript


    Скорее всего, long polling вам нужно использовать для обновления каких-либо данных в браузере, и для этого вам понадобится написать Javascript клиента.

    Я попробовал разные методы, но за эталон выбрал XMLHttpRequest. По сравнению с другими методами имеет следующие преимущества:
    • Отлично работает во вех браузерах Chrome, FireFox, Opera, IE 8, 9, 10
    • В браузерах не висит иконка загрузки страницы
    • Работает на разных доменах (кроссдоменно, если на сервер есть поддержка CORS)


    Пускай в переменной subID — хранится уникальное значение для подписчика
    var LongPolling = {
      etag: 0,
      time: null,
    
      init: function () {
        var $this = this, xhr;
        if ($this.time === null) {
          $this.time = $this.dateToUTCString(new Date());
        }
    
        if (window.XDomainRequest) {
          // Если IE, запускаем работу чуть позже (из-за бага IE8)
          setTimeout(function () {
            $this.poll_IE($this);
          }, 2000);
    
        } else {
          // Создает XMLHttpRequest объект
          mcXHR = xhr = new XMLHttpRequest(); 
          xhr.onreadystatechange = xhr.onload = function () {
            if (4 === xhr.readyState) {
              
              // Если пришло сообщение
              if (200 === xhr.status && xhr.responseText.length > 0) {
                
                // Берем Etag и Last-Modified из Header ответа
                $this.etag = xhr.getResponseHeader('Etag');
                $this.time = xhr.getResponseHeader('Last-Modified');
                
                // Вызываем обработчик сообщения
                $this.action(xhr.responseText);
              }
              
              if (xhr.status > 0) {
                // Если ничего не пришло повторяем операцию
                $this.poll($this, xhr);
              }
            }
          };
          
          // Начинаем long polling
          $this.poll($this, xhr);
        }
      },
    
      poll: function ($this, xhr) {
        var timestamp = (new Date()).getTime(),
          url = 'http://stream.example.com/sub/' + subID + '?callback=?&v=' + timestamp;
          // timestamp помогает защитить от кеширования в браузерах
    
        xhr.open('GET', url, true);
        xhr.setRequestHeader("If-None-Match", $this.etag);
        xhr.setRequestHeader("If-Modified-Since", $this.time);
        xhr.send();
      },
    
      // То же самое что и poll(), только для IE
      poll_IE: function ($this) {
        var xhr = new window.XDomainRequest();
        var timestamp = (new Date()).getTime(),
          url = 'http://stream.example.com/sub/' + subID + '?callback=?&v=' + timestamp;
    
        xhr.onprogress = function () {};
        xhr.onload = function () {
          $this.action(xhr.responseText);
          $this.poll_IE($this);
        };
        xhr.onerror = function () {
          $this.poll_IE($this);
        };
        xhr.open('GET', url, true);
        xhr.send();
      },
    
      action: function (event) {
        // получили сообщение, и теперь можем что-то обновить
        ...
      },
    
      valueToTwoDigits: function (value) {
        return ((value < 10) ? '0' : '') + value;
      },
    
      // представление даты в виде UTC
      dateToUTCString: function () {
        var time = this.valueToTwoDigits(date.getUTCHours())
            + ':' + this.valueToTwoDigits(date.getUTCMinutes())
            + ':' + this.valueToTwoDigits(date.getUTCSeconds());
        return this.days[date.getUTCDay()] + ', '
               + this.valueToTwoDigits(date.getUTCDate()) + ' '
               + this.months[date.getUTCMonth()] + ' '
               + date.getUTCFullYear() + ' ' + time + ' GMT';
      }
    }
    


    Важно сказать о двух параметрах etag и time.
    xhr.setRequestHeader("If-None-Match", $this.etag);
    xhr.setRequestHeader("If-Modified-Since", $this.time);
    

    Без них long polling работал далеко не всегда и сообщения приходили через раз. Эти два параметра, нужны модулю nginx-push-stream-module, для идентификации сообщений, которые ещё не получил подписчик. Так что для стабильной работы это просто необходимо.

    В заключении


    Метод описанный в данном топике используется и успешно работает в нашей системе комментирования Cackle. Каждый день у нас порядка 20 000 — 30 000 параллельных подписчиков, и мы ни разу не наблюдали каких либо ошибок в доставке сообщений. Для продакшн решения это именно то, что надо.
    Метки:
    Cackle 24,53
    Компания
    Поделиться публикацией
    Похожие публикации
    Комментарии 34
    • +2
      Статья хорошая, спасибо.
      Но, какой классный у вас проект! Блин, однозначно в закладки.
      • +2
        Да еще и к названию с юмором подошли.
        Интересует какие технологии используете на сервеной стороне? Программный язык, база данных?

        Нашел в прошлой статье: PHP, MySQL.
        • 0
          Если интересно, почему трудно обследовать сайт? ru.cackle.me/about
          • +1
            Дествительно, причем и ошибся в прошлом комментарии: Java для бизнес логики, для хранения данных кластер PostgreSQL.
            • +1
              Кстати, информация устарела. Сейчас уже более 17 000 виджетов и комментариев по 10 000 в день.
          • +2
            Несет ли какой-то смысл разбиение apt-get install на 4 строки?
          • +1
            Я правильно понимаю, что в реальных условиях у вас URL для получения событий это не
            http://stream.example.com/sub/1
            http://stream.example.com/sub/2

            а что-то посложнее, a-la
            http://stream.example.com/sub/njnnjnOIUIn8NUInYUG56
            http://stream.example.com/sub/bnYUBn789nbn8N90
            ?
            • +1
              Все зависит от того, какой идентификатор подписчика выбрать. Если хочется просто, то цифры вполне подойдут, если что-то сложнее + небольшая защита от ботов, то njnnjnOIUIn8NUInYUG56, bnYUBn789nbn8N90 — будет лучше.
              • +3
                Ну, я к тому, что простые последовательные числовые идентификаторы можно перебрать и читать данные других юзеров…
                • 0
                  >и читать данные других юзеров…

                  Это-то как раз в любом случае тривиально решается проверкой кукисов, который в любом случае делать надо.
                  • 0
                    А можно этот тривиальный момент в двух словах описать?
                    • 0
                      Это же должен делать не nginx, а «сайт», но он в этом процессе не участвует, поэтому ничего тривиального
              • +1
                У вас 1 сервер справляется?
                • +1
                  Да, вполне. Стабильно каждую секунду держит от 10 000 до 30 000 подписчиков. Нагрузочное не делали.
                • 0
                  почему не вебсокеты?
                  • +3
                    Не все версии браузеров поддерживают, пруфлинк caniuse.com/websockets
                    • 0
                      фоллбек на флеш? + надо учитывать, сколько людей из вашей аудиотории сидит на старых браузерах. сколько из них без флеша?
                      • +3
                        А зачем, если long polling работает отлично!
                        • 0
                          вебсокет коннекшены дешевле, нет?
                  • +1
                    А можно вопрос? Я, вот, например, не слышал про PUSH-модуль, который озвучен в статье, но делаю билды NgX с вот этим: pushmodule.slact.net/

                    Он, разве, не подойдёт? :)
                    • 0
                      На сколько мне известно, nginx-push-stream-module — это fork модуля о котором вы говорите, с множеством фич, например jsonp.
                    • +1
                      Как и коллеги из Cackle, мы в Битрикс24 тоже использует данный сервер очередей в продакшене, хорошее решение.
                      • +1
                        Недавно прикручивали real-time, но с использованием nginx http push module и jquery. Подобного руководства не хватало. Вопрос: при такой логике
                        if ($this.time === null) {
                            $this.time = $this.dateToUTCString(new Date());
                        }
                        

                        не возникает проблем когда время на клиенте не совпадает с серверным?
                        • 0
                          Не совсем понимаю, в action вы ещё раз запускаете poll? readyState меняется один раз, когда документ (сообщение) загружен целиком. После чего надо запускать новый xhr, вы так и делаете?

                          Чем не устраивает chunked? Есть вот такой вот вариант, использующий onprogress, и не требующий установления повторного соединения после получения каких-либо сообщений.
                          • 0
                            А каким образом реализована возможность защиты канала, т.е. чтобы левые пользователи не могли подписаться на прослушивание канала?
                            • 0
                              Возможно несколько наивный вопрос, я в этом абсолютный дилетант. Правильно ли я понимаю, что через каждые 25 секунд коннект рвется и заново инициируется скриптом?
                              • 0
                                Да, он живет 25 сек., если ничего не пришло, то снова подключаемся и т.д.
                              • 0
                                У вас в конфиге push_stream_last_received_message_tag и push_stream_last_received_message_time, а в яваскрипте вы все равно хедерами пользуетесь. зачем тогда эти строчки?
                                • 0
                                  А вы уже использовали какую-нибудь версию из ветки 0.4.х? Если да, то как впечатления?
                                  • 0
                                    Пока что нет, сейчас используем стабильную 0.3.5.
                                    А вообще 0.4.х очень ждем из-за встроенной поддержки балансера.
                                    • +2
                                      Как мне объяснил автор, текущая 0.4.x чуть ли не стабильнее, чем 0.3.5. Также в 0.4.x есть новые приятные плюшки, поэтому мы сразу стали внедрять ее. Чуть попозже проапгрэйдимся до релиза 0.4.0.
                                      Вчера гоняли 0.4.х с помощью jmeter, 10к соединений оно держит без проблем.
                                      • +2
                                        Это здорово.
                                        У нас в среднем 70-85К постоянных соединений (subscribers) и параллелить их приходится в ручную (просто раскидываем разные ренжы). Так что 0.4 ждем именно из-за балансинга out of the box.

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

                                Самое читаемое