LLTR Часть 1: Первые шаги в OMNeT++ и INET

OMNeT++ (Objective Modular Network Testbed in C++) Discrete Event Simulator – это модульная, компонентно‑ориентированная C++ библиотека и фреймворк для дискретно‑событийного моделирования, используемая прежде всего для создания симуляторов сетей. Попросту говоря это “симулятор дискретных событий”, включающий: IDE для создания моделей, и сам симулятор (GUI).

INET Framework – “библиотека” сетевых моделей для OMNeT++.

КДПВ: LLTR Часть 1 – OMNeT++ 5 the Open Simulator :: LLTR Model :: for freedom use

Полная версия GIF (15.7 MiB)

В предыдущих частях…

0. Автоматическое определение топологии сети и неуправляемые коммутаторы. Миссия невыполнима? (+ classic Habrahabr UserCSS)

В этой части:

Note: [про используемую структуру разделов] структура разделов tutorial/how‑to обычно отличается от структуры разделов в справочнике: в справочнике – структура разделов позволяет за минимальное количество шагов дойти до искомой информации (сбалансированное дерево); в tutorial/how‑to, где разделы сильно связаны логически, а отдельный раздел, по сути, является одним из шагов в последовательности шагов, структура представляет собой иерархию закладок (якорей), которая позволяет в любом месте tutorial/how‑to напомнить (сослаться) о фрагменте описанном ранее.

off‑topic: про html5 тег <section> и теги заголовков <h#>

Как хорошо, что в HTML5 появился тег <section>, с его помощью стало возможным напрямую задавать уровень вложенности раздела (при помощи манипуляции вложенностью тегов <section> друг в друга). Структуру текста теперь можно было явно отразить во вложенности (иерархии) тегов.

Это повлияло и на теги заголовков <h#>, т.к. теперь вложенность разделов определяется вложенностью тега <section>, то для указания названия раздела – достаточно было использовать всего лишь один тег <h1> в виде: “<section><h1>название раздела</h1>текст раздела</section>”.

Я этим пользовался уже давно (с самого появления <section>), но создавая эту статью, увидел еще одно достоинство использования <section>.

Хорошее название раздела должно точно отражать его суть, однако бывают случаи, когда нужно придержать (не раскрывать) суть до середины раздела. То есть, такой раздел должен вначале притворится “рутинным”, а в середине создать “wow/wtf‑эффект”. Логически это все – один раздел, но если раскрыть его название в самом начале раздела, то само название будет являться спойлером. Представьте книгу (детектив), на обложке которой будет вся информация о “убийце”.

Здесь “на сцену выходит” тег <section>. Он позволяет определить название раздела в любом месте внутри себя, т.е. не обязательно в самом начале. Пример: “<section>текст раздела<h1>название раздела</h1>продолжение текста раздела</section>”. Получается, мы можем одновременно сохранить логическую структуру текста, и показать название раздела в нужный момент. Можно даже сделать так, чтобы название раздела визуально появлялось в его начале, после того как читатель дойдет до определенного момента (до тега <h1> в html).

Вот только более чем за 9 лет существования <section>, браузеры так и не научились правильно строить “HTML5 document outline” для обеспечения доступности.

Почему не научились? В документе со сложной структурой трудно* определить, начиная с какого тега (section, article, …) следует начать нумерацию заголовков (h1, h2, h3, …). А теперь представьте, что сам документ размещен на странице подобной этой (с множеством дополнительных блоков, не имеющих отношение к самому документу, но имеющих заголовки), причем везде для заголовков используется h1. А если на одной странице не один документ, а несколько? Тем не менее, визуально все выглядит хорошо (пример документа).

* – на самом деле это не трудно, в стандарте все описано, но в реальности это не работает (объяснение см. ниже).

Почему визуально все выглядит хорошо? Здесь, благодаря стилям, появилась дополнительная информация – соответствие между иерархией section и уровнями заголовков (h#). Так может при построении “HTML5 document outline” следует воспользоваться информацией из CSS? Для этого потребуется добавить в CSS дополнительное свойство для элемента заголовка, указывающее его уровень, например:

body>section>h2                                 { heading-level: 1; font-size: 1.8em;  }
body>section>section>h2                         { heading-level: 2; font-size: 1.4em;  }
body>section>section>section>h2                 { heading-level: 3; font-size: 1.17em; }
body>section>section>section>section>h2         { heading-level: 4; font-size: 1em;    }
body>section>section>section>section>section>h2 { heading-level: 5; font-size: 0.83em; }

Либо более строгий вариант – в одной секции допускается использовать только один заголовок. В этом случае уровень заголовка задает сама секция:

body>section                                 { heading-level: 1; }
body>section>section                         { heading-level: 2; }
body>section>section>section                 { heading-level: 3; }
body>section>section>section>section         { heading-level: 4; }
body>section>section>section>section>section { heading-level: 5; }

, и неважно, какой в итоге будет использоваться тег заголовка: h1 или h5.

Однако, если раньше для создания “heading-level outline” достаточно было иметь только разметку (HTML), то теперь нужны еще и стили (CSS). Может можно ограничиться только разметкой (HTML)? Этим вопросом мы вплотную подошли к проблеме алгоритма построения “heading-level outline”, описанного в стандарте. Так вот, проблема не в самом алгоритме, а в том, что в качестве “sectioning root” элемента может выступать только ограниченный (фиксированный) набор тегов. Но у людей часто возникают “нестандартные желания”: “я хочу, чтобы на моей странице со списком статей тег article являлся ‘sectioning root’ элементом”, “а я хочу, чтобы произвольная секция стала ‘sectioning root’ элементом”. Раньше им достаточно было для этого использовать несколько тегов h1 на одной странице (и они это делали). Так может сделать так, чтобы любая секция (теги: section, article, …) становилась “sectioning root” элементом, если заголовок в ней задан при помощи тега h1?..

# Первые шаги: “перед моделированием” / “мозговой штурм”


Листочек

Обратная сторона листочка из предыдущей статьи.

# Детализация протокола

В начале определим, что нам нужно включить в протокол. На примере LLTR Basic.

Основа LLTR – это итерации сбора статистики на множестве хостов во время сканирования сети. Итераций в LLTR много ( >1), поэтому первое, что нужно включить в протокол – управление запуском и остановкой каждой итерации. Если учесть, что хостов тоже много ( >1), то управление будет заключаться в том, чтобы определенным способом сообщать всем хостам время начала итерации и время окончания итерации. То есть синхронизировать все хосты.

В каждой итерации есть свой unicast src хост и unicast dst хост, поэтому следующее, что нужно включить – способ назначения для каждой итерации unicast src и dst. То есть в каждой итерации один из хостов должен “осознавать” себя unicast src хостом, цель которого посылать трафик на unicast dst хост.

И последнее. По завершению всех итераций, всю собранную статистику со всех хостов нужно отправить на один хост для обработки. Этот хост проанализирует собранную статистику, и построит топологию сети.

Также, на этом шаге, можно подумать про некоторые детали реализации (ограничения) протокола. Например, мы хотим, чтобы программа, использующая LLTR, смогла работать без root прав, и из пространства пользователя (т.е. без установки в систему специального драйвера), значит, LLTR должен работать, например, поверх TCP и UDP.

Все остальные делали реализации, определятся сами, в процессе создания модели. То есть, конечно, можно сразу же продумать все до мелочей, но при этом есть риск “скатится в локальный оптимум”, и не заметить “более лучший” вариант реализации. Хорошо, когда моделей будет несколько – если для каждого варианта реализации будет своя модель, то появится возможность комбинировать модели, и шаг за шагом приходить к лучшей реализации. Вспоминая генетический алгоритм ;). Например, в одной реализации/модели может быть централизованное управление, в другой – децентрализованное, в третей – комбинация лучших частей из предыдущих двух вариантов.

# Выбор симулятора сети

Теперь настало время определится с симулятором сети, в котором будем создавать модели и ставить эксперименты.

В основном, от симулятора сети нам нужна возможность реализации “своего” протокола. Не все симуляторы позволяют легко это сделать.

А вот присутствие эмуляторов ОС реального сетевого оборудования “мировых брендов”, наоборот – не нужно. Скорее всего, эмуляторы создадут множество ограничений, которые будут только мешать в ходе экспериментов.

С выбором симулятора мне помогла статья Evaluating Network Simulation Tools (наши требования к симулятору во многом совпадали) и OMNeT++ General 'Network' Simulation.

# Установка OMNeT++ и INET


Загружаем OMNeT++ 5.0.

И так как OMNeT++ – это всего лишь “симулятор дискретных событий”, то понадобится еще и INET – библиотека сетевых моделей (протоколы и устройства). Качаем INET 3.4.0. На самом деле его можно было установить из IDE, но я рекомендую поставить вручную (позже будет ясно почему).

Установка в *nix и в Windows мало чем отличается. Продолжу на примере Windows.

Распаковываем OMNeT++ в %ProgramData% (C:\ProgramData\), и открываем файл INSTALL.txt (C:\ProgramData\omnetpp-5.0\INSTALL.txt). В нем сказано, что подробная инструкция находится в “doc/InstallGuide.pdf”, дальше написано, что если не хотите ее читать, то просто выполните:

$ . setenv
$ ./configure
$ make

Но не спешите это делать!

Во‑первых, обратите внимание на первую команду “. setenv”. В директории “omnetpp-5.0” нет файла “setenv” (в версии 5.0b1 он был). Он и не нужен (для Windows), поэтому просто запускаем “mingwenv.bat” (советую перед запуском посмотреть, что он делает… во избежание внезапного rm ). По окончании отколется терминал (mintty).

Во‑вторых, советую немного подправить файл “configure.user” (если упомянутый параметр закомментирован в файле, то его нужно раскомментировать):

Почему его стоит отключить?

Если его явно не использовать, то он не нужен (в теории). Подробнее в разделе 16.1, 16.3, и 16.3.2 “Parallel Simulation Example” в “doc/InstallGuide.pdf”, или тут.

Теперь в терминале (mintty) можно выполнить:

./configure && make clean MODE=release
make MODE=release –j17

Note:17” следует заменить на количество ядер CPU + 1, либо на 1.5×ядер.

Предостережение для любознательных (сборка 64bit)

В директории “tools/win32” находится MSYS2 его пакеты компиляторов можно обновлять:

А OMNeT++ можно собрать под 64bit.

Но OMNeT++ может просто не собраться более новой версией GCC (так было с первой бэткой пятой версии OMNeT++ – без правки исходников она нормально собиралась только с GCC 4.x). А для перехода на 64bit потребуется еще больше усилий. Для начала потребуется пересмотреть опции компиляции (fPIC, не нужен?). Затем, если пролистаете исходники OMNeT++, то увидите, что там часто используется тип long вместо int32_t, size_t и ptrdiff_t (а также uintptr_t и intptr_t). Чем это грозит? В *nix в 64bit (LP64) сборке размер long будет 64bit, а в Windows (LLP64) – 32bit (см. модели данных). Придется заменять long на size_t и ptrdiff_t, но и здесь вас будут поджидать “подводные камни”. Например, можно открыть “src/utils/opp_lcg32_seedtool.cc”, и взглянуть на строку 231 – index либо можно оставить 32bit (заменить на int32_t), либо сделать 64bit и модифицировать все битовые_маски+описания+(возможно)немного_логики. Поэтому часть long переменных нужно будет оставить 32bit, а другую часть сделать 64bit. В общем, для корректной работы, нужно проделать все пункты из:

Причем то же самое надо проделать и с многочисленными библиотеками для OMNeT++, например, с INET.

В общем, предостерегаю от попыток сделать 64bit сборку OMNeT++.

Под *nix я также рекомендую использовать 32bit сборку (по крайне мере с версией 5.0 и меньше).

Возможно, когда‑нибудь @Andrey2008 возьмется проверить код OMNeT++ и INET… А пока предлагаю просто найти и просмотреть все “FIXME”/“Fix” в коде ;).

P.S. упоминания о том, что код OMNeT++ проверяли статическим анализатором кода – отсутствуют, а вот в файлах “ChangeLog” INET 3.4.0 можно найти 70 упоминаний про устранение дефектов после сканирования в Coverity.

OMNeT++ использует Eclipse в качестве IDE. Для удобства можно создать ярлык на IDE “%ProgramData%\omnetpp-5.0\ide\omnetpp.exe”, и расположить его в легкодоступном месте. В директории “ide/jre/” находится JRE v1.8.0_66-b18. Если в системе уже установлен совместимый JRE/JDK, то директорию “ide/jre/” можно спокойно удалить, заменив символьной ссылкой на местоположение системного JRE.

При первом запуске Eclipse предлагает поместить workspace в директорию “samples”, однако лучше расположить ее в любой другой удобной вам директории вне “%ProgramData%”. Главное, чтобы в пути к новой директории использовались только латинские буквы (+ символы), и не было пробелов.

После закрытия Welcome, IDE предложит установить INET (как было написано выше), и импортировать примеры – откажитесь от обоих пунктов.

Настройки Eclipse, опции JVM, дополнительные плагины и темы

Опции JVM. Добавить в файл “ide/omnetpp.ini” (для правки подойдет любой редактор, понимающий LF перевод строки; notepad не подойдет), сохранив пустую последнюю строку:

-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+AggressiveOpts
-XX:+TieredCompilation
-XX:CompileThreshold=100
Eclipse tuning (un[7z]me)

Чтобы сделать Eclipse, таким как на картинке – загляни внутрь картинки.

Настало время установить INET. Директорию “inet” из скаченного ранее архива (inet-3.4.0-src.tgz) нужно перенести в workspace. В директории есть файл “INSTALL” с пошаговым описанием установки. Можно воспользоваться им (раздел “If you are using the IDE”), но только не собирайте (Build) проект!

Импортируем INET:

  1. В Eclipse открыть: File > Import.
  2. Выбрать: General / Existing Projects to the Workspace.
  3. В качестве “root directory” выбрать местоположение workspace.
  4. Удостоверьтесь, что опция “Copy projects into workspace” выключена.
  5. После нажатия на кнопку “Finish”, дождитесь окончания индексации проекта (% выполнения см. внизу, в строке статуса – “C/C++ Indexer”).

Настроим проект:

Перед настройкой {A} надо подправить один из файлов проекта. В файле “inet/.oppfeatures” есть строка “inet.examples.visualization” нужно добавить после нее пустую строку, в которой написать “inet.tutorials.visualization”, желательно сохранив отступ слева (по аналогии с другими параметрами “nedPackages” в файле). Если это не сделать, то ничего страшного не случится, просто после настройки в “Problems” (Alt+Shift+Q,X) будут всегда висеть ошибки, связанные с “inet.tutorials.visualization”. Можно вначале сделать {A}, и посмотреть на ошибки, а затем подправить файл “inet/.oppfeatures” – при этом Eclipse предупредит о нарушении целостности в настройках, и предложит профиксить их (соглашаемся на это).

Приступим (панель “Project Explorer” > проект “inet” > контекстное меню > Properties):

  1. Раздел “OMNeT++” > подраздел “Project Features”
    1. {A} убираем все, кроме:
      • TCP Common
      • TCP (INET)
      • IPv4 protocol
      • UDP protocol
      • Ethernet
    2. кнопка “Apply”.
  2. Раздел “С/С++ Build”:
    1. кнопка “Manage Configurations…” > сделать активным “gcc-release” {B};
    2. выбрать конфигурацию “gcc-release [ Active ]” {B}.
    3. Подраздел “Tool Chain Editor”:
      1. в качестве “Current builder” выбрать “GNU Make Builder” для обеих конфигураций: “gcc-debug” и “gcc-release” {C}, внимание: если в будущем изменить “Current builder”, то все придется перенастраивать заново!
      2. кнопка “Apply”.
    4. Вкладка “Behavior” (вернутся в корень раздела “С/С++ Build”):
      1. установить “Use parallel jobs” равным N (в качестве N можно выбрать либо число ядер CPU + 1, либо 1.5×ядер) – это позволит использовать все ядра CPU для компиляции {D} (настраиваем для “gcc-debug” и “gcc-release”).
    5. Вкладка “Build Settings”:
      1. отключить “Use default build command”;
      2. строку “Build command” заменить на “make MODE=release CONFIGNAME=${ConfigName} -j17” (“17” заменить на предыдущее значение в строке, т.е. на выбранный N) {E}, то же самое можно сделать и для конфигурации “gcc-debug”, заменив в строке “MODE=release” на “MODE=debug”, после этого не забудь переключиться обратно на “gcc-release [ Active ]”.
    6. кнопка “Apply”.
  3. Раздел “С/С++ General”:
    1. Подраздел “Paths and Symbols”:
      1. Вкладка “Includes”:
        1. кнопка Add: добавить директорию “../src” с выбранными “Add to all configurations” и “Add to all languages” {G} – изначально “../src” есть в языке “GNU C++”, но, в неопределенный момент, он может стереться из списка;
        2. кнопка “Apply”, и проверь, что “../src” появилось во всех языках и конфигурациях.
      2. Вкладка “Symbols”:
        1. кнопка Add: добавить символ “__cplusplus” со значением “201103L” и выбранными “Add to all configurations” и “Add to all languages” – {F} подробнее;
        2. кнопка “Apply”, и проверь, что в конфигурации “gcc-debug” у “__cplusplus” значение “201103L”.
      3. Вкладка “Source Location”:
        1. Проверь, что в списке один пункт, и он указывает на “/inet/src{G}, если там что‑то другое (например, просто “/inet”), то удаляй то, что есть и добавь (“Add Folder…”) “/inet/src”. Затем кнопка “Apply”, и возвращение к {A}, т.к. все фильтры при удалении были стерты. Кстати, “/inet” на самом деле можно оставить – с ним тоже все нормально собирается, но лучше сузить до оригинального “/inet/src”.
    2. Подраздел “Preprocessor Include Paths, Marcos etc.” > вкладка “Providers”:
      1. Выбрать “CDT GCC Build-in Compiler Settings”:
        1. В группе “Language Settings Provider Options” нажать на ссылку “Workspace Settings”:
          1. вкладка “Discovery”: опять выбрать “CDT GCC Build-in Compiler Settings”, и добавить “-std=c++11 ” перед “${FLAGS}” в “Command to get compiler specs”, должно получится примерно так `${COMMAND} -std=c++11 ${FLAGS} -E -P -v -dD "${INPUTS}"` {F}, подробнее здесь и здесь;
          2. кнопка “Apply”, “Ok” (закрываем окно).
        2. переместить “CDT GCC Build-in Compiler Settings” выше “CDT Managed Build System Entries” (для обеих конфигураций: “gcc-release” и “gcc-debug”) {F}, подробнее – после этого мы потеряем возможность переопределять символы “CDT GCC Build-in Compiler Settings” через “CDT Managed Build System Entries” (“С/С++ General” > “Paths and Symbols” > “Symbols”), переопределить можно будет только через добавление значений в “CDT User Settings Entries” во вкладке “Entries” для каждого языка по отдельности (альтернатива: не меняем порядок, т.к. в “CDT Managed Build System Entries” уже исправили значение “__cplusplus”; не меняем порядок, удаляем все упоминания “__cplusplus” из “CDT Managed Build System Entries”, и следим, чтобы он там не появлялся в будущем);
        3. кнопка “Apply”, и проверить, что во вкладке “Entries” у языка “GNU C++” в “CDT GCC Build-in Compiler Settings” (чекбокс [в нижней части окна] “Show build-in values” должен быть включен) есть запись “__cplusplus=201103L” (она будет ближе к концу).
    3. Подраздел “Indexer”:
      1. в качестве “Build configuration for indexer” выбрать “gcc-release” {B};
      2. кнопка “Apply”.

Некоторые проблемы могут возникнуть с {E}. Поясню. Если все нормально, то Eclipse должен подхватить те настройки, которые были заданы в “configure.user” перед конфигурированием OMNeT++ (./configure). В таком случае Eclipse передаст нужные параметры в g++ через make. Однако не всегда все идет, как планировалось, и лучше проверить, что происходит в реальности. Проверить можно, дописав в “Build command” {E}--just-print” или “--trace”, и, запустив сборку (панель “Project Explorer” > проект “inet” > контекстное меню > “Clean Project” и “Build Project”), открыть “Console” (Alt+Shift+Q,C), в нем должно выводится что‑то похожее на “g++ -c -std=c++11 -O2 -fpredictive-commoning -march=native -freorder-blocks-and-partition -pipe -DNDEBUG=1 …”. Если этого нет, то можно последовать совету из уже упомянутой статьи.

Либо подправить переменные окружения

Опять открываем настройки проекта (панель “Project Explorer” > проект “inet” > контекстное меню > Properties):

  1. Раздел “С/С++ Build”:
    1. Подраздел “Build Variables” (проверь, что текущая конфигурация “gcc-release [ Active ]”):
      1. кнопка “Add…”, имя “CFLAGS”, тип “String”, значение “-O2 -fpredictive-commoning -march=native -freorder-blocks-and-partition -pipe”;
      2. кнопка “Add…”, имя “CXXFLAGS”, тип “String”, значение “-std=c++11 -O2 -fpredictive-commoning -march=native -freorder-blocks-and-partition -pipe”;
      3. кнопка “Apply”.
    2. Подраздел “Environment”:
      1. кнопка “Add…”, имя “CFLAGS”, значение “${CFLAGS}”;
      2. кнопка “Add…”, имя “CXXFLAGS”, значение “${CXXFLAGS}”;
      3. кнопка “Apply”.

Кстати, при некоторой сноровке, параметры запуска g++ можно было посмотреть, не используя флаги “--just-print” и “--trace”, а используя Process Explorer. В Process Explorer также можно посмотреть, во что раскрывается “-march=native” при передаче в “cc1plus.exe”.

Теперь, наконец, можно собрать INET! Проверьте, что сейчас активна конфигурация “gcc-release” {B}, и если добавляли ранее флаги “--just-print” или “--trace” для проверки {E}, то их нужно убрать. Собираем (панель “Project Explorer” > проект “inet” > контекстное меню > “Clean Project” и “Build Project”), за процессом можно наблюдать в “Console” (Alt+Shift+Q,C).

Если все прошло хорошо, то рекомендую закрыть Eclipse, и сделать бекап файла “.cproject” и директории “.settings” с настройками проекта {B-G}, а также файлов: “.oppfeatures”, “.oppfeaturestate”, “.nedexclusions” – {A}.

Наконец, настройка завершена, и можно перейти к самому интересному.

# Создание первого проекта


Note: Первое, что я сделал после настройки окружения – стал изучать содержимое директории “doc” у OMNeT++ и INET. Это были Simulation Manual и User Guide, позже к ним присоединился Stack Overflow (в виде stackoverflow.com, и в виде состояния мозга). Ниже я покажу, как можно сделать первые шаги, не читая всю документацию, и расскажу, с какими “особенностями” можно столкнуться.

Note: Для тех, кто еще не успел установить себе OMNeT++ и INET, но уже хочет посмотреть на код, текст ниже содержит ссылки на исходники INET в GitHub. Все ссылки ведут на исходники версии 3.4.0 (эти ссылки будут доступны всегда, даже если в будущих версиях расположение файлов в INET изменится).

Перед созданием своего проекта хорошо бы посмотреть на уже готовые модели в INET, посмотреть, как они устроены. Может в нем уже реализовано то, что нам нужно?

После непродолжительного блуждания по дереву INET в “Project Explorer”, можно наткнуться на директорию “inet/src/inet/applications”, и обнаружить в ней “udpapp” (UDP Application). UDP пригодится нам для broadcast рассылки. Внутри директории лежат несколько моделей, и, судя по названию и размеру исходников, самый простой из них, это “UDPEchoApp”. Там есть еще и “UDPBasicApp”, но он оказался не таким уж и “Basic”. Каждая модель состоит из “.cc”, “.h” и “.ned” файлов. Пока не ясно, зачем нужны “.ned” файлы, но судя по их содержанию (наличию строчки “parameters:) в них могут описываться параметры модели.

Продолжим поиски интересных моделей. Посмотрим, какие примеры (inet/examples) есть в INET. И нам повезло, в нем есть пример с названием “broadcast” (inet/examples/inet/broadcast)! Этот пример помимо файлов “.cc”, “.h” и “.ned”, содержит еще “.ini” и “.xml” файлы. Пора разобраться, зачем эти файлы нужны:

К сожалению, этот пример (“broadcast”) нам не подойдет, т.к. в его сеть включены маршрутизаторы. Однако, по аналогии с ним, можно создать свой проект.

Note: Далее я продолжу ссылаться на разные разделы Simulation Manual. Как видите, он достаточно большой, браузеру требуется время (и RAM) для его открытия. Для решения этой проблемы я сделал небольшую . После первого нажатия на нее – все ссылки, ведущие на разделы Simulation Manual, перестанут плодить вкладки (пожирая ресурсы), и начнут переключать разделы в одной единственной дополнительной вкладке (на самом деле она просто прописывает target для каждой ссылки на Simulation Manual). А, для того, чтобы отличить ссылки на Simulation Manual от остальных ссылок, “кнопка” изменяет их цвет.

# Создаем проект

Пустой проект “LLTR”, с директориями “src” и “simulations”, и единственной конфигурацией “gcc-release” (File → New → OMNeT++ Project…):

New OMNeT++ project Wizard

Осталось настроить проект также как и “inet”, и можно будет двигаться дальше. В основном, настройка будет отличаться отсутствием необходимости настраивать “gcc-debug” (т.к. он отсутствует в “LLTR”), и добавлением в зависимости “inet”. Более конкретно: вместо {A,B,G} надо открыть раздел “Project References”, и включить зависимость от “inet”.

# Структура проекта

Если посмотрите на файлы, которые создал Wizard, то увидите, что файл “package.ned” встречается дважды: в директории “src”, и в “simulations”. Содержимое тоже отличается – “package lltr;” и “package lltr.simulations;” соответственно. Один из этих файлов нам не понадобится.

Если провести аналогию со структурой проекта INET, то директория “inet/src” – это “LLTR/src”, а “inet/examples” – это “LLTR/simulations”. То есть в “LLTR/simulations” лучше размещать файлы “.ned” c Network, а в “LLTR/src” – составные части сети (модули).

Существует еще один нюанс – в INET очень хорошая внутренняя структура директорий, и если в будущем нам потребуется изменить один из стандартных модулей в INET, то лучше будет создать новый модуль, и положить его рядом с оригиналом в INET. То же самое можно применить и к модулю, созданному с нуля – найти ему подходящее место в INET.

В свете вышеописанного, “.ned” в директории “LLTR/src” нам не нужен (все будет в “inet/src”), также как и не нужен дополнительный подпакет “package lltr.simulations;” в “LLTR/simulations”. Поэтому переносим “package.ned” из “LLTR/src” в “LLTR/simulations”.

# Пробный запуск

Попробуйте запустить LLTR. Для этого достаточно открыть файл “LLTR/simulations/omnetpp.ini”, и нажать (Run > Run As > 1 OMNeT++ Simulation):

Run simulation from toolbar

При этом Eclipse предложит создать новую конфигурацию “simulations” для запуска симулятора. Соглашаемся, и сразу же сталкиваемся с проблемой: “LLTR/src/LLTR.exe” не был найден. Все верно, ведь “LLTR.exe” никто не собирал, поэтому вначале собираем проект (меню Project → Build Project), а затем опять запускаем симулятор (тем же самым способом).

После запуска симулятора появилось предупреждение “No network specified in the configuration.”, его можно исправить, добавив строку “network = lltr.Network” в секцию “[General]” файла “omnetpp.ini”, и добавив строку “network Network {}” в конец файла “package.ned”. Этим мы создали пустую сеть (в “.ned” файле), и настроили (в “.ini” файле) симулятор на загрузку этой сети (Network – имя сети) при запуске.

