Transcend Library 4: Как организована архитектура

в 19:30, , рубрики: библиотека с++, Песочница, Сетевые технологии, метки: ,

Добрый день, сегодня я хочу продолжить рассказывать о своей библиотеке, в надежде найти сторонников идеи.
Этот пост является продолжением Transcend Library 4: Введение, а точнее логичным развитием дискуссии в комментариях.

Почему я пишу новый пост вместо вскрытия кода

— имеет простую причину. Причины.

  • После очередного рефактора архитектуры(июнь 2012) библиотека не работает. Точнее половины методов просто нет. Я понимаю, что четверть решается копипастом с прошлых версий, но факт остается фактом.
  • Рефакторинг еще не завершен. Половина библиотеки переведена под новый стандарт, а половина только готовится, просто не дошли руки.
  • Код не документирован, и на данном этапе местами не ясен.
  • Да и проще будет начать разработку зная хоть в общих чертах идею, чем грызя код с нуля.

Дисклеймер

Я амбициозен, молод, глуп и самоуверен. Максимализм еще не умер и временами возникает. Но все же, после более-менее здравых рассуждений, открытие кода и выход в Open Source мне кажется более простым и реальным способом найти коллег-разработчиков, чем заработать деньги от первых продаж и нанять персонал.

Паттерны

Большинство из того что я использовал имеет под собой распространненые паттерны. Что то не является таковым, но очень распространено. В любом случае, я надеюсь что любой участок будет ясным для тех кто их не знает. Для тех кто знаком — я буду рядом приписывать название.

TL4

Асинхронность

Природа сети

Природа сети асинхронна. Здесь нет понятий «одновременно» и «мгновенно». Все эффекты от вызова функций происходят с задержками зависящими от многих причин. Ваше право дожидаться результата, или выполнять свои действия в это время(blocking and non-blocking sockets), но факт не изменится. Между тем как вы захотите открыть соединение, и как оно фактически откроется пройдет значительное время. И это плохо.
Но есть и хорошие новости, вы можете одновременно приказать совершить множество действий, и ждать когда завершится первое. При этом, количество действий не сильно влияет на время выполнения каждого.
Все это прямо просит проектировать библиотеку с поддержкой событий, но это еще не все.

Перегруженность запросов

Есть еще одна цена использования сети — каждый вызов стоит времени. Большого времени. Оно тратится на освобождение ресурса, постановление в очередь операционной системы и подобное. К примеру мы хотим сделать 1000 запросов dns. Пусть каждый вызов стоит 1ms(адекватное время, бывает и больше), это значит что мы потеряем секунду только на вызовах. В это время, наше приложение, работающее в одном потоке «просядет» по FPS, а пользователь увидит подвисание на ровном месте.
Причем фактически драйвер сделает 50(число с потолка) запросов, для всех приложений в системе.
Я понимаю, где то может быть оправданно так поступать(и есть настройка что бы так происходило), но система разрабатывалась для realtime-приложений, а в них такое неприемлемо.

Порядок выполнения задачи

При вашем запросе OpenConnection, библиотека не делает ничего кроме выделения памяти и постановки задания в очередь. Все операции происходят в локальном адресном пространстве и локальном потоке, изза этого они быстрые.
Фактическое же выполнение операций происходит при вызове Response, который просматривает задачи и выполняет одно за другим, до timeout.
В нашем примере это позволяет после первых 50 запросов, начать устанавливать соединение, и возможно(если успеет), начать(или даже завершить) обмен информацией до получения всех 1000 адресов.
Внутреннее устройство можно сравнить с event-driven архитектурой, за исключением того что уведомление о событиях происходит не по паттерну observer.

Приоритет выполнения задания

Пусть мы играем в онлайн-игру по типу WoW. За одним исключением что администрация не хочет реализовывать новые системы через патчи. Что проку скачивать миллионам игроков меч, который получит дай бог 10 задротов, а увидит дай бог 1000 прохожих?
Пусть при этом меч выпал в бою, когда бой еще не завершен. Пусть так-же голосовое общение вшито в игру.
Сразу видно несколько одновременных потоков данных: урон боссу, уровень жизней всех вокруг, их перемещения, голос и меч. (Данные отсортировал по возрастанию обьема)
Приоритеты передачи же должны быть такие: перемещения, уровень жизни, голос, урон, меч.
Было бы обидно если библиотека не предоставляла такого функционала.

Связанные задания

Фича не предусмотренна ни в релизе ни в архитектуре, но гармонично туда впишется.
Характеристики меча, текстура, модель и иконка друг без друга не имеют большого смысла. Но при этом, было бы логично передать характеристики и пиктограмму, дав возможность использовать оружие, без ожидания загрузки модели.
Текстура и модель пронаследуют приоритет родителя, при передаче, и их приоритет будет задаваться относительным смещением. Осталось подобрать коэфициенты так, что бы передача произошла своевременно.

