Сегодня, когда Node.js находится в полном разгаре, мы уже можем использовать его, чтобы сделать все виды вещей. Некоторое время назад HOST UP принял участие в мероприятии Geek Song. Во время этого события мы намерены создать игру, которая позволяет «людям с головой» больше общаться. Основной функцией является многопользовательское взаимодействие в режиме реального времени концепции партии LAN. Гонка гиков была лишь жалко 36 часов, требуя, чтобы все было быстро и быстро. При такой предпосылке первоначальная подготовка кажется немного «естественной». Кроссплатформенное решение для приложения мы выбрали узлы-Webkit, что достаточно просто и соответствует нашим требованиям.
Согласно требованиям, наше развитие может проводиться отдельно в соответствии с модулями. В этой статье конкретно описывается процесс разработки Spaceroom (нашу многопользовательскую платформу в реальном времени), включая серию исследований и попыток, а также решения некоторых ограничений на Node.js и саму платформу Webkit, а также предложение решений.
Начиная
Пропейный взгляд
Вначале дизайн Spaceroom определенно был вызван спросом. Мы надеемся, что эта структура может обеспечить следующие основные функции:
Может различить группу пользователей на основе комнат (или каналов)
Возможность получать инструкции от пользователей в группе сбора
При сопоставлении между каждым клиентом игровые данные могут точно транслироваться в соответствии с указанным интервалом
Может максимально исключить влияние задержки сети
Конечно, на более поздних этапах кодирования мы предоставили Spaceroom с большим количеством функций, включая паузу игры, генерируя последовательные случайные числа между каждым клиентом и т. Д. (Конечно, они могут реализованы самим в соответствии с требованиями, и нет необходимости использовать Spaceroom, структуру, которая работает на уровне связи, что является большим уровнем связи).
Апис
Распространство разделен на две части: спереди и задний конец. Работа, требуемая сервером, включает в себя поддержание списка номеров и предоставление функций создания и присоединения к комнате. Наши клиентские API выглядят так:
Spaceroom.connect (адрес, обратный вызов) Подключитесь к серверу
Spaceroom.createroom (обратный вызов) Создайте комнату
Spaceroom.joinroom (Roomid) присоединяйтесь
Spaceroom.on (Event, обратный вызов) прослушивает события
...
После того, как клиент подключается к серверу, он получает различные события. Например, пользователь в комнате может получить событие, где присоединяется новый игрок, или событие, где начинается игра. Мы даем клиенту «жизненный цикл», который будет в одном из следующих состояний в любое время:
Вы можете получить текущее состояние клиента через Spaceroom.state.
Использование структуры на стороне сервера относительно просто. Если вы используете файл конфигурации по умолчанию, вы можете напрямую запустить структуру на стороне сервера. У нас есть основное требование: код сервера может работать непосредственно в клиенте без необходимости отдельного сервера. Игроки, которые играли в PS или PSP, должны знать, о чем я говорю. Конечно, это также отлично запустить на специальном сервере.
Реализация логического кода является краткой здесь. Первое поколение Spaceroom завершило функцию сервера сокета, который содержит список номеров, включая статус комнаты, и общение игрового времени, соответствующее каждой комнате (сбор инструкций, вещание ковша и т. Д.). Для конкретной реализации, пожалуйста, обратитесь к исходному коду.
Синхронный алгоритм
Итак, как мы можем сделать вещи, отображаемые между каждым клиентом, согласованным в режиме реального времени?
Эта вещь звучит интересно. Подумайте о том, что нам нужен сервер, чтобы помочь нам доставить? Естественно, вы подумаете о том, что может вызвать логические несоответствия между различными клиентами: инструкции пользователей. Поскольку коды, которые касаются игровой логики, одинаковы, учитывая те же условия, код заканчивает тот же результат. Единственная разница - это различные команды игроков, полученные во время игры. Правильно, нам нужен способ синхронизировать эти инструкции. Если все клиенты могут получить одинаковую инструкцию, то все клиенты теоретически могут иметь один и тот же результат работы.
Алгоритмы синхронизации онлайн -игр представляют собой все виды странных и применимых сценариев. Spaceroom использует алгоритмы синхронизации, аналогичные концепции блокировки кадров. Мы разделяем график на интервалы один за другим, и каждый интервал называется ведром. Ведро используется для загрузки инструкций и поддерживается на стороне сервера. В конце каждого периода времени ведра сервер транслирует ведро всем клиентам. После того, как клиент получает ведро, из него извлекаются инструкции, а затем выполняются после проверки.
Чтобы уменьшить влияние задержки сети, каждая инструкция, полученная сервером от клиента, будет доставлена в соответствующее ведро в соответствии с определенным алгоритмом, и специально выполняются следующие шаги:
Пусть order_start будет временем команды, которая несет команду, и t - время начала ведра, где находится order_start.
Если t + dower_time <= время начала ведра, которое в настоящее время собирает инструкцию, доставить команду в ведро, которое в настоящее время собирает инструкцию, в противном случае продолжайте шаг 3
Доставить инструкцию в соответствующее ведро T + Dolement_time
где Dolement_time - это согласованное время задержки сервера, которое можно воспринимать в качестве средней задержки между клиентами. Значение по умолчанию в Spaceroom составляет 80, а значение по умолчанию длины ведра составляет 48. В конце каждого периода ведра сервер транслирует это ведро всем клиентам и начинает получать инструкции для следующего ведра. Клиент управляет временной ошибкой в приемлемом диапазоне при автоматическом выполнении сопоставления в логике на основе полученного интервала ведра.
Это означает, что при нормальных обстоятельствах клиент будет получать ведро, отправляемое с сервера каждые 48 мс. Когда необходимо обработать ведро, клиент будет справляться с этим соответственно. Предполагая, что клиент FPS = 60, он будет получать ведро каждые 3 кадра или около того, и логика будет обновляться в соответствии с этим ведром. Если ведро не было получено после истечения времени из -за колебаний сети, клиент делает паузу игровой логики и ожидает. В то время как в ведре логика может быть обновлена с помощью метода LERP.
В случае Dolement_time = 80, bucte_size = 48, любая инструкция будет задерживается как минимум на 96 мс. Измените эти два параметра, например, в случае Dolement_time = 60, bucte_size = 32, любая инструкция будет задерживаться как минимум на 64 мс.
Кровавый инцидент, вызванный таймером
Глядя на все это, наша структура должна иметь точный таймер, когда он работает. Выполнить трансляцию ведра в фиксированном интервале. Конечно, мы сначала подумали об использовании setInterval (), но в следующую секунду мы поняли, насколько ненадежной эта идея: Neaughty SetInterval (), кажется, имеет очень серьезные ошибки. И что так плохо, так это то, что каждая ошибка будет накапливаться, вызывая все более серьезные последствия.
Таким образом, мы сразу подумали об использовании settimeout (), чтобы сделать нашу логику примерно устойчивой вокруг указанного интервала, динамически исправляя время следующего прибытия. Например, это время settimeout () на 5 мс меньше, чем ожидалось, поэтому мы позволим ему на 5 мс ранее в следующий раз. Тем не менее, результаты теста не являются удовлетворительными, и это не достаточно элегантно, независимо от того, как вы на это смотрите.
Поэтому нам нужно изменить наше мышление. Можно ли сделать SetTimeout () истекать как можно быстрее, и затем мы проверяем, достигло ли текущее время целевое время. Например, в нашем цикле, используя SetTimeout (обратный вызов, 1), чтобы продолжать проверять время, которое кажется хорошей идеей.
Разочаровывающий таймер
Мы сразу же написали кусок кода, чтобы проверить наши идеи, и результаты были разочаровывающими. В текущей последней стабильной версии Node.js (v0.10.32) и платформе Windows запустите этот кусок кода:
Кода -копия выглядит следующим образом:
var sum = 0, count = 0;
функциональный тест () {
var теперь = date.now ();
settimeout (function () {
var diff = date.now () - сейчас;
sum += diff;
count ++;
тест();
});
}
тест();
Через некоторое время введите сумму/подсчет в консоли, и вы можете увидеть результат, аналогичный:
Кода -копия выглядит следующим образом:
> Сумма / счет
15.624555160142348
Что?! Я хочу интервал 1 мс, но вы говорите мне, что фактический средний интервал составляет 15,625 мс! Эта картина просто слишком красивая. Мы сделали тот же тест на Mac, и результат составил 1,4 мс. Итак, мы были в замешательстве: какого черта это? Если бы я был фанатом Apple, я мог бы прийти к выводу, что Windows слишком мусор и отказался от Windows. К счастью, я был строгим фронтальным инженером, поэтому я начал продолжать думать об этом номере.
Подожди, почему этот номер так знакомо? Будет ли это число слишком похоже на максимальный интервал таймера под Windows? Я сразу же загрузил часы для тестирования, и после запуска консоли я получил следующие результаты:
Кода -копия выглядит следующим образом:
Максимальный интервал таймера: 15,625 мс
Минимальный интервал таймера: 0,500 мс
Текущий интервал таймера: 1,001 мс
Конечно,! Глядя на руководство Node.js, мы можем увидеть описание SetTimeout, как это:
Фактическая задержка зависит от внешних факторов, таких как гранулярность ОС и нагрузка на системную нагрузку.
Тем не менее, результаты теста показывают, что эта фактическая задержка является максимальным интервалом таймера (обратите внимание, что текущий интервал таймера системы составляет всего 1,001 мс), что в любом случае неприемлемо. Сильное любопытство заставляет нас просматривать исходный код node.js, чтобы увидеть истину.
Ошибка в node.js
Я считаю, что большинство из вас и у меня есть определенное понимание механизма ровного петли Node.js. Глядя на исходный код реализации таймера, мы можем приблизительно понять принцип реализации таймера. Начнем с основной петли цикла событий:
Кода -копия выглядит следующим образом:
while (r! = 0 && loop-> stop_flag == 0) {
/* Обновление глобального времени*/
uv_update_time (loop);
/* Проверьте, истекает ли таймер и выполнить соответствующий обратный вызов таймера*/
uv_process_timers (loop);
/* Позвоните на холостое время, если нечего делать. */
if (loop-> pending_reqs_tail == null &&
loop-> endgame_handles == null) {
/* Предотвратить выход с выходами события*/
uv_idle_invoke (loop);
}
uv_process_reqs (loop);
uv_process_endgames (loop);
uv_prepare_invoke (loop);
/* Соберите события io*/
(*Опрос) (Loop, Loop-> idle_handles == null &&
loop-> wending_reqs_tail == null &&
loop-> endgame_handles == null &&
! Loop-> Stop_flag &&
(loop-> active_handles> 0 ||
! ngx_queue_empty (& loop-> active_reqs)) &&
! (Mode & UV_RUN_NOWAIT));
/* setimmediate () и т. Д.*/
uv_check_invoke (loop);
r = uv__loop_alive (loop);
if (mode & (uv_run_once | uv_run_nowait))
перерыв;
}
Исходный код функции uv_update_time следующим образом: (https://github.com/joyent/libuv/blob/v0.10/src/win/timer.c))))
Кода -копия выглядит следующим образом:
void uv_update_time (uv_loop_t* loop) {
/* Получите текущее системное время*/
Dword ticks = getTickCount ();
/ * Предполагается, что BAGIN_INTEGER.QUADPART имеет тот же тип */
/* Loop-> время, которое случается. Есть ли способ утверждать это? */
Big_integer* time = (laign_integer*) & loop-> time;
/* Если таймер обернут, добавьте 1 к его слову высокого порядка. */
/ * UV_Poll должен убедиться, что таймер никогда не может переполняться больше, чем */
/* Однажды между двумя последующими вызовами uv_update_time. */
if (ticks <time-> lowpart) {
время-> HighPart += 1;
}
время-> lowpart = клещи;
}
Внутренняя реализация этой функции использует функцию getTickCount () Windows для установки текущего времени. Проще говоря, после вызова функции Settimeout, после серии борьбы, внутренний таймер-> reud будет установлен на текущее время для петли + тайм-аут. В цикле событий сначала обновите время текущего цикла через UV_UPDATE_TIME, а затем проверьте, истекает ли таймер в UV_PROCESS_TIMES. Если так, введите мир JavaScript. Прочитав всю статью, цикл событий примерно похож на этот процесс:
Обновить глобальное время
Проверьте таймер. Если таймер истекает, выполните обратный вызов.
Проверьте очередь REQS и выполните запрос на ожидание
Введите функцию опроса, чтобы собрать события ввода -вывода. Если прибывает событие IO, добавьте соответствующую функцию обработки в очередь REQS для выполнения в следующем цикле событий. Внутри функции опроса метод системы вызывается для сбора событий ввода -вывода. Этот метод приведет к блокированию процесса до тех пор, пока не появится событие IO или не будет достигнуто время ожидания. Когда этот метод называется, время ожидания устанавливается в то время, когда истекает самый последний таймер. Это означает, что события IO собираются путем блокировки, а максимальное время блокировки - последнее время следующего таймера.
Исходный код одной из функций опроса под Windows:
Кода -копия выглядит следующим образом:
static void uv_poll (uv_loop_t* loop, int block) {
Dword Bytes, тайм -аут;
Ulong_ptr Key;
Перекрывать* перекрывать;
uv_req_t* req;
if (block) {
/* Уберите время истечения срока последнего таймера*/
timeout = uv_get_poll_timeout (loop);
} еще {
Тайм -аут = 0;
}
GetQueuedCompletionStatus (Loop-> iocp,
и байты,
&ключ,
и перекрывать,
/* В большинстве блоков до истечения следующего таймера*/
тайм -аут);
if (перекрытие) {
/ * Пакет был отменен */
req = uv_overlapped_to_req (перекрытие);
/* Вставьте события IO в очередь*/
uv_insert_pending_req (loop, req);
} else if (getLasterRor ()! = wait_timeout) {
/ * Серьезная ошибка */
uv_fatal_error (getLasterRor (), "getQueuedCompletionStatus");
}
}
Следуя вышеуказанным шагам, предполагая, что мы устанавливаем тайм -аут = 1 мс, функция опроса будет блокировать не более 1 мс, а затем восстанавливаться после восстановления (если в течение периода нет событий IO). Продолжая вводить цикл цикла событий, UV_UPDATE_TIME обновит время, а затем UV_PROCESS_TIMERS обнаружит, что наш таймер истекает и выполнит обратный вызов. Таким образом, предварительный анализ заключается в том, что либо у UV_UPDATE_TIME есть проблема (текущее время не обновляется правильно), либо функция опроса ждет 1 мс, а затем восстанавливается. Есть проблема с этим 1 мс.
Глядя на MSDN, мы удивительно обнаружили описание функции GetTickCount:
Разрешение функции getTickCount ограничено разрешением системного таймера, который обычно находится в диапазоне от 10 миллионов секунд до 16 миллионов секунд.
Точность GetTickCount такая грубая! Предположим, что функция опроса правильно блокирует время 1 мс, но в следующий раз UV_UPDATE_TIME выполняется, текущее время цикла не обновляется правильно! Таким образом, наш таймер не был признан истекшим, поэтому опрос ждал еще 1 мс и вошел в следующий цикл событий. Пока getTickCount наконец-то обновляется правильно (так называемые 15,625 мс обновляются один раз), текущее время цикла обновляется, и наш таймер срок действия истекает в UV_PROCESS_TIMERS.
Спросите Webkit о помощи
Этот исходный код node.js очень беспомощен: он использовал функцию времени с низкой точностью и ничего не сделал. Но мы сразу подумали, что, поскольку мы используем Node-Webkit, в дополнение к SetTimeout Node.js, у нас также есть Settimeout Chromium. Напишите тестовый код и используйте наш браузер или Node-webkit для запуска: http://marks.lrednight.com/test.html#1 (# с последующим номером указывает измеренный интервал). Результат заключается в следующем:
Согласно спецификациям HTML5, теоретический результат должен заключаться в том, что первые 5 результатов составляют 1 мс, а следующие результаты - 4 мс. Результаты, отображаемые в тестовом случае, начинаются с третьего раз, что означает, что данные в таблице теоретически должны составлять 1 мс в первые три раза, а впоследствии результаты составляют 4 мс. Результат имеет определенные ошибки, и, согласно правилам, наименьший теоретический результат, который мы можем получить, составляет 4 мс. Хотя мы не удовлетворены, это, очевидно, гораздо более удовлетворительно, чем результат узла.js. Сильная тенденция любопытства давайте посмотрим на исходный код хрома, чтобы увидеть, как он реализован. (https://chromium.googlesource.com/chromium/src.git/+/38.0.2125.101/base/time/time_win.cc)
Во -первых, Chromium использует функцию Timegettime () при определении текущего времени цикла. Глядя на MSDN, вы можете обнаружить, что на точность этой функции влияет текущий интервал таймера системы. На нашей тестовой машине теоретически упомянуто выше 1,001 мс. Однако по умолчанию интервал таймера является его максимальным значением (15,625 мс на тестовой машине), если только приложение не изменяет глобальный интервал таймера.
Если вы следите за новостями в ИТ -индустрии, вы, должно быть, видели такие новости. Кажется, что наш хром установил интервал таймера очень маленьким! Кажется, нам не нужно беспокоиться о интервале таймера системы? Не будь слишком счастлив слишком рано, такой ремонт дал нам удар. Фактически, эта проблема была исправлена в Chrome 38. Должны ли мы использовать исправление предыдущего узла-Webkit? Это, очевидно, недостаточно элегантно и не позволяет нам использовать более эффективную версию хрома.
Посмотрев дальше на исходный код хрома, мы можем обнаружить, что при наличии таймера и тайм -аута тайм -аута <32 мс, Chromium изменит глобальный интервал таймера системы для достижения таймера с точностью менее 15,625 мс. (Просмотреть исходный код) При запуске таймера будет включено то, что называется HighresolutionTimerManager. Этот класс будет вызывать функцию enablehighresolutiontimer на основе типа мощности текущего устройства. В частности, когда текущее устройство использует батарею, оно будет вызовом enablehighresolutiontimer (false), и истина будет передаваться при использовании питания. Реализация функции EnableHighresolutionTimer заключается в следующем:
Кода -копия выглядит следующим образом:
void Time :: enableHighresolutionTimer (bool enable) {
Base :: AutoLock Lock (g_high_res_lock.get ());
if (g_high_res_timer_enabled == enable)
возвращаться;
g_high_res_timer_enabled = enable;
if (! g_high_res_timer_count)
возвращаться;
// Поскольку g_high_res_timer_count! = 0, Activatehighresolutiontimer (true)
// был вызван, который вызвал TimeBeginPeriod с G_HIGH_RES_TIMER_ENABLED
// со значением, которое является противоположностью | включить |. С этой информацией мы
// время вызовы с тем же значением, которое использовалось в TimeBeginPeriod и
// Поэтому отменить эффект периода.
if (inable) {
TimeendPeriod (kmintimerintervallowresms);
TimeBeginPeriod (kmintimerintervalhighresms);
} еще {
TimeendPeriod (kmintimerintervalhighresms);
TimeBeginPeriod (kmintimerintervallowresms);
}
}
Где, kmintimerintervallowresms = 4 и kmintimerintervalhighresms = 1. TimeBeginPeriod и TimeNdeRePiod - это функции, предоставляемые Windows для изменения интервала системного таймера. То есть при подключении к источнику питания самый маленький интервал таймера, который мы можем получить, составляет 1 мс, в то время как при использовании батареи это 4 мс. Поскольку наш цикл непрерывно вызывает Settimeout, согласно спецификации W3C, минимальный интервал также составляет 4 мс, поэтому я чувствую облегчение, это мало влияет на нас.
Еще одна точная проблема
В начале мы обнаружили, что результаты испытаний показывают, что интервал Settimeout не стабилен на 4 мс, но непрерывно колеблется. Http://marks.lrednight.com/test.html#48 Результаты испытаний также показывают, что интервалы побеждают от 48 до 49 мс. Причина в том, что в цикле событий Chromium и Node.js точность вызова функции Windows в ожидании события IO влияет на таймер текущей системы. Реализация игровой логики требует функции requestAnimationFrame (постоянно обновлять холст), которая может помочь нам установить интервал таймера, по крайней мере, для KmintimerIntervallowResms (потому что ему нужен таймер 16 мс, что запускает требование высокотехнологичного таймера). Когда тестовая машина использует мощность, интервал таймера системы составляет 1 мс, поэтому результат теста имеет ошибку ± 1 мс. Если ваш компьютер не изменил интервал таймера системы и запустит испытание #48 выше, Макс может достигать 48+16 = 64 мс.
Используя реализацию Chromium Settimeout, мы можем управлять ошибкой SetTimeout (FN, 1) до 4 мс, в то время как ошибка SetTimeout (FN, 48) может управлять ошибкой SetTimeout (FN, 48) до 1 мс. Итак, у нас есть новый план в наших умах, что делает наш код выглядеть следующим образом:
Кода -копия выглядит следующим образом:
/ * Получите максимальную отделку интервала */
вар декорация = getmaxIntervaldeviation (bucketsize); // Bucketsize = 48, отклонение = 2;
function gameloop () {
var теперь = date.now ();
if (предыдущий BucketSize <= сейчас) {
Предыдущий Bucket = сейчас;
dologic ();
}
if (premorbucket + bucketsize - date.now ()> украшение) {
// подождать 46 мс. Фактическая задержка составляет менее 48 мс.
SetTimeout (Gameloop, BucketSize - Design);
} еще {
// занят ожиданием. Используйте SetImmediate вместо Process.nexttick, потому что первое не блокирует события IO.
setimmediate (Gameloop);
}
}
Приведенный выше код позволяет нам ждать некоторое время с ошибкой, меньшей, чем bucte_size (определение Bucket_size) вместо непосредственно равного bucket_size. Даже если максимальная ошибка возникает в задержке 46 мс, согласно вышеуказанной теории, фактический интервал составляет менее 48 мс. В остальное время мы используем метод ожидания занятого ожидания, чтобы убедиться, что наш Gameloop выполняется в интервале с достаточной точностью.
Хотя мы в некоторой степени решили проблему с хромом, это, очевидно, недостаточно элегантно.
Помните наш первоначальный запрос? Наш код на стороне сервера должен быть в состоянии работать непосредственно на компьютере с средой Node.js без клиента Node-Webkit. Если вы запускаете вышеуказанный код напрямую, значение определения составляет не менее 16 мс, что означает, что за каждые 48 мс мы должны ждать 16 мс. Коэффициент использования процессора вырос.
Неожиданный сюрприз
Это так раздражает. Никто не заметил такую большую ошибку в node.js? Ответ действительно делает нас в восторге. Эта ошибка была исправлена в версии v.0.11.3. Вы также можете увидеть модифицированные результаты, непосредственно просмотрев основную ветвь кода Libuv. Конкретный подход состоит в том, чтобы добавить тайм -аут в текущее время цикла после того, как функция опроса ожидает завершения. Таким образом, даже если GetTickCount не отреагировал, мы все равно добавили это время ожидания после того, как опрос ждал. Таким образом, таймер может истекать плавно.
Другими словами, проблема, которая много работала в течение долгого времени, была решена в V.0.11.3. Однако наши усилия не были напрасными. Потому что, даже если функция GetTickCount устранена, на сами функцию опроса влияет таймер системы. Одним из решений является написание плагина Node.js, чтобы изменить интервалы системных таймеров.
Тем не менее, начальные настройки для нашей игры на этот раз не являются серверами. После того, как клиент создает комнату, он становится сервером. Код сервера может работать в среде Node-Webkit, поэтому приоритет проблем таймера в Windows Systems не является самым высоким. Следуя решению, которое мы дали выше, результатов достаточно, чтобы удовлетворить нас.
конец
После решения проблемы таймера наша реализация структуры в основном не будет затруднена. Мы предоставляем поддержку WebSocket (в чистых средах HTML5) и настраиваем протокол связи для достижения более высокой поддержки сокетов (в средах узлов-Webkit). Конечно, функции Spaceroom были относительно простыми в начале, но поскольку спрос был предложен и увеличивалось время, мы постепенно улучшаем эту структуру.
Например, когда мы обнаружили, что когда нам нужно генерировать последовательные случайные числа в нашей игре, мы добавили эту функцию в Spaceroom. В начале игры Spaceroom будет распространять случайные семена числа. Пространство клиента предоставляет метод для использования случайности MD5 для генерации случайных чисел с помощью случайных чисел семян.
Это кажется довольно облегченным. Я также многому научился в процессе написания такой основы. Если вы заинтересованы в Spaceroom, вы также можете принять участие в нем. Я считаю, что Spaceroom будет использовать свои кулаки и ноги в большем количестве мест.