Теперь можно попробовать опять запустить симулятор (Run > Run As > 1 OMNeT++ Simulation), и вместо ошибки должно открыться серое поле (прямоугольник) сети Network на зеленом фоне.

Note: Есть различие между запуском через (Run > Run As > 1 OMNeT++ Simulation), и через (Run > 1 simulations): в первом случае запуск проходит быстрее, т.к. во втором случае, перед запуском симулятора, Eclipse начинает собирать проект.

Note: (или можно форкнуть – тег a1_v0.1.0 (“a” – article) “git checkout -b ‹my_branch› tags/a1_v0.1.0”)

# Рекомендации по использованию репозитория

Репозиторий я создавал таким образом, чтобы:

Note: без веток “article_#” можно было бы обойтись, и указывать, при клонировании, название последнего тега части (которое еще надо найти), но с веткой проще/быстрее.

Как забрать репозиторий “к себе”? Лучше всего, вначале его форкнуть на GitHub, а затем свой форк:

Далее, для создания личной ветки на основе конкретного тега, можно использовать “git checkout -b ‹my_branch› tags/‹tag_name›”.

Как создавать свою версию кода, т.е. изменять код? Если в будущем не возникнет желания сделать Pull Request, то ничего вам не мешает делать с форком что хотите >:-) , однако я советую, при появлении изменений, которые хочется сохранить, делать так):

Git: History Control System flow

Одинаковая схема наименования тегов поможет в будущем избежать коллизий, даже не смотря на то, что теги при Pull Request не переносятся.

Note: Если я в будущем буду вносить изменения в репозиторий, то я поступлю также: оригинальный код сохранится, а измененный будет идти параллельно оригинальному (с “накатанными” всеми изменениями из остальных (будущих) тегов, и с новыми именами тегов). Только вместо добавления “-u” к именам новых тегов тегам, я буду увеличивать номер. Например, теги оригинального кода “a1_v0.1.0”, “a1_v0.2.0”, … – теги измененного кода “a1_v0.1.1”, “a1_v0.2.1”, … При следующем изменении, номер еще раз увеличится: “a1_v0.1.2”, “a1_v0.2.2”, …

Note: в tutorial все места, завершающие очередной “шажок”, помечены значком git git tag diff icon, и рядом с ним будет ссылка на соответствующий git tag.

Note: git diff использовался стандартный, патчи генерировались автоматически, и они редко будут показывать логической связи в произошедших изменениях (в частности, при добавлении нового кода и изменении уровня вложенности / форматирования существующего кода) (здесь бы пригодилось отслеживание изменений на уровне AST), похожее на этот проект для Java.


# Шаг −1: собираем сеть

Откроем “package.ned” в режиме графического редактирования схемы (вкладка “Design” снизу), и попробуем набросать сеть из КДПВ:

Network editor

Сеть построена из тех же модулей, которые были использованы в примере broadcast:

А вот в качестве “провода” (канала связи) выбран Eth100M (скорость: 100 Mbps; длина: 10 метров). Кстати, почему именно 10 метров, где они задаются, и можно ли поменять это значение? (ответ чуть ниже)

Если переключится в режим редактирования кода (вкладка “Source” снизу), то вы должны увидеть примерно это (git tag a1_v0.2.0) diff. Пояснение структуры:

package ‹имя пакета›; //особенности наименования

import ‹имя подключаемого пакета›;

network ‹название описываемой сети›
{
    @display(‹визуальные параметры сети, например, размер области›);
    submodules:
        ‹название узла›: ‹тип узла› { @display(‹визуальные параметры узла, например, местоположение›); }
    connections:
        ‹название узла›.‹точка соединения› <--> ‹тип канала связи› <--> ‹название узла›.‹точка соединения›;
}

Отдельно стоит сказать про “точки соединения” (Gates) и каналы связи:

  1. Gates могут быть объявлены как векторы, в этом случае подключатся к ним можно явно, указав номер gate‹название узла›.‹gate›[‹номер›]”, либо автоматически – инкрементально‹название узла›.‹gate›++”.
  2. Параметры канала либо могут быть заданы в месте использования (например: “… <--> { delay = 100ms; } <--> …”), либо могут иметь имя/тип, на которое можно ссылаться (как в примере broadcast: “… <--> C <--> …”), либо могут иметь тип и быть переопределены на месте (например: “… <--> FastEthernet {per = 1e-6;} <--> …”), либо…
  3. Gates могут быть однонаправленными (тип при объявлении: output / input; соединители при подключении: --> / <--), и двунаправленными (тип при объявлении: inout; соединитель при подключении: <--> ). Двунаправленные состоят из двух однонаправленных, к которым можно обратиться напрямую, дописав суффикс “$i” либо “$o.

# Исследуем параметры модулей

Так почему же у Eth100M длина 10 метров? Для ответа на этот вопрос, переключимся назад в редактор схемы (“Design”), выберем любое соединение, откроем его контекстное меню, и откроем “Parameters…”:

Eth100M parameters

Вот эти 10 метров, записанные как “default(10m)”.

Note: Только те параметры, которые были заданы как “default()” можно менять. Если же изменить, например, “datarate”, то IDE не ругнется на это, но симулятор откажется принимать такой “.ned” файл.

Вы наверняка уже заметили, что если навести указатель (мышку) на значок хоста или свитча, то появится всплывающая подсказка. В них есть несколько интересных моментов. Фрагмент подсказки хоста:

Can be connected via ethernet interface to other nodes using the ethg gate. By default full-duplex connections are supported only (twisted pair). Set **.eth[*].typename="EthernetInterface" for a full/half-duplex CSMA/CD implementation (coaxial cable)

Фрагмент подсказки свитча:

The duplexChannel attributes of the MACs must be set according to the medium connected to the port; if collisions are possible (it's a bus or hub) it must be set to false, otherwise it can be set to true. By default used half duples CSMA/CD mac

Начнем с хоста. Судя по описанию все хорошо – по умолчанию используется full-duplex. А вот со свитчем проблема – по умолчанию используется “half duples CSMA/CD mac”. Будим настраивать!

В подсказке упоминается параметр/атрибут “duplexChannel”, поэтому откроем “Parameters…” у “switch0” и:

switch parameters

… так, а где здесь “duplexChannel”? Параметра “eth[*].typename” из подсказки хоста тоже не видно. Однако есть другие параметры с говорящим названием. Пока их менять не будем, а посмотрим на “Parameters…” у “host0”, и:

host parameters

… и здесь “eth[*].typename” нет.

На самом деле отсутствие “eth[*].typename” вполне объяснимо – “Parameters…” отображает только параметры текущего модуля, а из параметров вложенных модулей он отображает только те, которые были переопределены в текущем модуле. У переопределенных значений вложенных модулей столбец “Type” пуст.

В OMNeT++ есть панель, которая отображает все параметры модуля, включая параметры вложенных модулей. Называется “NED Parameters”, находится внизу:

host NED Parameters

И опять же здесь нет “eth[*].typename”.

Вряд ли он будет в свитче, но все же посмотрим:

switch NED Parameters

Как и ожидалось, “eth[*].typename” отсутствует. Параметр “duplexChannel” тоже отсутствует. Скорее всего их заменили на “macType”, который зависит (csmacdSupport ? "EtherMAC" : "EtherMACFullDuplex") от “csmacdSupport”, а комментарий подправить забыли. Также видно (по точкам, и Remark), что “switch0.csmacdSupport” и “switch0.macType” переопределяют “switch0.eth[*].csmacdSupport” и “switch0.eth[*].macType” соответственно. Если выбрать “Open NED Declaration” на “switch0.macType”, то можно будет изучить “.ned” файл модуля “EtherSwitch” (там же будет текст всплывающей подсказки с “duplexChannel”).

Note: в исходниках “.ned” также работают горячие клавиши Eclipse, и можно быстро открывать файл с определением любого подключенного модуля, используя Ctrl+Click.

Можно попробовать открыть “Open NED Declaration” у “switch0.eth[*].csmacdSupport”, и у “host0.eth[*].csmacdSupport”, должен открыться один и тот же файл.

Что теперь со всем этим делать? Можно открыть “Parameters…” каждого свитча, и установить в них “csmacdSupport = false”. Также выглядит подозрительным, что “switch0.eth[*].mac.duplexMode” и “host0.eth[*].mac.duplexMode” ничему не назначены, поэтому для каждого хоста и свитча добавляем “eth[*].mac.duplexMode = true”.

Изменений получилось много, поэтому проще их внести в режиме редактора кода (вкладка “Source”). Должно получится примерно так (git tag a1_v0.3.0) diff.

# Выбор места для задания параметров модулей

Все равно то, что сделали выше, не очень хорошо выглядит – пришлось для каждого узла применять практически одинаковые параметры, и если в будущем захотим что‑то изменить, то придется вносить изменения для каждого узла. Благо, что можно вынести эти записи в “parameters:” к “Network”:

parameters:
    @display("bgb=693,416,grey99");
    **.csmacdSupport = false;
    **.eth[*].mac.duplexMode = true;

Должно получиться примерно так (git tag a1_v0.4.0) diff. Здесь нужно использовать именно “** (аналогия с Git).

Однако, эти же параметры можно задать и в “.ini” файле. Документация рекомендует использовать “.ini” файл для параметров модели, которые, скорее всего, будут манятся в процессе экспериментирования, а в “.ned” файлах следует оставить неизменяемые значения:

NOTE

How do you decide whether to assign a parameter from NED or from an ini file? The advantage of ini files is that they allow a cleaner separation of the model and experiments. NED files (together with C++ code) are considered to be part of the model, and to be more or less constant. Ini files, on the other hand, are for experimenting with the model by running it several times with different parameters. Thus, parameters that are expected to change (or make sense to be changed) during experimentation should be put into ini files.

Тем не менее, в документации не рассмотрен еще один случай: в будущем, скорее всего, нам понадобится проводить эксперименты с разными сетями (разными топологиями; каждая топология будет лежать в отдельном “.ned” файле), но с одинаковыми параметрами. Если следовать их рекомендации, то “csmacdSupport” и “duplexMode” нужно будет разместить в каждом “.ned” файле сети, т.к. вряд ли мы их будем менять в ходе экспериментов. Что опять приводит нас к дублированию одинаковых настроек.

Поэтому удобнее будет включить общие параметры сетей в секцию “[General]” “.ini” файла, и, используя именованные конфигурации, появится возможность переключатся между сетями с распространением на них общих параметров. В итоге должно получиться примерно так (git tag a1_v0.5.0) diff.

# Пробный запуск

Переключаемся на “omnetpp.ini”, запускаем (Run > Run As > 1 OMNeT++ Simulation), и симулятор сообщает о проблеме:

Error in module (inet::IPv4NodeConfigurator) Network.host0.networkLayer.configurator (id=73) during network initialization: Configurator module 'configurator' not found (check the 'networkConfiguratorModule' parameter).

В сообщении явно указывается место ошибки – “Network.host0.networkLayer.configurator”. Посмотрим на него: откроем “package.ned”, переключимся в редактор схемы (вкладка “Design”), и откроем панель “NED Parameters” на узле “host0”. Среди параметров будет “host0.networkLayer.configurator.networkConfiguratorModule”, посмотрим (Open NED Declaration) на “.ned” файл в котором он находится. Этот файл называется “IPv4NodeConfigurator.ned”, часть из его описания:

This module acts like a bridge between the node and the network's global configurator module - IPv4NetworkConfigurator.

IPv4NetworkConfigurator only computes and remembers configuration data (assigned IP addresses, static routes, etc), but doesn't configure the nodes routing tables and interfaces accordingly. The actual configuration is carried out by this module...

То есть в узлах сети есть локальный конфигуратор (*NodeConfigurator), который запрашивает параметры сети у глобального конфигуратора (*NetworkConfigurator), и настраивает локальный узел. В примере “broadcast” был глобальный конфигуратор – “IPv4NetworkConfigurator”, а в свою сеть мы забыли его включить…

# Назначение IP адресов устройствам в сети

Как выяснили выше, для настройки IP (адреса, маршруты, …) в сети, используется связка из глобального конфигуратора (*NetworkConfigurator) и локального (*NodeConfigurator). Но как локальный конфигуратор узнает, к какому модулю сети обращаться для получения конфигурации, ведь глобальный конфигуратор – это такой же модуль сети, как и все остальные модули (хосты, свитчи, …)?

Посмотрим опять на ошибку, которую вывел симулятор сети при запуске: “Configurator module 'configurator' not found”. То есть он искал модуль с названием “configurator”. Это его поведение по умолчанию. Поэтому нам надо добавить в сеть глобальный конфигуратор (*NetworkConfigurator), и дать ему имя “configurator”.

В доке “inet/doc/inet-manual-draft.pdf” на 80 странице начинается раздел “11.9 Configuring IPv4 networks”. В разделе рассказывается про 3 способа настройки сети: 1 новый (используя модуль “IPv4NetworkConfigurator”), и 2 старых (для автоматической настройки – модуль “FlatNetworkConfigurator” [страница 87], и для ручной – конфиг‑файлы). Ссылки на краткие описания работы модулей: “FlatNetworkConfigurator” и “IPv4NetworkConfigurator”.

Скачать “inet-manual-draft.pdf”, сгенерированный 2016-01-22 (включен в INET v3.4.0)

В этой статье я буду ссылаться именно на эту версию “inet-manual-draft.pdf”:

inet-manual-draft-2016-01-22-un_7z_me.pdf.png

Картинку нужно сохранить под именем “inet-manual-draft-2016-01-22-un_7z_me.pdf.png”, и распаковать (7zip).

Note: также эта версия “inet-manual-draft.pdf” сохранена в web.archive, и, возможно, сохранится на основном сайте.

Мы сделали плоскую сеть, поэтому возможностей “FlatNetworkConfigurator” должно хватить. Также его легче настраивать, и он проще устроен, чем “IPv4NetworkConfigurator”. Единственное, преимущество использования “IPv4NetworkConfigurator” для нас – это то, что он оптимизирует таблицу маршрутизации, а “FlatNetworkConfigurator” – не делает это (из “inet-manual-draft.pdf”):

NOTE: This configurator ("FlatNetworkConfigurator") does not try to optimize the routing tables. If the network contains n nodes, the size of all routing tables will be proportional to n2, and the time of the lookup of the best matching route will be proportional to n.

Попробуем оба конфигуратора, и посмотрим, как они работают.

Начнем с “FlatNetworkConfigurator”. Его можно добавить через редактор схемы (“Design”) или редактор кода (“Source”). В палитре редактора схемы его поможет найти фильтр “configurator”:

FlatNetworkConfigurator – filter

Note: Если заметили на скриншоте HostAutoConfigurator, то знайте, что он – deprecated.

После добавления задайте параметры: networkAddress “"10.0.1.0"”, netmask “"255.255.255.0"”. И, как помните, следует сменить его название на “configurator” (без этого локальные конфигураторы [*NodeConfigurator] его попросту не найдут).

Если же хотите использовать редактор кода, то просто, вначале подключите модуль “import inet.networklayer.configurator.ipv4.FlatNetworkConfigurator;”, а затем в “submodules:” добавьте:

configurator: FlatNetworkConfigurator {
    parameters:
        @display("p=62,31");
        networkAddress = "10.0.1.0";
        netmask = "255.255.255.0";
}

Должно получиться примерно так (git tag a1_v0.6.0) diff.

Запускаем симулятор, и получаем сообщение:

Error in module (inet::IPv4NodeConfigurator) Network.host0.networkLayer.configurator (id=74) during network initialization: check_and_cast(): cannot cast (inet::FlatNetworkConfigurator*)Network.configurator to type 'inet::IPv4NetworkConfigurator *'.

Похоже, что “host0.networkLayer.configurator” (IPv4NodeConfigurator) ожидает увидеть именно “IPv4NetworkConfigurator”. В Google Группе OMNeT++ Users предлагают выкинуть “FlatNetworkConfigurator” и использовать нормальный “IPv4NetworkConfigurator”, но мы все же заставим “FlatNetworkConfigurator” работать.

Вообще‑то проблема не в “FlatNetworkConfigurator”, а в “IPv4NodeConfigurator” – его сделали специально для “IPv4NetworkConfigurator” (вспомните предыдущий пробный запуск). То же самое написано и в описании “IPv4NetworkConfigurator”:

IMPORTANT: as of INET 2.2, this module does NOT assign addresses or add routes directly, just stores them in its internal data structures. Network nodes are supposed to contain an instance of IPv4NodeConfigurator (normally part of the network layer compound module) that actually configures the node's interface table and routing table based on the information stored in the global network configurator module.

Для функционирования “FlatNetworkConfigurator” модуль “IPv4NodeConfigurator” не нужен. Поэтому в “omnetpp.ini” добавляем:

**.networkLayer.configurator.networkConfiguratorModule = ""

Кстати, в той же теме на форуме это решение было написано в первом посте.

В итоге должно получиться примерно так (git tag a1_v0.7.0) diff.

Запускаем симулятор, и:

FlatNetworkConfigurator: network mask 32

… почему маска “/32”? Даже если изменить параметр “netmask” на любое другое (кроме “"0.0.0.0"” и “"255.255.255.255"”) значение, то маска останется та же – “/32”. Проверено.

Похоже, пора последовать совету с форума и забыть про “FlatNetworkConfigurator”, но т.к. мы уже запустили симулятор, посмотрим какие настройки внес этот конфигуратор в узлы сети.

В симуляторе есть очень удобный инструмент для отображения текущего состояния модулей сети. По сути, в данном случае, симулятор – это отладчик сети, наподобие отладчика программы, в котором выполнение программы можно приостановить, и посмотреть текущие значения переменных (на изображении сети выбираем “host0”, и переключаем режим отображения левой панели на “children mode”):

FlatNetworkConfigurator: qtenv host0 children

Можно просматривать не только текущие значения параметров, которые отображались в “NED Parameters”, но и некоторую дополнительную информацию. Значения можно менять (я двойным кликом открыл параметр “forwarding”, переключился в “grouped” режим, и завершающий двойной клик открыл поле для редактирования “value”):

FlatNetworkConfigurator: qtenv host0 grouped forwarding value

Кстати, это скриншоты из GUI симулятора, который появился в OMNeT++ 5.0 – Qtenv, он основан на Qt, и он быстрее предыдущего – Tkenv (Tcl/Tk). Однако в Tkenv визуализация дерева параметров была реализована лучше/аккуратнее (ИМХО):

FlatNetworkConfigurator: tkenv host0 grouped forwarding value

Note: Переключится на Tkenv можно в Eclipse > Run > Run Configurations.

Мы немного отвлеклись на GUI… Пора вспомнить, зачем мы полезли в параметры сети.

Мы хотели, перед тем как придать “FlatNetworkConfigurator” забвению, посмотреть в каком месте у хостов он прописал адреса, и маршруты. В его документации сказано следующее:

It is assumed that the routing table (IPv4RoutingTable module) is the "routingTable" or "networkLayer.routingTable" submodule in all hosts and routers.

...

In stage 0, interfaces register themselves in the InterfaceTable modules...

Посмотрим на “InterfaceTable”:

FlatNetworkConfigurator: qtenv host0 children grouped InterfaceTable
eth0 id=101  on:nwLayer.ifOut[1]  MTU:1500 BROADCAST MULTICAST  macAddr:0A-AA-00-00-00-07 IPv4:{inet_addr:10.0.1.1/32 mcastgrps:224.0.0.1}

Note: Копирование значений в Qtenv пока не поддерживается, а вот в Tkenv оно работает:

tkenv – context menu – copy

А теперь посмотрим на “routingTable”:

FlatNetworkConfigurator: qtenv host0 children grouped routingTable
dest:*  gw:*  mask:*  metric:0 if:eth0(10.0.1.1)  DIRECT MANUAL

Если “*” означает адрес “0.0.0.0”, то все будет работать нормально, и мы зря беспокоились насчет маски. Может он ставит маску “/32” специально? Вообще‑то так и есть.

Поищем исходники FlatNetworkConfigurator:

  1. раскроем дерево inet в “Project Explorer” (если не раскрыть, то на 3‑м этапе “Project Explorer” не перейдет на “FlatNetworkConfigurator.ned”),
  2. через редактор кода “package.ned” открыть место определения “FlatNetworkConfigurator” (Ctrl+Click),
  3. в “Project Explorer”, рядом с открывшимся файлом, увидеть “FlatNetworkConfigurator.cc”.
Open FlatNetworkConfigurator.cc

В “FlatNetworkConfigurator.cc” будет строчка, в которой назначается маска сети:

ie->ipv4Data()->setNetmask(IPv4Address::ALLONES_ADDRESS);    // full address must match for local delivery

Note:ALLONES_ADDRESS” равен “255.255.255.255”.

Комментарий весьма странный. Получается, что если переписать код, и назначить маску, отличную от “255.255.255.255”, то пакеты перестанут ходить между узлами одной подсети??? Вряд ли, т.к. единственное предназначение этого конфигуратора – настраивать плоскую сеть (одна подсеть). Возможно, имеется в виду случай, когда приложения хоста используют адрес внешнего интерфейса в качестве loopback адреса???

К счастью OMNeT++/INET не является черным ящиком: можно поменять “IPv4Address::ALLONES_ADDRESS” на “netmask”, можно просмотреть всю логику работы, можно … Предлагаю в будущем с этим поэкспериментировать, а мы уже переходим к “IPv4NetworkConfigurator”.

Чтобы перейти с “FlatNetworkConfigurator” на “IPv4NetworkConfigurator” нужно:

  1. удалить из “omnetpp.ini” строчку
    '**.networkLayer.configurator.networkConfiguratorModule = ""', чтобы включить “IPv4NodeConfigurator”;
  2. через редактор кода “package.ned” заменяем “FlatNetworkConfigurator” на “IPv4NetworkConfigurator” (заменяем в двух местах: в импорте, и в месте использования);
  3. убираем из параметров конфигуратора “networkAddress” и “netmask”, т.к. “IPv4NetworkConfigurator” не использует эти параметры.

Должно получиться примерно так (git tag a1_v0.8.0) diff.

Запускаем симулятор:

IPv4NetworkConfigurator: network

Все хорошо, маска как раз такая, чтобы вместить 4+2 адреса (2: 10.0.0.0 – адрес подсети; 10.0.0.7 – broadcast адрес).

А как изменились маршруты на хостах? Посмотрим на “host0.routingTable.routes”:

IPv4NetworkConfigurator: qtenv host0 grouped routingTable routes
[0] = dest:10.0.0.0  gw:*  mask:255.255.255.248  metric:0 if:eth0(10.0.0.1)  DIRECT MANUAL
[1] = dest:10.0.0.0  gw:*  mask:255.255.255.248  metric:20 if:eth0(10.0.0.1)  DIRECT IFACENETMASK

Самое время вспомнить цитату из документации (“inet-manual-draft.pdf”), в которой говорилось, что “IPv4NetworkConfigurator” оптимизирует таблицу маршрутизации, а “FlatNetworkConfigurator” не делает этого. В итоге у “FlatNetworkConfigurator” в таблице маршрутизации было 2 записи, а у “IPv4NetworkConfigurator” – 3 записи. Почему так произошло?

That, Detective, is the right question. Program terminated.
спойлер

Намек на параметр “addStaticRoutes”.

Сейчас “IPv4NetworkConfigurator” работает в автоматическом режиме, выдавая адреса по умолчанию. Попробуем настроить его на раздачу адресов, из подсети “10.0.1.0”, с маской “/24”.

Вначале посмотрим на дамп его текущей “автоматической” конфигурации. Для этого надо настроить параметр “dumpConfig”:

configurator: IPv4NetworkConfigurator {
    parameters:
        @display("p=62,31");
        dumpConfig = "config_dump.xml";
}

После запуска симулятора, появится файл “LLTR/simulations/config_dump.xml” (git tag a1_v0.9.0) diff. Однако, все, что в нем есть мы уже и так знали.

не совсем так

Обратите внимание на запись:

<route hosts="Network.host3" destination="10.0.0.0" netmask="255.255.255.252" gateway="*" interface="eth0" metric="0"/>

Маска “255.255.255.252”, при том, что IP этого хоста “10.0.0.4”:

<interface hosts="Network.host3" names="eth0" address="10.0.0.4" netmask="255.255.255.248" metric="20"/>

Говорит нам об оптимизации: 10.0.0.0” & “255.255.255.252” + 1 = “10.0.0.1”  ..  “10.0.0.0” | ~“255.255.255.252” = “10.0.0.3”, т.е. маска включает все адреса других хостов сети, за исключением адреса текущего хоста (“10.0.0.4”).

Более интересен конфиг, из которого получился этот дамп. Этот конфиг прописан прямо внутри “IPv4NetworkConfigurator.ned”:

<config><interface hosts='**' address='10.x.x.x' netmask='255.x.x.x'/></config>

Про “x” подробно написано в “inet-manual-draft.pdf” на 82-83 странице. Вкратце, “x” рассчитывается на основе количества устройств (точнее их интерфейсов) в сети, которым нужен IP. Для “address” вместо “x” подставляется номер текущего устройства, а “x” в “netmask” регулирует размер маски так, чтобы длина маски была наибольшей для данной подсети. Например, маска “255.x.x.0” может принимать итоговые значения:

Про “**” уже писалось ранее (полная аналогия с заданием параметров и с Git).

В нашем случае иерархии хостов нет, поэтому обойдемся “ hosts='*' ”. Остальные параметры: “ address='10.0.1.x' ”, “ netmask='255.255.255.0' ”. Конфиг с этими параметрами можно сохранить в файл “LLTR/simulations/config.xml”, и указать путь к нему в параметрах конфигуратора:

configurator: IPv4NetworkConfigurator {
    parameters:
        @display("p=62,31");
        config = xmldoc("config.xml");
        dumpConfig = "config_dump.xml";
}

Должно получиться примерно так (git tag a1_v0.10.0) diff.

Note:xmldoc()” нужен для загрузки xml из внешнего файла.

После запуска симулятора появится новый дамп конфига. Обратите внимание на отличия. Сохранилась прежняя маска в записях с метрикой “0”, а записи с метрикой “20” обновили маску на “255.255.255.0”. Выше, когда смотрели “host0.routingTable.routes”, можно было заметить, что запись с метрикой “0” подписана как “DIRECT MANUAL”, а метрика “20” как “DIRECT IFACENETMASK”. (здесь был намек на спойлеры ;-) )

Генерация дампа конфига нам уже не нужна (все, что надо мы уже посмотрели), поэтому убираем из “package.ned” параметр “dumpConfig”. Также симулятор на карте сети не очень хорошо отображает “IP/mask” (надписи наползают друг на друга), поэтому отключим вывод надписей. На одном из предыдущих скриншотов симулятора был виден параметр “displayAddresses” у “interfaceTable”. Достаточно добавить строку “**.interfaceTable.displayAddresses = false” в “omnetpp.ini”, чтобы отключить вывод “IP/mask”.

Должно получиться примерно так (git tag a1_v0.11.0) diff.

IPv4NetworkConfigurator: clear network

Да будет чистота! А IP определим по номеру хоста+1.

# А кто назначает MAC адреса?

Возможно, это делает “L2NetworkConfigurator”? Однако, по описанию, он предназначен для другого:

The STP and RSTP related parameters such as link cost, port priority and the "is-edge" flag...

Краткий ответ: никто. Точнее отдельного модуля, назначающего уникальные MAC адреса в INET нет.

Попробуем найти то место в INET, в котором генерируются MAC адреса. Ранее мы форсировали использование “EtherMACFullDuplex” на всех узлах сети. Он и будет нашей отправной точкой.

