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

в 11:29, , рубрики: Cackle, CORS, javascript, long polling, nginx, web-разработка, Блог компании Cackle, Веб-разработка, метки: , , , , ,

Как реализовать 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 параллельных подписчиков, и мы не разу не наблюдали каких либо ошибок в доставке сообщений. Для продакшн решения это именно то, что надо.

Автор: javist

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js