В этой статье я расскажу о разработке и эволюции технической части браузерной игры «Пути Истории».
Уделю внимание выбору языка программирования, базы данных, технологии и архитектуры. Расскажу о хостинге.
Пути Истории — это массовая браузерная стратегическая игра. Проект начинался с энтузиазма одного человека и вырос до серьзного проекта с немалой аудиторией.
Для разработки движка был выбран язык C++ по трем причинам:
Суть работы движка — это получение запроса, формирование и отдача страницы.
Базу данных выбрал MySQL лишь потому, что она достаточно популярна и подобные проекты часто делаются с использованием MySQL. В тот момент я не имел опыта работы с базами данных.
Сразу встал вопрос архитектуры. Была выбрана следующая модель:
Движок делится на две части (назовем их Д1, Д2).
Д1 получает запрос, передает в один из 8-ми свободных потоков. Поток анализирует запрос, запрашивает необходимые данные из базы, формирует страницу и возвращает её. Д1 не умеет вносить изменения в базу данных. Для уменьшения числа запросов к базе данных многие данные кэшируются на Д1.
В некоторых случаях Д1 получает запрос на изменение состояния мира (заказано строение в городе игрока, отправлены войска и т. п.). В этом случае Д1 передает запрос на Д2 (связь на сокетах). К Д2 может подключиться более одного Д1 (каждый из которых имеет по 8 нитей, и следовательно может одновременно передать 8 инструкций на Д2). Д2 одновременно выполняет только одну инструкцию, остальные ждут в очереди. Выполнение одной инструкции для базы данных осуществляется как одна транзакция. При успешной отработке инструкции в базу вносятся изменения, при недопустимой инструкции транзакция отменяется и все её изменения откатываются. Важно отметить, что недопустимые инструкции отсекаются еще на Д1, но бывает, что инструкция стала недопустимой уже после передачи её на Д2 (например, в предыдущей инструкции город потратил ресурсы, а в данной пытается заказать строение за несуществующие ресурсы, но обе инструкции пришли почти одновременно). Вся система может работать без Д2, но только в режиме «read only» — ничего изменить нельзя, все таймеры событий идут, но по окончании «зависают». Если после этого включить Д2, то работа системы восстановится, словно никакого сбоя не было, все события обработаются в правильном порядке.
Вначале использовался веб сервер Apache. Он был выбран потому, что популярен и имеет сборку под Windows. Д1 подключался к Apache по технологии ISAPI, тоесть как dll библиотека. Apache принимал запросы и передавал их в подключенную к себе библиотеку. Сам по себе Apache работал достаточно медленно. Поэтому в какой-то момент проект был переведен на связку nginx + FastCGI.
Веб сервер nginx очень удобен как в настройке, так и в использовании. Скорость отдачи страниц возросла. К тому же nginx очень быстро раздает статический контент.
Как работает FastCGI? Сам движок из dll библиотеки был переделан в самостоятельное приложение. Приложение принимает запросы от веб сервера через сокеты, обрабатывает их, генерирует страницы и, через те же сокеты, возвращает страницы веб серверу. При этом сокеты остаются открытыми и по ним поступают новые запросы. Узнать больше о разработке на C++ с использованием протокола FastCGI можно здесь.
Теперь о хостинге.
До запуска проекта все работало на обычном домашнем компьютере по обычному домашнему кабельному Интернету.
В то время не было финансовой возможности арендовать сервер в дата центре, поэтому первый игровой мир был запущен все на том же домашнем компьютере. Это создавало ряд неудобств: доступ к сети нестабилен, иногда выключался свет в доме, провайдер не выдает трафик по заявленному тарифу и часто проводит технические работы. Проект начал наполняться игроками, нагрузка росла. На обслуживание 1-го мира был поставлен еще один компьютер с другим подключением к Интернету. Сейчас один мир легко работает на 1-м сервере, но тогда еще не все было оптимизировано, а используемые компьютеры были слабыми.
Вскоре был открыт следующий мир. На него было использовано еще два компьютера и два подключения к сети. Уже готовился к запуску мир 3. Все эти компьютеры распологались у меня дома в жилой комнате. С ростом числа серверов росло число проблем. Я уже не мог покидать дом, так как постоянно что-то падало. Кроме форс-мажорных проблем были еще и обычные баги. При возникновении любой неправильной ситуации приложение сразу падало по assert'у, не пытаясь как-то выйти из сложившейся ситуации. Такое решение было выбрано специально. Это заставляло меня всегда в первую очередь бороться в багами, а не тащить их через весь период разработки.
Проект начал приносить доход, и был арендован сервер в дата центре. Сайт игры и оба мира были перенесены на него. Администрировать систему стало значительно проще, но расходы возросли. Третий мир тоже был запущен на этом сервере, но после DDoS атаки я перенес его на отдельный сервер, чтобы первые два мира были вне угрозы.
Разработка велась и тестировалась на ОС Windows. Но код писался сразу без привязки к данной ОС, и, в дальнейшем, понадобился всего один день, чтобы поправить код и скомпилировать проект под FreeBSD.
Для работы с потоками была выбрана библиотека POSIX. Для создания графических файлов я использовал библиотеку FreeImage.
Мониторинг системы.
Изначально мониторинг системы был при помощи мониторов! Любое «падение» сервера можно было обнаружить в виде окна с ошибкой или отсутствия исходящего трафика на графике. Приходилось даже ночью просыпаться по несколько раз и окидывать взглядом все мониторы.
Так долго продолжаться не могло и был написан специальный php скрипт, который постоянно опрашивает сервера, собирает с них данные о состоянии и, по необходимости, отправляет письмо на почту или SMS сообщение на телефон. Данный скрипт был запущен на бесплатном хостинге, где работает и по сей день. Благодаря ему всегда удается оперативно узнать о проблемах и, по возможности, устранить их немедленно.
В следующих статьях я расскажу про разработку проекта от идеи до релиза, про технические решения в движке и форматы хранения данных в базе, про резервное копирование данных и защиту от атак, про механизм формирования страниц.
Основа Д1:
Основа Д2:
Весь представленный код местами специально упрощен для наглядности. Некоторые классы и фукнции опущены.
Уделю внимание выбору языка программирования, базы данных, технологии и архитектуры. Расскажу о хостинге.
Пути Истории — это массовая браузерная стратегическая игра. Проект начинался с энтузиазма одного человека и вырос до серьзного проекта с немалой аудиторией.
Для разработки движка был выбран язык C++ по трем причинам:
- он быстр, что важно для данного проекта;
- гибок, что позволяет реализовывать некоторые возможности оптимально;
- я его знаю лучше, чем другие подходящие.
Суть работы движка — это получение запроса, формирование и отдача страницы.
Базу данных выбрал MySQL лишь потому, что она достаточно популярна и подобные проекты часто делаются с использованием MySQL. В тот момент я не имел опыта работы с базами данных.
Сразу встал вопрос архитектуры. Была выбрана следующая модель:
Движок делится на две части (назовем их Д1, Д2).
Д1 получает запрос, передает в один из 8-ми свободных потоков. Поток анализирует запрос, запрашивает необходимые данные из базы, формирует страницу и возвращает её. Д1 не умеет вносить изменения в базу данных. Для уменьшения числа запросов к базе данных многие данные кэшируются на Д1.
В некоторых случаях Д1 получает запрос на изменение состояния мира (заказано строение в городе игрока, отправлены войска и т. п.). В этом случае Д1 передает запрос на Д2 (связь на сокетах). К Д2 может подключиться более одного Д1 (каждый из которых имеет по 8 нитей, и следовательно может одновременно передать 8 инструкций на Д2). Д2 одновременно выполняет только одну инструкцию, остальные ждут в очереди. Выполнение одной инструкции для базы данных осуществляется как одна транзакция. При успешной отработке инструкции в базу вносятся изменения, при недопустимой инструкции транзакция отменяется и все её изменения откатываются. Важно отметить, что недопустимые инструкции отсекаются еще на Д1, но бывает, что инструкция стала недопустимой уже после передачи её на Д2 (например, в предыдущей инструкции город потратил ресурсы, а в данной пытается заказать строение за несуществующие ресурсы, но обе инструкции пришли почти одновременно). Вся система может работать без Д2, но только в режиме «read only» — ничего изменить нельзя, все таймеры событий идут, но по окончании «зависают». Если после этого включить Д2, то работа системы восстановится, словно никакого сбоя не было, все события обработаются в правильном порядке.
Вначале использовался веб сервер Apache. Он был выбран потому, что популярен и имеет сборку под Windows. Д1 подключался к Apache по технологии ISAPI, тоесть как dll библиотека. Apache принимал запросы и передавал их в подключенную к себе библиотеку. Сам по себе Apache работал достаточно медленно. Поэтому в какой-то момент проект был переведен на связку nginx + FastCGI.
Веб сервер nginx очень удобен как в настройке, так и в использовании. Скорость отдачи страниц возросла. К тому же nginx очень быстро раздает статический контент.
Как работает FastCGI? Сам движок из dll библиотеки был переделан в самостоятельное приложение. Приложение принимает запросы от веб сервера через сокеты, обрабатывает их, генерирует страницы и, через те же сокеты, возвращает страницы веб серверу. При этом сокеты остаются открытыми и по ним поступают новые запросы. Узнать больше о разработке на C++ с использованием протокола FastCGI можно здесь.
Теперь о хостинге.
До запуска проекта все работало на обычном домашнем компьютере по обычному домашнему кабельному Интернету.
В то время не было финансовой возможности арендовать сервер в дата центре, поэтому первый игровой мир был запущен все на том же домашнем компьютере. Это создавало ряд неудобств: доступ к сети нестабилен, иногда выключался свет в доме, провайдер не выдает трафик по заявленному тарифу и часто проводит технические работы. Проект начал наполняться игроками, нагрузка росла. На обслуживание 1-го мира был поставлен еще один компьютер с другим подключением к Интернету. Сейчас один мир легко работает на 1-м сервере, но тогда еще не все было оптимизировано, а используемые компьютеры были слабыми.
Вскоре был открыт следующий мир. На него было использовано еще два компьютера и два подключения к сети. Уже готовился к запуску мир 3. Все эти компьютеры распологались у меня дома в жилой комнате. С ростом числа серверов росло число проблем. Я уже не мог покидать дом, так как постоянно что-то падало. Кроме форс-мажорных проблем были еще и обычные баги. При возникновении любой неправильной ситуации приложение сразу падало по assert'у, не пытаясь как-то выйти из сложившейся ситуации. Такое решение было выбрано специально. Это заставляло меня всегда в первую очередь бороться в багами, а не тащить их через весь период разработки.
Проект начал приносить доход, и был арендован сервер в дата центре. Сайт игры и оба мира были перенесены на него. Администрировать систему стало значительно проще, но расходы возросли. Третий мир тоже был запущен на этом сервере, но после DDoS атаки я перенес его на отдельный сервер, чтобы первые два мира были вне угрозы.
Разработка велась и тестировалась на ОС Windows. Но код писался сразу без привязки к данной ОС, и, в дальнейшем, понадобился всего один день, чтобы поправить код и скомпилировать проект под FreeBSD.
Для работы с потоками была выбрана библиотека POSIX. Для создания графических файлов я использовал библиотеку FreeImage.
Мониторинг системы.
Изначально мониторинг системы был при помощи мониторов! Любое «падение» сервера можно было обнаружить в виде окна с ошибкой или отсутствия исходящего трафика на графике. Приходилось даже ночью просыпаться по несколько раз и окидывать взглядом все мониторы.
Так долго продолжаться не могло и был написан специальный php скрипт, который постоянно опрашивает сервера, собирает с них данные о состоянии и, по необходимости, отправляет письмо на почту или SMS сообщение на телефон. Данный скрипт был запущен на бесплатном хостинге, где работает и по сей день. Благодаря ему всегда удается оперативно узнать о проблемах и, по возможности, устранить их немедленно.
В следующих статьях я расскажу про разработку проекта от идеи до релиза, про технические решения в движке и форматы хранения данных в базе, про резервное копирование данных и защиту от атак, про механизм формирования страниц.
Основа Д1:
void* operateRequest(void* listen_socket)
{
//инициализация
FCGX_Request request;
assert(!FCGX_InitRequest(&request, *(int*)listen_socket, 0));
Session* s = new Session;
//условно бесконечный цикл по приему запросов
while(FCGX_Accept_r(&request) == 0) {
stringstream out;
stringstream header;
header << "Content-type: text/html";
//чтение параметров запроса
string query;
string addr;
string referer;
string post;
string cookie;
string agent;
int content_lenght = 0;
for(char** envp = request.envp; *envp; ++envp) {
string v = *envp;
string::size_type e = v.find('=');
string p = v.substr(0, e);
if(p == "REQUEST_URI") query = v.substr(e + 2, v.length());
if(p == "REMOTE_ADDR") addr = v.substr(e + 1, v.length());
if(p == "HTTP_COOKIE") cookie = v.substr(e + 1, v.length());
if(p == "HTTP_REFERER") referer = v.substr(e + 1, v.length());
if(p == "CONTENT_LENGTH") content_lenght = toInt(v.substr(e + 1, v.length()));
if(p == "HTTP_USER_AGENT") agent = v.substr(e + 1, v.length());
}
//чтение тела запроса
maximize(content_lenght, 9999);
char p[10000];
FCGX_GetStr(p, content_lenght, request.in);
p[content_lenght] = 0;
post = p;
//основная функция. Генерирует header и страницу
s->work(header, out, addr, cookie, referer, query, post);
//сборка и возвращение страницы
header << "\r\n\r\n" << out.str();
FCGX_PutStr(header.str().c_str(), int(header.str().length()), request.out);
FCGX_Finish_r(&request);
}
return 0;
}
int main() {
assert(initSocketSystem());
assert(!FCGX_Init());
int listen_socket = FCGX_OpenSocket(":8000", 400);
assert(listen_socket >= 0);
//создать потоки
for(int i = 0; i < threads; ++i) {
pthread_t thread;
assert(pthread_create(&thread, 0, operateRequest, (void*)&listen_socket) == 0);
}
while(true) sleep(1000);
return 0;
}
Основа Д2:
void operateCommand(asComType com, Socket& sock)
{
//критическая секция. В ней работает одновременно только один поток
pthread_mutex_lock(&ascs);
bool res;
//сообщить бд о начале транзакции
assert(sql.put("BEGIN"));
switch(com) {
case ASC_TOWNUPDATE:
{
//инструкция на простое обновление состояния города
int id = sock.readVal<int>();
res = asUpdateTown(id);
}
break;
//другие инструкции
//…
//…
//…
}
//в зависимости от успешности выполнения инструкции заканчиваем или отменяем транзакцию
assert(sql.put(res ? "COMMIT" : "ROLLBACK"));
//возвращаем результат
sock.sendVal(res);
//критическая секция заканчивается
pthread_mutex_unlock(&ascs);
}
void* clientThread(void* client_socket)
{
Socket& sock = *(Socket*)client_socket;
asComType com;
int bytes;
//висеть в ожидании запроса
while((bytes = sock.readVal(com)) && bytes >= 0) {
//обработать запрос
operateCommand(com, sock);
}
delete &sock;
return 0;
}
int main()
{
while(Socket* client = sock.listen()) {
//создать поток для чтения и обработки данных сокета
pthread_t thread;
assert(pthread_create(&thread, 0, clientThread, (void*)client) == 0);
}
return 0;
}
Весь представленный код местами специально упрощен для наглядности. Некоторые классы и фукнции опущены.