Посмотрим на метод “initialize()” (подробнее про его назначение, и про стадии инициализации будет сказано ниже):

void EtherMACFullDuplex::initialize(int stage)
{
    EtherMACBase::initialize(stage);

Так как “EtherMACFullDuplex” наследуется от “EtherMACBase”, то здесь просто вызывается аналогичный метод родителя. Заглянем в него. Здесь интересен вызовinitializeMACAddress()” → “EtherMACBase::initializeMACAddress()” → “MACAddress::generateAutoAddress()”.

И так, мы дошли до места, в котором генерируются уникальные MAC адреса. А сама генерация происходит во время стадии “INITSTAGE_LOCAL.

# Где еще можно посмотреть параметры и структуру модулей?

Настройки визуализации любого элемента сети можно посмотреть (и настроить) в панели “Properties”:

Properties palette

Либо через контекстное меню → “Properties…”:

Properties context menu

А по структуре (иерархии) модуля можно быстро пройтись в панели “Module Hierarchy”:

Module Hierarchy palette

# Шаг 0: посылаем unicast, пробуем его принять

Наконец‑то, GUI с конфигами позади, и можно уже что‑нибудь накодить. Для того чтобы описать свою логику отправки и принятия UDP пакетов, нужно внутри INET написать два мини UDP‑приложения: первое будет отправлять пакеты, а второе – принимать их. Эти приложения будем запускать на разных хостах, например, на “host0” запустим приложение, отправляющее пакеты, а на всех остальных хостах – приложение, принимающее пакеты.

Note: внутренние элементы навигации в этом подразделе отключены, т.е. он линейный (последовательный), и не содержит в себе подразделов.

Как было сказано выше в OMNeT++ “Network” (сеть) состоит из “Compound module” (host0), которые, в свою очередь, состоят из “Simple modules” (udpApp). Поэтому, чтобы создать новое UDP‑приложение (udpApp), нужно создать новый “Simple module”…

… И все же придется вернуться в GUI, чтобы посмотреть место, в которое будет подключаться модуль UDP‑приложения. Раскроем “host0” (двойной клик):

host0 StandardHost udpApp

Вот он этот модуль – “udpApp” (точнее вектор модулей “udpApp[numUdpApps]”), на месте которого и будет подключено наше UDP‑приложение.

В том же разделе “Создание первого проекта”, мы смотрели код готовых приложений (UDP Application) и примеров (broadcast), а также решили расположить код приложения LLTR рядом с кодом других приложений в INET (раздел “Структура проекта”). В свете этого, создадим директорию “inet/src/inet/applications/lltrapp”.

Создадим приложение, отправляющее пакеты (LLTRSuperApp):

  1. запустим Wizard (панель “Project Explorer” > проект “inet” > директория “src/inet/applications/lltrapp” > контекстное меню > “New” > “Simple Module”)
  2. NED файл назовем “LLTRSuperApp.ned
  3. В качестве шаблона (template) выберем “A simple module”.

Попробуем сравнить то, что сгенерировал Wizard с готовым приложением “UDPEchoApp”. Начнем с заголовочных файлов.

Первое, что бросается в глаза – это то, что “UDPEchoApp” наследуется от “ApplicationBase”, а Wizard для “LLTRSuperApp” сгенерировал наследование от “cSimpleModule”. Попробуем пройти вверх (Ctrl+Клик по классу‑родителю в редакторе кода) по иерархии наследования, и посмотреть чем “ApplicationBase” отличается от “cSimpleModule”, и нужен ли он нам? Получится следующая картина: “ApplicationBase” ← “OperationalBase” ← “cSimpleModule” + “ILifecycle”. В комментарии к “ILifecycle” сказано следующее:

Interface to be implemented by modules that want to support failure/recovery, shutdown/restart, suspend/resume, and similar scenarios.

Получается, что если мы хотим моделировать эти сценарии, то должны наследоваться от “ApplicationBase”, если же они нам не нужны, то достаточно наследования от “cSimpleModule”. Эти сценарии нам не пригодятся, поэтому оставляем наследование от “cSimpleModule”.

Осталось еще несколько несоответствий, после их устранения, объединения объявления с определением класса “LLTRSuperApp” (извне он нам не нужен), и создания модуля “LLTRApp” (путем копирования файлов “LLTRSuperApp” и замены имени) получилось это (git tag a1_v0.12.0) diff.

Что делает строчка “Define_Module(LLTRSuperApp);” в коде?

Вкратце: она регистрирует класс в качестве модуля, чтобы при использовании модуля “LLTRSuperApp” в сети, автоматически создавался экземпляр класса “LLTRSuperApp”.

Более подробно: “Define_Module()” – это макрос, который за 34 шага (Eclipse → навести указатель на название макроса → навести указатель на всплывающую подсказку; либо Ctrl+#; Ctrl+Shift+3) раскрывается в:

static omnetpp::cObject *__factoryfunc_43() {omnetpp::cModule *ret = new LLTRSuperApp; return ret; } \
  static void *__castfunc_43(omnetpp::cObject *obj) {return (void*)dynamic_cast<LLTRSuperApp*>(obj);} \
  namespace { \
    void __onstartup_func_43() {omnetpp::classes.getInstance()->add(new omnetpp::cObjectFactory(omnetpp::opp_typename(typeid(LLTRSuperApp)), __factoryfunc_43, __castfunc_43, "module"));;} \
    static omnetpp::CodeFragments __onstartup_obj_43(__onstartup_func_43, omnetpp::CodeFragments::STARTUP); \
  };

Начну с конца. Конструктор “omnetpp::CodeFragments” (omnetpp-5.0/src/sim/onstartup.cc) добавляет “__onstartup_obj_43” в односвязный список. Затем, в определенный момент времени вызовется “CodeFragments::executeAll()” (omnetpp-5.0/src/sim/onstartup.cc), который пройдет по всему списку, начиная с “CodeFragments::head”, и вызовет “__onstartup_func_43”. А “__onstartup_func_43()” зарегистрирует новый модуль, и свяжет с ним две функции: “__factoryfunc_43()” – для создания экземпляра класса “LLTRSuperApp”, и “__castfunc_43()”.

В OMNeT++ для передачи сообщения от одного модуля к другому (через цепочку):

Module1[gate$o]-->--[channel]-->--[gate$i]Module2

используется функция “send(‹отправляемое сообщение›, ‹“gate”, через который отправить сообщение›)” (примеры использования из “Simulation Manual”). После отправки сообщения, его получит модуль на другом конце “channel”. А так как INET построен поверх OMNeT++, то мы не можем просто так взять “send()” и отправить сообщение на определенный unicast IP и порт.

send() UDP

send()” попросту не принимает на вход такие аргументы как IP и порт.

Как же сформировать UDP пакет, и отправить его? На схеме “StandardHost” модуля видно, что все “udpApp” подключаются к модулю “udp”. В документации про модуль “UDP” сказано следующее:

... For sending an UDP packet, the application should attach an UDPControlInfo object to the payload, and send it to UDP. UDP will also attach an UDPControlInfo object to any payload message in sends up to the application.

For receiving UDP packets, the connected applications should first "bind" to the given UDP port. This can be done by sending an arbitrary message with message kind UDP_C_BIND and an UDPControlInfo attached with srcPort filled in...

See also: ..., UDPCommandCode

То есть, для отправки UDP пакета, нужно посылать сообщение в модуль “udp” с просьбой отправить пакет… И для совершения любых других операций нужно отправлять сообщения с просьбами в модуль “udp”.

Например, нам надо задать TTL, как это сделать? Как‑то так:

  1. создать новый “cMessage”, и указать в качестве “Message kind” значение “UDP_C_SETOPTION” (“UDPCommandCode”);
  2. в качестве контрольной информации добавить к сообщению “UDPControlInfo” (точнее “UDPSetOptionCommand”, точнее “UDPSetTimeToLiveCommand”) с указанием желаемого значения TTL, и некоторой другой информации;
  3. и, наконец, отправить получившееся сообщение при помощи “send()”.

И как же понять, что нужно делать именно так? Это “очень просто”:

  1. находим исходники “udp” модуля – “inet/src/inet/transportlayer/udp/UDP.cc”;
  2. находим функцию, обрабатывающую входящие сообщения (подробнее про обработку сообщений я напишу чуть ниже) – “handleMessage()”;
  3. ходим по веткам → “processCommandFromApp()” → “case UDP_C_SETOPTION” –[доходим до ужаса, состоящего из “else-if-dynamic_cast”]→ “((UDPSetTimeToLiveCommand *)ctrl)->getTtl()” → “setTimeToLive()”.

Ах, да, чуть не забыл про иерархию наследования (советую обратить внимание на комментарии к коду): “UDPSetTimeToLiveCommand” ← “UDPSetOptionCommand” ← “UDPControlInfo”. Вот только код совсем не похож на C++, и файл имеет расширение “.msg”.

Note: Файлы “.msg” – это очень удобная вещь в OMNeT++, они позволяют в простом виде описать структуру пакета/сообщения. Из этих файлов OMNeT++ создаст “*_m.h” и “*_m.cc” файлы со всей необходимой обвязкой. Подробнее про “.msg” файлы напишу, когда будем создавать свой “.msg” файл.

Если опять взглянуть на документацию (“inet-manual-draft.pdf”; раздел 13.2 “The UDP module”, страница 96), то найдем подтверждение нашим “блужданиям” по коду:

The UDP module can be connected to several applications, and each application can use several sockets to send and receive UDP datagrams. The state of the sockets are stored within the UDP module and the application can configure the socket by sending command messages to the UDP module. These command messages are distinguished by their kind and the type of their control info. The control info identifies the socket and holds the parameters of the command.

Это все отличается от того, как мы привыкли работать с сетью (через сокеты) в прикладных программах внутри ОС. Однако, если прочесть чуть дальше “inet-manual-draft.pdf”; раздел 13.2 “The UDP module”, страница 96):

Applications don’t have to send messages directly to the UDP module, as they can use the UDPSocket utility class, which encapsulates the messaging and provides a socket like interface to applications.

Аллилуйя! Можем работать с сетью привычным способом (“inet-manual-draft.pdf”; раздел 13.3 “UDP sockets”, страница 98)! Именно этим способом работают с сетью стандартные “UDP applications”, например, “UDPEchoApp”:

socket.sendTo(pk, srcAddress, srcPort);

Вернемся к нашим программам, и начнем с “LLTRSuperApp”. Как было написано выше, для работы с UDP сокетами нужно создать экземпляр класса “UDPSocket”, добавим его в наш класс (в виде поля):

class INET_API LLTRSuperApp: public cSimpleModule
{
       UDPSocket socket;

Note: почему я везде пишу “экземпляр класса *”, а не “объект класса *” – чтобы ни у кого не возникало путаницы.

Что сделаем далее:

  1. настроим сокет;
  2. отправим “пустой” пакет одному из хостов в сети;
  3. закроем сокет.

В OMNeT++ настройкой чего либо (в данном случае сокета) лучше заниматься во время инициализации модуля. Выше, в разделе “А кто назначает MAC адреса?”, упоминался метод “initialize()OMNeT++ вызывает его несколько раз для инициализации модуля.

Зачем вызывать “initialize()” несколько раз? Почему одного раза недостаточно для инициализации модуля? Между модулями могут быть циклические зависимости (например, первому модулю, для инициализации, нужны данные из второго модуля, а второй модуль, в свою очередь, ждет пока первый модуль инициализируется, чтобы получить из него нужные данные для своей инициализации – так появляются циклические зависимости), чтобы их обойти, OMNeT++ использует несколько стадий инициализации. Предполагается, что внутри одной стадии, циклических зависимостей не будет. Количество стадий не фиксировано – каждый модуль, переопределяя метод “numInitStages()задает необходимое ему количество стадий:

Multi-stage initialization can be achieved by redefining the initialize(int stage) method instead, and also redefining the numInitStages() const method to return the required number of stages.

Note: более подробно про “numInitStages()”, и про то, почему этот метод обязательно должен быть “чистой функцией” (модификатор const), написано в “Simulation Manual”.

Однако, в INET задано фиксированное число стадий. Все стадии и их описания перечислены в файле “inet/src/inet/common/InitStages.h”.

Как выбрать нужную стадию для инициализации каждого из компонентов модуля? Правила простые:

  1. выбираем наименьшую (по номеру) стадию;
  2. если для компонента модуля нужны данные из внешних модулей (либо нужны данные других компонент текущего модуля), то все эти данные должны быть готовы (проинициализированы) на предыдущих стадиях (относительно выбранной стадии);
  3. лучше выбирать стадию исходя из ее предназначения.

Так, например, для приложений, лучше всего использовать стадии:

Наша функция “initialize()” будет выглядеть так:

void initialize(int stage)
{
       cSimpleModule::initialize(stage);

       switch(stage){
       case INITSTAGE_APPLICATION_LAYER:
             socket.setOutputGate(gate("udpOut"));
             socket.setTimeToLive(1);

             break;
       case INITSTAGE_LAST:
             socket.sendTo(new cPacket("=Packet name="), IPv4Address(10,0,1,4), 1100);

             break;
       }
}

Вначале не забываем про инициализацию “родителей”: “cSimpleModule::initialize(stage);”.

Затем, на стадии INITSTAGE_APPLICATION_LAYER, используя функцию “setOutputGate()”, задаем для сокета “gate”, через который будут отправляться сообщения. Функция принимает один параметр – адрес объекта (класс “cGate”). Адрес можно получить, использую функцию “gate(‹имя нужного gate›)”.

И так, как у нашего модуля пока что нет ни одного “gate”, то пора их добавить (в файл “LLTRSuperApp.ned”):

simple LLTRSuperApp
{
    parameters:
        @display("i=block/app");
    gates:
        input  udpIn  @labels(UDPControlInfo/up);
        output udpOut @labels(UDPControlInfo/down);
}

И, заодно, мы назначили модулю иконку приложения “"i=block/app"”.

Далее, в INITSTAGE_APPLICATION_LAYER, назначаем TTL (“setTimeToLive()”), для всех пакетов, отправленных через этот сокет. Это получилось сделать намного проще, чем описывалось ранее, однако внутри все происходит именно так, как описывалось. В нашей сети нет маршрутизаторов (или любых других промежуточных узлов, уменьшающих TTL), поэтому TTL 1 должно хватить для доставки пакетов на любой хост в сети. Если какой‑либо пакет не дойдет, из‑за достижения TTL нуля, то это будет указывать на ошибку в конфигурировании модели/сети/модулей.

Настало время отправить “пустой” пакет! Пакет отправляется на стадии INITSTAGE_LAST, при помощи функции “sendTo(‹пакет›, ‹IP-адрес назначения›, ‹порт назначения›)”. Так как стадия INITSTAGE_LAST идет после стадии INITSTAGE_APPLICATION_LAYER, то мы можем гарантировать, что все приложения уже инициализировались, и могут обрабатывать входящие сообщения. Однако, в реальных программах/модулях лучше не отправлять пакет сразу в INITSTAGE_LAST, а использовать “scheduleAt()”, чтобы запланировать его отправку на время начала симуляции. Но и такой простой вариант тоже работает :)

Создать “пустой” пакет очень легко: `new cPacket("‹имя пакета›")`. Важно помнить, что “‹имя пакета›” – это всего лишь имя, которым будет помечен пакет, при отображении в логе Qtenv. Созданный таким способом пакет пуст! Подробнее про cPacket и его отличия от cMessage можно почитать в “Simulation Manual”.

Адрес назначения тоже легко указать – это можно сделать через конструкторы “L3Address”, либо напрямую, через конструкторы “IPv4Address”. Выше, в разделе “Назначение IP адресов устройствам в сети”, настраивался “IPv4NetworkConfigurator” на назначение адресов, начиная с “10.0.1.1”, поэтому указанный IP “IPv4Address(10,0,1,4)” – это адрес одного из 4‑х хостов в сети.

Пакет отправлен, и осталось закрыть сокет. Возможно, в будущем, нам понадобится отправить еще несколько пакетов через этот сокет, поэтому не будем его сразу закрывать, а закроем в методе “finish()”:

void finish()
{
       socket.close();

       cSimpleModule::finish();
}

Этот метод вызывается при успешном завершении симуляции модели. Здесь важно заметить, что при запуске симуляции – создаются объекты моделей (вызываются конструкторы), а при завершении симуляции – объекты уничтожаются (вызываются деструкторы). Так чем “finish()” отличается от деструктора? Тем, что “finish()” вызывается только тогда, когда симуляция завершилась без ошибок (причиной завершения симуляции не является ошибка), а деструктор вызывается всегда. Поэтому “finish()” чаще всего используют для вывода собранной статистики за время симуляции, а деструктор для “очистки” объекта. Поэтому логичным было бы расположить “socket.close()” именно в деструкторе, но это очень плохая идея. Если вспомнить, что все методы в “UDPSocket” – это всего лишь “обвертки” для отправки сообщений, и добавить к этому информацию из раздела “Initialization and Finalization” и раздела “Invocation Order” в “Simulation Manual”:

The finish() functions are called when the event loop has terminated, and only if it terminated normally.

То становится ясно, что закрывать сокет в “finish()”, и тем более в деструкторе, – бесполезно, т.к. “по документации” цикл обработки событий уже будет разрушен.

И так, мы уже:

  1. [x] настроили сокет;
  2. [x] отправили “пустой” пакет одному из хостов в сети;
  3. [x] закрыли сокет.

Похоже, с “LLTRSuperApp” мы уже закончили, и пора переходить к “LLTRApp”.

Что будем делать в “LLTRApp”:

  1. настроем сокет на прослушивание порта;
  2. при получении пакета – выведем в лог сообщение с именем пришедшего пакета;
  3. закроем сокет.

Но перед этим, также как и в “LLTRSuperApp”, добавим экземпляр класса “UDPSocket”:

class INET_API LLTRApp: public cSimpleModule
{
       UDPSocket socket;

Привяжем сокет к порту 1100, при помощи функции “bind()”, на стадии INITSTAGE_APPLICATION_LAYER:

void initialize(int stage)
{
       cSimpleModule::initialize(stage);

       switch(stage){
       case INITSTAGE_APPLICATION_LAYER:
             socket.bind(1100);

             break;
       }
}

А так как, в “LLTRApp” мы будем только принимать пакеты (отправлять ничего не будем), то отпадает необходимость в указании выходного “gate” функцией “setOutputGate()”. При этом, все входящие пакеты будут автоматически отправлены модулем “udp” на наш входной “gate”, и мы сможем обработать их в методе “handleMessage()”.

Кстати, про входной и выходной “gate” – в “LLTRSuperApp” мы их добавляли в описание модели (“.ned” файл), пора сделать то же самое и для “LLTRApp” (в файле “LLTRSuperApp.ned”):

simple LLTRApp
{
    parameters:
        @display("i=block/app");
    gates:
        input  udpIn  @labels(UDPControlInfo/up);
        output udpOut @labels(UDPControlInfo/down);
}

С настройкой сокета закончили. Теперь надо ожидать прибытия пакета, и, при его получении, вывести в лог сообщение с его именем.

Выше я несколько раз упоминал про функцию‑обработчик входящих сообщений, пора познакомится с ней поближе. В OMNeT++, на самом деле, существует не одна, а целых две функции для обработки сообщений: handleMessage()” и “activity(), в пределах одного модуля можно одновременно использовать только одну из них. Посмотрим что написано в документации про “handleMessage()”:

One has to redefine handleMessage() to contain the internal logic of the module. handleMessage() is called by the simulation kernel when the module receives a message.

И про “activity()”:

An alternative to handleMessage() is activity(), but activity() is not recommended for serious model development because of scalability and debugging issues. activity() also tends to lead to messy module implementations.

Они явно не рекомендуют использовать “activity()”:

Should be redefined to contain the module activity function.
For several good reasons, you should prefer handleMessage() to activity().

А также (из “Simulation Manual”):

handleMessage() is in most cases a better choice than activity():

- When you expect the module to be used in large simulations, involving several thousand modules. In such cases, the module stacks required by activity() would simply consume too much memory.

- For modules which maintain little or no state information, such as packet sinks, handleMessage() is more convenient to program.

- Other good candidates are modules with a large state space and many arbitrary state transition possibilities (i.e. where there are many possible subsequent states for any state). Such algorithms are difficult to program with activity(), and better suited for handleMessage() (see rule of thumb below). This is the case for most communication protocols.

В свое время, все эти доводы привели к “слепому” использованию “handleMessage()” вместо “activity()”.

Note: сейчас я думаю иначе – скорее всего, если бы я использовал “activity()”, то создание модели LLTR заняло бы намного меньше времени…

Как происходит обработка событий при использовании “handleMessage()”?:

  1. пакет приходит;
  2. в функции “handleMessage()” он обрабатывается;
  3. функция возвращает управление.

Если нужно сохранить результаты обработки, либо сохранить определенное “состояние”, то надо создавать дополнительные поля в классе для хранения всего этого. С течением времени полей становится все больше и больше, и что самое неприятное – часть из этих полей используется только на небольшом промежутке работы “программы” (для передачи состояния от одного вызова “handleMessage()” к другому). Это усугубляется тем, что все, по сути разные, сообщения приходят через одно место – “handleMessage()”. В реальной программе я бы мог, для каждого типа сообщения, зарегистрировать свой обработчик, но не здесь… В этот момент появляются мысли про создание новых классов, либо про использование этого, либо этого. И код попеременно превращается то в спагетти, то в лазанью, то в равиоли, и становится более запутанным чем сам INET. И, наконец, происходит откат к более простому первоначальному виду.

Ладно, я забежал слишком далеко вперед, а пока наденем розовые очки белую маску и примем Joy pill.

Однажды нелегкая завела меня в “подвал” файла “omnetpp-5.0/include/omnetpp/csimplemodule.h” (doxygen убирает комментарии из кода), а затем в “omnetpp-5.0/include/omnetpp/ccoroutine.h” (комментарии), и стало ясно, что “activity()” основаны на fibers/сопрограммах.

Как происходит обработка событий при использовании “activity()”?:

  1. пакет приходит и функция “activity()” “размораживается”;
  2. функция обрабатывает пакет;
  3. функция “замораживается”.

Все результаты обработки, и “состояния” можно хранить в локальных переменных функции (ровно также, как и в языках, поддерживающих “синхронно‑асинхронное” программирование “из коробки”, например Go). Однако предупреждения по поводу повышенного использования памяти были даны не зря:

- limited scalability: coroutine stacks can unacceptably increase the memory requirements of the simulation program if you have several thousands or ten thousands of simple modules;

- run-time overhead: switching between coroutines is somewhat slower than a simple function call

- does not enforce a good programming style: using activity() tends to lead to unreliable, spaghetti code

Если не контролировать размер стека, то симуляция “съест” много памяти. Есть несколько практик позволяющих уменьшить используемое место на стеке (все они связаны с использованием области видимости “{}):

  1. максимально ограничить время жизни (область видимости) временных переменных, заключая их в “{}”;
  2. если обработка проходит в несколько стадий, то создать иерархию областей видимости: на верхнем уровне – те переменные, которые используются во всех стадиях, уровень ниже для переменных, нужных для определенной стадии (каждая стадия имеет свою область видимости);
  3. точки “заморозки/разморозки” лучше располагать в местах наименьшего использования стека, т.е. ближе к верхнему уровню иерархии областей видимости.

В “Simulation Manual” написано еще несколько полезных вещей про “handleMessage()” и “activity()”, например, как переключится с “handleMessage()” на “activity(), но, а мы идем дальше.

Наша функция “handleMessage()”, для вывода в лог имени пришедшего пакета, выглядит так:

void handleMessage(cMessage *msg)
{
       switch(msg->getKind()){
       case UDP_I_DATA:{
             EV << "Arrived: " << msg->getName() << endl;
             delete msg;

       }break;
       case UDP_I_ERROR:{
             EV_WARN << "Ignoring UDP error report" << endl;
             delete msg;

       }break;
       default: throw cRuntimeError("Unrecognized message (%s)%s", msg->getClassName(), msg->getName());
       }
}

Вначале смотрим на “kind” сообщения. В UDP пакетах, “kind” может иметь только 2 значения:

Note: “kind” – это всего лишь некоторое целое число, которое можно присвоить сообщению. Отрицательные значения зарезервированы под внутренние нужды OMNeT++, а все положительные (включая 0) можно свободно использовать для любых целей. Обычно его используют для задания типа/вида/предназначения конкретного сообщения.

Для вывода сообщения в лог Qtenv используется поток “EV”, а для получения имени пришедшего пакета – метод “getName()”.

Note:EV” – это на самом деле макрос, который раскрывается в “EV_INFO, который опять же является макросом…

В конце (перед выходом из функции “handleMessage()”) нужно обязательно освободить память, удалив пришедшее сообщение (“delete msg”), иначе мы получим утечку памяти.

Почему произойдет утечка памяти?

Сообщение (пакет) создавался в “LLTRSuperApp” при помощи вызова `new cPacket("=Packet name=")` (выделилась память под его хранение). Теперь сообщение пришло в “LLTRApp”, и так как мы не собираемся его перенаправлять в другое место, то следует освободить занимаемую им память (“delete msg”).

Если этого не делать, то в памяти накопятся куча уже доставленных, обработанных, но не удаленных сообщений/пакетов… Не у всех есть рабочие станции с 128 GiB оперативки для запуска симуляции. Поэтому все, что не нужно, лучше сразу удалять :)

Если “kind” не равен UDP_I_DATA и не равен UDP_I_ERROR, то бросаем исключение “throw cRuntimeError()”. Это может случиться если:

Обратите внимание, что в каждом “case” создается своя область видимости (“{}”) – это нам пригодится в будущем.

С выводом в лог сообщения мы закончили, осталось закрыть сокет. Сделаем это так же, как и в “LLTRSuperApp”:

void finish()
{
       socket.close();

       cSimpleModule::finish();
}

Посмотрим, что получилось (git tag a1_v0.13.0) diff. Собираем INET, запускаем.

Note: если открыт один из файлов INET (например, “LLTRSuperApp.*” или “LLTRApp.*”), и этот файл находится в фокусе, то для сборки INET достаточно нажать “Ctrl+B” (предварительно воспользовавшись этим советом).

И жмем на “Run”:

qtenv: run with full animation (F5)

Хммм… Ничего не произошло…

qtenv: no App, no Events (macOS style)

Так, а как симулятор поймет, что на хостах надо запускать “LLTRSuperApp” и “LLTRApp”, а не, например, “UDPEchoApp”? Мы же нигде не указали, какой конкретный “udpApp” хотим запустить на хостах…
Посмотрим, из чего сейчас состоит “host0”, и есть ли в нем хоть один “udpApp”:

qtenv: host0 – no UdpApp

Как и предполагалось, “udpApp” отсутствуют, и параметр “numUdpApps” равен 0.
А как сейчас выглядит схема “host0”? (двойной клик по “host0”):

qtenv: in host0 – no UdpApp

В нем не только отсутствуют “udpApp”, но и самого модуля “udp” тоже нет. Сравните эту “похудевшую” схему с изначальной схемой модуля “StandardHost”.

Как привязать “LLTRSuperApp” и “LLTRApp” к хостам? Посмотрим, как это сделано в “UDPBroadcastNetwork”:

**.client.numUdpApps = 1
**.client.udpApp[0].typename = "UDPBasicApp"

И, заодно, заглянем в “StandardHost.ned”:

int numUdpApps = default(0);  // no of UDP apps. Specify the app types in INI file with udpApp[0..1].typename="UDPVideoStreamCli" syntax

Получается, чтобы привязать “udpApp” к хосту – надо задать “numUdpApps” и “udpApp[0].typename”.

А что делать, если надо, например, к “host0” привязать “LLTRSuperApp”, а ко всем остальным хостам – “LLTRApp”? Неужели придется для каждого хоста копипастить практически одинаковую строчку?:

**.host?.numUdpApps = 1
**.host0.udpApp[0].typename = "inet.applications.lltrapp.LLTRSuperApp"
**.host1.udpApp[0].typename = "inet.applications.lltrapp.LLTRApp"
**.host2.udpApp[0].typename = "inet.applications.lltrapp.LLTRApp"
**.host3.udpApp[0].typename = "inet.applications.lltrapp.LLTRApp"

Что на этот счет сказано в “Simulation Manual”?:

If you use wildcards, the order of entries is important; if a parameter name matches several wildcard-patterns, the first matching occurrence is used. This means that you need to list specific settings first, and more general ones later. Catch-all settings should come last.

Это значит, что мы можем одновременно использовать и хосты с “подстановочными знаками”, и указывать конкретный хост, а OMNeT++ будет использовать первое совпадение параметра с шаблоном. Получится как‑то так:

**.host?.numUdpApps = 1
**.host0.udpApp[0].typename = "inet.applications.lltrapp.LLTRSuperApp"
**.host?.udpApp[0].typename = "inet.applications.lltrapp.LLTRApp"

Note: Это будет работать, если в сети используются только хосты с “host0” по “host9”, но “host?” не будет работать для “host10”, “host11”, и остальных. <sarcasm>В этом случае поможет переход на 16‑ю систему счисления, в которой все будет работать вплоть до “hostF” хоста :)</sarcasm>