Динамичный приоритет

Пусть наш многогигабайтный вояж подходит к концу, и имел низкий в системе приоритет. И в этот самый момент(99.9%) появилась необходимость передать несколько мегабайт срочных данных. Не смотря на то что мнимый гиг превратился в десяток килобайт.
Здесь крайне разумно было бы изменять приоритет задания в зависимости от оставшегося и буфферезированного количества данных.
Мое текущее решение в библиотеке мне не нравится, нужно поразмыслить и если получится улучшить.

Устройство памяти

Модель хранения данных

В сложной системе может потребоваться доступ к одним ресурсам из разных мест. К примеру отправить сообщение захочет chat_listener и attack_listener. Причем каждый именно своему набору соединений.
Что бы избежать дублирование и рассинхронизацию данных используется хранение ключей к базе данных в обьектах отдаваемых пользователю(ближе всего здесь Bridge, только для хранимых данных).
База данных одна на виртуальную сеть(именно поэтому операции с данными внутри одной сети потоко не безопасны), и как и все данные хранимые в ней управляется через shared_self_controlled.

Ответственность за память

Изза особенности задачи пользователь не может сохранить полный контроль над выделяемой памятью.
Идея библиотеки — поставить на отправку и быть уверенным что оно дойдет несколько отравляется, если еще и придется следить а когда оно дойдет и удалять.
Как ни странно, библиотека в общем случае тоже не знает когда именно следует удалить обьект. Она знает что он должен быть передан, знает когда это передача завершится, но следует ли удалять его после этого? Это знает только обьект. (о боги он использует delete this)

Сборщик мусора

Громкие слова, совершенно не отражающие сути происходящего. Но отдаленно это так — как только обьект никому не нужен он удаляется. Можно сранить это с shared_ptr, за исключением что обьект является сам и контейнером и обьектом.
shared_self_conrolled дословно означает следующее. Обьект может разделяться(в отличие от простого self_controlled), и должен следить когда ему нужно самоуничтожится(в отличие от shared).
Как только обьект был передан, он не нужен библиотеке, она вычеркивает себя из списка пользователей. Если их стало ноль — к обьекту больше никто не сможет обратиться, он не нужен.

Обработка данных

Отложенное чтение

Предположим нам нужно передать фаил размером несколько мегабайт через килобайтное соединение. Следует ли выгружать фаил в память на несколько минут? Или даже так, наш фаил весит несколько гигабайт.
Смысл этого примера в том что данные не всегда стоит обрабатывать мгновенно. Лучше откладывать работу на завтра, ведь сегодня может наступить конец света, верно? ;)

Натяжение нити

Если мы захотим потянуть машинку за цепочку, это произойдет только когда натянется вся нить. Сила будет передаваться от одного слоя к другому, равномерно. Если же в каком то месте жесткость меньше других — оно растянется, беря всю нагрузку на себя.
Когда обработчики данных выстраиваются в цепочку нас не интересует что происходит по середине. Всегда только концы.
Пусть происходит разжатие, и клиенту нужно 30 байт. При разжатии использовалось только 5 байт исходных данных. Мораль в том что каждое звено знает сколько требуется от соседнего, и ничего больше. Это отражается и в библиотеке — все вычисление происходит по запросу.

Кеширование

Пусть нам нужно обработать террабайт данных, которые запрашиваются раз в несколько секунд. Логично создать такую цепочку:
загрузка => расчет => сохранение в временный фаил => загрузка из временного файла => передача
При этом в памяти будут храниться только данные необходимые для обработки, а в файле только готовые к передаче.
Глупо ждать несколько секунд что бы узнать сколько данных требуется, нужно что бы они были уже готовы.
Логичное решение очевидно — в свободное время перетягивать данные так, что бы их было минимальное количество на стадии расчет и загрузка, и максимальное в временном файле.
Для этого существуют параметры контейнера: минимизировать до, максимизировать до. (Это значит что при нарушении лимита он перестанет передавать запросы далее)
Еще один замечательный пример — прокси. Не стоит скачивать данные быстрее чем мы их можем выдать, это увеличит расход памяти на локальной машине, нас это не устраивает.

Заключение

Надеюсь данным постом я смог раскрыть немного те идеи что послужили причиной для создания библиотеки, и почему она мне кажется конкурентно-способной. Также я верю в то, что несколько смельчаков захотят принять участие в создании чего то настолько замечательного(имхо), как предложенная здесь архитектура.
Это точно не все идеи спрятанные в проекте, некоторые из них ушли в подсознание и спрятались в костном мозге, кажутся очевидными. Это только наиболее значимые, и пришедшие на ум.
Удачных выходных.

Автор: Offenso

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js