Во время разработки игры мы столкнулись с необходимостью обеспечения максимального риалтайм обмена данных между пользователями, что повлекло за собой эксперименты с различными 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
На данной технологии полностью построены бои (собственно говоря, они и требовали этого риалтайма), казино, сообщения о получении / трате ресурсов и множество других событий.