Либо, если сгруппировать по хостам, так:

**.host0.udpApp[0].typename = "inet.applications.lltrapp.LLTRSuperApp"
**.host?.numUdpApps = 1
**.host?.udpApp[0].typename = "inet.applications.lltrapp.LLTRApp"

Note: так как наш “omnetpp.ini” находится в одном проекте (“LLTR”), а приложения – в другом (“INET”), то при задании “typename” нужно указывать полный путь до приложения в “INET”.

Должно получиться примерно так (git tag a1_v0.14.0) diff.

Посмотрим, что получилось. В исходном коде изменений не было, поэтому собирать INET необязательно. Просто запускаем, и…

Error in module (omnetpp::cModule) Network.host0 (id=5) during network setup: Submodule udpApp: no module type named `inet.applications.lltrapp.LLTRSuperApp' found that implements module interface inet.applications.contract.IUDPApp (not in the loaded NED files?).

Оказывается “udpApp” должны реализовывать интерфейс “IUDPApp” (“inet-manual-draft.pdf”; раздел 13.4 “UDP applications”, страница 98):

All UDP applications should be derived from the IUDPApp module interface, so that the application of StandardHost could be configured without changing its NED file.

Посмотрим, как это выглядит в “UDPEchoApp.ned”:

import inet.applications.contract.IUDPApp;

simple UDPEchoApp like IUDPApp
{

Сделаем то же самое для “LLTRSuperApp” и “LLTRApp” (git tag a1_v0.15.0) diff.

Еще раз, просто запускаем

Error in module (inet::LLTRApp) Network.host1.udpApp[0] (id=104) during network initialization: UDPSocket: setOutputGate() must be invoked before socket can be used.

Опять?..

Текст ошибки просит обратить внимание на “LLTRApp” и сокеты. Я ранее написал:

А так как, в “LLTRApp” мы будем только принимать пакеты (отправлять ничего не будем), то отпадает необходимость в указании выходного “gate” функцией “setOutputGate()”.

Эта мысль и привела к ошибке. “UDPSocket” – это всего лишь обвертка для отправки сообщений в модуль “udp”. Даже при настройке сокета она отправляет управляющие сообщения, и естественно ей надо знать через какой “gate” отправлять эти сообщения. Например, возьмем тот же “bind()”, который использовали в “LLTRApp”, что он делает? Он вызывает “sendToUDP()”, который использует “gateToUdp” для управляющих команд, а эта переменная, в свою очередь, задавалась через “setOutputGate()”. А вот и та часть кода, которая инициировала вывод сообщения про ошибку. Кстати, если попробуете найти это предупреждение в “inet-manual-draft.pdf”, то в текущей версии ничего не найдете (предупреждения нет)…

Исправим ошибки, добавив вызов “setOutputGate()” в “LLTRApp” (git tag a1_v0.16.0) diff, соберем INET, и попробуем запустить

Ура! Ошибок нет!(:если бы они были, я бы вставил скриншот:)

В прошлый “удачный” запуск (когда не было сообщений с ошибками) мы обнаружили отсутствие “udpApp” и модуля “udp”. Так обнаружатся ли отсутствующие модули сейчас, либо они будут склонны продолжать отсутствовать без обнаружения :)Проверим это:

qtenv: host0 udpApp

И так, “numUdpApps” стал равен 1 – это очень хорошо; “hasUdp” стал равен “true” – это еще лучше; и, наконец, появился “udpApp[0]” с типом “LLTRSuperApp”, и модуль “udp” (типа “UDP”).

А что стало со схемой “host0”? (двойной клик по “host0”):

qtenv: in host0 – udpApp

Появился модуль “udp” и “udpApp[0]”.

Вернемся назад (на уровень выше):

qtenv: in host0 – Back | Go to parent module

Запустим симуляцию (run):

qtenv: run with full animation (F5)

И…

qtenv: App Events (macOS style)

Все работает, пакет “=Packet name=” прибыл в пункт назначения. Это происходило в точности, как в реальной сети:

  1. вначале был ARP запрос (“arpREQ”) “А где же хост с IP 10.0.1.4? Какой у него MAC адрес?”;
  2. затем APR ответ (“arpREPLY”) “Это я, вот мой MAC…”;
  3. и, в конце, отправился созданный нами пакет “=Packet name=”.

Note: обратите внимание, что размер содержимого пакета – 0 bytes, “UDP” (не поместилось на скриншоте) – 8 bytes, “IPv4” – 28 bytes, “ETH” – 64 bytes, “EtherPhyFrame” – 72 bytes.

В логе (нижняя часть скриншота), видно, что когда пакет достиг “Network.host3.udpApp[0]”, “LLTRApp” создал запись в логе “Arrived: =Packet name=” (событие #88).

Note: перед снятием скриншота, я отфильтровал (Ctrl+H) события в логе (оставил только события от “Network.host3.udpApp[0]”):

qtenv: log messages filter – host3 – udpApp0

Поздравляю, у нас получилось отправить и принять пакет! Однако, одна вещь на скриншоте продолжает меня смущать. Она находится в строке статуса:

Msg status: 4 scheduled / 104 existing / 173 created

Симуляция завершена, но у нас еще 4 запланированных сообщения. Посмотрим на эти сообщения поближе:

qtenv: finish – scheduled events – CLOSE – UDPCloseCommand – host0

Здесь мы видим 4 “CLOSE” (“UDPCloseCommand”) сообщения (по одному на каждый хост). Помните, ранее я упоминал, что закрывать сокет в “finish()”, и тем более в деструкторе – бесполезно, т.к.:

“по документации” цикл обработки событий уже будет разрушен.

А то, что произойдет, если все‑таки отправить сообщение после “разрушения” цикла обработки событий, мы увидели – “висящие” сообщения. Они никогда не достигнут пункта назначения, т.к. симуляция уже завершена.

Подправим это. Закомментируем “socket.close();” в “LLTRSuperApp” и “LLTRApp” (git tag a1_v0.17.0) diff.

Посмотрим, исчезли ли “CLOSE” сообщения (соберем INET, запустим симулятор, запустим симуляцию (run)):

Msg status: 0 scheduled / 100 existing / 169 created

Все “висящие” сообщения исчезли.

Если вы ранее заглядывали в примеры сетей, и в “.ned” файлы приложений, то заметили, что часть параметров приложений можно менять прямо в “.ned” и “.ini” файлах (без пересборки INET). В OMNeT++ хорошим тоном является предоставление будущем “пользователям” возможности изменения параметров приложения через “.ned” и “.ini” файлы.

У “LLTRSuperApp” и “LLTRApp” есть один общий параметр – номер порта – отличный кандидат для задания “извне”. Так, что нужно сделать, чтобы “пользователь” мог задавать номер порта? Сделаем по аналогии с “UDPSink.ned” и “UDPSink.cc”:

  1. добавим в “.ned” файлы новый параметрint port;”;
  2. в “.cc” файлах добавим новое поле “int port = -1;” к классу;
  3. считаем значение параметра в методе “initialize()”:
    case INITSTAGE_LOCAL:
           port = par("port");
    
           break;
  4. заменим все явные задания порта “1100” на переменную “port”.
Почему изначально “port” равен “-1” (“int port = -1;”)?

Если ответ в стиле: “потому что в UDPSink так написано”, не устроит, то добро пожаловать в исходники!

Причем поведение будет отличаться в зависимости от приложения.

Для “LLTRApp” (приложение, которое слушает порт): посмотрим цепочку вызова функции “bind()”, т.к. именно в нее передается порт:

  1. “UDPSocket.cc”:“bind()” “-1: ephemeral port”;
  2. “UDP.cc”:“bind()” “-1: ephemeral port”;
  3. “UDP.cc”:“bind()” “localPort != -1”;
  4. “UDP.cc”:“bind()” “createSocket()” → “localPort == -1 ? getEphemeralPort() : localPort” → “getEphemeralPort()”.

Здесь “-1” означает “используй временный порт” (“ephemeral port” – любой свободный порт).

Для “LLTRSuperApp” (приложение, которое отправляет пакеты) все иначе:

  1. “UDPSocket.cc”:“connect()” “invalid remote port number”;
  2. “UDPSocket.cc”:“sendTo()” → “UDP.cc”:“processPacketFromApp()” “ctrl->getDestPort() == -1 ? sd->remotePort : ctrl->getDestPort()”, “missing destination address or port when sending over unconnected port”.

Здесь “-1” означает либо “попробуй использовать ранее заданный порт”, либо это ошибка.

Осталось только задать номер порта через “.ini” файл для всех хостов, и все будет готово (git tag a1_v0.18.0) diff.

Сборка INET, запуск симулятора, и запуск симуляции (run), должны пройти успешно.

# Шаг 1: рассылаем broadcast, и подсчитываем количество принятых пакетов

Судя по примеру “UDPBroadcastNetwork” – это легко. Достаточно в “sendTo()” указать broadcast IP, и все заработает. С текущими настройками “IPv4NetworkConfigurator” для нашей сети из 4‑х хостов broadcast адресом будет “10.0.1.7”.

И, чуть не забыл, в том же “UDPSink.ned” был параметр “receiveBroadcast”:

bool receiveBroadcast = default(false); // if true, makes the socket receive broadcast packets

Он используется в этом месте “UDPSink.cc”:

bool receiveBroadcast = par("receiveBroadcast");
if (receiveBroadcast)
    socket.setBroadcast(true);

Также аналогичный параметр используется и в других “UDP*App”, например в “UDPBasicApp”: “.ned”, “.cc”.

Значит, в принимающий broadcast пакеты модуль (“LLTRApp”), нужно добавить вызов “setBroadcast(true)”.

Где происходит проверка на broadcast? Что делает “setBroadcast(true)”? И немного про “UDPBroadcastNetwork”.
  1. Начнем с “UDPSocket.cc”:“setBroadcast()” в нем используется “UDPSetBroadcastCommand”.
  2. Продолжим в “UDP.cc”:“processCommandFromApp()” → “setBroadcast()”-“sd->isBroadcast = broadcast”.
  3. Найдем места использования “isBroadcast” в коде (в Eclipse: контекстное меню > References > Project; либо сразу: контекстное меню > Open Call Hierarchy) ← “findSocketsForMcastBcastPacket()”-“if (sd->isBroadcast)” ← “processUDPPacket()”.

То есть “setBroadcast()” влияет только на входящие пакеты, т.к. именно “processUDPPacket()” обрабатывает входящие пакеты.

Если вспомнить пример (“UDPBroadcastNetwork”), в котором приложение “UDPBasicApp” отправляет broadcast пакеты, а “UDPSink” – принимает, то либо в “.ini”, либо в “.ned” файле параметр “receiveBroadcast” должен устанавливаться в “true” для всех “UDPSink”. Но этого нет ни в “omnetpp.ini”, нет и в “UDPBroadcastNetwork.ned”. Так, как же оно работает, или оно работает неправильно?..

К тому же в “UDPBroadcastNetwork” используется directed broadcast адрес и роутер, значит на роутере по умолчанию включен directed broadcasts forwarding? Однако, на реальном оборудовании, по умолчанию directed broadcasts forwarding обычно выключен

В итоге, должно получиться примерно так (git tag a1_v0.19.0) diff.

Посмотрим на то, как выглядит broadcast в симуляторе (собираем INET, запускаем симулятор, запускаем симуляцию (run)):

qtenv: App Events broadcast 10.0.1.7

That, Detective, is the right question…Вот он – broadcast, только рассылался не наш (“=Broadcast Packet=”) пакет, а “arpREQ”. “host0” стал искать хост с IP “10.0.1.7” в сети. Похоже, он воспринял “10.0.1.7” как unicast адрес. Почему так произошло?..

Note: в INET у класса “IPv4Address” есть метод “makeBroadcastAddress()”.

# Упс…

Попробуем воспользоваться “[1]” записью из “host0.routingTable.routes”:

[0] = dest:10.0.1.0  gw:*  mask:255.255.255.248  metric:0  if:eth0(10.0.1.1)  DIRECT MANUAL
[1] = dest:10.0.1.0  gw:*  mask:255.255.255.0    metric:20 if:eth0(10.0.1.1)  DIRECT IFACENETMASK
[2] = dest:127.0.0.0 gw:*  mask:255.0.0.0        metric:1  if:lo0(127.0.0.1)  DIRECT IFACENETMASK

И укажем, в качестве direct broadcast адреса – “10.0.1.255”:

socket.sendTo(new cPacket("=Broadcast Packet="), IPv4Address(10,0,1,255), port);

Если и сейчас не увидим рассылки “=Broadcast Packet=”, то останется только использовать адрес “255.255.255.255”. Пальцы крестиком, поехали (собираем INET, запускаем симулятор, запускаем симуляцию (run)):

qtenv: App Events broadcast 10.0.1.255 and 255.255.255.255

Теперь вообще пакетов нет… Остался “255.255.255.255”, и, как помнится, ранее в коде мы находили константу с этим адресом – “IPv4Address::ALLONES_ADDRESS”. В общем, перейдем на limited broadcast:

socket.sendTo(new cPacket("=Broadcast Packet="), IPv4Address::ALLONES_ADDRESS, port);

Собираем INET, запускаем симулятор, запускаем симуляцию (run)):

qtenv: App Events broadcast 10.0.1.255 and 255.255.255.255

Лог в точности совпал с предыдущим логом – пакет не выходит за приделы “host0”…

Note: так как логи полностью совпали, я вставил один и тот же скриншот(: надо экономить трафик :)

# Мозговой штурм

Note: изначально это был небольшой note, но он разросся до целого раздела…

Я не сразу нашел как отправить broadcast пакет в INET. Вначале еще раз прочитал главу про UDP в “inet-manual-draft.pdf” (глава 13 “The UDP Model”; страница 95), что только навело на ложный путь. В разделе 13.2.1 “Sending UDP datagrams” (страница 96) говорилось, что, перед отправкой broadcast сообщения, нужно сконфигурировать сокет:

Before sending broadcast messages, the socket must be configured for broadcasting. This is done by sending an message to the UDP module. The message kind is UDP_C_SETOPTION and its control info (an UDPSetBroadcastCommand) tells if the broadcast is enabled.

То есть использовать функцию “setBroadcast()”, но, как мы уже выяснили, она влияет только на получение broadcast пакета. Естественно, когда я ее “на всякий случай” добавил в “LLTRSuperApp” – это не помогло. Причем, в разделе 13.2.2 “Receiving UDP datagrams” (страница 97), где “setBroadcast()” действительно нужен, ограничились фразой:

The socket receives the broadcast packets only if it is configured for broadcast.

К тому же в предыдущем разделе (13.2.1 “Sending UDP datagrams”; страница 96) присутствует еще более запутывающий note:

NOTE: The UDP module supports only local broadcasts (using the special 255.255.255.255 address). Packages that are broadcasted to a remote subnet are handled as undeliverable messages.

Note: Этот фрагмент запутывает вдвойне. В INET есть функция “isLocalBroadcastAddress()”, которая работает так, и функция “isLimitedBroadcastAddress()”, которая сравнивает адрес с “255.255.255.255”. Так может они имели в виду “limited broadcasts”, а не “local broadcasts”?

Опять же, это примечание больше касается принимающей стороны. На отправляющей стороне directed broadcast пакеты должны спокойно отправляться (в одной подсети), и пример “UDPBroadcastNetwork” работает (между подсетями; хотя он и не работает, как должен, выше я про это упоминал). А вот принимающая сторона из‑за этой строчки обрабатывает пакет, не как directed broadcast, а как unicast пакет. Положительная сторона этого: можно “setBroadcast(true)” выкинуть, либо вообще заменить на “setBroadcast(false)”, и все будет работать! Отрицательная сторона: работать может не всегда. Оно будет точно работать (сокет “получит” пакет), если сокет создавался без привязки к адресу/интерфейсу, а вот если его привязали…

Это объясняет, почему в названии присутствует “draft” (“inet-manual-draft.pdf”) – намек, на то, что лучше смотрите на код (“ищите ответы в коде, а не в словах”). На самом деле в “inet-manual-draft.pdf” был ответ на вопрос, но он был в другой главе/разделе (надо было просто искать слово “broadcast” по всему документу)…

Далее, в OMNeT++ “Simulation Manual”, нашел раздел “Broadcasts and Retransmissions”, но в нем просто объясняется, как сделать широковещательную рассылку на уровне OMNeT++, а мы работаем на уровне выше (INET). Где‑то внутри модулей INET все так и происходит, но пока эта информация оказалась бесполезной (можно найти все использования “dup()” в INET, но разбор результатов поиска займет слишком много времени, т.к. “dup()” используется не только для broadcast но и для retransmission; пока будим считать, что это был запасной вариант).

Далее последовал Stack Overflow: “Send the same message to several hosts (Broadcast in Ethernet LAN)”. Но там использовался Ethernet LAN и “EtherHost”, что не подходило для “StandardHost”. Однако помеченный ответ, содержит упоминание IP уровня. Может кто‑нибудь дополнит этот ответ описанным ниже решением…

Далее была переписка “INETMANET: how send Broadcast IP packet”. Хотя она и была связанна с MANET, но, возможно, именно она натолкнула на решение.

Я уже не помню, как нашел решение, но это могло бы быть так…

Note: в этот момент наступает ощущение, что проходишь обряд посвящения в элитный закрытый клуб (клубы при “Оксфорде”, “Гарварде”, …)

# Победа разума над инстинктом

Запустим (run) симуляцию заново, но теперь заглянем внутрь модуля “host0”, и воспользуемся режимом пошаговой симуляции:

qtenv: in host0 – run until next event in this module btn – broadcast 255.255.255.255

Лог:

qtenv: in host0 – log – broadcast 255.255.255.255

По сути, этот лог ничего нового не сказал – “=Broadcast Packet=” отправляется в “networkLayer” (событие #20) и в нем исчезает. Точнее, последнее событие (#21) связано с модулем “networkLayer.ip”. Надо спуститься еще глубже:

qtenv: in host0 – in networkLayer – ip drop forceBroadcast – broadcast 255.255.255.255

Note: можно пересобрать сеть (Qtenv меню > Simulate > Rebuild Network), и в пошаговом режиме посмотреть, что происходит на этом уровне.

Здесь, на схеме появилось сообщение “DROP: 1” (рядом с “ip” модулем). Заглянем в параметры (слева) этого модуля (“ip (IPv4)”). Здесь привлекают внимание две записи:

Note: если сейчас, через “omnetpp.ini” файл, изменить “forceBroadcast” на “true”, то все заработает, но…

Поищем параметр “forceBroadcast” в коде модуля “ip”:

Переменная “forceBroadcast” используется только в методе “routeLocalBroadcastPacket()”:

void IPv4::routeLocalBroadcastPacket(IPv4Datagram *datagram, const InterfaceEntry *destIE)
{
    if (destIE != nullptr) {
        fragmentPostRouting(datagram, destIE, IPv4Address::ALLONES_ADDRESS);
    } else if (forceBroadcast) {
        // forward to each interface including loopback
        for (int i = 0; i < ift->getNumInterfaces(); i++) {
            const InterfaceEntry *ie = ift->getInterface(i);
            fragmentPostRouting(datagram->dup(), ie, IPv4Address::ALLONES_ADDRESS);
        }
        delete datagram;
    } else {
        numDropped++;
        delete datagram;
    }
}

То есть, если установить “forceBroadcast” в “true”, то broadcast пакет пройдет, но его разошлют сразу на все интерфейсы! Нам этого не надо, поэтому смотрим условие выше (“destIE != nullptr”). То есть, если при вызове “routeLocalBroadcastPacket()” указать “destIE”, то пакет отправится только на один интерфейс – на указанный в “destIE”.

Note: и “numDropped” здесь же.

Похоже, по крайне мере, в последних двух (“10.0.1.255” и “255.255.255.255”) попытках отправить broadcast пакет, “destIE” был равен нулю. Так, как же изменить “destIE” на нужный интерфейс? Можно подняться по иерархии вызовов вверх (“datagramLocalOut()” ← “handlePacketFromHL()”), и понять, что в нашем случае “destIE” задается на этой строчке, но и так уже ясно, что раз “destIE” не устанавливается автоматически в нужное значение (да и как это будет возможно при “255.255.255.255”), то это надо делать вручную через конфигурирование UDP сокета.

Заглянем в файл “UDPSocket.cc”, и посмотрим, как можно задать конкретный интерфейс для отправки пакета. Мы, для отправки пакета, использовали функцию “sendTo()”, у которой, оказывается, есть четвертый опциональный аргумент – “SendOptions *options”. Через “options” можно задать “srcAddr” и “interfaceId” – это именно то, что нам и нужно сделать.

Осталось решить одну проблему – как из своего приложения получить доступ к информации про IP текущего хоста (на котором “запущено” приложение), и к информации про его интерфейсы?
Начну с интерфейсов.

При просмотре внутренней схемы “host0” и его параметров, в ней был модуль “interfaceTable”, возможно именно он хранит список интерфейсов. Посмотрим:

qtenv: host0 interfaceTable

Осталось понять, как добраться до этого модуля, и вытащить из него информацию про интерфейсы. Я каким‑то образом нашел 3 способа, как добраться до этого модуля (последствия мозгового штурма?). Вначале я использовал “findInterfaceTableOf()”:

IInterfaceTable *inet_ift = L3AddressResolver().findInterfaceTableOf(getParentModule());

Затем, я посмотрел на его реализацию, на реализацию “interfaceTableOf()”, и написал так:

IInterfaceTable *inet_ift = check_and_cast<IInterfaceTable*>(getParentModule()->getModuleByPath(".interfaceTable"));

И, в определенный момент, просматривая код других UDP приложений (“UDPBasicBurst.cc”), я написал так:

IInterfaceTable *inet_ift = getModuleFromPar<IInterfaceTable>(par("interfaceTableModule"), this);

Note: возможно вы уже заметили, просматривая “инспектор объектов” Qtenv, что у каждого модуля внутри хоста есть параметр “interfaceTableModule”, и кто‑то прописывает в нем полный путь до модуля “interfaceTable” текущего хоста.

После добавления в модуль “LLTRSuperApp” нового параметра, прописывания недостающих “#include”, создания “UDPSocket::SendOptions” с сохранением в нем информации о нужном интерфейсе, и использования расширенного варианта “sendTo()”, код станет выглядеть примерно так (git tag a1_v0.20.0) diff.

Собираем и смотрим лог (собираем INET, запускаем симулятор, запускаем симуляцию (run)):

qtenv: App Events – broadcast 255.255.255.255 fixed

Наконец‑то! Broadcast пакеты успешно покинули хост, и были приняты в “LLTRApp”! А что в этот момент происходило внутри “host0”?:

qtenv: in host0 – log – broadcast 255.255.255.255 fixed

Note: формат лога немного изменился (стал чище), ниже я покажу, как это сделать.

В прошлый раз все закончилось на событии #21, и само событие (#21) выглядело иначе…

И, возвращаясь немного назад, как я уже упоминал (в “разросшемся” note) в “inet-manual-draft.pdf” был ответ на вопрос (раздел 11.2.3 “Routing, and interfacing with lower layers”, страница 70):

If the destination is the limited broadcast address, or a local broadcast address, then it will be broadcasted on one or more interface. If the higher layer specified am outgoing interface (interfaceId in the control info), then it will be broadcasted on that interface only. Otherwise if the forceBroadcast module parameter is true, then it will broadcasted on all interfaces including the loopback interface.

The default value of the forceBroadcast is false.

Раздел 11.2.4 “Parameters”, страница 72:

forceBroadcast if true, then link-local broadcast datagrams are sent out through each interface, if the higher layer did not specify the outgoing interface.

И раздел 11.2.5 “Statistics”, страница 72:

numDropped number of dropped packets. Either because there is no any interface, the interface is not specified and no forceBroadcast, or received from the network but IP forwarding disabled.

Note: если вы используете “UDPBasicBurst”, и полагаетесь на документацию (раздел 13.4.6 “UDPBasicBurst”, страница 100):

You can use the "Broadcast" string as address for sending broadcast messages.

То знайте – это не сработает. Исходники – в них нигде не задается конкретный интерфейс, и не форсируется broadcast на IP уровне (это придется делать самому).

# delete msg;

А что будет, если забыть удалить пакет на принимающей стороне? Проверим это! В “LLTRApp” закомментируем строчки “delete msg;”, и посмотрим на лог (собираем INET, запускаем симулятор, запускаем симуляцию (run)):

qtenv: App Events – broadcast 255.255.255.255 fixed – no delete msg

Ничего… Лог в точности такой же, как и в предыдущем запуске. Но документация уверяла, что OMNeT++ будет следить за утечками памяти:

OMNeT++ prints the list of unreleased objects at the end of the simulation. When a simulation model dumps "undisposed object ..." messages, this indicates that the corresponding module destructors should be fixed.

Симуляция закончилась, но никаких сообщений “undisposed object ...” не было. В документации ошибка?

Все в порядке, мы просто не там и не в то время смотрели. Не закрывая Qtenv, вернемся в Eclipse (OMNeT++ IDE), внизу должна быть открыта панель “Console” (там). Сейчас она должна содержать примерно такой текст:

...
Setting up Qtenv...
Loading NED files from ../src:  0
Loading NED files from .:  1
Loading NED files from ../../inet/examples:  162
Loading NED files from ../../inet/src:  563
Loading NED files from ../../inet/tutorials:  5

Здесь тоже никаких “undisposed object ...” сообщений нет, но это только пока… Вернемся в Qtenv, и пересоберем сеть (меню Simulate > Rebuild Network) (время). Теперь посмотрим, изменилось ли содержимое “Console”?:

...
Setting up Qtenv...
Loading NED files from ../src:  0
Loading NED files from .:  1
Loading NED files from ../../inet/examples:  162
Loading NED files from ../../inet/src:  563
Loading NED files from ../../inet/tutorials:  5
undisposed object: (omnetpp::cPacket) Network.host1.udpApp.=Broadcast Packet= -- check module destructor
undisposed object: (omnetpp::cPacket) Network.host2.udpApp.=Broadcast Packet= -- check module destructor
undisposed object: (omnetpp::cPacket) Network.host3.udpApp.=Broadcast Packet= -- check module destructor

Сообщения “undisposed object ...” появились. Как видите, в них указывается последнее местонахождение неосвобожденного объекта.

Note: помните, чтобы увидеть список “утечки памяти” нужно не только запустить симуляцию, но и после ее завершения – пересобрать сеть, и смотреть в “Console” (OMNeT++ IDE).

Note: после экспериментов не забудьте раскомментировать “delete msg;”, либо откатиться на (git tag a1_v0.20.0).

# Настройка GUI Qtenv

Мы уже продолжительное время используем Qtenv, поэтому настало время настроить его под себя.

Note: ниже я приведу настройки, которые оказались удобными для меня, и для снятия скриншотов (для этой статьи).

Вначале, надо сказать, что все настройки Qtenv хранятся в домашней директории пользователя, в файле “.qtenvrc” (Windows):

%USERPROFILE%\.qtenvrc

А Tkenv – хранит в файле “.tkenvrc” (Windows):

%HOMEDRIVE%%HOMEPATH%\.tkenvrc

Перейдем к настройкам:

qtenv: preferences buttons

Все настройки с небольшими комментариями (а также отключение отображения таймлинии – она редко нужна, и когда не нужна – отвлекает):

qtenv: preferences

Note: Если в Tkenv у объектов в canvas отсутствуют надписи, то в Fonts > Canvas нужно выбрать векторный шрифт. Надписи отсутствовали из‑за попытки использовать растровый шрифт (например, “MS Sans Serif”).

Note: После применения настроек и запуска симуляции (run) в логе будут только события (синие), без детализации (черные), после перезапуска Qtenv детализация вернется.

Также, о настройках написано в “omnetpp-5.0/doc/UserGuide.pdf” (раздел 7.7. “The Preferences Dialog”).

# Шаг 2: детализация протокола

В начале статьи я описал набросок протокола. Настало время его уточнить, чтобы двигаться дальше.

Начнем с синхронизации:

Основа LLTR – это итерации сбора статистики на множестве хостов во время сканирования сети. Итераций в LLTR много ( >1), поэтому первое, что нужно включить в протокол – управление запуском и остановкой каждой итерации. Если учесть, что хостов тоже много ( >1), то управление будет заключаться в том, чтобы определенным способом сообщать всем хостам время начала итерации и время окончания итерации. То есть синхронизировать все хосты.

Первое, что приходит на ум – перед первой (нулевой) итерацией, используя протоколы синхронизации времени – синхронизировать время на всех хостах. Добавим к этому единую точку отсчета (на синхронизированных часах) – время начала первой итерации, которая вкупе с длительностью итерации, позволит каждому из хостов определять точный (+/-) момент начала и окончания каждой итерации. Похоже, это будет работать, если сделать все правильно…, а если сделать неправильно, то получится отличная сцена для комедии:

Taxi 4:  Жебер: Сверим часы, Генерал: — 16:20, Жебер: — 12:05, созвонимся через два часа!, Генерал: o_O

Собираемые данные (для определения снижения скорости / потери пакетов):

И последнее. По завершению всех итераций, всю собранную статистику со всех хостов нужно отправить на один хост для обработки. Этот хост проанализирует собранную статистику, и построит топологию сети.

Опять же, первое, что приходит на ум – на хостах, записываем время прихода каждого пакета. По этим данным можно построить гистограмму, можно анализировать задержку между получением пакетов, …

Либо…

Либо, broadcast src хост последовательно нумерует все пакеты, а на остальных хостах записываются номера пришедших пакетов.

Но это все слишком усложняет реализацию. Для синхронизации хостов нужно будет полностью реализовать протокол для синхронизации времени. К тому же – это дополнительный шаг – мы не сможем начать сканирование сети пока не синхронизируем время на всех хостах. А если во время этого процесса пропадут пакеты… таймауты, ретрансмиты…

Тоже самое и с хранением времени всех пришедших пакетов с последующей отправкой этих данных на один хост для обработки. Слишком много данных придется хранить и передавать по сети, а затем, все эти данные должен обработать один хост…

Есть ли более простой, надежный и элегантный способ реализовать задуманное?

С отправкой данных все достаточно просто. Каждый из хостов мог бы независимо произвести предварительную обработку своих данных (свернуть их; local-reduce; map) перед отправкой их на центральный хост (reduce) – это уменьшит количество передаваемых данных и ускорит расчет.

А вот с синхронизацией все интереснее.

# Протокол, версия v.Basic.GlobalWave

Если посмотреть внимательней, то у нас уже есть то, что позволит всем хостам работать синхронно.

Что нам нужно? Нужно определенным способом сообщать всем хостам момент начала итерации и момент окончания итерации.

А кто заполняет своими пакетами всю сеть (вещает свои пакеты на всю сеть) на каждой итерации? Это broadcast src хост (LLTRSuperApp).

⇒ Достаточно поместить, в каждый broadcast пакет, номер текущей итерации, и все хосты смогут определить:

Причем, последний пункт позволяет нам закрыть еще один раздел протокола:

В каждой итерации есть свой unicast src хост и unicast dst хост, поэтому следующее, что нужно включить – способ назначения для каждой итерации unicast src и dst. То есть в каждой итерации один из хостов должен “осознавать” себя unicast src хостом, цель которого посылать трафик на unicast dst хост.

У этого решения есть и недостаток – изначально предполагалось, что broadcast src и unicast src хост стартуют (начинают отправлять пакеты) одновременно. Сейчас же unicast src хост будет стартовать с задержкой относительно broadcast src хоста.

Note: Вообще‑то нам не важно стартуют ли broadcast src и unicast src хост одновременно или нет, важно другое – одновременно ли их первые пакеты в итерации достигают unicast dst хоста. Допустим также вариант, когда пакеты от unicast src начинают приходить первыми, но тогда и последний пакет unicast src должен прийти не ранее последнего пакета broadcast src. То есть временной диапазон принятия unicast src пакетов на unicast dst хосте должен покрывать соответствующий временной диапазон принятия broadcast src пакетов.

Также хосты могут (и будут) иметь разное расстояние (время передачи пакета) до broadcast src хоста, это приведет к тому, что время начала и окончания итерации у каждого хоста будет свое, т.е. будет отличаться.

Note: это не хорошо и не плохо, это просто нужно будет учитывать в будущем.

Note: Представьте космос, звезды, и наблюдение за ними… (время распространения сигнала)

На текущий момент, реализация протокола unicast src/dst хостов (LLTRApp) должна выполнить следующие действия:

  1. В начале: “разогреть” (заполнить) ARP‑таблицы в хостах и MAC‑таблицы в свитчах. Зачем это нужно – описано ниже. Как это реализовать: все хосты начинают одновременно, каждый из хостов последовательно отправляет пустой UDP пакет на discard порт 9 на все остальные хосты (включая broadcast src хост).
  2. Слушать порт, на который будут приходить broadcast пакеты от broadcast src хоста, и по содержимому приходящих пакетов – определять номер текущей итерации.
  3. Если началась новая итерация, и в ней текущей хост является unicast src хостом, то запустить отправку m пакетов на unicast dst хост.
  4. Собирать статистику по пакетам в текущей итерации: подсчитывать количество принятых broadcast пакетов от broadcast src хоста.
  5. В конце: ожидать входящего TCP подключения, и передать по нему собранную статистику по каждой итерации.

Зачем нужен 1‑й шаг: посмотрите логи предыдущих симуляций – вначале эти таблицы пусты, и хосты вынуждены использовать ARP запросы для определения MAC‑адреса других хостов. То же самое касается и свитчей. А теперь представьте, что во время 3‑го шага, вместо отправки наших пакетов, отправляется ARP‑запрос… Через какое время он достигнет unicast dst хоста? И достигнет ли его вообще?

Реализация протокола broadcast src хоста (LLTRSuperApp) должна:

  1. В начале: дождаться завершения первого шага unicast src/dst хостов (LLTRApp).
  2. Посылать m broadcast пакетов в каждой итерации, и в каждый пакет записывать номер текущей итерации.
  3. После каждой итерации делать паузу. Пауза нужна для опустошения очередей пакетов в свитчах. Если не дождаться опустошения очередей, то текущая итерация будет влиять на следующую итерацию, и исказит ее статистику.
  4. В конце: оповестить unicast src/dst хосты о завершении последней итерации, и последовательно подключаясь по TCP к каждому unicast src/dst хосту, загрузить статистику.

Количество пакетов (m) выбирается исходя из:

Про количество итераций уже было сказано в предыдущей статье.

Для передачи статистики, в модели, мы могли бы использовать UDP вместо TCP, и это даже работало бы, но в реальном приложении я бы предпочел использовать протокол с гарантией (проверкой) доставки данных, поэтому и в модели лучше использовать соответствующий протокол.

Здесь есть еще несколько нюансов, мы с ними столкнемся, когда будем создавать модель, и смотреть на результаты симуляции ;)В общем, скучать не придется.

# Шаг 3: расширяем модель (TCP+UDP app…)

Мы уже посмотрели, как в INET нужно работать с UDP, но для полноценной модели нужен еще и TCP. Посмотрим на примеры работы с TCP, и попробуем добавить в “LLTRSuperApp” и “LLTRApp” передачу данных по TCP.

Примеры:

Как видно из примеров, в “LLTRSuperApp.cc” достаточно дописать:

//...
#include "inet/transportlayer/contract/tcp/TCPSocket.h"

namespace inet {
class INET_API LLTRApp: public cSimpleModule, public TCPSocket::CallbackInterface
{
       //...
       int gateTcpId;
       TCPSocket socketTcp;
       //...
       void initialize(int stage)
       {
             //...
             switch(stage){
             case INITSTAGE_LOCAL:
                    //...
                    gateTcpId = gateBaseId("tcpIn");
                    //...
                    break;
             case INITSTAGE_APPLICATION_LAYER:
                    //...
                    socketTcp.setOutputGate(gate("tcpOut"));
                    socketTcp.setDataTransferMode(TCP_TRANSFER_BYTECOUNT);
                    socketTcp.setCallbackObject(this);
                    //...
                    break;
             case INITSTAGE_LAST:
                    //...
                    socketTcp.connect(IPv4Address(10,0,1,2), port+1);
                    //...
                    break;
             }
       }

       void handleMessage(cMessage *msg)
       {
             if(msg->arrivedOn(gateTcpId) && socketTcp.belongsToSocket(msg)) socketTcp.processMessage(msg);
             else delete msg;
       }

       void socketDataArrived(int, void*, cPacket *msg, bool)
       {
             socketTcp.close();

             EV << "Arrived (TCP): " << msg->getName() << endl;

             delete msg;
       }

       void socketPeerClosed(int, void*)
       {
             socketTcp.close();
       }
       //...
};
//...
}

Note: Чтобы определить предназначен ли пакет для определенного сокета, обычно используется метод “belongsToSocket()”, но он медленный (внутри неоднократно используется “dynamic_cast”). Поэтому вначале лучше проверить, что пришел именно TCP пакет. Самый быстрый способ это сделать – это посмотреть через какой “gate” пришел пакет. Все UDP пакеты приходят через свой “udpIn” “gate”, а TCP будут приходить через “tcpIn”. Для того чтобы проверить факт прихода пакета через “tcpIn”, используют метод “arrivedOn()”, он очень быстрый – внутри всего лишь проверяется равенство двух целочисленных переменных.

Note: если в модуле создается только один TCP сокет, то “belongsToSocket()” проверку можно отбросить, оставив только “arrivedOn()”.

Note: разный номер порта для TCP (“port+1”) и UDP – мне помогал быстро ориентироваться в логе симуляции, в следующих версиях номера портов (TCP и UDP) будут совпадать.

Note: я бы не хотел останавливаться на деталях реализации TCP в INET, при том, что это описано в доках, а как воспринимать доки я думаю вы уже поняли (читая исходники ;), однако хочу немного пояснить картину: “setCallbackObject(this)” напрямую связан с появлением множественного наследования “, public TCPSocket::CallbackInterface”, и с появлением новых методов “socketDataArrived()” и “socketPeerClosed()”, которые вызываются внутри “processMessage()”.

В “LLTRSuperApp.ned” допишем:

//...
import inet.applications.contract.ITCPApp;

simple LLTRSuperApp like IUDPApp, ITCPApp
{
   //...
   gates:
        //...
        input  tcpIn  @labels(TCPCommand/up);
        output tcpOut @labels(TCPCommand/down);
   //...
}

В “LLTRApp.cc”:

//...
#include "inet/transportlayer/contract/tcp/TCPSocket.h"

namespace inet {
class INET_API LLTRApp: public cSimpleModule, public TCPSocket::CallbackInterface
{
       //...
       int gateTcpId;
       TCPSocket socketTcp;
       //...
       void initialize(int stage)
       {
             //...
             switch(stage){
             case INITSTAGE_LOCAL:
                    //...
                    gateTcpId = gateBaseId("tcpIn");
                    //...
                    break;
             case INITSTAGE_APPLICATION_LAYER:
                    //...
                    socketTcp.setOutputGate(gate("tcpOut"));
                    socketTcp.setDataTransferMode(TCP_TRANSFER_BYTECOUNT);
                    socketTcp.setCallbackObject(this);
                    socketTcp.bind(port+1);
                    socketTcp.listenOnce();
                    //...
                    break;
             }
       }

       void handleMessage(cMessage *msg)
       {
             if(msg->arrivedOn(gateTcpId)){
                    socketTcp.processMessage(msg);
                    return;
             }
             //...
       }

       void socketEstablished(int, void*)
       {
             socketTcp.send(new cPacket("=TCP Packet="));
       }

       void socketDataArrived(int, void*, cPacket*, bool)
       {

       }

       void socketPeerClosed(int, void*)
       {
             socketTcp.close();
             socketTcp.renewSocket();
             socketTcp.bind(port+1);
       }
       //...
};
//...
}

Note: Мы используем самый простой (без создания нового сокета для каждого нового соединения; “non-forking”) режим прослушивания TCP порта – “одноразовый сокет” (метод “listenOnce()”). Для принятия новых соединений, после закрытия текущего соединения, нужно вызвать методы “renewSocket()” и “bind()”.

Note: Метод “socketDataArrived()” (чистая виртуальная функция “=0) пустой, т.к. мы не ожидаем входящих данных, и в нем отсутствует “delete msg”, чтобы, если все‑таки что‑то придет, OMNeT++ сообщил об “утечке пакетов”. Однако, можно было здесь же вывести сообщение о проблеме и удалить пакет.

В “LLTRApp.ned”:

//...
import inet.applications.contract.ITCPApp;

simple LLTRApp like IUDPApp, ITCPApp
{
   //...
   gates:
        //...
        input  tcpIn  @labels(TCPCommand/up);
        output tcpOut @labels(TCPCommand/down);
   //...
}

Поясню, что “LLTRSuperApp” и “LLTRApp” должны будут сделать:

  1. “LLTRSuperApp” устанавливает соединение с “LLTRApp”;
  2. “LLTRApp” отправляет в ответ пакет “=TCP Packet=”;
  3. “LLTRSuperApp” закрывает соединение, и выводит имя полученного пакета;
  4. “LLTRApp” “обновляет” (“renewSocket()”) свой сокет.

Осталось прописать приложения в хостах (связать “gate” c приложением). Для этого добавим в “omnetpp.ini” строчки:

[General]
//...
**.host0.tcpApp[0].typename = "inet.applications.lltrapp.LLTRSuperApp"
//...
**.host?.numTcpApps = 1
**.host?.tcpApp[0].typename = "inet.applications.lltrapp.LLTRApp"
**.host?.tcpApp[0].port = 1100

Должно получиться примерно так (git tag a1_v0.21.0) diff.

Соберем, запустим, и… (как уже поняли, я бы не стал описывать это, если бы здесь не было интересного момента с ошибкой ;)

Error in module (inet::LLTRSuperApp) Network.host0.tcpApp[0] (id=74) during network initialization: send()/sendDelayed(): gate `udpOut' not connected.

