Во время разработки игры мы столкнулись с необходимостью обеспечения максимального риалтайм обмена данных между пользователями, что повлекло за собой эксперименты с различными comet библиотеками.
Первый велосипед был построен на dklab realplexor, который, при очередной попытке его использования, как и ожидалось, нас подвел. Может у нас руки кривые, но добиться получения ивентов без задержек в 5-10-15 секунд у нас, к сожалению, не получилось.
Танцы с бубном продолжались долго, в результате чего мы решили остановиться на nginx_http_push_module, и потраченное время все же стоило того.
Основные проблемы, которые возникли при работе с данным модулем:
Из недостатков можно выделить только невозможность отправки сообщений на несколько каналов одновременно, на уровне самой библиотеки, что есть в realplexor, а так же отсутствие складирования нескольких ивентов, приходящих на канал, в короткий промежуток времени (время реконекта).
Немного ягугла помогло найти JS функцию, которая, на первый взгляд, решала проблему:
Немного танцев с бубнами дали понять, что и у нее есть свои скелеты в шкафу:
Первую проблему удалось устранить довольно просто, заменив
на
где LOAD_TIME задавался константой в индексе, как
Вторую проблему, я надеялся, решить будет так же просто, заменив
на
создав принудительный реконект к nginx каждые Х секунд (в нашем случая оптимальным интервалом стало 20 сек).
Но начав тестирования, получил еще больше коллизий, чем было до этого – после реконектов начинался адский флуд ивентов. Спасибо console, за пояснение ситуации. Проблема была в том, что когда кверик делал реконект, обработчик complete продолжал обновлять данные по хедерам, в связи с чем следующий запрос уходил с If-None-Match и If-Modified-Since null.
В конце концов, получили максимально стабильную функцию:
Вызов обработчика ивентов выглядит примерно так:
Поскольку в примерах на сайте модуля нет кода, который мог бы отправлять сообещение сразу в несколько каналов, мы решили написать свою функцию.
Реализация на php:
С установкой Dklab Realplexor, никаких проблем не возникло и полная документация доступна на сайте (http://dklab.ru/lib/dklab_realplexor) разработчика.
Однако, с nginx_http_push_module не всё так просто, если честно, на CentOS готовый пакет, включающий данный модуль, найти не удалось, поэтому собирали всё из исходников.
Итак приступим:
Создадим папочку куда будем скачивать все необходимые элементы
Скачаем последние исходники с гитхаба:
Качаем последнюю версию nginx, в нашем случае это nginx-1.1.15
Качаем дополнительные необходимые библиотеки:
Распаковываем архивы:
Переходим в папку с исходниками nginx'а
И конфигурируем nginx
Затем
теперь создадим файл /etc/init.d/nginx
и положим туда такой скрипт:
И теперь
Всё. на этом установка nginx завершена, и можно запускать сервис nginx'a
Теперь перейдем к настройкам:
/publisher — служит для записи в канал и должен быть доступен только вашему серверу, иначе писать в него сможет кто угодно.
/listener – доступен всем и служит для раздачи сообщений подписчикам канала.
На этом по настройки всё. Решение всем хорошо, однако у нас возникли трудности с настройкой crossdomain-ajax, а в модуле, в главной ветке, поддержка jsonp, на данный момент, отсутствует. НО, был найден форк с данным функционалом (https://github.com/Kronuz/nginx_http_push_module). Его установка никак не отличается от оригинала.
Чтобы включить поддержку jsonp в раздел location /listener нужно добавить
Теперь, в случае передачи в параметр callback данных, модуль будет заворачивать данные в jsonp.
Отдельной демки нет, но зарегистрированные пользователи в вк могут посмотреть работу на живом примере в нашей игре — vk.com/app2814545
На данной технологии полностью построены бои (собственно говоря, они и требовали этого риалтайма), казино, сообщения о получении / трате ресурсов и множество других событий.
Первый велосипед был построен на dklab realplexor, который, при очередной попытке его использования, как и ожидалось, нас подвел. Может у нас руки кривые, но добиться получения ивентов без задержек в 5-10-15 секунд у нас, к сожалению, не получилось.
Танцы с бубном продолжались долго, в результате чего мы решили остановиться на nginx_http_push_module, и потраченное время все же стоило того.
Основные проблемы, которые возникли при работе с данным модулем:
- Отключенный кеш влечет за собой потерю ивентов (цикл из 250 проходов отдавал клиенту всего 3 ивента).
- Включенный кеш влечет за собой рекурсивное получение последнего ивента.
Из недостатков можно выделить только невозможность отправки сообщений на несколько каналов одновременно, на уровне самой библиотеки, что есть в realplexor, а так же отсутствие складирования нескольких ивентов, приходящих на канал, в короткий промежуток времени (время реконекта).
Немного ягугла помогло найти JS функцию, которая, на первый взгляд, решала проблему:
function Listener(url, successCallback, failureCallback) { var scope = this; var etag = 0, lastModified = 0; var launched = false; var failure = false; this.successTimeout = 0; this.failureTimeout = 5000; var getTimeout = function () { return failure ? this.failureTimeout : this.successTimeout; }; var listen = function () { $.ajax(scope.ajaxOptions); } var beforeSend = function (jqXHR) { jqXHR.setRequestHeader("If-None-Match", etag); jqXHR.setRequestHeader("If-Modified-Since", lastModified); }; var complete = function (jqXHR) { var timeout = getTimeout(); etag = jqXHR.getResponseHeader('Etag'); lastModified = jqXHR.getResponseHeader('Last-Modified'); var timeout = jqXHR.statusText == 'success' ? scope.successTimeout : scope.failureTimeout; if (timeout > 0) { setTimeout(listen, timeout); } else { listen(); } }; this.ajaxOptions = { url : url, type : 'GET', async : true, error : failureCallback, success : successCallback, dataType : 'json', complete : complete, beforeSend : beforeSend, timeout: 1000 * 60 * 60 * 24 }; this.start = function (timeout) { if (!launched) { if (typeof(timeout) == 'undefined' || timeout == 0) { listen(); } else { setTimeout(listen, timeout); } launched = true; } }; }
Немного танцев с бубнами дали понять, что и у нее есть свои скелеты в шкафу:
- Если на клиент пришло несколько ивентов, и сразу после этого следует рефреш страницы — они придут к вам еще раз (из кеша).
- Если вкладка была неактивной в течении длительного времени — реквест умирал, и больше не обрабатывался до рефреша страницы.
Первую проблему удалось устранить довольно просто, заменив
var etag = 0, lastModified = 0;на
var etag = 0:
lastModified = LOAD_TIME;где LOAD_TIME задавался константой в индексе, как
var LOAD_TIME = "<?=date('r');?>";Вторую проблему, я надеялся, решить будет так же просто, заменив
timeout: 1000 * 60 * 60 * 24на
timeout: 1000 * 20создав принудительный реконект к nginx каждые Х секунд (в нашем случая оптимальным интервалом стало 20 сек).
Но начав тестирования, получил еще больше коллизий, чем было до этого – после реконектов начинался адский флуд ивентов. Спасибо console, за пояснение ситуации. Проблема была в том, что когда кверик делал реконект, обработчик complete продолжал обновлять данные по хедерам, в связи с чем следующий запрос уходил с If-None-Match и If-Modified-Since null.
В конце концов, получили максимально стабильную функцию:
<script> var LOAD_TIME = "<?=date('r');?>"; /* * ВАЖНО! строка выше должна быть определена в том месте, где есть возможно задать серверное время, например в index.php, но не в отдельном JS файле, так как получим ошибку. */ function Listener(url, successCallback, failureCallback) { var scope = this; var etag = 0; var lastModified = LOAD_TIME; var launched = false; var failure = false; this.successTimeout = 1000; this.failureTimeout = 0; var getTimeout = function () { return failure ? this.failureTimeout : this.successTimeout; }; var listen = function () { $.ajax(scope.ajaxOptions); } var beforeSend = function (jqXHR) { jqXHR.setRequestHeader("If-None-Match", etag); jqXHR.setRequestHeader("If-Modified-Since", lastModified); cw("BEFORE None-Match : "+etag); cw("BEFORE Modified : "+lastModified); }; var complete = function (jqXHR) { var timeout = getTimeout(); if (jqXHR.getResponseHeader('Etag') != null && jqXHR.getResponseHeader('Last-Modified') != null) { etag = jqXHR.getResponseHeader('Etag'); lastModified = jqXHR.getResponseHeader('Last-Modified'); } var timeout = jqXHR.statusText == 'success' ? scope.successTimeout : scope.failureTimeout; if (timeout > 0) { setTimeout(listen, timeout); } else { listen(); } }; this.ajaxOptions = { url : url, type : 'GET', async : true, error : failureCallback, success : successCallback, dataType : 'json', complete : complete, beforeSend : beforeSend, timeout: 1000 * 20 }; this.start = function (timeout) { if (!launched) { if (typeof(timeout) == 'undefined' || timeout == 0) { listen(); } else { setTimeout(listen, timeout); } launched = true; } }; this.start(); } </script>
Вызов обработчика ивентов выглядит примерно так:
<script> $(document).ready(function() { Listener('/listener?cid=chanel_1', onSuccess, onError); }); /* *onSuccess и onError - два обработчика (две функции) полученных данных от кометы, в случае успешного и неудачного выполнения запроса соответственно. */ function onSuccess (data) { if (data) { $("#messages").prepend(data.time + " | " +data.msg); } else { console.log("EMPTY DATA"); } } function onError () { alert("ERROR"); } </script>
Поскольку в примерах на сайте модуля нет кода, который мог бы отправлять сообещение сразу в несколько каналов, мы решили написать свою функцию.
Реализация на php:
<? function push ($cids, $text) { /* * $cids - ID канала, либо массив, у которого каждый элемент - ID канала * $text - сообщение, которое необходимо отправить */ $c = curl_init(); $url = 'http://server_name/publisher?cid='; $message = array( 'time' => time(), 'msg' => $text ); curl_setopt($c, CURLOPT_RETURNTRANSFER, true); curl_setopt($c, CURLOPT_POST, true); if (is_array($cids)) { foreach ($cids as $v) { curl_setopt($c, CURLOPT_URL, $url.$v); curl_setopt($c, CURLOPT_POSTFIELDS, json_encode($message)); $r = curl_exec($c); } } else { curl_setopt($c, CURLOPT_URL, $url.$cids); curl_setopt($c, CURLOPT_POSTFIELDS, json_encode($message)); $r = curl_exec($c); } curl_close($c); } // push (1, "Привет"); // push (array(1, 2, 3), "Привет"); // push (array('chanel_1' => 1, 'chanel_2' => 'user_100500', 'chanel_3' => 'global'), "Привет");
С установкой Dklab Realplexor, никаких проблем не возникло и полная документация доступна на сайте (http://dklab.ru/lib/dklab_realplexor) разработчика.
Однако, с nginx_http_push_module не всё так просто, если честно, на CentOS готовый пакет, включающий данный модуль, найти не удалось, поэтому собирали всё из исходников.
Итак приступим:
Создадим папочку куда будем скачивать все необходимые элементы
mkdir ~/nginx_push; cd ~/nginx_push;
Скачаем последние исходники с гитхаба:
git clone git://github.com/slact/nginx_http_push_module.git
Качаем последнюю версию nginx, в нашем случае это nginx-1.1.15
wget http://nginx.org/download/nginx-1.1.15.tar.gz;
Качаем дополнительные необходимые библиотеки:
wget ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/pcre-8.21.tar.gz; wget http://zlib.net/zlib-1.2.6.tar.gz;
Распаковываем архивы:
tar -zxf nginx-1.1.15.tar.gz; tar -zxf pcre-8.21.tar.gz tar -zxf zlib-1.2.6.tar.gz
Переходим в папку с исходниками nginx'а
cd nginx-1.1.15;
И конфигурируем nginx
./configure --prefix=/usr --sbin-path=/usr/sbin/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --pid-path=/var/run/nginx/nginx.pid --lock-path=/var/lock/nginx.lock --user=nginx --group=nginx --with-http_ssl_module --with-http_gzip_static_module --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/tmp/nginx/client/ --http-proxy-temp-path=/tmp/nginx/proxy/ --http-fastcgi-temp-path=/tmp/nginx/fcgi --with-pcre=../pcre-8.21 --with-zlib=../zlib-1.2.6 --with-http_perl_module --with-http_stub_status_module –add-module=../nginx_http_push_module/
Затем
make make install
теперь создадим файл /etc/init.d/nginx
nano /etc/init.d/nginx;
и положим туда такой скрипт:
#!/bin/sh ### BEGIN INIT INFO # Provides: nginx # Required-Start: $local_fs $remote_fs $network $syslog # Required-Stop: $local_fs $remote_fs $network $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: starts the nginx web server # Description: starts nginx using start-stop-daemon ### END INIT INFO PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin DAEMON=/usr/sbin/nginx NAME=nginx DESC=nginx # Include nginx defaults if available if [ -f /etc/default/nginx ]; then . /etc/default/nginx fi test -x $DAEMON || exit 0 set -e . /lib/lsb/init-functions test_nginx_config() { if $DAEMON -t $DAEMON_OPTS >/dev/null 2>&1; then return 0 else $DAEMON -t $DAEMON_OPTS return $? fi } case "$1" in start) echo -n "Starting $DESC: " test_nginx_config # Check if the ULIMIT is set in /etc/default/nginx if [ -n "$ULIMIT" ]; then # Set the ulimits ulimit $ULIMIT fi start-stop-daemon --start --quiet --pidfile /var/run/$NAME.pid \ --exec $DAEMON -- $DAEMON_OPTS || true echo "$NAME." ;; stop) echo -n "Stopping $DESC: " start-stop-daemon --stop --quiet --pidfile /var/run/$NAME.pid \ --exec $DAEMON || true echo "$NAME." ;; restart|force-reload) echo -n "Restarting $DESC: " start-stop-daemon --stop --quiet --pidfile \ /var/run/$NAME.pid --exec $DAEMON || true sleep 1 test_nginx_config start-stop-daemon --start --quiet --pidfile \ /var/run/$NAME.pid --exec $DAEMON -- $DAEMON_OPTS || true echo "$NAME." ;; reload) echo -n "Reloading $DESC configuration: " test_nginx_config start-stop-daemon --stop --signal HUP --quiet --pidfile /var/run/$NAME.pid \ --exec $DAEMON || true echo "$NAME." ;; configtest|testconfig) echo -n "Testing $DESC configuration: " if test_nginx_config; then echo "$NAME." else exit $? fi ;; status) status_of_proc -p /var/run/$NAME.pid "$DAEMON" nginx && exit 0 || exit $? ;; *) echo "Usage: $NAME {start|stop|restart|reload|force-reload|status|configtest}" >&2 exit 1 ;; esac exit 0
И теперь
chkconfig --add nginx chkconfig --level 345 nginx on
Всё. на этом установка nginx завершена, и можно запускать сервис nginx'a
service nginx start
Теперь перейдем к настройкам:
/publisher — служит для записи в канал и должен быть доступен только вашему серверу, иначе писать в него сможет кто угодно.
location /publisher { // обозначаем параметр для выбора канала, в нашем случае это ?cid= set $push_channel_id $arg_cid; push_publisher; push_message_timeout 1m; // таймаут жизнии сообщения push_message_buffer_length 20000; // буфер сообщений allow 127.0.0.1; deny all; }
/listener – доступен всем и служит для раздачи сообщений подписчикам канала.
location /listener { // обозначаем параметр для выбора канала, в нашем случае это ?cid= set $push_channel_id $arg_cid; push_subscriber; push_subscriber_concurrency broadcast; default_type text/plain; }
На этом по настройки всё. Решение всем хорошо, однако у нас возникли трудности с настройкой crossdomain-ajax, а в модуле, в главной ветке, поддержка jsonp, на данный момент, отсутствует. НО, был найден форк с данным функционалом (https://github.com/Kronuz/nginx_http_push_module). Его установка никак не отличается от оригинала.
Чтобы включить поддержку jsonp в раздел location /listener нужно добавить
// Обозначаем параметр для колбека /?callback= push_channel_jsonp_callback $arg_callback;
Теперь, в случае передачи в параметр callback данных, модуль будет заворачивать данные в jsonp.
Отдельной демки нет, но зарегистрированные пользователи в вк могут посмотреть работу на живом примере в нашей игре — vk.com/app2814545
На данной технологии полностью построены бои (собственно говоря, они и требовали этого риалтайма), казино, сообщения о получении / трате ресурсов и множество других событий.
