В этой статье я расскажу о разработке и эволюции технической части браузерной игры «Пути Истории».
Уделю внимание выбору языка программирования, базы данных, технологии и архитектуры. Расскажу о хостинге.
Пути Истории — это массовая браузерная стратегическая игра. Проект начинался с энтузиазма одного человека и вырос до серьзного проекта с немалой аудиторией.
Для разработки движка был выбран язык 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; }
Весь представленный код местами специально упрощен для наглядности. Некоторые классы и фукнции опущены.