Заглянем внутрь “host0”, и посмотрим, почему “udpOut” не подключен:

qtenv: in host0 – double app

Сюрприз, появилось два “LLTRSuperApp”: один подключен к “udp” модулю, а другой к “tcp”, именно поэтому появилась ошибка – у “tcp” попросту нет “udpOut”. Но нам важно не это, а то, что теперь в каждом хосте стало по два экземпляра приложения, а нам нужен только один экземпляр в каждом хосте.

Собственно это уже было ясно при редактировании “omnetpp.ini” – мы указали “inet.applications.lltrapp.LLTR*App” в нескольких точках подключения, вот он и создал их несколько…

Похоже, используя “StandardHost” не удастся объединить в одном приложении работу с UDP, и с TCP. Нам ничего не остается, как сделать свой “StandardHost”, и, заодно, выкинуть все лишние части.

The Fast and the Furious 8 – nothing extra

# SimpleUdpTcpHost и ITcpUdpApp

Создадим “SimpleUdpTcpHost” на основе “StandardHost”, и “ITcpUdpApp” объединяющий “IUDPApp” с “ITCPApp”. Получилось так (git tag a1_v0.22.0) diff.

SimpleUdpTcpHost

Осталось перевести модель на использование нового хоста и интерфейса. Получилось так (git tag a1_v0.23.0) diff.

Попробуем собрать и запустить… Ошибок нет. Запускаем симуляцию (run):

qtenv: App Events – tcp – zero size

Соединение установилось, но пакет “=TCP Packet=” не дошел до “LLTRSuperApp”, и “LLTRSuperApp”, соответственно, не вывел надпись “Arrived (TCP): =TCP Packet=”.

Пакет “=TCP Packet=” виден в событии #155. Детализация события намекает нам, что модуль “tcp” не хочет отправлять данные нулевого размера в режиме “TCP_TRANSFER_BYTECOUNT”. Попробуем задать размер пакета, используя метод “setByteLength()”. Должно получиться примерно так (git tag a1_v0.24.0) diff.

Собираем, запускаем, запускаем симуляцию (run) (лог отфильтрован и показан только фрагмент):

qtenv: App Events – tcp – 1 byte size

Теперь “tcp” отправил “=TCP Packet=”, но его имя изменилось на “tcpseg(l=1)”. А по прибытии в “LLTRSuperApp” он стал называться просто “data” – “Arrived (TCP): data”.

Note: опционально, в “omnetpp.ini” можно прописать настройки для TCP стека:

**.tcp.delayedAcksEnabled = true
**.tcp.increasedIWEnabled = true
**.tcp.sackSupport = true
**.tcp.windowScalingSupport = true
**.tcp.timestampSupport = true

Note: также можно задать “TCP congestion control algorithm”, указав “tcpAlgorithmClass” (доступно несколько алгоритмов на выбор).

Note: Если не закрывать сокет “socketTcp.close()” в методе “socketPeerClosed()” (в “LLTRApp”), то симуляция завершится быстрее (на 240 моделируемых секунд быстрее – это длительность 2MSL), но ни один из сокетов не перейдет в состояние “CLOSED”. Последнее состояние для сокета в “LLTRSuperApp” будет “FIN_WAIT_2”, а для “LLTRApp” – “CLOSE_WAIT”. Картинка‑напоминатель про состояния TCP.

# Помещаем хосты в вектор

Note: в этот момент мне пришла идея поместить все хосты в вектор… я был столь наивен… Не делайте этого!

Чем нам это поможет? Сейчас мы для соединения с “host1” используем его IP‑адрес “10.0.1.2”. Однако, все адреса распределяются автоматически, и не факт, что “host1” всегда будет иметь адрес “10.0.1.2”. Например, если сейчас в “package.ned” переподключить один из хостов (“host3”) к другому свитчу, то хосты поменяют свои IP‑адреса. Поэтому нужно обращаться к хосту не по IP‑адресу, а по его имени.

Note: используя имя хоста, можно получить его точный IP‑адрес:

const L3Address getIPByHostPath(const char *path)
{
       IInterfaceTable *inet_ift = dynamic_cast<IInterfaceTable*>(
            getModuleByPath(path)->getSubmodule("interfaceTable"));

       return inet_ift->getInterfaceById( inet_ift->getBiggestInterfaceId() )
                      ->getNetworkAddress();
}

Так чем же нам поможет помещение хостов в вектор? Можно будет использовать метод “getSubmodule()” для обращения к хостам. Например, чтобы получить “host1” (новое полное имя – “"host[1]"”, сокращенное – “"host"”) достаточно написать:

getSimulation()->getSystemModule()->getSubmodule("host", 1);

Этим же способом можно легко перебрать все хосты в цикле:

cModule *sysModule = getSimulation()->getSystemModule();
for(std::ptrdiff_t i=0; i<length; i++){  //length – vector size (number of hosts)
       cModule *host = sysModule->getSubmodule("host", i);
       // do something...
}

Note: без использования вектора, на каждой итерации пришлось бы создавать строки (“"host0"”, “"host1"”, “"host2"”, …, “"host121"”).

Как же поместить хосты в вектор?

В “OMNeT++ – Simulation Manual” есть пример создания вектора из 100 хостов:

network Network
{
    submodules:
        host[100]: Host {
            ping.timeToLive = default(3);
            ping.destAddress = default(0);
        }
        ...
}

Отлично, мы можем сразу же задать общие параметры для всех хостов в векторе!

А как задать индивидуальные параметры для конкретного хоста в векторе? Мы размещали каждый хост в определенной точке на холсте, и хотели бы сохранить это расположение. В том же разделеOMNeT++ - Simulation Manual” есть пример как сделать это:

network Network
{
    parameters:
        host[*].ping.timeToLive = default(3);
        host[0..49].ping.destAddress = default(50);
        host[50..].ping.destAddress = default(0);

    submodules:
        host[100]: Host;
        ...
}

Все просто. Однако расположение хоста задается через запись “@display("p=149,269");”, и ни один из этих попыток задать “@display” (display string) не сработает:

host[0].@display = "p=149,269";
host[0].display = "p=149,269";
host[0].@display("p=149,269");
host[0]@display("p=149,269");
host[0]{ @display("p=149,269"); }
host[0] = { @display("p=149,269"); }
host[0] = @display("p=149,269");
...

Note: через “инспектор объектов” (grouped mode) Qtenv можно посмотреть где находится display string, а где остальные параметры.

Список дальнейших действий:

  1. Мы не можем задать “@display”, но мы можем использовать parameter substitution (альтернатива для расположения: в строку, в столбец, в сетку, по кругу).
  2. Мы не можем добавлять новые параметры в модуль, но можем создать новый тип (“types:”), расширяющий (“extends”) существующий модуль.
  3. Осталось переписать конфиги на использование новых хостов, и стереть создание старых хостов.

В итоге получилось так (ветка hosts-in-vector, коммт “Put hosts in vector”).

При этом сеть, в визуальном редакторе OMNeT++, превратилась в это:

hosts in vector – OMNeT++ IDE

Note: в симуляторе все выглядит нормально.

Мы потеряли возможность задавать местоположение хостов через визуальный редактор, но хотя бы теперь мы можем взять адрес вектора и быстро получить адрес на объект нужного хоста.

Кстати, а при помощи какой функции можно получить адрес вектора или итератор по вектору?
Ответ: такой функции нет, как нет и вектора – сейчас (OMNeT++ v5.0) это всего лишь иллюзия!

А метод “getSubmodule()” работает так (из файла “omnetpp-5.0/src/sim/cmodule.cc”):

cModule *cModule::getSubmodule(const char *name, int index) const
{
    for (SubmoduleIterator it(this); !it.end(); ++it) {
        cModule *submodule = *it;
        if (submodule->isName(name) && ((index == -1 && !submodule->isVector()) || submodule->getIndex() == index))
            return submodule;
    }

    return nullptr;
}

То есть, он полностью перебирает все подмодули текущего модуля, в поисках совпадения по имени и “isVector()”…

Note: однажды мне пришла идея поместить все хосты в вектор… я был столь наивен… Не делайте этого!

# Шаг 4: реализация протокола (версия v.Basic.GlobalWave)

Вначале сделаем имена у хостов более удобными для дальнейшего использования. Как было сказано в разделе “Помещаем хосты в вектор”, надежнее будет перейти, при обращении к хостам, от обращения по IP‑адресу к обращению по имени хоста (“"host#"”). Чуть ниже я буду использовать несколько формул для расчета номера “#” unicast src/dst хоста. В этих формулах предполагается, что индексация “#” unicast src/dst хостов начинается с “0”. На данный момент номер “0” занят под broadcast src хост, поэтому переименуем его в “hostS” (“S” – “LLTRSuperApp”), а все остальные хосты (unicast src/dst) переиндексируем, начиная с “0”:

Также нужно подправить имена хостов в “omnetpp.ini” файле.

Теперь займемся соответствием последнего октета IP‑адреса номеру хоста. Забегая вперед, скажу, что бывали случаи, когда, при просмотре лога симуляции, обращаешь внимание не на номер хоста, а на последний октет IP‑адреса. Например, видишь адрес “10.0.1.2”, и думаешь про “host2”, а на самом деле это “host1” (до переименования/переиндексации). Чтобы при просмотре лога у вас не возникало путаницы, сделаем так, чтобы у “host0” был адрес “10.0.1.0”, у “host1” – “10.0.1.1”, и т.д. (файл “config.xml” для “IPv4NetworkConfigurator”):

<config>
   <interface hosts='*.hostS' address='10.0.0.88' netmask='255.255.254.0'/>
   <interface hosts='*.host0' address='10.0.1.0'  netmask='255.255.254.0'/>
   <interface hosts='*'       address='10.0.1.x'  netmask='255.255.254.0'/>
</config>

Что мы сделали:

Теперь адреса хостов стали следующими:

Note: для быстрого просмотра IP‑адресов, я закомментировал “#**.interfaceTable.displayAddresses = false” в “omnetpp.ini”.

Note: Если изменить адрес хоста “*.host0” на “10.0.1.11”, то остальные хосты (“*”; “10.0.1.x”) получат адреса, начиная с “10.0.1.12”, т.е. при использовании “x” просматривается последний назначенный адрес в этом диапазоне, и выбирается следующий не назначенный адрес после него. Можете посмотреть, что будет, в следующих ситуациях (“*.host0”-“*”): “10.0.0.254”-“10.0.x.x”, “10.0.1.254”-“10.0.1.x” (упс) и “10.0.0.87”-“10.0.x.x”.

Note: это соответствие будет соблюдаться для хостов “host0” – “host255”, и по понятным причинам, с “host256” работать уже не будет.

Note: это соответствие очень легко сломать, достаточно подключить хосты к другим свитчам.

Должно получиться примерно так (git tag a1_v0.25.0) diff.

Теперь допишем все остальное… (git tag a1_v0.26.0) diff

How to draw an Owl

Note: Если описывать реализацию модели LLTR такими же мелкими шажками, какими было описано все предыдущее, то эту статью придется издавать в виде нескольких книжных томов. Поэтому я остановлюсь только на нескольких интересных моментах, важных для понимания устройства и структуры модели.

Лучше начать с “LLTRSuperApp” – он проще, и в нем хорошо видны шаги из описания протокола:

  1. [INIT] В начале: дождаться завершения первого шага unicast src/dst хостов (LLTRApp) [строка 120].
  2. [PROBING] Посылать m [строка 135] broadcast пакетов в каждой итерации, и в каждый пакет записывать номер текущей итерации [строка 130].
  3. [PROBING] После каждой итерации делать паузу [строка 142]…
  4. [COLLECT] В конце: оповестить unicast src/dst хосты о завершении последней итерации [строка 151], и последовательно [строка 186] подключаясь по TCP [строка 175] к каждому unicast src/dst хосту, загрузить статистику [строка 183].

