Stockfighter — это игра-симулятор. Каждый уровень (с возрастающей сложностью) управляется через браузерный промокательный пользовательский интерфейс, хотя после первого или двух уровней единственный способ победить — написать клиент для REST API симулятора. (Кроме того, компания, издающая игру, имеет для этого интересную причину.)
Реальность такова, что я не могу очень хорошо программировать веб: у меня не было особой потребности. Я могу свернуть адрес просто отлично, но редко нуждаюсь в хорошо структурированном виде. Веб-задержка требует асинхронной обработки вызовов, что является еще одним навыком, который я пытался улучшить, учитывая новые встроенные функции C++11. Предстоящие работы в офисе потребуют этих навыков, поэтому Stockfighter — это хорошая возможность одновременно заниматься архитектурой и учиться.
Все приложение, от супа до орехов, написано на C++11. Я пользуюсь некоторыми сторонними инструментами (читай: Excel) для создания базовых диаграмм, но даже в этом случае данные поступают из журналов, которые мне нужно было построить.
Я использовал некоторые сторонние библиотеки, чтобы немного упростить низкоуровневую работу:
- libcurl (для деталей REST API)
- буст (незаменимые безделушки)
- websocketpp (детали веб-сокета)
- json11 (для кругового обмена JSON)
Websocketpp использует библиотеку asio от Boost, которую было достаточно легко интегрировать, учитывая, что я уже собирался использовать Boost. Сетевые библиотеки также используют OpenSSL для шифрования связи, хотя я не взаимодействовал с ним напрямую.
Я не буду описывать каждую кровавую строчку C++. Я также не буду вдаваться в подробности, связанные с прохождением того или иного уровня Stockfighter. Я надеюсь, что то, что я создал, способно решить любую из них (с учетом некоторых относительно незначительных изменений). Скорее, меня больше всего интересует выявление конкретных областей, в которых, как мне кажется, я узнал что-то новое или создал что-то интересное.
Цели
При создании клиента у меня не было много целей. Очевидно, что победа на различных уровнях была важна. Но я также был заинтересован в том, чтобы приложение было стабильным, удобным в сопровождении и расширяемым. Это означает:
- Нет петель для блокировки. Я сделал что-то не так, если процессор загружается до 100%, ожидая, что что-то произойдет.
- Представление. Если реакция на событие биржевой симуляции займет слишком много времени, мое окно возможностей будет упущено.
- Безопасность резьбы. Если приложение наступает на собственные ноги, оно не продвинется далеко.
Итак, с чего начать?
Как насчет начала:
int main(int argc, char** argv) try { std::string settings_path(argc > 1 ? argv[1] : ""); if (!config::init(binary_path, settings_path)) { // usage, error, etc return 1; } log_t log(config::derivative_file(".log"), true, false); task_queue_t queue{6}; recur::engine_t recur{queue}; game_t game(log, recur, queue); std::thread([&](){ console(log, recur, queue, game); }).detach(); recur.insert(std::chrono::minutes(1), [&]() { keepalive(log, recur); }); log("MAIN") << "Startup"; queue.push([&](){ game.start(); }); recur.run(); return 0; } catch (const std::exception& error) { std::cerr << "Fatal error : " << error.what() << '\n'; return 1; } catch (...) { std::cerr << "Fatal error : Unknown" << '\n'; return 1; }
Заряжено, да? Давайте сломаем это.
Файл настроек
Приложение принимает единственный параметр: путь к файлу настроек. Файл представляет собой сериализованный объект JSON с одним значением — моим ключом API Stockfighter. (Я намеревался немного больше использовать файл настроек, но причина так и не материализовалась.)
Как оказалось, наиболее важной функцией файла настроек является предоставление домашнего каталога для файлов журнала.
Журнал
Для многих уровней у меня было несколько журналов, куда я выбрасывал большое количество данных. Вызов config::derivative_file начинается с расположения файла настроек и возвращает родственный файл в виде boost::filesystem::path. Я построил журналы так, чтобы они вели себя так же, как iostreams, но также включили поддержку потоков, временные метки и т. д.
Этот журнал, в частности, является основным журналом. Думайте об этом как о std::cout.
Очередь задач
Как я упоминал ранее, асинхронная обработка событий необходима в современных приложениях. Это удваивается при работе с ресурсами с высокой задержкой (например, веб-API). Очередь задач позволяет мне настраивать задания в нескольких потоках для одновременной обработки. По мере того, как информация поступает от клиента в Stockfighter и уходит от него, возможность своевременно реагировать на все это будет иметь решающее значение. Очередь задач позволяет мне сделать это, но при этом я должен учитывать асинхронные проблемы. Вызов принят.
Рекуррентный двигатель
Многие вещи должны происходить на постоянной основе и через определенные промежутки времени. Примеры этого включают опрос событий веб-сокета или подтверждение того, что серверы Stockfighter работают (тактовые импульсы). Механизм повторяющихся событий отслеживает эти задания и при необходимости вставляет их в очередь задач. После завершения задания оно переносится в «очередь ожидания» для повторного вызова в более позднее время.
Даже когда очередь задач пуста, рекуррентный движок всегда бдителен, решая, какое задание (если оно есть) должно быть запущено следующим и когда. Все, что делает приложение при взаимодействии со Stockfighter, уходит своими корнями в рекуррентный движок.
Аналогичным образом, демонтаж приложения начинается с остановки рекуррентного движка и представляет собой структуру, используемую для определения того, "готово" ли приложение. Когда этот движок останавливается, все приложение рушится. Очередь задач пустеет. Веб-сокеты не работают. Сердцебиение умирает.
Интерфейс игры
Структура игры является инкапсуляцией всей системы Stockfighter. Вы можете видеть, что он получает журнал, рекуррент и очередь задач по мере создания. И не зря, так как почти все, что происходит в приложении, находится в игровом блоке. (Игровые подсистемы разбиты на несколько других файлов, все во имя инкапсуляции и ясности.)
Консоль
Большая часть намерений приложения состоит в том, чтобы обеспечить бесперебойный интерфейс со Stockfighter. Тем не менее, бывают случаи, когда необходимо небольшое ручное вмешательство. Консоль — это отдельная единица приложения, которая асинхронно обрабатывает std::cout и std::cin. В зависимости от пользовательского ввода различные задачи помещаются в очередь задач для выполнения. В итоге я написал около полудюжины команд для консоли. Большинство из них носило исследовательский характер и мало что делало для непосредственного контроля поведения клиента на уровне.
Консоль живет в своем собственном потоке и использует рекуррентный движок только для того, чтобы увидеть, завершился ли он. Как только он завершится, мне не нужно напрямую с ним общаться, и нить можно безопасно отсоединить.
Keepalive
keepalive мало что делает. Он установлен в рекуррентном двигателе, чтобы стрелять ежеминутно. У Stockfighter есть API для проверки работы серверов, и если они не работают, периодический движок прекращает работу:
void keepalive(log_t& log, recur::engine_t& recur) { // If the API dies, so should we. if (!stock::heartbeat()) { log(“MAIN”) << “Heartbeat failure”; recur.terminate(); } }
Это одно из первых применений механизма ведения журнала. Я хотел, чтобы он вел себя как iostream, насколько это возможно, поэтому я мог обращаться с ним как с std::cout. Я не хотел думать об его использовании, я просто хотел, чтобы он работал. Я могу более подробно остановиться на механизме ведения журнала в следующем посте.
Начало игры
Со всеми настройками пришло время что-то делать! Игра начинается с… ну… game.start(). Я помещаю его в очередь задач, потому что хочу, чтобы он выполнялся где-то еще — мой рекуррентный движок должен быть занят повторением.
Запуск игрового движка выполняет несколько важных действий, таких как запуск экземпляра игры, установка необходимых веб-сокетов, настройка еще одного пульса и регистрация некоторых основных сведений об уровне. Что происходит после этого — история для другого поста.
Конец главного
Наконец, рекуррентный движок — это последняя структура, упомянутая в main: вызов подпрограммы движка run переводит основной поток в цикл ожидания до планирования следующего повторяющегося задания, или бросить.