Сокращенный вариант потока выполнения программы выглядит так:

# Циклы “с задержкой”: setTimeout() + Self-Messages

Если вы периодически пишите на чистом JavaScript, то наверняка уже заметили “setTimeout()” – с его помощью все “зацикливается”.

Note: В чистом OMNeT++setTimeout()” отсутствует, вместо него используется функция “scheduleAt()”. Разница: “scheduleAt()” принимает абсолютное время, а “setTimeout()” – относительное время. Для “LLTRSuperApp” setTimeout()” реализован так.

Для создания циклов “с задержкой” также нужны “Event's” (строки 29,30) – эти сообщения называются “Self-Messages”. По сути – это обычные сообщения, которые модуль отправляет сам себе (именно это и происходит при использовании “setTimeout()”).

“Self-Messages” так же, как и другие сообщения в OMNeT++ поступают на обработку в “handleMessage()”, и чтобы отличить их от других сообщений используется метод “isSelfMessage()” (строка 103).

# Еще один способ создания цикла “for”

Цикл получается путем взаимодействия с другим модулем (хостом). Так, например, организован цикл “получения статистики с хостов” {handleStat() ⤳...⤳ handleMessage(socketStat)socketDataArrived()} – в нем не используется “setTimeout()”.

Этот цикл начинается в (строке 145), где устанавливается начальное значение счетчика, затем через строки (148) (единственное место, в котором используется “setTimeout()”, и используется только для создания задержки) и (105) попадает в “handleStat()”, после чего ждет получения данных от другого хоста. При получении данных, через “handleMessage()” (строка 108) попадает в “socketDataArrived()”, где счетчик инкрементируется, и проверяется условие выхода (строка 186).

# Файлы “.msg”

Ранее я упоминал шаги (INIT, PROBING, COLLECT; тип “LLTRStep”), которые используются в “handleFill()”:

switch(step){
case INIT:
case PROBING:
case COLLECT:
default:
}

и задаются при отправке пакета на другой хост (строка 129). IDE подскажет, что они объявлены в “FillPayload_m.h”, но на самом деле этот файл (вместе с “FillPayload_m.cc”) был автоматически сгенерирован из “FillPayload.msg”.

Файлы “.msg” (“message definitions”), по сути, представляют собой декларативное описание содержимого пакетов. А всю необходимую “императивную обвязку” OMNeT++ добавит сам во время создания “*_m.cc” и “*_m.h” файлов (они генерируются автоматически, перед компиляцией проекта). В “OMNeT++ - Simulation Manual” целая глава отведена только под создание “.msg” файлов. Я использовал только базовые возможности “message definitions”, а список всех возможностей приведен в конце той главы.

# Значение параметра “packetLength” в “.ned” и “.ini” файлах

Ранее мы не указывали размер UDP пакета (указывали только для TCP), теперь же, для корректной симуляции, нужно его указать, используя метод “setByteLength()” (строка 131). Размер пакета задается через параметр “packetLength” (“.ned” файл) в “.ini” файле (строка 22).

Опять же ранее, я показывал размер “ETH” (Ethernet II кадр) для “пустого” UDP пакета:

Note: обратите внимание, что размер содержимого пакета – 0 bytes, “UDP” (не поместилось на скриншоте) – 8 bytes, “IPv4” – 28 bytes, “ETH” – 64 bytes, “EtherPhyFrame” – 72 bytes.

И, если мы хотим полностью заполнить кадр (увеличить размер “ETH” до максимально возможного значения – 1518 байт; MTU), то следует установить “packetLength” в значение “1472B”. При этом размеры будут следующими:

Если разбить на заголовки, то получим:

1472 + 8:UDP + 20:IPv4 + 18:ETH + 8:EtherPhyFrame

Все в точности совпадает с тем, что описано в “inet-manual-draft.pdf” (раздел 5.5.1 “Frame types”; страница 27). Но что такое “EtherPhyFrame”? Это синхрослово/преамбула + “start frame delimiter” (структура заголовков в целом).

Однако, при моделировании я уже успел поварьировать значения задержки между передачей пакетов, и размер пакетов, для достижения приемлемой плотности трафика. Поэтому в “.ini” файле вы не найдете значения “1472B”, вместо него там будет стоять “1446B”. Что, по сути, приводит размер “EtherPhyFrame” (1526 байт) к размеру IPv4 (1500 байт), т.е., в этой версии модели, размер “EtherPhyFrame” составляет 1500 байт.

# Магические числа

Раз уже заговорили про плотность трафика, то продолжим. В предыдущей статье было сказано:

Что касается скорости broadcast и unicast, то broadcast трафик можно держать в диапазоне 75% - 100% от “чистой скорости передачи данных” (net bitrate; поиск по “Ethernet 100Base-TX”), а unicast в диапазоне 80% - 100%.

Теперь мы можем рассчитать задержку между отправками broadcast пакетов (период отправки пакетов).

Note: “чистая скорость передачи данных” (net bitrate), “пропускная способность” (throughput), “утилизация/использование канала” (channel utilization) – означают разные вещи.

В нашей модели сети используется соединения с net bitrate 100 Мбит/с (Fast Ethernet), для которых межкадровый интервал равен (96 [бит] ∕ 100 000 000 [бит/с] = 0.96 мкс; 96 [бит] ∕ 8 [бит/байт] = 12 байт). Поэтому добавим к размеру “EtherPhyFrame” (1500 байт) еще 12 байт, и получим итоговый размер одной отправки данных, равный 1512 байт.

Note: Обычно, при вычислении утилизации канала, размер межкадрового интервала не включается в размер самого кадра (что логично), но нам понадобится смотреть на них (воспринимать их) как на единое целое. Поэтому мы и добавили размер межкадрового интервала к размеру самого кадра. Это, к тому же, позволит использовать более наглядное значение процентов утилизации канала, например, здесь получили 99.22% утилизации канала, у нас же будет 100%.

Поехали:

Note: все в точности, как в этом расчете для 10 Мбит/с из книги “Networking Explained, Second Edition”.

Note: На самом деле утилизация канала – это не тот параметр, на который мы сейчас должны обращать внимание. Есть более важный параметр. Поясню: например, мы решили использовать 95% от net bitrate что сделает зазор между двумя кадрами (в размер кадра включен межкадровый интервал) равным 605 бит или 75.625 байт ( ⌈ 1512 [байт] × (1−0.95) [%] × 8 [бит/байт] ⌉ ∕ 8 [бит/байт] = ⌈ 604.8 [бит] ⌉ ∕ 8 [бит/байт] = 605 [бит] ∕ 8 [бит/байт] = 75.625 байт ). Если взять минимальный размер “EtherPhyFrame” (72 байта), и добавить к нему размер межкадрового интервала, то получим минимально необходимое “пространство” для отправки кадра – 84 байта. Получается, что созданного нами зазора не достаточно для отправки другими участниками сети своих данных (даже минимального размера; без накопления кадров в очереди/буфере промежуточного сетевого оборудования). Теперь можно посчитать верхний предел утилизации канала (для нашего случая): ⌊ 1512 [байт] ∕ ( 1512 [байт] + 84 [байта] ) ⌋ = 94% (128.68 мкс).

Просматривая код, возможно, вы уже заметили “магическое число” 136. Для тех, кто лучше запоминает/оперирует с числами, чем со словами, на ранних этапах разработки лучше всего будет оставить короткие, уникальные и часто используемые константы именно в виде чисел. И так, знакомьтесь 136 – используемый в “LLTRSuperApp” период отправки broadcast пакетов (строка 136 :)

setTimeout(evFill, step, SimTime(136,SIMTIME_US));

И производные от нее:

setTimeout(evFill, PROBING, SimTime(136*10,SIMTIME_US));
setTimeout(evFill, step, SimTime(136*100,SIMTIME_US));      //before x10 (136*10)
setTimeout(evFill, COLLECT, SimTime(136*110,SIMTIME_US));
setTimeout(evStat, 0,       SimTime(136*120,SIMTIME_US));
setTimeout(evFill, step, SimTime(136*5,SIMTIME_US));

Я перебрал несколько значений (запуская симуляции), и 136 меня полностью устроило: во‑первых, оно лежит в диапазоне [151.2 (80%) .. 128.68 (94%)], и может использоваться как в “LLTRSuperApp”, так и в “LLTRApp”; во‑вторых, … а это уже сами далее увидите.

Note: чтобы быстро менять значения подобных констант во всех точках использования, можно использовать многое: от специальных плагинов для IDE, до записи в виде обычной константы с именем в формате “_#” (а в комментарии описать его сущность), например “_136” (если понадобится временно заменить значение, то просто меняем значение, если установить новое постоянное значение, то через refactoring-tools меняем и имя).

Такое “имя” намного проще использовать, чем, например, “timeToSend112PercentOfFrameIncludeInterPacketGap” или “frameIntervalFor89PercentChannelUtilization”. Другое имя “baseDelay” – уже короче, но все же больше, чем 3 цифры числа, и к тому же оно отвечает на меньшее количество вопросов (например, базовая задержка чего?), чем первые два варианта. Так что, на раннем этапе создания модели, творческие ресурсы лучше расходовать на что‑нибудь другое (структура модели, схемы, графики, перебор вариантов, поиск элегантного решения, …).

Note: Можно уменьшить негативное влияние сканирования на сеть путем изменения баланса в (‹частота отправки кадров›×‹размер кадра›) в сторону значительного уменьшения размера кадра (увеличения уровня гранулярности) для broadcast трафика. Я не моделировал эту ситуацию, поэтому приведу лишь результаты мысленного эксперимента. Это приведет к тому, что промежуточное сетевое оборудование сможет чаще вставлять, в промежутки между кадрами broadcast трафика, кадры других хостов в сети (хостов, не участвующих в сканировании). Это может по‑разному сказаться на точности собираемой статистики: отрицательно, из‑за сопутствующего не уменьшения (относительно обычной работы сети) объема трафика с других хостов в сети; положительно, из‑за уменьшения задержек в сети, и предотвращения лавинообразного увеличения размера окна у TCP (на некоторых алгоритмах контроля перегрузки; bufferbloat) до появления первых недошедших пакетов.

# Последовательности чисел

Раз уже заговорили про числа, то продолжим. В предыдущей статье было сказано (дежавю?):

Количество комбинаций, которые нужно проверить, можно посчитать по формуле (n−1) {каждому (n) нужно “поздороваться” со всеми остальными (n−1), даже если с ним ранее они уже здоровались}, где n – количество всех хостов минус один (broadcast хост).

В коде это вычисляется на строке 71:

numHosts = getHostsCount(getParentModule()->getComponentType()) - 1;      //"host*" without "hostS" (-1)
combHosts = (numHosts-1)*(numHosts);

Как в OMNeT++/INET посчитать количество “наших” хостов (“host*”: “hostS”, “host0”, “host1”, …)? Самый быстрый способ это сделать (функция “getHostsCount()”):

const uint32 getHostsCount(cComponentType const *const hostType)
{
       uint32 counter = 0;

       for(cModule::SubmoduleIterator i(getSimulation()->getSystemModule()); !i.end(); i++){
             if((*i)->getComponentType() == hostType) counter++;
       }

       return counter;
}

Так как для “своих” хостов мы создали “SimpleUdpTcpHost”, то нам достаточно подсчитать количество модулей в сети с таким типом, что и делает функция “getHostsCount()”.

Более быстрый способ

Note: всегда найдется еще более быстрый способ. USB: Full Speed, High Speed, SuperSpeed, SuperSpeed+, …

Нужно создать одну общую (для “LLTRSuperApp” и “LLTRApp”) переменную (статическую либо глобальную, внутри класса либо вне его). Начальное значение переменной должно быть 0. Затем, на стадии INITSTAGE_LOCAL, каждый “LLTRApp” инкрементирует эту переменную. На следующих стадиях каждый “*App” может читать эту переменную (numHosts).

И главное: в деструкторе “LLTRSuperApp” необходимо обнулять эту переменную.

В какой последовательности будем здороваться с хостами? В самом простом способе, цепочка номеров хостов зациклена (после “последнего” хоста идет нулевой хост; перед нулевым хостом стоит “последний” хост), и каждый unicast src хост последовательно выбирает следующие за собой хосты (в качестве unicast dst хоста), пока не дойдет до самого себя. Выглядит это так (числами обозначены номера хостов):

  s   d
  r   s
  c   t
┬
⁞ 0
⁞ ├─˃ 1
⁞ └─˃ 2
⁞ 1
⁞ ├─˃ 2
⁞ └─˃ 0
⁞ 2
⁞ ├─˃ 0
⁞ └─˃ 1
˅t

А для просмотра реализации, уже пора перейти к коду “LLTRApp”, т.к. в v.Basic.GlobalWave именно unicast src делает этот выбор, а broadcast src всего лишь сообщает им текущий шаг и номер итерации.

Зная текущий номер итерации можно вычислить номер unicast src хоста, и номер unicast dst хоста (строки 146,147; я немного изменил форматирование, чтобы связь была более явной):

int uSrcHostId =               iteration/(numHosts-1);
int uDstHostId = (uSrcHostId + iteration%(numHosts-1) + 1)%numHosts;

Хост может сравнить “uSrcHostId” со своим номером (“thisHostId”), и понять, что в текущей итерации он – unicast src хост (строка 149):

if(uSrcHostId == thisHostId) {

# Утилиты

Но как хост (“LLTRApp”) определяет свой номер? Через свое имя (“"host#"”; строка 64):

thisHostId = atoi(&getParentModule()->getName()[4]);

т.к. ранее мы отказались от “размещения хостов в векторе”.

А как выглядит обратный процесс: получить имя хоста, зная его номер, либо получить его IP‑адрес? Для этого создан отдельный класс “HostPath_defSuperHost_”, при помощи которого очень удобно размещать на стеке строку “"hostS\0\0\0\0\0\0\0\0\0"”, и работать с ней. Один из примеров использования (заполняем ARP‑таблицу хостов и MAC‑таблицу в свитчах – 1‑й шаг для “LLTRApp” в описании v.Basic.GlobalWave; строка 95):

case INITSTAGE_LAST:{
       //послать ARP-запросы всем родным хостам, т.к. дальше пакеты пройдут с трудом
       HostPath_defSuperHost_ path;
       socketTrickle.sendTo(new cPacket("To ARP"), path.GetIp(), 9);
       for(int i=0; i<numHosts; i++) socketTrickle.sendTo(new cPacket("To ARP"), path(i), 9);
}break;

Вначале посылаем пакет на “hostS”, затем на остальные хосты.

Еще один пример (строка 150):

destHost = HostPath_defSuperHost_(uDstHostId).GetIp();

Получаем IP‑адрес хоста с номером “uDstHostId”.

Note:_defSuperHost_” всегда напомнит, что в исходной строке находится “"hostS"”.

# Обработка смены текущего шага/итерации

Дошли до самого интересного. “LLTRApp” узнает о текущем шаге и текущей итерации из пакетов, рассылаемых “LLTRSuperApp”, но как быть в ситуациях, когда:

  1. один из пакетов текущей итерации/шага “затерялся”, началась новая итерация/шаг и ее пакеты уже дошли до “LLTRApp”, а затем приходит “потерянный” пакет…
  2. кто‑то сторонний разошлет пакеты с номером итерации, превышающем (или равным) “combHosts”…

И еще вопросы:

  1. Как определить, что началась новая итерация/шаг?
  2. Стоит ли подсчитывать пакеты, предназначенные для предыдущих итераций (предполагается, что уже началась новая итерация)?
  3. Если уже началась новая итерация, но предыдущий unicast src хост еще не завершил отправку трафика на unicast dst хост?

Ответы на все вопросы в коде (строка 120):

LLTRStep step;
uint32_t  iteration;
{
       FillPayload* fill = check_and_cast<FillPayload*>(msg);
       step       = fill->getStep();
       iteration  = fill->getIterationNumber();
}
delete msg;

if(iteration >= combHosts) iteration=0; //anti buffer overflow

if(step < this->step) break;
if(step > this->step) this->step = step;

switch(step){
case PROBING:{
       /*if(iteration+1 == iN)*/countFill[iN]++;      //calc only LLTRSuperApp fill (LLTRApp trickle always have step==0)
       if(iteration+1 <= iN) break;
       if(iteration+1 >  iN) iN = iteration+1;

       cancelEvent(evTrickle);    //stop transfer
       //...
}break;
case COLLECT:{
       cancelEvent(evTrickle);    //stop transfer
}break;
}

Note: просматривая эти строчки кода, представьте, что через них может проходить 8.3 миллионов пакетов каждую секунду…

А теперь обратим внимание на размер вектора (“countFill”), хранящего количество принятых broadcast пакетов (строка 71):

countFill.resize(1 + combHosts);

И на то, что в коде выше, постоянно к итерации добавлялся “1” (“iteration+1”). “iN” в “LLTRApp” всегда (кроме самого начала) на один больше текущего номера итерации. Следовательно, в “countFill” количество посчитанных пакетов за нулевою итерацию следует смотреть не в нулевом элементе “countFill”, а в первом.

Если этого не сделать, то мы не сможем запустить нулевую итерацию. Поэтому, в нулевом элементе вектора “countFill”, по завершению сканирования, всегда будет значение “1” – посчитан пакет, который переключил “iN” с “0” на “1”, и запустил “нулевую итерацию”.

В дальнейшем, мы не будем обращать внимание на нулевой элемент “countFill”.

Note: Альтернатива – использовать знаковый тип для хранения/передачи номера итерации, тогда начальное значение “iN” можно было сделать равным “-1”, убрать “+1” в нужных местах, и переписать подсчет пакетов, т.к. “countFill[-1]++;” – это явно не есть хорошо. Однако, если в текущем варианте мы “жертвуем” только одним значением “iN” из 232 возможных, то в альтернативном варианте, придется “жертвовать” уже половину (231) из всех возможных значений (232).

Note: если очень хочется привязать поля классов “LLTRSuperApp” и “LLTRApp” к конкретным шагам, дать им более конкретные названия, и, одновременно, сократить количество памяти, занимаемое полями этих классов, то можно все поля, специфичные для конкретного шага, поместить в union (так и так).

# Для отладки в симуляторе (Qtenv/Tkenv)

Хорошо было бы во время симуляции наблюдать за параметрами “step”, “iN” и количеством посчитанных пакетов (“countFill”) на каждом хосте (“LLTRApp”):

qtenv: host0 watch

В OMNeT++ это сделать легко – при помощи макроса “WATCH()” и WATCH_*()” макросов для просмотра “массивов” (строка 73):

WATCH(step);
WATCH(iN);
WATCH_VECTOR(countFill);

Еще было бы здорово, во время симуляции, на карте сети сразу же видеть какой из хостов сейчас считает себя unicast src хостом, а какой – unicast dst хостом. В OMNeT++ это можно сделать при помощи “Status Icon”.

Unicast src хост:

qtenv: unicast src host

(Строка 155):

parentDispStr.setTagArg("i2", 0, "status/up");

Unicast dst хост:

qtenv: unicast dst host

(Строка 160):

parentDispStr.setTagArg("i2", 0, "status/down");

Если стандартных иконок не достаточно, то всегда можно использовать свои иконки.

# Баг с finish() и логом в Qtenv

В то время, когда это все создавалось, последней версией OMNeT++ была 5.0 beta1 (5.0b1), именно ее я и использовал. В ней еще не было Qtenv, был только Tkenv.

Я чему я клоню? Посмотрите на то, как сейчас выглядит метод “finish()” у “LLTRSuperApp”, и сравните с тем, как он выглядел в те времена:

void finish()
{
       cancelEvent(trafEvnt);
       cancelEvent(statEvnt);
       socketTraf.close();

       for(int j=0;j<hostsCombs;j++){
             EV << "{" << trafCount[0*hostsCombs + j];

             for(int i=1;i<hostsCount;i++) EV << "," << trafCount[i*hostsCombs + j];

             EV << "}," << endl;
       }

       cSimpleModule::finish();
}

Note: в то время и переменные назывались по другому.

Перед написанием этой статьи, я обновился до последней версии OMNeT++ (релиз 5.0), в которой уже появился Qtenv – он мне сразу понравился, т.к. был заметно быстрее Tkenv. Но…

До этого момента я уже успел познакомиться с одним из багов лога Qtenv – неверный расчет границ текста, копируемого в буфер обмена. Теперь к нему добавился еще один баг: Qtenv не заносил в лог записи, которые были созданы после завершения симуляции (в данном случае – во время вызова “finish()” у всех модулей).

Раньше, с функционирующим выводом в лог (в “finish()”), было очень удобно работать: после завершения симуляции, в последних строчках лога, сразу же были видны ее результаты. Результаты из лога можно было легко скопировать, и использовать для последующей обработки.

Теперь же пришлось сделать несколько “костылей”, пока Qtenv не доделают. Мне они не очень подходят, но зато, при помощи них, я смогу показать, какие еще существуют способы вывода результатов симуляции из OMNeT++.

Самый простой “костыль”: после того, как “LLTRSuperApp” завершит загрузку статистики со всех “LLTRApp” – сразу же вывести ее в лог (строка 190). Минус этого “костыля” – после этой записи, в логе будут еще множество записей, и по окончании симуляции придется прокручивать (искать) лог до записи с результатами. Это придется делать каждый раз! Чуть позже покажу, как это выглядит.

Note: Только что замерил разницу в скорости release и beta версии 5.0: в версии 5.0b был только Tkenv и скорость его run была в 1.4 раза быстрее чем run в Tkenv/Qtenv release версии 5.0 (при одинаковых настройках). Зато теперь есть Qtenv и его fast выставленный в настройках на 17ms очень хорош. Не хватает еще одной кнопки – промежуточной между run и fast(500ms)).

# Вывод результатов симуляции в файл с помощью “cOutVector”

В OMNeT++ есть несколько средств по накоплению данных во время симуляции, вывода их в файл, и последующего анализа. Все они описаны в главе “12 Result Recording and Analysis” и разделе “7.9 Recording Simulation Results”.

Мне из этого всего был нужен только вывод двумерного массива в файл. Однако, этого он делать не умеет – он может выводить либо скалярные (одиночные) значение, либо одномерные массивы. Поэтому двумерный (либо n‑мерные) массив выводятся через несколько одномерных (строка 246 LLTRSuperApp).

Здесь есть еще несколько нюансов:

  1. в файл попадет не только наши данные, но и данные других модулей, которые захотели что‑то сохранить;
  2. в файлах (“simulations/results/*.sca”, “simulations/results/*.vec”) все данные хранятся в специальном формате, и их так просто из него не скопируешь.

Первая проблема решается просто – в “.ini” файле отключим сбор статистики для всего (строка 24), кроме “LLTRSuperApp” (строка 17).

Вторая проблема решается при помощи “scavetool”, который переведет данные из внутреннего формата в “csv” (также, поддерживаются и другие форматы; запускать из директории “simulations/results/”):

scavetool vector -F csv -O count.csv *-0.vec

А теперь сравним вывод данных в лог, с выводом в файл + scavetool. Плюсы вывода в лог я уже приводил, добавлю лишь то, что при выводе в лог я сам могу форматировать данные нужным образом, например, представив их в виде двухмерного массива для C. Вывод в файл, в данном случае, неудобен из‑за необходимости в дополнительных действиях, и не совсем подходящего формата вывода данных (даже после scavetool), но он хорошо подойдет для потоковых (автоматизированных) симуляций без GUI (“Cmdenv”).

# Вывод результатов симуляции на карту сети

Точнее, результаты появятся рядом с хостом “hostS”. Для этого используем “Text and Tooltip” (строка 239).

Ниже покажу, как это выглядит.

Плюсы:

Минусы:

# Запуск

Тем временем, в реальности…

Как уже упомянул в разделе “Баг с finish() и логом в Qtenv”, в реальности, переменные имели другое название (без общего стиля наименования), шаги и итерации были названы по‑другому:

Реальность --> Статья
---------------------
state      --> step
stepN      --> iN
stepSubN   --> pktN

И поля класса “LLTRSuperApp” выглядели так:

/* save state in "global" variables is practical to slow down && bugging your code */

cMessage *trafEvnt = nullptr;
cMessage *statEvnt = nullptr;

uint32    stepN = 0;
int    stepSubN = 0;
uint32    hostN = 1;

int  trafPort = -1;
long packetLength = 0;

UDPSocket::SendOptions udpSendOpt;
int gateTcpId; //socketStat

TCPSocket socketStat;
UDPSocket socketTraf;

std::vector<int> trafCount;       //[hostsCount][hostsCombs]
uint32 hostsCount;  //without SuperHost
uint32 hostsCombs;  //(hostsCount-1)*(hostsCount)

/*=================================================================================*/

Да и сам класс “LLTRSuperApp” назывался по‑другому.

Но код с этими переменными выглядел более сбалансированным (оптимальная длина названия переменной у большей части переменных) (“LLTRApp”):

if(step >= hostsCombs) step=0;    //anti buffer overflow

if(state < this->state) break;
if(state > this->state) this->state = state;

switch(state){
case PROBING:{
       /*if(step+1 == stepN)*/trafCount[stepN]++;     //calc only SuperClient traf (Client traf always have state==0)
       if(step+1 <= stepN) break;
       if(step+1 >  stepN) stepN = step+1;

       cancelEvent(trafEvnt);     //stop transfer

       if(step/(hostsCount-1) == hostId-1) {
             char path[] = "host0000000000";
             itoa10(((hostId-1) + (step%(hostsCount-1)+1))%hostsCount + 1, &path[4]);

             destC = getIPByHostPath(path);

Новые названия лучше отражают суть, а со старыми – код смотрелся лучше.

… И не было никакого “LLTRUtils” c “HostPath_defSuperHost_” (он появился позже) – все дублировалось для возможности быстрого внесения изменений, специфичных для LLTRSuperApp, или для LLTRApp.

А “HostPath_defSuperHost_” создавался таким, чтобы компилятор генерировал в точности такой же код, каким он был сейчас. За исключением того, что строку “"host0000000000"” я сразу заменил на “"host0\0\0\0\0\0\0\0\0\0"”.

Также не было разделения на “hostS” и “host#” – все хосты именовались в формате “host#”. В точности как было до раздела “Шаг 4: реализация протокола (версия v.Basic.GlobalWave)”. Что приводило к множеству “+1” и “-1” в коде.

Собираем, запускаем, запускаем симуляцию (run, или можете попробовать более скоростной режим – fast):

Run faster: no animation and rare inspector updates (F6)

Результаты симуляции:

qtenv: sim results – network – …

Странно, до хостов дошли все пакеты, кроме последней итерации (не дошел 1 пакет)… А что в логе?:

qtenv: sim results – log – …

То же самое… А что в статистике самих хостов?:

qtenv: sim results – open details for – …

И такая картина в каждом хосте… А если запустить “Tkenv” (Run > Run Configurations…):

run configurations – tkenv tkenv: sim results – log – …

Все также, за исключением того, что “finish()” нормально сработал, и запуск в режиме fast был не совсем “fast”.

Так в чем же дело? Почему все 300 (строка 135 LLTRSuperApp) посланных пакетов дошли до каждого “LLTRApp”?

Note: на самом деле в коде ошибки нет, и я покажу как исправить ситуацию, не меняя ни строчки в коде.

Вначале предлагаю посмотреть, почему в последней итерации каждый из “LLTRApp” получил только 299 пакетов, вместо 300? Ранее я упоминал, что:

Поэтому, в нулевом элементе вектора “countFill”, по завершению сканирования, всегда будет значение “1” – посчитан пакет, который переключил “iN” с “0” на “1”, и запустил “нулевую итерацию”.

В каждой новой итерации расходуется ровно один пакет для переключения на эту новую итерацию. Этот один пакет попадает в предыдущую итерацию (относительно новой итерации). Чтобы увидеть истинную картину, то надо произвести простую операцию:

[0]  [1]   [2]   [3]   [4]   [5]   [6]     [0]  [1]   [2]   [3]   [4]   [5]   [6]
 ┌─1─┐ ┌─1─┐ ┌─1─┐ ┌─1─┐ ┌─1─┐ ┌─1─┐
 │   ˅ │   ˅ │   ˅ │   ˅ │   ˅ │   ˅
 1   300   300   300   300   300   299  =   0   300   300   300   300   300   300  

Что немного не соответствует “бритве Оккама”, исправим:

[0]  [1]   [2]   [3]   [4]   [5]   [6]     [0]  [1]   [2]   [3]   [4]   [5]   [6]
 ┌────────────────1─────────────────┐
 │                                  ˅
 1   300   300   300   300   300   299  =   0   300   300   300   300   300   300  

Note: В будущем, при построении топологии сети, мы будем рассматривать каждую итерацию независимо от других итераций. Следовательно, набор [299 299 299] для нас будет аналогичен [300 300 300], [1 1 1], и даже с [0 0 0] разницы никакой не будет. Следовательно (на самом деле нет, не хватает примеров: [199 299 299]→[200 300 300], [2 3 3]→[3 4 4], [0 1 1]→[1 2 2]), данное исправление излишне.

Вернемся к тому, что у нас ничего не работает. И почему сразу несколько хостов считают себя unicast dst хостами?:

qtenv: multiple unicast dst – …

И почему, если закомментировать (строку 98 LLTRApp) (ARP для “hostS”), то “LLTRSuperApp” уже не сможет собрать статистику с хостов?

Можно долго искать ошибку там, где ее нет (в коде). Поэтому сразу же вспомним, а за счет чего должны были происходить потери пакетов? Подсказка в разделе “Протокол, версия v.Basic.GlobalWave”:

• их должно быть достаточно, чтобы они смогли потеряться (потеря пакетов будет происходить при одновременном “вливании” свитчем broadcast и unicast пакетов в один и тот же порт, с последующим переполнением очереди пакетов этого порта, т.е. количества пакетов должно быть достаточно для переполнения очереди);

Очевидно, что мы “вливаем” недостаточное количество пакетов, но даже этого количества пакетов хватило, чтобы сети “стало плохо” (ARP). Поэтому:

  1. посмотрим на текущий размер очереди;
  2. уменьшим его.

Читать подробности поиска “а где же в EtherSwitch находится очередь пакетов” – скучно, поэтому сразу покажу путь к параметру, определяющему ее размер (Network.switch0.eth[2].mac.txQueueLimit):

qtenv: mac queue

Note: Буфер, вместимостью 10000 пакетов =-O. Привет bufferbloat (Wi-Fi на Linux станет быстрее, приглашение на ланч, Dark Buffers in the Internet (тесты, начиная с 22 слайда), FQ_CoDel и FQ (FairQueue), sch_fq (страница 37)).

Да, чуть не забыл, если попробуете переопределить его значение через “.ini” файл, то у вас ничего не получится, и все из‑за этой строчки в “EthernetInterface.ned”:

txQueueLimit = (queueType == "" ? 10000 : 1); // queue sends one packet at a time

Посмотрим на “queueType”:

string queueType = default(""); // ~DropTailQueue, or a Diffserv queue; set to "" for use of an internal queue

То есть, управлять размером внутренней очереди не получится. Однако, можно создать внешнюю очередь, указав ее тип. При выборе типа очереди, нужно учитывать, что в обычных неуправляемых свитчах:

Если нужно сделать что‑то сложнее этого, то с выбором нужных компонентов поможет глава 15 “Differentiated Services” (“inet-manual-draft.pdf”, страница 129; страница 131):

Нам подходит очередь “DropTailQueue”, вот часть из ее описания (“inet-manual-draft.pdf”, страница 132):

Its capacity can be specified by the frameCapacity parameter. When the number of stored packet reached the capacity of the queue, further packets are dropped.

Возможно значение по умолчанию для “frameCapacity” нас устроит, посмотрим чему оно равно (“DropTailQueue.ned”):

int frameCapacity = default(100);

100 в несколько раз меньше “вливаемых” 600 пакетов (2×300), поэтому оно должно подойти нам.

Осталось добавить в “omnetpp.ini” строчку:

**.switch*.eth[*].queueType = "DropTailQueue"

И, на всякий случай, зафиксируем вместимость очереди (“eth[*].queue.dataQueue.frameCapacity”):

**.switch*.eth[*].queue.dataQueue.frameCapacity = 100

Note: применяем только для “switch*”, чтобы уменьшить “overhead” на хостах (“inet-manual-draft.pdf”, глава 3 “Node Architecture”, страница 7):

Most MACs also have an internal queue to allow operation without an external queue module, the advantage being smaller overhead.

В итоге, должно получиться примерно так (git tag a1_v0.27.0) diff.

Запускаем, запускаем симуляцию (fast):

qtenv: sim results – network

Победа! Собранная статистика настолько чистая, что, достаточно просто посмотреть на нее, чтобы определить, где какой хост находится.

Анимация нулевой итерации на примере жучков (899 KiB; фрагмент из КДПВ – картинки для привлечения внимания): zero iteration

#жучки-пакетики #bag-animation

Однако, обратите внимание на разное количество пакетов 214 и 164. Во всех итерациях, в которых “host0” был unicast dst хостом, он получал больше broadcast пакетов (214), чем получали остальные хосты в роли “unicast dst хоста”.

Для наглядности, к каждой итерации я добавил подпись “кто кому передает” (‹# unicast src хоста› -> ‹# unicast dst хоста›):

300 164 164   0 -> 1
300 164 164   0 -> 2
300 300 164   1 -> 2
214 300 300   1 -> 0
214 300 300   2 -> 0
299 163 299   2 -> 1

Я ожидал, что, во всех итерациях, значения счетчиков пакетов у unicast dst хостов будут примерно одинаковыми, т.е. лежать в окрестности одного числа (в данном случае – в окрестности числа 164). Но появился еще один фактор, который я пока не учел…

Note: я “приоткрою” этот фактор чуть ниже, и окончательно раскрою его в одной из следующих статей, а пока оставим все как есть – он нам абсолютно не мешает.

Выше я уже упоминал, что в симуляторе режим запуска run – слишком медленный, а fast – слишком быстрый, поэтому, чтобы детальнее просмотреть каждую итерацию, и при этом не ждать 126 минут (21 [минуту на нулевую итерацию] × 6 [итераций]), предлагаю запустить симуляцию со следующими настройками:

qtenv: fast run 17ms – log filter app

Запускаем (fast) симуляцию, и наблюдаем за инспектором сети (картой сети).

Возможно, в логе вы заметите мелькающие сообщения вида:

** Event #1121  t=0.0016001  Network.host0.app[0] (LLTRApp, id=130)  on >>> 0/299 time:0.00136 (inet::FillPayload, id=1713)
INFO:   !!!!!! Iteration: 0 | 0 ~ 0
INFO:   10.0.1.1
qtenv: log – new iteration start – info – find

Часть из них можно найти поиском по “!!!!!! Iteration:”, по завершении симуляции:

** Event #39453  t=0.11024815  Network.host1.app[0] (LLTRApp, id=163)  on >>> 2/299 time:0.109888
INFO:   !!!!!! Iteration: 2 | 1 ~ 0
INFO:   10.0.1.2
** Event #58994  t=0.16451215  Network.host1.app[0] (LLTRApp, id=163)  on >>> 3/299 time:0.164152
INFO:   !!!!!! Iteration: 3 | 1 ~ 1
INFO:   10.0.1.0
** Event #80954  t=0.21877615  Network.host2.app[0] (LLTRApp, id=196)  on >>> 4/299 time:0.218416
INFO:   !!!!!! Iteration: 4 | 2 ~ 0
INFO:   10.0.1.0
** Event #102913  t=0.27304015  Network.host2.app[0] (LLTRApp, id=196)  on >>> 5/299 time:0.27268
INFO:   !!!!!! Iteration: 5 | 2 ~ 1
INFO:   10.0.1.1

Note: Другая часть этих сообщений уже не отображается в логе – превышен лимит на максимальное количество событий и строчек в логе. Лимит можно изменить в настройках (Preferences > Logs: “Overall history size (in events)” и “Scrollback buffer (lines)”).

Эти сообщения выводятся в начале каждой итерации. Сообщение генерирует unicast src хост новой итерации. Часть кода, в которой генерируются сообщения (строка 152 LLTRApp):

if(uSrcHostId == thisHostId) {
       destHost = HostPath_defSuperHost_(uDstHostId).GetIp();

       EV << "!!!!!! Iteration: "
          << iteration << " | " << thisHostId << " ~ " << iteration%(numHosts-1) << endl;
       EV << destHost << endl;

       parentDispStr.setTagArg("i2", 0, "status/up");
       //...
}

Я выводил эти сообщения, чтобы:

Возможно, и вам тоже они пригодятся.

Подробнее про “iteration%(numHosts-1)

По сути, для unicast src хоста, он значит “с каким по счету хостом я здороваюсь”.

В разделе “Последовательности чисел” я уже приводил код для расчета (строка 146):

int uSrcHostId =               iteration/(numHosts-1);
int uDstHostId = (uSrcHostId + iteration%(numHosts-1) + 1)%numHosts;

И последовательность “рукопожатий” хостов, в которую я теперь добавил “iteration%(numHosts-1)”:

  s   d
  r   s
  c   t
┬
⁞ 0
⁞ ├─˃ 1 [0]
⁞ └─˃ 2 [1]
⁞ 1
⁞ ├─˃ 2 [0]
⁞ └─˃ 0 [1]
⁞ 2
⁞ ├─˃ 0 [0]
⁞ └─˃ 1 [1]
˅t

Еще одной из подобных вещей, упрощающих отладку, являются названия broadcast пакетов:

qtenv: traffic – broadcast pkt name

Их формат:

>>> ‹номер итерации›/‹обратный отсчет: сколько еще осталось отправить broadcast пакетов за текущую итерацию› time:‹время отправки пакета›

Время отправки пакета позволяет визуально оценить (сравнивая со столбцом “Time”), сколько времени прошло от момента отправки пакета до наступления этого события. То есть по нему сразу видно, насколько задержали этот пакет промежуточные узлы в сети.

А по “обратному отсчету” можно быстро оценить время до начала следующей итерации. Например, мне, во время симуляции, нужно посмотреть определенные (изменяющиеся) параметры хостов в моменты перехода на новую итерацию: я могу, во время симуляции, переключится между режимами until+fast и run/step, и, смотря обратный отсчет, быстро находить (предполагаемое время ±12%) моменты перехода на новую итерацию.

Note: Альтернатива из двух проходов: можно вначале записать весь лог, выбрать в нем нужное время или номер события, а затем опять запустить симуляцию в режиме until, подставляя нужное время или номер события. Возможно, существует и более легкий способ, например, программно приостановить симуляцию в нужный момент.

Одна из частей кода, в которой генерируется имя пакета (строка 123 LLTRSuperApp):

FillPayload *fill; {
       char msgName[64], time[64]; //see min buf size in ".str()" doc
       snprintf(msgName, sizeof msgName,
                ">>> %d/%d time:%s", iN, 300-1-pktN, simTime().str(time));
       // http://demin.ws/blog/english/2013/01/28/use-snprintf-on-different-platforms/
       msgName[sizeof(msgName)-1] = '\0';
       fill = new FillPayload(msgName);
}

Все хорошо, все работает, но меня тревожат ошибки на последних двух скриншотах:

Посмотрим на ошибки поближе (новый запуск симуляции; режим run; трафик от свитчей скрыт):

qtenv: traffic – icmp error port unreachable

Уже сейчас ясна причина появления ошибок, но, тем не менее, я хочу точно увидеть, какой пакет стал причиной отправки “ICMP Error”. Например, посмотрим в инспекторе объектов на событие #728 (“ICMP-error-#6-type3-code3”):

qtenv: inspector – icmp error port unreachable – to arp pkt

Причиной события #728 был “To ARP” пакет от “host0”.

Получается, что мы рассылаем “To ARP” пакеты на DISCARD (9) порт, ожидая, что он будет отброшен, но в тех стандартных модулях INET, которые мы используем, не реализованы “обработчики поведения по умолчанию для стандартных портов”. Поэтому сделаем это вручную:

UDPSocket socketDISCARD;
socketDISCARD.setOutputGate(gate("udpOut"));
socketDISCARD.bind(9);

Просто привязываем сокет к порту 9.

И подправим метод “handleMessage()” у “LLTRApp”, чтобы по UDP обрабатывать только “socketTrickle”:

if     (msg->arrivedOn(gateUdpId) && socketTrickle.belongsToSocket(msg)) handleUdp(msg);

Note: альтернатива: можно было сделать отдельный DISCARD‑модуль, либо настроить “UDPSink”.

Должно получиться примерно так (git tag a1_v0.28.0) diff.

# Шаг 5: больше тестов (эксперименты)

Вначале предлагаю представить, что произойдет, если раскомментировать это условие в “LLTRApp” (строка 140):

/*if(iteration+1 == iN)*/countFill[iN]++;
Спойлер! Спойлер! Осторожно, спойлер содержит ответ!

Я предупреждал…

Симуляция с этим условием даст следующие результаты:

{299,163,163},
{299,163,163},
{299,299,163},
{213,299,299},
{213,299,299},
{299,163,299},

Предположение: те единичные пакеты, которые переключали “LLTRApp” на новую итерацию, теперь нигде не подсчитываются.

Данное предположение согласуется с условием [(номер итерации, записанный в пакете) +1 != (номеру итерации в “LLTRApp”)], и с результатами симуляции. Дополнительно, можно проверить нулевой элемент вектора “countFill” одного из “host#” – его значение должно быть равно “0”:

qtenv: host0 – watch – countfill – if iteration

Также это означает, что за все время симуляции ни разу не возникала ситуация:

1. один из пакетов текущей итерации/шага “затерялся”, началась новая итерация/шаг и ее пакеты уже дошли до “LLTRApp”, а затем приходит “потерянный” пакет...

А если заменить его на:

if(iteration+1 >= iN) countFill[iN]++;
Спойлер! Спойлер! Осторожно, спойлер содержит ответ!

Я предупреждал…

И мы получаем уже привычные результаты симуляции:

{300,164,164},
{300,164,164},
{300,300,164},
{214,300,300},
{214,300,300},
{299,163,299},

Note: перед продолжением откатите назад эти изменения.

# Варьируем параметры

Ранее я упоминал про варьирование параметров:

Однако, при моделировании я уже успел поварьировать значения задержки между передачей пакетов, и размер пакетов, для достижения приемлемой плотности трафика. Поэтому в “.ini” файле вы не найдете значения “1472B”, вместо него там будет стоять “1446B”. Что, по сути, приводит размер “EtherPhyFrame” (1526 байт) к размеру IPv4 (1500 байт), т.е., в этой версии модели, размер “EtherPhyFrame” составляет 1500 байт.

Все это я делал примерно в этот момент, т.е. после настройки размера очереди у свитчей. Также у меня были другие начальные константы, из‑за чего результаты симуляции не выглядели столь “идеально”. Однако, это позволило лучше изучить поведение модели.

Note: представьте многомерные пространства (оси – параметры модели; значения – описывают реакцию модели; пространства – характеристики модели), в каждой точке которых модель реагирует “по своему”, можно объединить точки в кластеры по схожести реакции модели, …

В спойлерах я приведу реакцию модели (результаты симуляции) на варьирование двух параметров:

Задержка между итерациями (x150).

Реакция модели
INFO:   {300,164,164},
INFO:   {300,164,164},
INFO:   {300,300,164},
INFO:   {214,300,300},
INFO:   {214,300,300},
INFO:   {299,163,299},

Задержка между итерациями (x50).

Реакция модели
INFO:   {300,164,164},
INFO:   {300,126,126},
INFO:   {300,300,300},
INFO:   {214,300,300},
INFO:   {187,300,300},
INFO:   {299,163,299},

Задержка между итерациями (x80).

Реакция модели
INFO:   {300,164,164},
INFO:   {300,155,155},
INFO:   {300,300,278},
INFO:   {214,300,300},
INFO:   {205,300,300},
INFO:   {299,163,299},

Задержка между итерациями (x80); период отправки пакетов (“LLTRSuperApp”: 136 мкс; “LLTRApp”: 128 мкс).

Реакция модели
INFO:   {300,223,223},
INFO:   {300,216,216},
INFO:   {300,300,216},
INFO:   {223,300,300},
INFO:   {225,300,300},
INFO:   {299,222,299},

Задержка между итерациями (x80); период отправки пакетов (“LLTRSuperApp”: 128 мкс; “LLTRApp”: 136 мкс).

Реакция модели
INFO:   {300,217,217},
INFO:   {300,204,204},
INFO:   {300,300,300},
INFO:   {219,300,300},
INFO:   {204,300,300},
INFO:   {299,216,299},

Note: если результаты последних двух симуляций кажутся вам странными, то не волнуйтесь – это пройдет после прочтения одной из следующих статей ;)

Note: и опять, напоминаю: перед продолжением откатите назад эти изменения.

# Больше сетей с разными топологиями, добавляем новые сети

Пора проверить работу протокола (и адекватность модели) на нескольких сетях с разной топологией. Все сети здесь… (git tag a1_v0.29.0) diff

Файлы новых сетей:

При запуске симулятора, теперь можно выбрать одну из сетей:

qtenv: set config – set network

Note: “(General)” – это наша первая сеть (4 хоста, 2 свитча, т.е. “N2_2”).

Если просто добавить файлы новых сетей, рядом с файлом существующей сети, то симулятор не покажет этот диалог. Чтобы появился этот диалог, в “omnetpp.ini” нужно добавить новые секции, точнее “Named Configurations” (OMNeT++ - Simulation Manual).

По сути, я создал несколько новых “именованных конфигураций”/секций, которые переопределяют параметр “network” секции “[General]” (строка 33):

[Config N3_2]
network = lltr.Network_3_2

[Config N2_3]
network = lltr.Network_2_3

[Config Tree]
network = lltr.Network_tree

[Config Serial]
network = lltr.Network_serial

Посмотрим на новые сети поближе.

Сеть “Network_3_2” (конфигурация “N3_2”):

Network 3_2

Note: Попробуйте закомментировать “**.interfaceTable.displayAddresses = false” в “omnetpp.ini”, и посмотреть на назначенные IP‑адреса хостов. Попробовали? А теперь переподключите “hostS” на “switch1”, и снова посмотрите на IP‑адреса хостов…

Результаты симуляции
INFO:   {300,164,164,300},
INFO:   {300,164,164,300},
INFO:   {300,300,300,164},

INFO:   {300,300,164,300},
INFO:   {300,300,300,214},
INFO:   {214,300,300,300},

INFO:   {300,300,300,214},
INFO:   {214,300,300,300},
INFO:   {300,164,300,300},

INFO:   {164,300,300,300},
INFO:   {300,164,164,300},
INFO:   {299,163,163,299},

Note: пустые строки расставлены для улучшения восприятия – они визуально группируют итерации по unicast src хосту.

Заметили закономерность? Если unicast трафик проходит в направлении от “switch1” к “switch0”, то мы получаем число 214. Во всех остальных случаях (трафик направлен от “switch0” к “switch1”, либо когда unicast src и dst хосты подключены к одному и тому же свитчу) получаем число 164.

В чем отличие между “switch0” и “switch1”? В том, что “hostS” подключен к “switch0” ?!

Сеть “Network_2_3” (конфигурация “N2_3”):

Network 2_3
Результаты симуляции
INFO:   {300,164,164,164},
INFO:   {300,164,164,164},
INFO:   {300,164,164,164},

INFO:   {300,300,164,300},
INFO:   {300,300,300,164},
INFO:   {214,300,300,300},

INFO:   {300,300,300,164},
INFO:   {214,300,300,300},
INFO:   {300,164,300,300},

INFO:   {214,300,300,300},
INFO:   {300,164,300,300},
INFO:   {299,299,163,299},

Note: пустые строки расставлены для улучшения восприятия – они визуально группируют итерации по unicast src хосту.

Сеть “Network_tree” (конфигурация “Tree”):

Network tree
Результаты симуляции
INFO:   {300,164,164,164,164},
INFO:   {300,164,164,164,164},
INFO:   {300,164,164,164,164},
INFO:   {300,164,164,164,164},

INFO:   {300,300,164,300,300},
INFO:   {300,300,300,214,214},
INFO:   {300,300,300,214,214},
INFO:   {264,300,300,300,300},

INFO:   {300,300,300,214,214},
INFO:   {300,300,300,214,214},
INFO:   {264,300,300,300,300},
INFO:   {300,164,300,300,300},

INFO:   {300,300,300,300,164},
INFO:   {264,300,300,300,300},
INFO:   {300,214,214,300,300},
INFO:   {300,214,214,300,300},

INFO:   {264,300,300,300,300},
INFO:   {300,214,214,300,300},
INFO:   {300,214,214,300,300},
INFO:   {299,299,299,163,299},

Note: пустые строки расставлены для улучшения восприятия – они визуально группируют итерации по unicast src хосту.

Сеть “Network_serial” (конфигурация “Serial”):

Network serial
Результаты симуляции
INFO:   {300,164,164,164,164,164,164,300,300},
INFO:   {300,164,164,164,164,164,164,300,300},
INFO:   {300,164,164,164,164,164,164,300,300},
INFO:   {300,164,164,164,164,164,164,300,300},
INFO:   {300,164,164,164,164,164,164,300,300},
INFO:   {300,164,164,164,164,164,164,300,300},
INFO:   {300,300,300,300,300,300,300,164,164},
INFO:   {300,300,300,300,300,300,300,164,164},

INFO:   {300,300,164,300,300,300,300,300,300},
INFO:   {300,300,300,164,164,164,164,300,300},
INFO:   {300,300,300,164,164,164,164,300,300},
INFO:   {300,300,300,164,164,164,164,300,300},
INFO:   {300,300,300,164,164,164,164,300,300},
INFO:   {300,300,300,300,300,300,300,214,214},
INFO:   {300,300,300,300,300,300,300,214,214},
INFO:   {214,300,300,300,300,300,300,300,300},

INFO:   {300,300,300,164,164,164,164,300,300},
INFO:   {300,300,300,164,164,164,164,300,300},
INFO:   {300,300,300,164,164,164,164,300,300},
INFO:   {300,300,300,164,164,164,164,300,300},
INFO:   {300,300,300,300,300,300,300,214,214},
INFO:   {300,300,300,300,300,300,300,214,214},
INFO:   {214,300,300,300,300,300,300,300,300},
INFO:   {300,164,300,300,300,300,300,300,300},

INFO:   {300,300,300,300,164,300,300,300,300},
INFO:   {300,300,300,300,300,164,164,300,300},
INFO:   {300,300,300,300,300,164,164,300,300},
INFO:   {300,300,300,300,300,300,300,264,264},
INFO:   {300,300,300,300,300,300,300,264,264},
INFO:   {264,300,300,300,300,300,300,300,300},
INFO:   {300,214,300,300,300,300,300,300,300},
INFO:   {300,300,214,300,300,300,300,300,300},

INFO:   {300,300,300,300,300,164,164,300,300},
INFO:   {300,300,300,300,300,164,164,300,300},
INFO:   {300,300,300,300,300,300,300,264,264},
INFO:   {300,300,300,300,300,300,300,264,264},
INFO:   {264,300,300,300,300,300,300,300,300},
INFO:   {300,214,300,300,300,300,300,300,300},
INFO:   {300,300,214,300,300,300,300,300,300},
INFO:   {300,300,300,164,300,300,300,300,300},

INFO:   {300,300,300,300,300,300,164,300,300},
INFO:   {300,300,300,300,300,300,300,300,300},
INFO:   {300,300,300,300,300,300,300,300,300},
INFO:   {300,300,300,300,300,300,300,300,300},
INFO:   {300,264,300,300,300,300,300,300,300},
INFO:   {300,300,264,300,300,300,300,300,300},
INFO:   {300,300,300,214,300,300,300,300,300},
INFO:   {300,300,300,300,214,300,300,300,300},

INFO:   {300,300,300,300,300,300,300,300,300},
INFO:   {300,300,300,300,300,300,300,300,300},
INFO:   {300,300,300,300,300,300,300,300,300},
INFO:   {300,264,300,300,300,300,300,300,300},
INFO:   {300,300,264,300,300,300,300,300,300},
INFO:   {300,300,300,214,300,300,300,300,300},
INFO:   {300,300,300,300,214,300,300,300,300},
INFO:   {300,300,300,300,300,164,300,300,300},

INFO:   {300,300,300,300,300,300,300,300,164},
INFO:   {214,300,300,300,300,300,300,300,300},
INFO:   {300,214,214,214,214,214,214,300,300},
INFO:   {300,214,214,214,214,214,214,300,300},
INFO:   {300,214,214,214,214,214,214,300,300},
INFO:   {300,214,214,214,214,214,214,300,300},
INFO:   {300,214,214,214,214,214,214,300,300},
INFO:   {300,214,214,214,214,214,214,300,300},

INFO:   {214,300,300,300,300,300,300,300,300},
INFO:   {300,214,214,214,214,214,214,300,300},
INFO:   {300,214,214,214,214,214,214,300,300},
INFO:   {300,214,214,214,214,214,214,300,300},
INFO:   {300,214,214,214,214,214,214,300,300},
INFO:   {300,214,214,214,214,214,214,300,300},
INFO:   {300,214,214,214,214,214,214,300,300},
INFO:   {299,299,299,299,299,299,299,163,299},

Note: пустые строки расставлены для улучшения восприятия – они визуально группируют итерации по unicast src хосту.

Note:Я уже даже не вижу код. Я вижу блондинку, брюнетку, рыжую.” (The Matrix) → “Я уже даже не вижу числа. Я вижу, топологию.”

Все, что имеет начало, имеет и конец. Я вижу конец…”, точнее вижу проблему с итерациями, в которых хост из левого конца (“host6” или “host5”) посылает unicast трафик на хост из правого конца (“host7” или “host8” или “host0”). Однако, собранных данных достаточно, чтобы корректно построить топологию сети. (Пифия)

Похоже, что проблема не одинакового количества принятых broadcast пакетов (164, 214, 264, 300) зависит:

Проверим это предположение. Я добавил еще две сети… (git tag a1_v0.30.0) diff

Файлы новых сетей:

Сеть “Network_serial-len-test” (конфигурация “SerialLenTest”):

Network serial-len-test
Результаты симуляции
INFO:   {300,164,164,300,300},
INFO:   {300,164,164,300,300},
INFO:   {300,300,300,164,164},
INFO:   {300,300,300,164,164},

INFO:   {300,300,164,300,300},
INFO:   {300,300,300,300,300},
INFO:   {300,300,300,300,300},
INFO:   {300,300,300,300,300},

INFO:   {300,300,300,300,300},
INFO:   {300,300,300,300,300},
INFO:   {300,300,300,300,300},
INFO:   {300,164,300,300,300},

INFO:   {300,300,300,300,164},
INFO:   {214,300,300,300,300},
INFO:   {300,214,214,300,300},
INFO:   {300,214,214,300,300},

INFO:   {214,300,300,300,300},
INFO:   {300,214,214,300,300},
INFO:   {300,214,214,300,300},
INFO:   {299,299,299,163,299},

Note: пустые строки расставлены для улучшения восприятия – они визуально группируют итерации по unicast src хосту.

Удаление хостов не повлияло на проблему с крайними хостами (из “Network_serial”), что логично.

Сеть “Network_serial-len-test-simple” (конфигурация “SerialLenTestSimple”):

Network serial-len-test-simple
Результаты симуляции
INFO:   {300,164,164},
INFO:   {300,164,164},
INFO:   {300,300,164},
INFO:   {300,300,300},
INFO:   {300,300,300},
INFO:   {299,163,299},

Получилось воспроизвести проблему на топологии исходной сети, изменяя количество промежуточных свитчей.

Мы немного “приоткрыли” неучтенный фактор, и, как я писал ранее, окончательно раскрою его в одной из следующих статей.

# Шаг 6: шаг вперед, шаг назад

Этот шаг должен был называться “последние штрихи”, и начинаться примерно так:

Последние штрихи

Мы уже несколько раз пытались бороться с адресацией хостов:

  1. вначале пытались поместить хосты в несуществующий вектор;
  2. затем просто сделали более удобную индексацию (в именах) у хостов, и назначили “не вводящие в заблуждение” IP‑адреса.

И, на данный момент, чтобы получить IP‑адрес хоста нам приходится каждый раз делать так (немного упростил):

Берем индекс хоста (число)
–-[itoa]--> получаем имя хоста "host#" (строка)
–-[getModuleByPath; последовательно перебирая все модулей сети, ищем модуль хоста по его имени]--> получаем модуль хоста (указатель)
-–[getNetworkAddress]--> наконец, получаем IP-адрес хоста (число).

То есть:

0 -> "host0" -> *** -> 10.0.1.0
1 -> "host1" -> *** -> 10.0.1.1
2 -> "host2" -> *** -> 10.0.1.2
...

Не кажется странным, что проще было бы сразу получить число (10.0.1.0) из числа (0)?

А теперь вспомним, из‑за чего нам приходится делать именно так:

Сейчас мы для соединения с “host1” используем его IP‑адрес “10.0.1.2”. Однако, все адреса распределяются автоматически, и не факт, что “host1” всегда будет иметь адрес “10.0.1.2”.

Однако, далее мы добились соответствия номера unicast src/dst хоста его IP‑адресу, с некоторыми ограничениями.

А заканчиваться:

Проверка соответствия IP‑адрес↔индекс хоста должна была выглядеть так:

  1. пройтись по списку всех дочерних модулей сети:
  2. искать “"host*"”, пропускать “"hostS"”;
  3. использовать “atoi()” для получения индекса хоста;
  4. преобразовать число в “IP‑адрес”;
  5. сложить полученный “IP‑адрес” с базовым/начальным IP‑адресом (10.0.1.0);
  6. сверить с IP‑адресом хоста
  7. если не совпало, то кинуть “throw cRuntimeError()”.

Это позволило бы точно определить нарушение соответствия. Например, оно сразу бы обнаружило несоответствие в “Network_3_2”, и предотвратило запуск симуляции (для запуска нужно было бы переименовать хосты: “"host3"” → “"host1"”′; “"host1"” → “"host2"”′; “"host2"” → “"host3"”′ – в точности как были назначены IP‑адреса).

Почему же я это все не сделал? При изменении индексов хостов, также меняется порядок сканирования сети (порядок итераций). Но я не хотел менять порядок сканирования сети. Например, в “Network_3_2” было важно, чтобы “host3” был unicast src хостом именно в последних итерациях (для корректного сравнения с результатами “Network_2_3” и “Network”/“N2_2”), и я не могу изменить его имя на “host1”, т.к. это изменит порядок сканирования.

К тому же, изменение индекса хоста – это самый быстрый способ изменения последовательности сканирования, и не хотелось бы от него отказываться (от возможности быстро проверить реакцию модели на другой порядок сканирования).

В итоге, оставался только один вариант для сохранения соответствия (IP‑адрес↔индекс) – реализовать свой “*NetworkConfigurator” на основе “IPv4NetworkConfigurator”. Причем, код, проверяющий соответствие (из INITSTAGE_APPLICATION_LAYER в “LLTRSuperApp”) помог бы и здесь.

Однако, не зря этот шаг называется “шаг вперед, шаг назад” – предлагаю потренироваться с созданием “*NetworkConfigurator”, сделав собственный “последний штрих”.

Note: Следующие статьи не будут требовать строгого соответствия IP‑адреса хоста и его индекса. Поэтому можете, не торопясь, хорошо продумать реализацию.

Теперь самое время прочитать оригинальную документацию (“inet/doc/inet-manual-draft.pdf” и “omnetpp-5.0/doc/*”) от начала и до конца. В частности, рекомендую посмотреть “UserGuide.pdf” и “manual/index.html” (в нем находится Simulation Manual именно для версии 5.0).

Про ссылки на Simulation Manual

В этой статье все ссылки вели на online версию Simulation Manual, которая предназначена для последней версии OMNeT++. Жалко, что они не хранят документацию, раздельно для каждой вышедшей версии (я нашел только для версии 4.6: “omnetpp.org/doc/omnetpp4/manual/usman.html”). Поэтому надеюсь, что новые версии документации (в пределах 5.*) будут совместимы с 5.0. Во всяком случае, всегда есть локальная версия, и “web.archive.org/web/20170802125337/https://omnetpp.org/doc/omnetpp/manual/” (начиная с 5.1.1).

# P.S.


Если вам, так же как и мне, не нравится стандартные иконки устройств, и фон инспектора сети в симуляторе, то это можно легко исправить.

# Замена стандартных иконок

Если вам понравилась иконка хоста (“"device/pc2"”) и свитча (“"device/switch"”) из этой статьи, то разархивируйте (7zip) эту картинку в “omnetpp-5.0/images/device/”:

new device icons (un[7z]me)

# Замена цвета фона (зеленого) в Tkenv

tkenv: green background – x64dbg (жучки атакуют!)

Чтобы заменить цвет, надо знать место, где он задается (в коде) или находится (в памяти). Чтобы найти это место, надо определить числовое значение цвета (RGB):

  1. делаем скриншот окна симулятора;
  2. в любом растровом графическом редакторе, “пипеткой” определяем цвет.

Числовое значение “зеленого” цвета фона: A0E0A0 (hex); R:160, G:224, B:160.

Я пока еще не подобрал цвет фона на замену, и у меня есть 3 варианта (пути) дальнейших действий:

  1. [долгий] поиск цвета в исходниках, {замена цвета на один из вариантов, компиляция, просмотр результата}(повторять в цикле, пока не будет найден “оптимальный цвет”);
  2. [повышающий скилы Reverse Engineering; хакерский] поиск цвета в памяти запущенной программы, {замена цвета на один из вариантов в “интерактивном режиме”}(повторять в цикле, пока не будет найден “оптимальный цвет”);
  3. [прототипный] сделать скриншот, и менять цвет в любом графическом редакторе.

Начнем с конца: “прототипный” вариант – самый быстрый, однако, сам графический редактор (его GUI) может повлиять на выбор итогового цвета, поэтому при подборе цвета нужно скрыть весь GUI редактора, оставив только холст, а еще лучше – переключится в полноэкранный режим. Минус этого пути – он позволяет только подобрать цвет, в то время как остальные варианты позволяют изменить (зафиксировать) цвет в самой программе.

Долгий” вариант можно сразу отбросить, к тому же “собирать пазл”, пробираясь сквозь дебри классово‑объектного (с привкусом лазании) кода OMNeT++ уже поднадоело. Хочется простоты, и возможности сразу видеть финальный результат. Как в графическом редакторе, поддерживающем слои – мы можем работать, как и с отдельными слоями или группами слоев, так и с итоговым изображением в целом. Причем, работая с отдельными слоями можно сразу же видеть влияние внесенных изменений на всю картину в целом.

Я опишу “хакерский” вариант с простым asm, и работой с “финальным результатом” (скомпилированной, слинкованной, и запущенной программой). Он состоит из нескольких шагов:

  1. запускаем симулятор (Tkenv);
  2. запускаем отладчик x64_dbg (32bit версию, т.к. Tkenv – 32bit) (его возможностей вполне достаточно для наших целей);
  3. подключаемся к процессу Tkenv (File > Attach; “LLTR.exe”), и “размораживаем” (нижний левый угол – “Paused”) Tkenv (Debug > Run);
  4. ищем в памяти нужную константу (“A0E0A0RGB или “A0E0A0BGR ;) ;
  5. меняем ее в нужном месте.
Подробнее про “ищем в памяти нужную константу” и “меняем ее в нужном месте”

Для поиска константы (паттерна):

Note: x64_dbg ищет начиная с текущего адреса (выбранной строки), и до конца адресного пространства. Поэтому, для поиска по всему адресному пространству важно следить за тем, чтобы была выбрана первая строка, и список был отсортирован по столбцу “Address”.

Результаты поиска отобразятся во вкладке “References” (после поиска опять нужно разморозить (Debug > Run) Tkenv):

references – pattern a0e0a0
Address  | Data
===================
0497801C | A0 E0 A0
0497A6C0 | A0 E0 A0
0497DE60 | A0 E0 A0
0497DED0 | A0 E0 A0
0497DF60 | A0 E0 A0
0497DFD0 | A0 E0 A0

078E7EFB | A0 E0 A0
079A8235 | A0 E0 A0
079C8235 | A0 E0 A0
079E8235 | A0 E0 A0

662CE88D | A0 E0 A0

7340A1A7 | A0 E0 A0
7340A31D | A0 E0 A0
7340A4CF | A0 E0 A0
7340A693 | A0 E0 A0
7340A80F | A0 E0 A0
7340A935 | A0 E0 A0
7340AA69 | A0 E0 A0
7340AB71 | A0 E0 A0
7340ADAF | A0 E0 A0
7340B1B7 | A0 E0 A0
7344E1ED | A0 E0 A0

77E1924B | A0 E0 A0
77E1936B | A0 E0 A0

Совпадений несколько, поэтому перед “лихорадочным” изменением значений в памяти программы, хорошо бы знать, кто владеет областью памяти, в которой нашлось совпадение. Поэтому возвращаемся в “Memory Map”, и смотрим, в какой диапазон попали найденные адреса:

Address  | Size     | Page Info                 | Alloc Type | Current Prot | Alloc Prot
========================================================================================
048A0000 | 00400000 |                           | PRV        | -RW--        | -RW--
07430000 | 00930000 | ...\Fonts\StaticCache.dat | MAP        | -R---        | -R---
6621F000 | 000DD000 | libiconv-2.dll > ".rdata" | IMG        | -R---        | ERWC-
733B0000 | 0005C000 | netapi32.dll >            | IMG        | -R---        | ERWC-
77C70000 | 001AA000 | imm32.dll >               | IMG        | -R---        | ERWC-

Было бы очень странно, если значение, задающее цвет фона инспектора сети, находилось бы в последних 4‑х диапазонах памяти. Остался только первый диапазон памяти (0x048A0000 + 0x00400000), это означает, что стоит исключить все результаты поиска, кроме первых 6‑и.

Изменять значения в памяти все еще “опасно”, сперва стоит взглянуть на код, использующий найденные области памяти. Последовательно повторяем, для каждого из 6‑и найденных адресов:

  1. [открываем дамп памяти по найденному адресу] в “References” → контекстное меню результата поиска → “Follow in Dump
  2. [создаем точку останова на доступ (чтение/запись) к памяти] в “Dump 1” (вкладка в нижней части окна) (начало паттерна будет выделено) → контекстное меню → BreakpointHardware, Access → Dword;
  3. [пытаемся вызвать код, перерисовывающий фон] c Tkenv:
    • сворачиваем/разворачиваем окно;
    • меняем размер окна;
    • перемещаем (скролим) область сети/модуля;
    • увеличиваем/уменьшаем (лупы +/-) область сети/модуля;
    • открываем модуль‑хост/свитч (двойной клик по модулю), и возвращаемся назад к сети;
    • открываем дочернее окно инспектора сети (Inspect > Network);
    • перестраиваем сеть (“Re‑layout”, кнопка “обновить”);
  4. Если код нам нравится (напоминает код “перерисовывающий что‑либо”), то пробуем изменить “A0 E0 A0” на “A0 00 A0” (двойной клик на “E0” в “Dump 1”), и смотрим результат. Дополнительно можно поставить точку останова на сам код, и посмотреть, с какими параметрами (окружением) он также работает (т.е. просмотреть стек и регистры).
  5. Отключаем точку останова во вкладке “Breakpoints”, секция “Hardware” – выключаем нажатием пробела (если адрес нам “подходит”), либо удаляем нажатием Del (если адрес нам “не подходит”).

Note: слово “нравится”: по сути я подменил “подходит” (результат) на “нравится” (причину). При этом саму причину я детально не раскрыл. В данном случае “подходит” – это положительный результат оценки (тестирования) фрагмента кода на присутствие нескольких признаков. Также как и искусственная нейросеть сама не может дать ответ на вопрос “Почему?” (“Почему ты выбрала именно этот ответ?”), так и опытный человек (неосознанная компетентность – 2 ссылки) часто не сможет сразу дать ответ на этот вопрос.

К первому адресу (0x0497801C [0]) было обращение только при “Re‑layout”:

cpu – hardware breakpoint 0x0497801C

В этом фрагменте привлекают внимание вызовы “tk86.XSetWindowBackground” и “tk86.XChangeWindowAttributes”. Однако, при попытке изменить “A0 E0 A0” на “A0 00 A0” ничего не происходит. Более того, при повторном “Re‑layout” (либо при изменении значения в процессе “Re‑layout”) значение возвращается назад – на “A0 E0 A0”.

Подсказка, почему это происходит (на скриншоте помечено синим):

= EСX{00 A0 E0 A0} =

EСX{00 A0 E0 A0} => ds:[eax{04977FC0}+5C = 0497801C] -> Hardware Breakpoint (write)
EСX{00 A0 E0 A0} <= ss:[esp{0028EC90}+24 = 0028ECB4] <- Stack

Note: если подзабыли назначение/наименование регистров/сегментов x86 (cs, ds, ss, …).

Следующий адрес (0x0497A6C0 [1]) был более интересным (обращение также – только при “Re‑layout”):

cpu – hardware breakpoint 0x0497A6C0

Здесь привлекает внимание переход на “tk86.Tk_SetWindowBackground”, вызов “tk86.Tk_FreeBitmap” и уже знакомый адрес стека (0x0028ECB4).

Еще одна подсказка:

= EAX{00 A0 E0 A0} =

EAX{00 A0 E0 A0} <= ds:[eax{?    ??    ?}] -> Hardware Breakpoint (read)

ss:[esp{0028ECAC}+8  = 0028ECB4]{0497DDC8} => EAX
ds:[eax{0497DDC8}+18 = 0497DDE0]{? 0497A6C0 ?} => EAX

EAX{00 A0 E0 A0} <= ds:[eax{? 0497A6C0 ?}] -> Hardware Breakpoint (read)
EAX{00 A0 E0 A0} => ss:[esp{0028ECAC}+8 = 0028ECB4] -> Stack

А теперь попробуем заменить “A0 E0 A0” на “A0 00 A0”, и пошагать немного вперед (“Step Over”, F8)… и мы пришли к уже знакомому коду (с дополнительной информацией):

cpu – step over – from 0x0497A6C0 to 0x0497801C

Будет еще несколько остановок, а в конце:

Мы уже нашли то место, где задается цвет фона, но для его изменения нужно делать “Re‑layout”. У нас осталось еще несколько адресов, и я бы хотел найти тот адрес, в котором значение цвета кешируется, и используется при перерисовке содержимого окна.

К адресам 0x0497DE60 [2] и 0x0497DED0 [3] обращений не было. Вдобавок, по этим адресам до сих пор хранится значение “A0 E0 A0”, а должно – “A0 00 A0”, т.к. цвет фона изменился.

А вот по адресам 0x0497DF60 [4] и 0x0497DFD0 [5] сейчас хранится “98 00 00” и “37 00 00” соответственно. Предположение: старый “кеш” после “Re‑layout” стирается, и создается новый “кеш” по новым адресам. Похоже, именно в этих двух адресах ранее (до “Re‑layout”) был “закеширован” цвет. Теперь они располагаются по новому адресу.

Чтобы найти новый адрес, снова запустим поиск паттерна “A0 E0 A0” в памяти:

references – pattern a0e0a0

Что‑то не то – слишком мало адресов в диапазоне (0x048A0000 + 0x00400000), к тому же здесь отсутствуют адреса 0x0497A6C0 [1] и 0x0497801C [0]!

Ах да, я забыл, что мы поменяли “A0 E0 A0” на “A0 00 A0”, придется вернуть обратно (или искать “A0 00 A0”). Однако, теперь мы будем уверены, что найденные сейчас адреса не имеют отношения к цвету фона.

Вернем старое значение по адресу 0x0497A6C0 [1] сделаем “Re‑layout”, и повторим поиск (после поиска разморозим (Debug > Run) Tkenv):

references – pattern a0e0a0

Появились 2 новых адреса из другого диапазона памяти. Найдем его в “Memory Map”:

Address  | Size     | Page Info                 | Alloc Type | Current Prot | Alloc Prot
========================================================================================
06460000 | 0026E000 |                           | PRV        | -RW--        |-RW--

Похоже все нормально. Проверим новые адреса.

К адресу 0x065B5AC8 [4′] обращаются при:

cpu – hardware breakpoint 0x065B5AC8

Но я пока не буду менять значение, и переду к следующему адресу 0x065E4E18 [5′]:

cpu – hardware breakpoint 0x065E4E18

Какой интересный фрагмент: ESI:“gdi32.CreateSolidBrush”; вызов “tk86.TkWinGetDrawableDC”, EAX′:user32.FillRect”, и в стеке “tk86.XFillRectangles”. Причем, если изменить “A0 E0 A0” на “A0 00 A0”, и свернуть/развернуть окно, то цвет фона изменится! Однако, по адресу 0x065B5AC8 [4′] осталось прежнее значение, и даже если вызвать код, использующий его [4′], то в Tkenv цвет фона останется “A0 00 A0”, а в 0x065B5AC8 [4′] останется “A0 E0 A0”.

Note: Если вы не знакомы с механизмом работы Software Breakpoints, то попробуйте прямо сейчас поставить Breakpoint (это можно сделать сразу во вкладке с результатами поиска – “References” → контекстное меню → Toggle Breakpoint; клавиша F2) на адрес, хранящий цвет фона для перерисовки окна (у меня, в данный момент, это адрес 0x065E4E18 [5′], а у вас будет другой адрес). Теперь сверните/разверните окно Tkenv… как вам взгляд на сеть через “розовые очки” :)? А теперь попробуйте заменить “A0 00 A0” на “00 00 00”, и посмотрите на итоговый цвет (должен быть черным, но…). Теперь вы поняли, почему в IDE метка Breakpoint имеет именно этот цвет ;)(возможно, эта фраза была шуткой, а возможно и нет).

Я остановился на цвете “E6E6E6”.

Теперь можно зафиксировать изменения, и, чтобы изменения сохранялись при перекомпиляции Tkenv, цвет придется заменять в исходниках. В исходниках цвет может быть задан множеством способов (3 одно‑байтных числа, одно 4‑х байтное число, строка с hex значением или названием цвета, …), я начал поиск со строки “a0e0a0” (без учета регистра) в директории с исходниками Tkenv (“omnetpp-5.0/src/tkenv/”). И мне повезло:

Так вот он какой Tcl/Tk… Тот факт, что в названиях всех файлов присутствует “inspector” – обнадеживает, поэтому просто поменяем цвет, и пересоберем OMNeT++:

make MODE=release –j17

Note: вспомните про “-j” и “17.

Note: пересобирать проект “LLTR” (через Eclipse) нет необходимости, т.к. сам Tkenv (и внесенные нами изменения) находится в библиотеке “omnetpp-5.0/bin/libopptkenv.dll”.

Цвет должен был измениться:

tkenv: bg color e6e6e6 – bg images (un[7z]me)

Note: Подложку (с тенью) для фона сети, используемую в этой статье (как на скриншоте выше), можно достать разархивировав (7zip) эту картинку. В архиве 3 фона под несколько используемых размеров холста, и файл с готовыми display string для “.ned” файлов сетей. Фоны можно распаковать прямо в “omnetpp-5.0/images/background/”, либо расположить их в любом другом удобном месте.

# Замена фона (зеленого) на картинку (узор) в Qtenv

Поступаем точно также как и с Tkenv, с той лишь разницей, что теперь цвет будем менять на изображение, и не будем использовать отладчик для интерактивного просмотра результата замены.

“На глаз” зеленые цвета фона в Tkenv и Qtenv совпадают, но совпадают ли их числовые значения (RGB)? Тест “пипеткой” показал, что совпадают: A0E0A0 (hex); R:160, G:224, B:160.

Поиск “оптимального узора для фона” отложим на потом, и сразу перейдем к поиску цвета в исходниках (директория “omnetpp-5.0/src/qtenv/”):

Здесь можно сразу же заменить зеленый цвет на другой цвет, но нам нужно поменять цвет на картинку. В дополнение к этому представим, что мы видим Qt впервые в жизни, и не знаем, как он устроен. Что же делать?

В Qtenv уже используются иконки (изображения) для кнопок, а в директории с исходниками есть поддиректория “icons/” где и располагаются (в своих поддиректориях) иконки. Теперь можно поискать места/варианты использования в коде нескольких из этих иконок по их имени, либо можно осмотреть файлы в директории с исходниками и заметить:

Не буду долго тянуть – для продолжения нужно познакомится с Qt чуть поближе:

  1. описание “setBackgroundBrush()”, и пример использование картинки с заданием режима кеширования чуть ниже;
  2. что лучше: переопределить “drawBackground” или использовать “setBackgroundBrush();
  3. попытка перевода Qt v4.4.3 документации: описание класса QGraphicsView;
  4. подробнее про “icons.qrc, и про использование изображений (ресурсов) в приложении;
  5. QBrush;
  6. QImage и QPixmap;
  7. производительность: QPixmap vs. QImage;
  8. привыкаем писать без “new”: “setBackgroundBrush(new ...);
  9. привыкаем писать без “new”: “setBackgroundBrush(new ...);
  10. привыкаем писать без “new”: “setBackgroundBrush(new ...);

В итоге: на фон влияет только строка 92 в “modulecanvasviewer.cc”. Строка 371 в этом же файле должна задавать фон при “Re‑layout” (если в настройках Qtenv включена анимация процесса “Re‑layout”, и есть незакрепленные модули в сети), но я не заметил эффекта от изменения цвета в этой строке (даже если ее закомментировать, то процесс “Re‑layout” выглядел, так же как и прежде). Влияние строки 46 в “canvasviewer.cc” также не удалось установить.

Патч будет выглядеть примерно так:

В конце “icons.qrc” (перед закрывающим “</RCC>”) добавить один путь до одной картинки с фоном:

    <qresource prefix="/backgrounds">
        <file alias="bg.png">icons/backgrounds/greennoise_grid.png</file>
    </qresource>

Note: несколько изображений (узоров, паттернов) для фона, я прикрепил ниже.

В “modulecanvasviewer.cc” строка 92:

   //setBackgroundBrush(QColor("#a0e0a0"));
   // https://doc.qt.io/qt-5/qgraphicsview.html#backgroundBrush-prop
   // http://forum.sources.ru/index.php?showtopic=349342#post_3063091
   // http://doc.crossplatform.ru/qt/4.4.3/qgraphicsview.html
   // icons.qrc https://doc.qt.io/qt-5/resources.html
   // https://doc.qt.io/qt-5/qbrush.html#details
   // https://doc.qt.io/qt-5/qimage.html#details
   // https://doc.qt.io/qt-5/qpixmap.html#details
   // http://www.prog.org.ru/topic_19289_0.html
   // new vs no new https://stackoverflow.com/a/17889388
   // new vs no new https://stackoverflow.com/a/8457129
   // new vs no new https://doc.qt.io/qt-5/implicit-sharing.html
   setBackgroundBrush(QPixmap(":/backgrounds/bg.png"));

В остальных местах заменять цвет изображением – бесполезно, однако можно заменить на “цвет основы узора”.

В “modulecanvasviewer.cc” строка 371 (383):

        //layoutScene->setBackgroundBrush(QColor("#a0e0a0"));
        layoutScene->setBackgroundBrush(QColor("#2db612"));

В “canvasviewer.cc” строка 46:

    //setBackgroundBrush(QColor("#a0e0a0"));
    setBackgroundBrush(QColor("#2db612"));

Осталось пересобрать OMNeT++ (так же, как и пересобрали при патчинге Tkenv), и замерить падение скорости (залить одним цветом – легче, чем замостить картинку).

# Увеличилась ли нагрузки на CPU после патча?

Я замерял “CPU time” (user + sys time) для процесса симулятора, привязанного к одному наиболее свободному ядру CPU. В симуляторе запускалась модель с конфигурацией “(General)” (сеть “Network” – “N2_2”) в режиме “until-run” 999 первых событий (примерно в этот момент, чуть ранее, начинается нулевая итерация). После каждого испытания симулятор перезапускался. Я хотел сравнить с оригинальным Qtenv, сравнить скорость при использовании QPixmap и QImage (как с использованием кеширования, так и без). В итоге получилось 5 конфигураций:

Также я проверял нагрузку на CPU при горизонтальной прокрутке содержимого инспектора сети.

В результатах будет указано (в секундах):

Результаты (без комментариев):

Affinity: core 1
Until event #1000

Original Qtenv:
start -  1.419
end   - 50.403
diff  - 48.984


[cache off] QPixmap:
start -  1.762
end   - 50.528
diff  - 48.766

[cache off] QImage:
start -  1.638
end   - 52.073
diff  - 50.435


[cache on] QImage:
start -  1.700
end   - 55.629
diff  - 53.929

[cache on] QPixmap:
start -  1.528
end   - 52.509
diff  - 50.981

При горизонтальной прокрутке, наибольшая нагрузка на CPU была в тесте “[cache off] QImage”, в остальных же тестах нагрузка была примерно одинаковая.

Note: рассматривайте эти тесты только как призыв к действию – ваши результаты (относительные) могут отличатся от моих.

# Набор узоров для фона

Обещанные выше фоны (узоры, паттерны):

qtenv: background patterns (un[7z]me)

Note: справа на картинке указаны числовые значения “цвета основы узора”.

После разархивирования (7zip) можно сразу же открыть “demo/page.htm” и посмотреть, как будет выглядеть конкретный узор в GUI Qtenv.

Для смены узора я просто использую “инструментарий разработчика” (Ctrl+Shift+I). Путь к нужному узору указывается в стиле (“style”; второй фон в свойстве “background”) единственного “div”. Этим же способом можно посмотреть, как будет выглядеть любая картинка в качестве фона. Например, мне понравились фракталы “Julia Pattern Map”, в частности “Cognitive Architecture (11)” и “Patchwork Design (24)” (он напоминает мне Куб1,2):

Fractal: Julia pattern map: Cognitive Architecture and Cube 2 [Hypercube]

Я брал ¼ этих изображений, и смотрел, как они будут выглядеть в GUI. Результат – слишком привлекали внимание на себя, т.е. отвлекали.

Моя субъективная оценка некоторых из узоров, включенных в архив

В итоге, я выбрал “print-26.png”.

# В следующих частях / To be continued…


# Обратная связь


Небольшой опрос. Первый вопрос поможет мне лучше определить время для публикации следующей части. Второй – улучшить статью. Остальные вопросы – чистое любопытство.

Опрос на Хабре →

Открыть комментарии →

DOI: 10.5281/zenodo.1407029