INET Framework – “библиотека” сетевых моделей для
В предыдущих частях…
0. Автоматическое определение топологии сети и неуправляемые коммутаторы. Миссия невыполнима? (+ classic Habrahabr UserCSS)
В этой части:
Note: [про используемую структуру разделов] структура разделов tutorial/how‑to обычно отличается от структуры разделов в справочнике: в справочнике – структура разделов позволяет за минимальное количество шагов дойти до искомой информации (сбалансированное дерево); в tutorial/how‑to, где разделы сильно связаны логически, а отдельный раздел, по сути, является одним из шагов в последовательности шагов, структура представляет собой иерархию закладок (якорей), которая позволяет в любом месте tutorial/how‑to напомнить (сослаться) о фрагменте описанном ранее.
Как хорошо, что в 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 много
В каждой итерации есть свой unicast src хост и unicast dst хост, поэтому следующее, что нужно включить – способ назначения для каждой итерации unicast src и dst. То есть в каждой итерации один из хостов должен “осознавать” себя unicast src хостом, цель которого посылать трафик на unicast dst хост.
И последнее. По завершению всех итераций, всю собранную статистику со всех хостов нужно отправить на один хост для обработки. Этот хост проанализирует собранную статистику, и построит топологию сети.
Также, на этом шаге, можно подумать про некоторые детали реализации (ограничения) протокола. Например, мы хотим, чтобы программа, использующая LLTR, смогла работать без root прав, и из пространства пользователя (т.е. без установки в систему специального драйвера), значит, LLTR должен работать, например, поверх TCP и UDP.
Все остальные делали реализации, определятся сами, в процессе создания модели. То есть, конечно, можно сразу же продумать все до мелочей, но при этом есть риск “скатится в локальный оптимум”, и не заметить “более лучший” вариант реализации. Хорошо, когда моделей будет несколько – если для каждого варианта реализации будет своя модель, то появится возможность комбинировать модели, и шаг за шагом приходить к лучшей реализации. Вспоминая генетический алгоритм ;). Например, в одной реализации/модели может быть централизованное управление, в другой – децентрализованное, в третей – комбинация лучших частей из предыдущих двух вариантов.
Теперь настало время определится с симулятором сети, в котором будем создавать модели и ставить эксперименты.
В основном, от симулятора сети нам нужна возможность реализации “своего” протокола. Не все симуляторы позволяют легко это сделать.
А вот присутствие эмуляторов ОС реального сетевого оборудования “мировых брендов”, наоборот – не нужно. Скорее всего, эмуляторы создадут множество ограничений, которые будут только мешать в ходе экспериментов.
С выбором симулятора мне помогла статья Evaluating Network Simulation Tools (наши требования к симулятору во многом совпадали) и
И так как
Установка в *nix и в Windows мало чем отличается. Продолжу на примере Windows.
Распаковываем
$ . setenv
$ ./configure
$ make
Но не спешите это делать!
Во‑первых, обратите внимание на первую команду “. setenv
”. В директории “omnetpp-5.0” нет файла “setenv
” (в версии 5.0b1 он был). Он и не нужен (для Windows), поэтому просто запускаем “mingwenv.bat” (советую перед запуском посмотреть, что он делает… во избежание внезапного rm
). По окончании отколется терминал (mintty).
Во‑вторых, советую немного подправить файл “configure.user” (если упомянутый параметр закомментирован в файле, то его нужно раскомментировать):
PREFER_CLANG=yes
CFLAGS_RELEASE='-O2 -march=native -DNDEBUG=1'
PREFER_CLANG=no
CFLAGS_RELEASE='-O2 -mfpmath=sse,387 -ffast-math -fpredictive-commoning -ftree-vectorize -march=native -freorder-blocks-and-partition -pipe -DNDEBUG=1'
CFLAGS_RELEASE='-O2 -fpredictive-commoning -march=native -freorder-blocks-and-partition -pipe -DNDEBUG=1'
CFLAGS_RELEASE='-O2 -march=native -freorder-blocks-and-partition -pipe -DNDEBUG=1'
-std=c++11
'+CFLAGS_RELEASE. Например:CXXFLAGS='-std=c++11 -O2 -fpredictive-commoning -march=native -freorder-blocks-and-partition -pipe -DNDEBUG=1'
JAVA_CFLAGS=-fno-strict-aliasing
PREFER_QTENV=yes
WITH_OSG=no
WITH_PARSIM=yes
Если его явно не использовать, то он не нужен (в теории). Подробнее в разделе 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
” следует заменить на количество ядер
В директории “tools/win32” находится MSYS2 его пакеты компиляторов можно обновлять:
А
Но index
либо можно оставить 32bit (заменить на int32_t), либо сделать 64bit и модифицировать все битовые_маски+описания+(возможно)немного_логики. Поэтому часть long переменных нужно будет оставить 32bit, а другую часть сделать 64bit. В общем, для корректной работы, нужно проделать все пункты из:
Причем то же самое надо проделать и с многочисленными библиотеками для
В общем, предостерегаю от попыток сделать 64bit сборку
Под *nix я также рекомендую использовать 32bit сборку (по крайне мере с версией 5.0 и меньше).
Возможно, когда‑нибудь @Andrey2008 возьмется проверить код FIXME
”/“Fix
” в коде ;).
P.S. упоминания о том, что код
При первом запуске Eclipse предлагает поместить workspace в директорию “samples”, однако лучше расположить ее в любой другой удобной вам директории вне “%ProgramData%”. Главное, чтобы в пути к новой директории использовались только латинские буквы (+ символы), и не было пробелов.
После закрытия Welcome, IDE предложит установить INET (как было написано выше), и импортировать примеры – откажитесь от обоих пунктов.
Опции JVM. Добавить в файл “ide/omnetpp.ini” (для правки подойдет любой редактор, понимающий LF перевод строки; notepad не подойдет), сохранив пустую последнюю строку:
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+AggressiveOpts
-XX:+TieredCompilation
-XX:CompileThreshold=100
Чтобы сделать Eclipse, таким как на картинке – загляни внутрь картинки.
Настало время установить INET. Директорию “inet” из скаченного ранее архива (inet-3.4.0-src.tgz) нужно перенести в workspace. В директории есть файл “INSTALL” с пошаговым описанием установки. Можно воспользоваться им (раздел “If you are using the IDE”), но только не собирайте (Build) проект!
Импортируем INET:
Настроим проект:
#include
” (случается, если несколько раз менять “Current builder”; может случиться и в других случаях).
Перед настройкой {A} надо подправить один из файлов проекта. В файле “inet/.oppfeatures” есть строка “inet.examples.visualization
” нужно добавить после нее пустую строку, в которой написать “inet.tutorials.visualization
”, желательно сохранив отступ слева (по аналогии с другими параметрами “nedPackages
” в файле). Если это не сделать, то ничего страшного не случится, просто после настройки в “Problems” (Alt+Shift+Q,X) будут всегда висеть ошибки, связанные с “inet.tutorials.visualization
”. Можно вначале сделать {A}, и посмотреть на ошибки, а затем подправить файл “inet/.oppfeatures” – при этом Eclipse предупредит о нарушении целостности в настройках, и предложит профиксить их (соглашаемся на это).
Приступим (панель
make MODE=release CONFIGNAME=${ConfigName} -j17
” (“17
” заменить на предыдущее значение в строке, т.е. на выбранный N) {E}, то же самое можно сделать и для конфигурации “gcc-debug”, заменив в строке “MODE=release
” на “MODE=debug
”, после этого не забудь переключиться обратно на “gcc-release [ Active ]”.../src
” с выбранными “Add to all configurations” и “Add to all languages” {G} – изначально “../src
” есть в языке “GNU ../src
” появилось во всех языках и конфигурациях.__cplusplus
” со значением “201103L
” и выбранными “Add to all configurations” и “Add to all languages” – {F} подробнее;__cplusplus
” значение “201103L
”./inet/src
” {G}, если там что‑то другое (например, просто “/inet
”), то удаляй то, что есть и добавь (“Add Folder…”) “/inet/src
”. Затем кнопка “Apply”, и возвращение к {A}, т.к. все фильтры при удалении были стерты. Кстати, “/inet
” на самом деле можно оставить – с ним тоже все нормально собирается, но лучше сузить до оригинального “/inet/src
”.-std=c++11
” перед “${FLAGS}
” в “Command to get compiler specs”, должно получится примерно так `${COMMAND} -std=c++11 ${FLAGS} -E -P -v -dD "${INPUTS}"
` {F}, подробнее здесь и здесь;__cplusplus
”; не меняем порядок, удаляем все упоминания “__cplusplus
” из “CDT Managed Build System Entries”, и следим, чтобы он там не появлялся в будущем);__cplusplus=201103L
” (она будет ближе к концу).
Некоторые проблемы могут возникнуть с {E}. Поясню. Если все нормально, то Eclipse должен подхватить те настройки, которые были заданы в “configure.user” перед конфигурированием --just-print
” или “--trace
”, и, запустив сборку (панель g++ -c -std=c++11 -O2 -fpredictive-commoning -march=native -freorder-blocks-and-partition -pipe -DNDEBUG=1 …
”. Если этого нет, то можно последовать совету из уже упомянутой статьи.
Опять открываем настройки проекта (панель
CFLAGS
”, тип “String”, значение “-O2 -fpredictive-commoning -march=native -freorder-blocks-and-partition -pipe
”;CXXFLAGS
”, тип “String”, значение “-std=c++11 -O2 -fpredictive-commoning -march=native -freorder-blocks-and-partition -pipe
”;CFLAGS
”, значение “${CFLAGS}
”;CXXFLAGS
”, значение “${CXXFLAGS}
”;
Кстати, при некоторой сноровке, параметры запуска g++ можно было посмотреть, не используя флаги “--just-print
” и “--trace
”, а используя Process Explorer. В Process Explorer также можно посмотреть, во что раскрывается “-march=native
” при передаче в “cc1plus.exe”.
Теперь, наконец, можно собрать INET! Проверьте, что сейчас активна конфигурация “gcc-release” {B}, и если добавляли ранее флаги “--just-print
” или “--trace
” для проверки {E}, то их нужно убрать. Собираем (панель
Если все прошло хорошо, то рекомендую закрыть Eclipse, и сделать бекап файла “.cproject” и директории “.settings” с настройками проекта {B-G}, а также файлов: “.oppfeatures”, “.oppfeaturestate”, “.nedexclusions” – {A}.
Наконец, настройка завершена, и можно перейти к самому интересному.
Note: Первое, что я сделал после настройки окружения – стал изучать содержимое директории “doc” у
Note: Для тех, кто еще не успел установить себе
Перед созданием своего проекта хорошо бы посмотреть на уже готовые модели в 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 →
Осталось настроить проект также как и “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”, и нажать (
При этом 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 – имя сети) при запуске.
Теперь можно попробовать опять запустить симулятор (
Note: Есть различие между запуском через (
Note: (или можно форкнуть – тег a1_v0.1.0 (“a” – article) “git checkout -b ‹my_branch› tags/a1_v0.1.0
”)
Репозиторий я создавал таким образом, чтобы:
Note: без веток “article_#” можно было бы обойтись, и указывать, при клонировании, название последнего тега части (которое еще надо найти), но с веткой проще/быстрее.
Как забрать репозиторий “к себе”? Лучше всего, вначале его форкнуть на GitHub, а затем свой форк:
git clone
”;git clone --branch ‹article_#› --single-branch
” (без использования “--depth
”), а для получения коммитов следующей части использовать “git remote set-branches –add
” (и если что‑то пойдет не так…)
Далее, для создания личной ветки на основе конкретного тега, можно использовать “git checkout -b ‹my_branch› tags/‹tag_name›
”.
Как создавать свою версию кода, т.е. изменять код? Если в будущем не возникнет желания сделать Pull Request, то ничего вам не мешает делать с форком что хотите >:-) , однако я советую, при появлении изменений, которые хочется сохранить, делать так):
Одинаковая схема наименования тегов поможет в будущем избежать коллизий, даже не смотря на то, что теги при 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.
Note: git diff использовался стандартный, патчи генерировались автоматически, и они редко будут показывать логической связи в произошедших изменениях (в частности, при добавлении нового кода и изменении уровня вложенности / форматирования существующего кода) (здесь бы пригодилось отслеживание изменений на уровне AST), похожее на этот проект для Java.
Откроем “package.ned” в режиме графического редактирования схемы (вкладка “Design” снизу), и попробуем набросать сеть из КДПВ:
Сеть построена из тех же модулей, которые были использованы в примере broadcast:
А вот в качестве “провода” (канала связи) выбран Eth100M (скорость: 100 Mbps; длина: 10 метров). Кстати, почему именно 10 метров, где они задаются, и можно ли поменять это значение? (ответ чуть ниже)
Если переключится в режим редактирования кода (вкладка “Source” снизу), то вы должны увидеть примерно это (git tag a1_v0.2.0) . Пояснение структуры:
package ‹имя пакета›; //особенности наименования
import ‹имя подключаемого пакета›;
network ‹название описываемой сети›
{
@display(‹визуальные параметры сети, например, размер области›);
submodules:
‹название узла›: ‹тип узла› { @display(‹визуальные параметры узла, например, местоположение›); }
connections:
‹название узла›.‹точка соединения› <--> ‹тип канала связи› <--> ‹название узла›.‹точка соединения›;
}
Отдельно стоит сказать про “точки соединения” (Gates) и каналы связи:
‹название узла›.‹gate›[‹номер›]
”, либо автоматически – инкрементально “‹название узла›.‹gate›++
”.… <--> { delay = 100ms; } <--> …
”), либо могут иметь имя/тип, на которое можно ссылаться (как в примере broadcast: “… <--> C <--> …
”), либо могут иметь тип и быть переопределены на месте (например: “… <--> FastEthernet {per = 1e-6;} <--> …
”), либо…output
/ input
; соединители при подключении: -->
/ <--
), и двунаправленными (тип при объявлении: inout
; соединитель при подключении: <-->
). Двунаправленные состоят из двух однонаправленных, к которым можно обратиться напрямую, дописав суффикс “$i
” либо “$o
”.Так почему же у Eth100M длина 10 метров? Для ответа на этот вопрос, переключимся назад в редактор схемы (“Design”), выберем любое соединение, откроем его контекстное меню, и откроем “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” и:
… так, а где здесь “duplexChannel
”? Параметра “eth[*].typename
” из подсказки хоста тоже не видно. Однако есть другие параметры с говорящим названием. Пока их менять не будем, а посмотрим на “Parameters…” у “host0”, и:
… и здесь “eth[*].typename
” нет.
На самом деле отсутствие “eth[*].typename
” вполне объяснимо – “Parameters…” отображает только параметры текущего модуля, а из параметров вложенных модулей он отображает только те, которые были переопределены в текущем модуле. У переопределенных значений вложенных модулей столбец “Type” пуст.
В
И опять же здесь нет “eth[*].typename
”.
Вряд ли он будет в свитче, но все же посмотрим:
Как и ожидалось, “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) .
Все равно то, что сделали выше, не очень хорошо выглядит – пришлось для каждого узла применять практически одинаковые параметры, и если в будущем захотим что‑то изменить, то придется вносить изменения для каждого узла. Благо, что можно вынести эти записи в “parameters:
” к “Network”:
parameters:
@display("bgb=693,416,grey99");
**.csmacdSupport = false;
**.eth[*].mac.duplexMode = true;
Должно получиться примерно так (git tag a1_v0.4.0) . Здесь нужно использовать именно “**
” (аналогия с 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) .
Переключаемся на “omnetpp.ini”, запускаем (
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 (адреса, маршруты, …) в сети, используется связка из глобального конфигуратора (*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”:
Картинку нужно сохранить под именем “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”:
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) .
Запускаем симулятор, и получаем сообщение:
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 Группе
Вообще‑то проблема не в “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) .
… почему маска “/32”? Даже если изменить параметр “netmask
” на любое другое (кроме “"0.0.0.0"
” и “"255.255.255.255"
”) значение, то маска останется та же – “/32”. Проверено.
Похоже, пора последовать совету с форума и забыть про “FlatNetworkConfigurator”, но т.к. мы уже запустили симулятор, посмотрим какие настройки внес этот конфигуратор в узлы сети.
В симуляторе есть очень удобный инструмент для отображения текущего состояния модулей сети. По сути, в данном случае, симулятор – это отладчик сети, наподобие отладчика программы, в котором выполнение программы можно приостановить, и посмотреть текущие значения переменных (на изображении сети выбираем “host0”, и переключаем режим отображения левой панели на “children mode”):
Можно просматривать не только текущие значения параметров, которые отображались в “NED Parameters”, но и некоторую дополнительную информацию. Значения можно менять (я двойным кликом открыл параметр “forwarding
”, переключился в “grouped” режим, и завершающий двойной клик открыл поле для редактирования “value
”):
Кстати, это скриншоты из GUI симулятора, который появился в
Note: Переключится на Tkenv можно в
Мы немного отвлеклись на 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”:
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 оно работает:
А теперь посмотрим на “routingTable
”:
dest:* gw:* mask:* metric:0 if:eth0(10.0.1.1) DIRECT MANUAL
Если “*
” означает адрес “0.0.0.0”, то все будет работать нормально, и мы зря беспокоились насчет маски. Может он ставит маску “/32” специально? Вообще‑то так и есть.
Поищем исходники FlatNetworkConfigurator:
В “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 адреса???
К счастью IPv4Address::ALLONES_ADDRESS
” на “netmask
”, можно просмотреть всю логику работы, можно … Предлагаю в будущем с этим поэкспериментировать, а мы уже переходим к “IPv4NetworkConfigurator”.
Чтобы перейти с “FlatNetworkConfigurator” на “IPv4NetworkConfigurator” нужно:
**.networkLayer.configurator.networkConfiguratorModule = ""
', чтобы включить “IPv4NodeConfigurator”;FlatNetworkConfigurator
” на “IPv4NetworkConfigurator
” (заменяем в двух местах: в импорте, и в месте использования);networkAddress
” и “netmask
”, т.к. “IPv4NetworkConfigurator” не использует эти параметры.Должно получиться примерно так (git tag a1_v0.8.0) .
Все хорошо, маска как раз такая, чтобы вместить 4+2 адреса (2: 10.0.0.0 – адрес подсети; 10.0.0.7 – broadcast адрес).
А как изменились маршруты на хостах? Посмотрим на “host0.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 записи. Почему так произошло?
Намек на параметр “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) . Однако, все, что в нем есть мы уже и так знали.
Обратите внимание на запись:
<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"/>
Говорит нам об оптимизации:
Более интересен конфиг, из которого получился этот дамп. Этот конфиг прописан прямо внутри “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) .
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) .
Да будет чистота! А IP определим по номеру хоста+1.
Возможно, это делает “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…”:
А по структуре (иерархии) модуля можно быстро пройтись в панели “Module Hierarchy”:
Наконец‑то, GUI с конфигами позади, и можно уже что‑нибудь накодить. Для того чтобы описать свою логику отправки и принятия UDP пакетов, нужно внутри INET написать два мини UDP‑приложения: первое будет отправлять пакеты, а второе – принимать их. Эти приложения будем запускать на разных хостах, например, на “host0” запустим приложение, отправляющее пакеты, а на всех остальных хостах – приложение, принимающее пакеты.
Note: внутренние элементы навигации в этом подразделе отключены, т.е. он линейный (последовательный), и не содержит в себе подразделов.
Как было сказано выше в
… И все же придется вернуться в GUI, чтобы посмотреть место, в которое будет подключаться модуль UDP‑приложения. Раскроем “host0” (двойной клик):
Вот он этот модуль – “udpApp” (точнее вектор модулей “udpApp[numUdpApps]
”), на месте которого и будет подключено наше UDP‑приложение.
В том же разделе “Создание первого проекта”, мы смотрели код готовых приложений (UDP Application) и примеров (broadcast), а также решили расположить код приложения LLTR рядом с кодом других приложений в INET (раздел “Структура проекта”). В свете этого, создадим директорию “inet/src/inet/applications/lltrapp”.
Создадим приложение, отправляющее пакеты (LLTRSuperApp):
LLTRSuperApp.ned
”Попробуем сравнить то, что сгенерировал 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) .
Вкратце: она регистрирует класс в качестве модуля, чтобы при использовании модуля “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()
”.
В
Module1[gate$o]-->--[channel]-->--[gate$i]Module2
используется функция “send(‹отправляемое сообщение›, ‹“gate”, через который отправить сообщение›)
” (примеры использования из “Simulation Manual”). После отправки сообщения, его получит модуль на другом конце “channel”. А так как INET построен поверх send()
” и отправить сообщение на определенный unicast IP и порт.
“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, как это сделать? Как‑то так:
cMessage
”, и указать в качестве “Message kind” значение “UDP_C_SETOPTION
” (“UDPCommandCode”);UDPSetTimeToLiveCommand
”) с указанием желаемого значения TTL, и некоторой другой информации;send()
”.И как же понять, что нужно делать именно так? Это “очень просто”:
handleMessage()
”;processCommandFromApp()
” → “case UDP_C_SETOPTION
” –[доходим до ужаса, состоящего из “else-if-dynamic_cast”]→ “((UDPSetTimeToLiveCommand *)ctrl)->getTtl()
” → “setTimeToLive()
”.
Ах, да, чуть не забыл про иерархию наследования (советую обратить внимание на комментарии к коду): “UDPSetTimeToLiveCommand
” ← “UDPSetOptionCommand
” ← “UDPControlInfo
”. Вот только код совсем не похож на
Note: Файлы “.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: почему я везде пишу “экземпляр класса *”, а не “объект класса *” – чтобы ни у кого не возникало путаницы.
Что сделаем далее:
В initialize()
” –
Зачем вызывать “initialize()
” несколько раз? Почему одного раза недостаточно для инициализации модуля? Между модулями могут быть циклические зависимости (например, первому модулю, для инициализации, нужны данные из второго модуля, а второй модуль, в свою очередь, ждет пока первый модуль инициализируется, чтобы получить из него нужные данные для своей инициализации – так появляются циклические зависимости), чтобы их обойти, 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”.
Как выбрать нужную стадию для инициализации каждого из компонентов модуля? Правила простые:
Так, например, для приложений, лучше всего использовать стадии:
scheduleAt()
”) первых пакетов в сеть.
Наша функция “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()
”, и тем более в деструкторе, – бесполезно, т.к. “по документации” цикл обработки событий уже будет разрушен.
И так, мы уже:
Похоже, с “LLTRSuperApp” мы уже закончили, и пора переходить к “LLTRApp”.
Что будем делать в “LLTRApp”:
Но перед этим, также как и в “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);
}
С настройкой сокета закончили. Теперь надо ожидать прибытия пакета, и, при его получении, вывести в лог сообщение с его именем.
Выше я несколько раз упоминал про функцию‑обработчик входящих сообщений, пора познакомится с ней поближе. В 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()
”?:
handleMessage()
” он обрабатывается;
Если нужно сохранить результаты обработки, либо сохранить определенное “состояние”, то надо создавать дополнительные поля в классе для хранения всего этого. С течением времени полей становится все больше и больше, и что самое неприятное – часть из этих полей используется только на небольшом промежутке работы “программы” (для передачи состояния от одного вызова “handleMessage()
” к другому). Это усугубляется тем, что все, по сути разные, сообщения приходят через одно место – “handleMessage()
”. В реальной программе я бы мог, для каждого типа сообщения, зарегистрировать свой обработчик, но не здесь… В этот момент появляются мысли про создание новых классов, либо про использование этого, либо этого. И код попеременно превращается то в спагетти, то в лазанью, то в равиоли, и становится более запутанным чем сам INET. И, наконец, происходит откат к более простому первоначальному виду.
Ладно, я забежал слишком далеко вперед, а пока наденем розовые очки белую маску и примем Joy pill.
Однажды нелегкая завела меня в “подвал” файла “omnetpp-5.0/include/omnetpp/csimplemodule.h” (doxygen убирает комментарии из кода), а затем в “omnetpp-5.0/include/omnetpp/ccoroutine.h” (комментарии), и стало ясно, что “activity()
” основаны на fibers/сопрограммах.
Как происходит обработка событий при использовании “activity()
”?:
activity()
” “размораживается”;Все результаты обработки, и “состояния” можно хранить в локальных переменных функции (ровно также, как и в языках, поддерживающих “синхронно‑асинхронное” программирование “из коробки”, например 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
Если не контролировать размер стека, то симуляция “съест” много памяти. Есть несколько практик позволяющих уменьшить используемое место на стеке (все они связаны с использованием области видимости “{}
”):
{}
”;
В “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 значения:
processICMPError()
” и “sendUpErrorIndication()
”.
Note: “kind” – это всего лишь некоторое целое число, которое можно присвоить сообщению. Отрицательные значения зарезервированы под внутренние нужды
Для вывода сообщения в лог 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()
”. Это может случиться если:
handleMessage()
” про них не знает;
Обратите внимание, что в каждом “case
” создается своя область видимости (“{}
”) – это нам пригодится в будущем.
С выводом в лог сообщения мы закончили, осталось закрыть сокет. Сделаем это так же, как и в “LLTRSuperApp”:
void finish()
{
socket.close();
cSimpleModule::finish();
}
Посмотрим, что получилось (git tag a1_v0.13.0) . Собираем INET, запускаем.
Note: если открыт один из файлов INET (например, “LLTRSuperApp.*” или “LLTRApp.*”), и этот файл находится в фокусе, то для сборки INET достаточно нажать “Ctrl+B” (предварительно воспользовавшись этим советом).
И жмем на “Run”:
Хммм… Ничего не произошло…
Так, а как симулятор поймет, что на хостах надо запускать “LLTRSuperApp” и “LLTRApp”, а не, например, “UDPEchoApp”? Мы же нигде не указали, какой конкретный “udpApp” хотим запустить на хостах…
Посмотрим, из чего сейчас состоит “host0”, и есть ли в нем хоть один “udpApp”:
Как и предполагалось, “udpApp” отсутствуют, и параметр “numUdpApps
” равен 0.
А как сейчас выглядит схема “host0”? (двойной клик по “host0”):
В нем не только отсутствуют “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.
Это значит, что мы можем одновременно использовать и хосты с “подстановочными знаками”, и указывать конкретный хост, а
**.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) .
Посмотрим, что получилось. В исходном коде изменений не было, поэтому собирать INET необязательно. Просто запускаем, и…
Оказывается “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) .
Еще раз, просто запускаем…
Опять?..
Текст ошибки просит обратить внимание на “LLTRApp” и сокеты. Я ранее написал:
А так как, в “LLTRApp” мы будем только принимать пакеты (отправлять ничего не будем), то отпадает необходимость в указании выходного “gate” функцией “setOutputGate()
”.
Эта мысль и привела к ошибке. “UDPSocket
” – это всего лишь обвертка для отправки сообщений в модуль “udp”. Даже при настройке сокета она отправляет управляющие сообщения, и естественно ей надо знать через какой “gate” отправлять эти сообщения. Например, возьмем тот же “bind()
”, который использовали в “LLTRApp”, что он делает? Он вызывает “sendToUDP()
”, который использует “gateToUdp
” для управляющих команд, а эта переменная, в свою очередь, задавалась через “setOutputGate()
”. А вот и та часть кода, которая инициировала вывод сообщения про ошибку. Кстати, если попробуете найти это предупреждение в “inet-manual-draft.pdf”, то в текущей версии ничего не найдете (предупреждения нет)…
Исправим ошибки, добавив вызов “setOutputGate()
” в “LLTRApp” (git tag a1_v0.16.0) , соберем INET, и попробуем запустить…
Ура! Ошибок нет!(:если бы они были, я бы вставил скриншот:)
В прошлый “удачный” запуск (когда не было сообщений с ошибками) мы обнаружили отсутствие “udpApp” и модуля “udp”. Так обнаружатся ли отсутствующие модули сейчас, либо они будут склонны продолжать отсутствовать без обнаружения :)Проверим это:
И так, “numUdpApps
” стал равен 1 – это очень хорошо; “hasUdp
” стал равен “true
” – это еще лучше; и, наконец, появился “udpApp[0]
” с типом “LLTRSuperApp
”, и модуль “udp
” (типа “UDP
”).
А что стало со схемой “host0”? (двойной клик по “host0”):
Появился модуль “udp
” и “udpApp[0]
”.
Вернемся назад (на уровень выше):
Запустим симуляцию (run):
И…
Все работает, пакет “=Packet name=
” прибыл в пункт назначения. Это происходило в точности, как в реальной сети:
arpREQ
”) “А где же хост с IP 10.0.1.4? Какой у него MAC адрес?”;arpREPLY
”) “Это я, вот мой MAC…”;=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]
”):
Поздравляю, у нас получилось отправить и принять пакет! Однако, одна вещь на скриншоте продолжает меня смущать. Она находится в строке статуса:
Msg status: 4 scheduled / 104 existing / 173 created
Симуляция завершена, но у нас еще 4 запланированных сообщения. Посмотрим на эти сообщения поближе:
Здесь мы видим 4 “CLOSE” (“UDPCloseCommand
”) сообщения (по одному на каждый хост). Помните, ранее я упоминал, что закрывать сокет в “finish()
”, и тем более в деструкторе – бесполезно, т.к.:
“по документации” цикл обработки событий уже будет разрушен.
А то, что произойдет, если все‑таки отправить сообщение после “разрушения” цикла обработки событий, мы увидели – “висящие” сообщения. Они никогда не достигнут пункта назначения, т.к. симуляция уже завершена.
Подправим это. Закомментируем “socket.close();
” в “LLTRSuperApp” и “LLTRApp” (git tag a1_v0.17.0) .
Посмотрим, исчезли ли “CLOSE” сообщения (соберем INET, запустим симулятор, запустим симуляцию (run)):
Msg status: 0 scheduled / 100 existing / 169 created
Все “висящие” сообщения исчезли.
Если вы ранее заглядывали в примеры сетей, и в “.ned” файлы приложений, то заметили, что часть параметров приложений можно менять прямо в “.ned” и “.ini” файлах (без пересборки INET). В
У “LLTRSuperApp” и “LLTRApp” есть один общий параметр – номер порта – отличный кандидат для задания “извне”. Так, что нужно сделать, чтобы “пользователь” мог задавать номер порта? Сделаем по аналогии с “UDPSink.ned” и “UDPSink.cc”:
int port;
”;int port = -1;
” к классу;initialize()
”:
case INITSTAGE_LOCAL:
port = par("port");
break;
1100
” на переменную “port
”.Если ответ в стиле: “потому что в UDPSink так написано”, не устроит, то добро пожаловать в исходники!
Причем поведение будет отличаться в зависимости от приложения.
Для “LLTRApp” (приложение, которое слушает порт): посмотрим цепочку вызова функции “bind()
”, т.к. именно в нее передается порт:
bind()
” “-1: ephemeral port”;bind()
” “-1: ephemeral port”;bind()
” “localPort != -1
”;bind()
” “createSocket()
” → “localPort == -1 ? getEphemeralPort() : localPort
” → “getEphemeralPort()
”.
Здесь “-1
” означает “используй временный порт” (“ephemeral port” – любой свободный порт).
Для “LLTRSuperApp” (приложение, которое отправляет пакеты) все иначе:
connect()
” “invalid remote port number”;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) .
Сборка INET, запуск симулятора, и запуск симуляции (run), должны пройти успешно.
Судя по примеру “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)
”.
setBroadcast()
” в нем используется “UDPSetBroadcastCommand
”.processCommandFromApp()
” → “setBroadcast()
”-“sd->isBroadcast = broadcast
”.isBroadcast
” в коде (в Eclipse: 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) .
Посмотрим на то, как выглядит broadcast в симуляторе (собираем INET, запускаем симулятор, запускаем симуляцию (run)):
Вот он – 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)):
Теперь вообще пакетов нет… Остался “255.255.255.255”, и, как помнится, ранее в коде мы находили константу с этим адресом – “IPv4Address::ALLONES_ADDRESS
”. В общем, перейдем на limited broadcast:
socket.sendTo(new cPacket("=Broadcast Packet="), IPv4Address::ALLONES_ADDRESS, port);
Собираем INET, запускаем симулятор, запускаем симуляцию (run)):
Лог в точности совпал с предыдущим логом – пакет не выходит за приделы “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” по всему документу)…
Далее, в 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”, и воспользуемся режимом пошаговой симуляции:
Лог:
По сути, этот лог ничего нового не сказал – “=Broadcast Packet=
” отправляется в “networkLayer” (событие #20) и в нем исчезает. Точнее, последнее событие (#21) связано с модулем “networkLayer.ip
”. Надо спуститься еще глубже:
Note: можно пересобрать сеть (Qtenv
Здесь, на схеме появилось сообщение “DROP: 1
” (рядом с “ip” модулем). Заглянем в параметры (слева) этого модуля (“ip (IPv4)
”). Здесь привлекают внимание две записи:
numDropped
”, равный 1 – скорее всего именно “ip” дропнул наш пакет, и теперь гордо сообщает об этом;forceBroadcast
”, равный “false
” – возможно именно он препятствовал прохождению broadcast пакетов.
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”, возможно именно он хранит список интерфейсов. Посмотрим:
Осталось понять, как добраться до этого модуля, и вытащить из него информацию про интерфейсы. Я каким‑то образом нашел 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) .
Собираем и смотрим лог (собираем INET, запускаем симулятор, запускаем симуляцию (run)):
Наконец‑то! Broadcast пакеты успешно покинули хост, и были приняты в “LLTRApp”! А что в этот момент происходило внутри “host0”?:
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 уровне (это придется делать самому).
А что будет, если забыть удалить пакет на принимающей стороне? Проверим это! В “LLTRApp” закомментируем строчки “delete msg;
”, и посмотрим на лог (собираем INET, запускаем симулятор, запускаем симуляцию (run)):
Ничего… Лог в точности такой же, как и в предыдущем запуске. Но документация уверяла, что
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 (
...
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, и пересоберем сеть (меню
...
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” (
Note: после экспериментов не забудьте раскомментировать “delete msg;
”, либо откатиться на (git tag a1_v0.20.0).
Мы уже продолжительное время используем Qtenv, поэтому настало время настроить его под себя.
Note: ниже я приведу настройки, которые оказались удобными для меня, и для снятия скриншотов (для этой статьи).
Вначале, надо сказать, что все настройки Qtenv хранятся в домашней директории пользователя, в файле “.qtenvrc” (Windows):
%USERPROFILE%\.qtenvrc
А Tkenv – хранит в файле “.tkenvrc” (Windows):
%HOMEDRIVE%%HOMEPATH%\.tkenvrc
Перейдем к настройкам:
Все настройки с небольшими комментариями (а также отключение отображения таймлинии – она редко нужна, и когда не нужна – отвлекает):
Note: Если в Tkenv у объектов в canvas отсутствуют надписи, то в
Note: После применения настроек и запуска симуляции (run) в логе будут только события (синие), без детализации (черные), после перезапуска Qtenv детализация вернется.
Также, о настройках написано в “omnetpp-5.0/doc/UserGuide.pdf” (раздел 7.7. “The Preferences Dialog”).
В начале статьи я описал набросок протокола. Настало время его уточнить, чтобы двигаться дальше.
Начнем с синхронизации:
Основа LLTR – это итерации сбора статистики на множестве хостов во время сканирования сети. Итераций в LLTR много( >1) , поэтому первое, что нужно включить в протокол – управление запуском и остановкой каждой итерации. Если учесть, что хостов тоже много( >1) , то управление будет заключаться в том, чтобы определенным способом сообщать всем хостам время начала итерации и время окончания итерации. То есть синхронизировать все хосты.
Первое, что приходит на ум – перед первой (нулевой) итерацией, используя протоколы синхронизации времени – синхронизировать время на всех хостах. Добавим к этому единую точку отсчета (на синхронизированных часах) – время начала первой итерации, которая вкупе с длительностью итерации, позволит каждому из хостов определять точный (+/-) момент начала и окончания каждой итерации. Похоже, это будет работать, если сделать все правильно…, а если сделать неправильно, то получится отличная сцена для комедии:
Собираемые данные (для определения снижения скорости / потери пакетов):
И последнее. По завершению всех итераций, всю собранную статистику со всех хостов нужно отправить на один хост для обработки. Этот хост проанализирует собранную статистику, и построит топологию сети.
Опять же, первое, что приходит на ум – на хостах, записываем время прихода каждого пакета. По этим данным можно построить гистограмму, можно анализировать задержку между получением пакетов, …
Либо, broadcast src хост последовательно нумерует все пакеты, а на остальных хостах записываются номера пришедших пакетов.
Но это все слишком усложняет реализацию. Для синхронизации хостов нужно будет полностью реализовать протокол для синхронизации времени. К тому же – это дополнительный шаг – мы не сможем начать сканирование сети пока не синхронизируем время на всех хостах. А если во время этого процесса пропадут пакеты… таймауты, ретрансмиты…
Тоже самое и с хранением времени всех пришедших пакетов с последующей отправкой этих данных на один хост для обработки. Слишком много данных придется хранить и передавать по сети, а затем, все эти данные должен обработать один хост…
Есть ли более простой, надежный и элегантный способ реализовать задуманное?
С отправкой данных все достаточно просто. Каждый из хостов мог бы независимо произвести предварительную обработку своих данных (свернуть их; local-reduce; map) перед отправкой их на центральный хост (reduce) – это уменьшит количество передаваемых данных и ускорит расчет.
А вот с синхронизацией все интереснее.
Если посмотреть внимательней, то у нас уже есть то, что позволит всем хостам работать синхронно.
Что нам нужно? Нужно определенным способом сообщать всем хостам момент начала итерации и момент окончания итерации.
А кто заполняет своими пакетами всю сеть (вещает свои пакеты на всю сеть) на каждой итерации? Это 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‑адреса других хостов. То же самое касается и свитчей. А теперь представьте, что во время 3‑го шага, вместо отправки наших пакетов, отправляется ARP‑запрос… Через какое время он достигнет unicast dst хоста? И достигнет ли его вообще?
Реализация протокола broadcast src хоста (LLTRSuperApp) должна:
Количество пакетов (m) выбирается исходя из:
Про количество итераций уже было сказано в предыдущей статье.
Для передачи статистики, в модели, мы могли бы использовать UDP вместо TCP, и это даже работало бы, но в реальном приложении я бы предпочел использовать протокол с гарантией (проверкой) доставки данных, поэтому и в модели лучше использовать соответствующий протокол.
Здесь есть еще несколько нюансов, мы с ними столкнемся, когда будем создавать модель, и смотреть на результаты симуляции ;)В общем, скучать не придется.
Мы уже посмотрели, как в INET нужно работать с UDP, но для полноценной модели нужен еще и TCP. Посмотрим на примеры работы с TCP, и попробуем добавить в “LLTRSuperApp” и “LLTRApp” передачу данных по TCP.
Примеры:
TCPSocket
” в “inet-manual-draft.pdf” (глава 14.5 “TCP socket”; страница 121).Как видно из примеров, в “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
”, чтобы, если все‑таки что‑то придет,
В “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” должны будут сделать:
=TCP Packet=
”;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) .
Соберем, запустим, и… (как уже поняли, я бы не стал описывать это, если бы здесь не было интересного момента с ошибкой ;)
Заглянем внутрь “host0”, и посмотрим, почему “udpOut
” не подключен:
Сюрприз, появилось два “LLTRSuperApp”: один подключен к “udp” модулю, а другой к “tcp”, именно поэтому появилась ошибка – у “tcp” попросту нет “udpOut
”. Но нам важно не это, а то, что теперь в каждом хосте стало по два экземпляра приложения, а нам нужен только один экземпляр в каждом хосте.
Собственно это уже было ясно при редактировании “omnetpp.ini” – мы указали “inet.applications.lltrapp.LLTR*App
” в нескольких точках подключения, вот он и создал их несколько…
Похоже, используя “StandardHost” не удастся объединить в одном приложении работу с UDP, и с TCP. Нам ничего не остается, как сделать свой “StandardHost”, и, заодно, выкинуть все лишние части.
Создадим “SimpleUdpTcpHost” на основе “StandardHost”, и “ITcpUdpApp” объединяющий “IUDPApp” с “ITCPApp”. Получилось так (git tag a1_v0.22.0) .
Осталось перевести модель на использование нового хоста и интерфейса. Получилось так (git tag a1_v0.23.0) .
Попробуем собрать и запустить… Ошибок нет. Запускаем симуляцию (run):
Соединение установилось, но пакет “=TCP Packet=
” не дошел до “LLTRSuperApp”, и “LLTRSuperApp”, соответственно, не вывел надпись “Arrived (TCP): =TCP Packet=
”.
Пакет “=TCP Packet=
” виден в событии #155. Детализация события намекает нам, что модуль “tcp” не хочет отправлять данные нулевого размера в режиме “TCP_TRANSFER_BYTECOUNT
”. Попробуем задать размер пакета, используя метод “setByteLength()
”. Должно получиться примерно так (git tag a1_v0.24.0) .
Собираем, запускаем, запускаем симуляцию (run) (лог отфильтрован и показан только фрагмент):
Теперь “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"
”).
Как же поместить хосты в вектор?
В “
network Network
{
submodules:
host[100]: Host {
ping.timeToLive = default(3);
ping.destAddress = default(0);
}
...
}
Отлично, мы можем сразу же задать общие параметры для всех хостов в векторе!
А как задать индивидуальные параметры для конкретного хоста в векторе? Мы размещали каждый хост в определенной точке на холсте, и хотели бы сохранить это расположение. В том же разделе “
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, а где остальные параметры.
Список дальнейших действий:
@display
”, но мы можем использовать parameter substitution (альтернатива для расположения: в строку, в столбец, в сетку, по кругу).types:
”), расширяющий (“extends
”) существующий модуль.В итоге получилось так (ветка hosts-in-vector, коммт “Put hosts in vector”).
При этом сеть, в визуальном редакторе
Note: в симуляторе все выглядит нормально.
Мы потеряли возможность задавать местоположение хостов через визуальный редактор, но хотя бы теперь мы можем взять адрес вектора и быстро получить адрес на объект нужного хоста.
Кстати, а при помощи какой функции можно получить адрес вектора или итератор по вектору?
Ответ: такой функции нет, как нет и вектора – сейчас (
А метод “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: однажды мне пришла идея поместить все хосты в вектор… я был столь наивен… Не делайте этого!
Вначале сделаем имена у хостов более удобными для дальнейшего использования. Как было сказано в разделе “Помещаем хосты в вектор”, надежнее будет перейти, при обращении к хостам, от обращения по IP‑адресу к обращению по имени хоста (“"host#"
”). Чуть ниже я буду использовать несколько формул для расчета номера “#
” unicast src/dst хоста. В этих формулах предполагается, что индексация “#
” unicast src/dst хостов начинается с “0
”. На данный момент номер “0
” занят под broadcast src хост, поэтому переименуем его в “hostS
” (“S
” – “LLTRSuperApp”), а все остальные хосты (unicast src/dst) переиндексируем, начиная с “0
”:
host1
” → “host0
”;host2
” → “host1
”;host3
” → “host2
”.Также нужно подправить имена хостов в “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) .
Теперь допишем все остальное… (git tag a1_v0.26.0)
Note: Если описывать реализацию модели LLTR такими же мелкими шажками, какими было описано все предыдущее, то эту статью придется издавать в виде нескольких книжных томов. Поэтому я остановлюсь только на нескольких интересных моментах, важных для понимания устройства и структуры модели.
Лучше начать с “LLTRSuperApp” – он проще, и в нем хорошо видны шаги из описания протокола:
Сокращенный вариант потока выполнения программы выглядит так:
initialize(INITSTAGE_LAST)
→ handleMessage(evFill)
→ handleFill(INIT)
→handleMessage(evFill)
→ handleFill(PROBING)
→ }for(uint32 iN=0; iN<combHosts; iN++) for(int pktN=0; pktN<300; pktN++) {...}
)finish()
…
Если вы периодически пишите на чистом JavaScript, то наверняка уже заметили “setTimeout()
” – с его помощью все “зацикливается”.
Note: В чистом setTimeout()
” отсутствует, вместо него используется функция “scheduleAt()
”. Разница: “scheduleAt()
” принимает абсолютное время, а “setTimeout()
” – относительное время. Для “LLTRSuperApp” “setTimeout()
” реализован так.
Для создания циклов “с задержкой” также нужны “Event's” (строки 29,30) – эти сообщения называются “Self-Messages”. По сути – это обычные сообщения, которые модуль отправляет сам себе (именно это и происходит при использовании “setTimeout()
”).
“Self-Messages” так же, как и другие сообщения в handleMessage()
”, и чтобы отличить их от других сообщений используется метод “isSelfMessage()
” (строка 103).
Цикл получается путем взаимодействия с другим модулем (хостом). Так, например, организован цикл “получения статистики с хостов” { → handleStat()
⤳...⤳ handleMessage(socketStat)
→ socketDataArrived()
→ } – в нем не используется “setTimeout()
”.
Этот цикл начинается в (строке 145), где устанавливается начальное значение счетчика, затем через строки (148) (единственное место, в котором используется “setTimeout()
”, и используется только для создания задержки) и (105) попадает в “handleStat()
”, после чего ждет получения данных от другого хоста. При получении данных, через “handleMessage()
” (строка 108) попадает в “socketDataArrived()
”, где счетчик инкрементируется, и проверяется условие выхода (строка 186).
Ранее я упоминал шаги (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”), по сути, представляют собой декларативное описание содержимого пакетов. А всю необходимую “императивную обвязку”
Ранее мы не указывали размер 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” до максимально возможного значения – 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”
Раз уже заговорили про плотность трафика, то продолжим. В предыдущей статье было сказано:
Что касается скорости broadcast и unicast, то broadcast трафик можно держать в диапазоне75% - 100% от “чистой скорости передачи данных” (net bitrate; поиск по “Ethernet 100Base-TX”), а unicast в диапазоне80% - 100% .
Теперь мы можем рассчитать задержку между отправками broadcast пакетов (период отправки пакетов).
Note: “чистая скорость передачи данных” (net bitrate), “пропускная способность” (throughput), “утилизация/использование канала” (channel utilization) – означают разные вещи.
В нашей модели сети используется соединения с net bitrate
Note: Обычно, при вычислении утилизации канала, размер межкадрового интервала не включается в размер самого кадра (что логично), но нам понадобится смотреть на них (воспринимать их) как на единое целое. Поэтому мы и добавили размер межкадрового интервала к размеру самого кадра. Это, к тому же, позволит использовать более наглядное значение процентов утилизации канала, например, здесь получили 99.22% утилизации канала, у нас же будет 100%.
Поехали:
Note: все в точности, как в этом расчете для
Note: На самом деле утилизация канала – это не тот параметр, на который мы сейчас должны обращать внимание. Есть более важный параметр. Поясню: например, мы решили использовать 95% от net bitrate что сделает зазор между двумя кадрами (в размер кадра включен межкадровый интервал) равным
Просматривая код, возможно, вы уже заметили “магическое число” 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 меня полностью устроило: во‑первых, оно лежит в диапазоне
Note: чтобы быстро менять значения подобных констант во всех точках использования, можно использовать многое: от специальных плагинов для IDE, до записи в виде обычной константы с именем в формате “_#
” (а в комментарии описать его сущность), например “_136
” (если понадобится временно заменить значение, то просто меняем значение, если установить новое постоянное значение, то через refactoring-tools меняем и имя).
Такое “имя” намного проще использовать, чем, например, “timeToSend112PercentOfFrameIncludeInterPacketGap
” или “frameIntervalFor89PercentChannelUtilization
”. Другое имя “baseDelay
” – уже короче, но все же больше, чем 3 цифры числа, и к тому же оно отвечает на меньшее количество вопросов (например, базовая задержка чего?), чем первые два варианта. Так что, на раннем этапе создания модели, творческие ресурсы лучше расходовать на что‑нибудь другое (структура модели, схемы, графики, перебор вариантов, поиск элегантного решения, …).
Note: Можно уменьшить негативное влияние сканирования на сеть путем изменения баланса в (‹частота отправки кадров›×‹размер кадра›) в сторону значительного уменьшения размера кадра (увеличения уровня гранулярности) для broadcast трафика. Я не моделировал эту ситуацию, поэтому приведу лишь результаты мысленного эксперимента. Это приведет к тому, что промежуточное сетевое оборудование сможет чаще вставлять, в промежутки между кадрами broadcast трафика, кадры других хостов в сети (хостов, не участвующих в сканировании). Это может по‑разному сказаться на точности собираемой статистики: отрицательно, из‑за сопутствующего не уменьшения (относительно обычной работы сети) объема трафика с других хостов в сети; положительно, из‑за уменьшения задержек в сети, и предотвращения лавинообразного увеличения размера окна у TCP (на некоторых алгоритмах контроля перегрузки; bufferbloat) до появления первых недошедших пакетов.
Раз уже заговорили про числа, то продолжим. В предыдущей статье было сказано (дежавю?):
Количество комбинаций, которые нужно проверить, можно посчитать по формулеn×(n−1) {каждому (n) нужно “поздороваться” со всеми остальными(n−1) , даже если с ним ранее они уже здоровались}, где n – количество всех хостов минус один (broadcast хост).
В коде это вычисляется на строке 71:
numHosts = getHostsCount(getParentModule()->getComponentType()) - 1; //"host*" without "hostS" (-1)
combHosts = (numHosts-1)*(numHosts);
Как в 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”, но как быть в ситуациях, когда:
combHosts
”…И еще вопросы:
Ответы на все вопросы в коде (строка 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 (так и так).
Хорошо было бы во время симуляции наблюдать за параметрами “step
”, “iN
” и количеством посчитанных пакетов (“countFill
”) на каждом хосте (“LLTRApp”):
В WATCH()
” и “WATCH_*()
” макросов для просмотра “массивов” (строка 73):
WATCH(step);
WATCH(iN);
WATCH_VECTOR(countFill);
Еще было бы здорово, во время симуляции, на карте сети сразу же видеть какой из хостов сейчас считает себя unicast src хостом, а какой – unicast dst хостом. В
Unicast src хост:
parentDispStr.setTagArg("i2", 0, "status/up");
Unicast dst хост:
parentDispStr.setTagArg("i2", 0, "status/down");
Если стандартных иконок не достаточно, то всегда можно использовать свои иконки.
В то время, когда это все создавалось, последней версией
Я чему я клоню? Посмотрите на то, как сейчас выглядит метод “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: в то время и переменные назывались по другому.
Перед написанием этой статьи, я обновился до последней версии
До этого момента я уже успел познакомиться с одним из багов лога Qtenv – неверный расчет границ текста, копируемого в буфер обмена. Теперь к нему добавился еще один баг: Qtenv не заносил в лог записи, которые были созданы после завершения симуляции (в данном случае – во время вызова “finish()
” у всех модулей).
Раньше, с функционирующим выводом в лог (в “finish()
”), было очень удобно работать: после завершения симуляции, в последних строчках лога, сразу же были видны ее результаты. Результаты из лога можно было легко скопировать, и использовать для последующей обработки.
Теперь же пришлось сделать несколько “костылей”, пока Qtenv не доделают. Мне они не очень подходят, но зато, при помощи них, я смогу показать, какие еще существуют способы вывода результатов симуляции из
Самый простой “костыль”: после того, как “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)).
В
Мне из этого всего был нужен только вывод двумерного массива в файл. Однако, этого он делать не умеет – он может выводить либо скалярные (одиночные) значение, либо одномерные массивы. Поэтому двумерный (либо n‑мерные) массив выводятся через несколько одномерных (строка 246 LLTRSuperApp).
Здесь есть еще несколько нюансов:
Первая проблема решается просто – в “.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):
Результаты симуляции:
Странно, до хостов дошли все пакеты, кроме последней итерации (не дошел 1 пакет)… А что в логе?:
То же самое… А что в статистике самих хостов?:
И такая картина в каждом хосте… А если запустить “Tkenv” (
Все также, за исключением того, что “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 хостами?:
И почему, если закомментировать (строку 98 LLTRApp) (ARP для “hostS”), то “LLTRSuperApp” уже не сможет собрать статистику с хостов?
Можно долго искать ошибку там, где ее нет (в коде). Поэтому сразу же вспомним, а за счет чего должны были происходить потери пакетов? Подсказка в разделе “Протокол, версия v.Basic.GlobalWave”:
• их должно быть достаточно, чтобы они смогли потеряться (потеря пакетов будет происходить при одновременном “вливании” свитчем broadcast и unicast пакетов в один и тот же порт, с последующим переполнением очереди пакетов этого порта, т.е. количества пакетов должно быть достаточно для переполнения очереди);
Очевидно, что мы “вливаем” недостаточное количество пакетов, но даже этого количества пакетов хватило, чтобы сети “стало плохо” (ARP). Поэтому:
Читать подробности поиска “а где же в EtherSwitch находится очередь пакетов” – скучно, поэтому сразу покажу путь к параметру, определяющему ее размер (Network.switch0.eth[2].mac.txQueueLimit
):
Note: Буфер, вместимостью 10000 пакетов
Да, чуть не забыл, если попробуете переопределить его значение через “.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):
- queue: container of packets, accessed as FIFO
- dropper: attached to one or more queue, it can limit the queue length below some threshold by selectively dropping packets
- scheduler: decide which packet is transmitted first, when more packets are available on their inputs
- classifier: classify the received packets according to their content (e.g. source/destination, address and port, protocol, dscp field of IP datagrams) and forward them to the corresponding output gate.
- meter: classify the received packets according to the temporal characteristic of their traffic stream
- marker: marks packets by setting their fields to control their further processing
Нам подходит очередь “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) .
Запускаем, запускаем симуляцию (fast):
Победа! Собранная статистика настолько чистая, что, достаточно просто посмотреть на нее, чтобы определить, где какой хост находится.
#жучки-пакетики #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 [минуту на нулевую
17
500
).**.host*.app[*]
”. Самый быстрый способ сделать это – вначале выключить все (отключить “Network”), а затем включить нужное.Запускаем (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
Часть из них можно найти поиском по “!!!!!! 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: Другая часть этих сообщений уже не отображается в логе – превышен лимит на максимальное количество событий и строчек в логе. Лимит можно изменить в настройках (
Эти сообщения выводятся в начале каждой итерации. Сообщение генерирует 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");
//...
}
Я выводил эти сообщения, чтобы:
uDstHostId
”) – в лог выводится “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 пакетов:
Их формат:
>>> ‹номер итерации›/‹обратный отсчет: сколько еще осталось отправить 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);
}
Все хорошо, все работает, но меня тревожат ошибки на последних двух скриншотах:
Ignoring UDP error report
” – сгенерировано строкой 169 в “LLTRApp”;ICMP-error-#22-type3-code3
” – “Destination Unreachable, Port Unreachable”.Посмотрим на ошибки поближе (новый запуск симуляции; режим run; трафик от свитчей скрыт):
Уже сейчас ясна причина появления ошибок, но, тем не менее, я хочу точно увидеть, какой пакет стал причиной отправки “ICMP Error”. Например, посмотрим в инспекторе объектов на событие #728 (“ICMP-error-#6-type3-code3
”):
Причиной события #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) .
Вначале предлагаю представить, что произойдет, если раскомментировать это условие в “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
”:
Также это означает, что за все время симуляции ни разу не возникала ситуация:
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)
Файлы новых сетей:
При запуске симулятора, теперь можно выбрать одну из сетей:
Note: “(General)” – это наша первая сеть (4 хоста, 2 свитча, т.е. “N2_2”).
Если просто добавить файлы новых сетей, рядом с файлом существующей сети, то симулятор не покажет этот диалог. Чтобы появился этот диалог, в “omnetpp.ini” нужно добавить новые секции, точнее “Named Configurations” (
По сути, я создал несколько новых “именованных конфигураций”/секций, которые переопределяют параметр “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”):
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”):
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”):
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”):
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)
Файлы новых сетей:
Network_serial
”, для более быстрой симуляции убраны все хосты, кроме подключенных к крайним свитчам, и подключенных к “switch0”;Network
” (“N2_2”, “(General)”), добавлены несколько промежуточных свитчей (это еще более упрощенный вариант сети “Network_serial
”).
Сеть “Network_serial-len-test
” (конфигурация “SerialLenTest”):
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”):
INFO: {300,164,164},
INFO: {300,164,164},
INFO: {300,300,164},
INFO: {300,300,300},
INFO: {300,300,300},
INFO: {299,163,299},
Получилось воспроизвести проблему на топологии исходной сети, изменяя количество промежуточных свитчей.
Мы немного “приоткрыли” неучтенный фактор, и, как я писал ранее, окончательно раскрою его в одной из следующих статей.
Этот шаг должен был называться “последние штрихи”, и начинаться примерно так:
Мы уже несколько раз пытались бороться с адресацией хостов:
И, на данный момент, чтобы получить 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‑адресу, с некоторыми ограничениями.
А заканчиваться:
HostPath_defSuperHost_
”.Проверка соответствия IP‑адрес↔индекс хоста должна была выглядеть так:
"host*"
”, пропускать “"hostS"
”;atoi()
” для получения индекса хоста;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).
В этой статье все ссылки вели на online версию Simulation Manual, которая предназначена для последней версии
Если вам, так же как и мне, не нравится стандартные иконки устройств, и фон инспектора сети в симуляторе, то это можно легко исправить.
Если вам понравилась иконка хоста (“"device/pc2"
”) и свитча (“"device/switch"
”) из этой статьи, то разархивируйте (7zip) эту картинку в “omnetpp-5.0/images/device/”:
Чтобы заменить цвет, надо знать место, где он задается (в коде) или находится (в памяти). Чтобы найти это место, надо определить числовое значение цвета (RGB):
Числовое значение “зеленого” цвета фона: A0E0A0 (hex); R:160, G:224, B:160.
Я пока еще не подобрал цвет фона на замену, и у меня есть 3 варианта (пути) дальнейших действий:
Начнем с конца: “прототипный” вариант – самый быстрый, однако, сам графический редактор (его GUI) может повлиять на выбор итогового цвета, поэтому при подборе цвета нужно скрыть весь GUI редактора, оставив только холст, а еще лучше – переключится в полноэкранный режим. Минус этого пути – он позволяет только подобрать цвет, в то время как остальные варианты позволяют изменить (зафиксировать) цвет в самой программе.
“Долгий” вариант можно сразу отбросить, к тому же “собирать пазл”, пробираясь сквозь дебри классово‑объектного (с привкусом лазании) кода
Я опишу “хакерский” вариант с простым asm, и работой с “финальным результатом” (скомпилированной, слинкованной, и запущенной программой). Он состоит из нескольких шагов:
LLTR.exe
”), и “размораживаем” (нижний левый угол – “Paused”) Tkenv (A0E0A0
” RGB или “A0E0A0
” BGR ;) ;Для поиска константы (паттерна):
A0 E0 A0
”) для поиска.Note: x64_dbg ищет начиная с текущего адреса (выбранной строки), и до конца адресного пространства. Поэтому, для поиска по всему адресному пространству важно следить за тем, чтобы была выбрана первая строка, и список был отсортирован по столбцу “Address”.
Результаты поиска отобразятся во вкладке “References” (после поиска опять нужно разморозить (
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‑и найденных адресов:
A0 E0 A0
” на “A0 00 A0
” (двойной клик на “E0
” в “Dump 1”), и смотрим результат. Дополнительно можно поставить точку останова на сам код, и посмотреть, с какими параметрами (окружением) он также работает (т.е. просмотреть стек и регистры).Note: слово “нравится”: по сути я подменил “подходит” (результат) на “нравится” (причину). При этом саму причину я детально не раскрыл. В данном случае “подходит” – это положительный результат оценки (тестирования) фрагмента кода на присутствие нескольких признаков. Также как и искусственная нейросеть сама не может дать ответ на вопрос “Почему?” (“Почему ты выбрала именно этот ответ?”), так и опытный человек (неосознанная компетентность – 2 ссылки) часто не сможет сразу дать ответ на этот вопрос.
К первому адресу (0x0497801C [0]) было обращение только при “Re‑layout”:
В этом фрагменте привлекают внимание вызовы “tk86.XSetWindowBackground
” и “tk86.XChangeWindowAttributes
”. Однако, при попытке изменить “
” на “
” ничего не происходит. Более того, при повторном “Re‑layout” (либо при изменении значения в процессе “Re‑layout”) значение возвращается назад – на “
”.
Подсказка, почему это происходит (на скриншоте помечено синим):
= 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”):
Здесь привлекает внимание переход на “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
А теперь попробуем заменить “
” на “
”, и пошагать немного вперед (“Step Over”, F8)… и мы пришли к уже знакомому коду (с дополнительной информацией):
Будет еще несколько остановок, а в конце:
A0 00 A0
”;Мы уже нашли то место, где задается цвет фона, но для его изменения нужно делать “Re‑layout”. У нас осталось еще несколько адресов, и я бы хотел найти тот адрес, в котором значение цвета кешируется, и используется при перерисовке содержимого окна.
К адресам 0x0497DE60 [2] и 0x0497DED0 [3] обращений не было. Вдобавок, по этим адресам до сих пор хранится значение “
”, а должно – “
”, т.к. цвет фона изменился.
А вот по адресам 0x0497DF60 [4] и 0x0497DFD0 [5] сейчас хранится “
” и “
” соответственно. Предположение: старый “кеш” после “Re‑layout” стирается, и создается новый “кеш” по новым адресам. Похоже, именно в этих двух адресах ранее (до “Re‑layout”) был “закеширован” цвет. Теперь они располагаются по новому адресу.
Чтобы найти новый адрес, снова запустим поиск паттерна “
” в памяти:
Что‑то не то – слишком мало адресов в диапазоне (0x048A0000 + 0x00400000), к тому же здесь отсутствуют адреса 0x0497A6C0 [1] и 0x0497801C [0]!
Ах да, я забыл, что мы поменяли “
” на “
”, придется вернуть обратно (или искать “
”). Однако, теперь мы будем уверены, что найденные сейчас адреса не имеют отношения к цвету фона.
Вернем старое значение по адресу 0x0497A6C0 [1] сделаем “Re‑layout”, и повторим поиск (после поиска разморозим (
Появились 2 новых адреса из другого диапазона памяти. Найдем его в “Memory Map”:
Address | Size | Page Info | Alloc Type | Current Prot | Alloc Prot
========================================================================================
06460000 | 0026E000 | | PRV | -RW-- |-RW--
Похоже все нормально. Проверим новые адреса.
К адресу 0x065B5AC8 [4′] обращаются при:
Но я пока не буду менять значение, и переду к следующему адресу 0x065E4E18 [5′]:
Какой интересный фрагмент: ESI:“gdi32.CreateSolidBrush
”; вызов “tk86.TkWinGetDrawableDC
”, user32.FillRect
”, и в стеке “tk86.XFillRectangles
”. Причем, если изменить “
” на “
”, и свернуть/развернуть окно, то цвет фона изменится! Однако, по адресу 0x065B5AC8 [4′] осталось прежнее значение, и даже если вызвать код, использующий его [4′], то в Tkenv цвет фона останется “
”, а в 0x065B5AC8 [4′] останется “
”.
Note: Если вы не знакомы с механизмом работы Software Breakpoints, то попробуйте прямо сейчас поставить Breakpoint (это можно сделать сразу во вкладке с результатами поиска – “References” → контекстное меню → Toggle Breakpoint; клавиша F2) на адрес, хранящий цвет фона для перерисовки окна (у меня, в данный момент, это адрес 0x065E4E18 [5′], а у вас будет другой адрес). Теперь сверните/разверните окно Tkenv… как вам взгляд на сеть через “розовые очки” :)? А теперь попробуйте заменить “
” на “
”, и посмотрите на итоговый цвет (должен быть черным, но…). Теперь вы поняли, почему в IDE метка Breakpoint имеет именно этот цвет ;)(возможно, эта фраза была шуткой, а возможно и нет).
Я остановился на цвете “E6E6E6
”.
Теперь можно зафиксировать изменения, и, чтобы изменения сохранялись при перекомпиляции Tkenv, цвет придется заменять в исходниках. В исходниках цвет может быть задан множеством способов (3 одно‑байтных числа, одно 4‑х байтное число, строка с hex значением или названием цвета, …), я начал поиск со строки “a0e0a0
” (без учета регистра) в директории с исходниками Tkenv (“omnetpp-5.0/src/tkenv/”). И мне повезло:
tkp::canvas $c -background "#a0e0a0" -relief raised -closeenough 2
`;tkp::canvas $c -background "#a0e0a0" -relief raised -closeenough 2
`;canvas $c -background #a0e0a0 -relief raised
`.
Так вот он какой Tcl/Tk… Тот факт, что в названиях всех файлов присутствует “inspector” – обнадеживает, поэтому просто поменяем цвет, и пересоберем
make MODE=release –j17
Note: вспомните про “-j
” и “17
”.
Note: пересобирать проект “LLTR” (через Eclipse) нет необходимости, т.к. сам Tkenv (и внесенные нами изменения) находится в библиотеке “omnetpp-5.0/bin/libopptkenv.dll”.
Цвет должен был измениться:
Note: Подложку (с тенью) для фона сети, используемую в этой статье (как на скриншоте выше), можно достать разархивировав (7zip) эту картинку. В архиве 3 фона под несколько используемых размеров холста, и файл с готовыми display string для “.ned” файлов сетей. Фоны можно распаковать прямо в “omnetpp-5.0/images/background/”, либо расположить их в любом другом удобном месте.
Поступаем точно также как и с Tkenv, с той лишь разницей, что теперь цвет будем менять на изображение, и не будем использовать отладчик для интерактивного просмотра результата замены.
“На глаз” зеленые цвета фона в Tkenv и Qtenv совпадают, но совпадают ли их числовые значения (RGB)? Тест “пипеткой” показал, что совпадают: A0E0A0 (hex); R:160, G:224, B:160.
Поиск “оптимального узора для фона” отложим на потом, и сразу перейдем к поиску цвета в исходниках (директория “omnetpp-5.0/src/qtenv/”):
setBackgroundBrush(QColor("#a0e0a0"))
`;setBackgroundBrush(QColor("#a0e0a0"))
`;layoutScene->setBackgroundBrush(QColor("#a0e0a0"))
`.Здесь можно сразу же заменить зеленый цвет на другой цвет, но нам нужно поменять цвет на картинку. В дополнение к этому представим, что мы видим Qt впервые в жизни, и не знаем, как он устроен. Что же делать?
В Qtenv уже используются иконки (изображения) для кнопок, а в директории с исходниками есть поддиректория “icons/” где и располагаются (в своих поддиректориях) иконки. Теперь можно поискать места/варианты использования в коде нескольких из этих иконок по их имени, либо можно осмотреть файлы в директории с исходниками и заметить:
QImage *image = new QImage(fileName);
”.Не буду долго тянуть – для продолжения нужно познакомится с Qt чуть поближе:
setBackgroundBrush()
”, и пример использование картинки с заданием режима кеширования чуть ниже;drawBackground
” или использовать “setBackgroundBrush()
”;setBackgroundBrush(new ...)
”;setBackgroundBrush(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"));
Осталось пересобрать
Я замерял “CPU time” (user + sys time) для процесса симулятора, привязанного к одному наиболее свободному ядру CPU. В симуляторе запускалась модель с конфигурацией “(General)” (сеть “Network
” – “N2_2”) в режиме “until-run” 999 первых событий (примерно в этот момент, чуть ранее, начинается нулевая итерация). После каждого испытания симулятор перезапускался. Я хотел сравнить с оригинальным Qtenv, сравнить скорость при использовании QPixmap и QImage (как с использованием кеширования, так и без). В итоге получилось 5 конфигураций:
Также я проверял нагрузку на CPU при горизонтальной прокрутке содержимого инспектора сети.
В результатах будет указано (в секундах):
start
” – “CPU time” перед нажатием кнопки для запуска симуляции; end
” – “CPU time” после остановки симуляции на 999 событии;diff
” – разность между “end
” и “start
”.Результаты (без комментариев):
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: рассматривайте эти тесты только как призыв к действию – ваши результаты (относительные) могут отличатся от моих.
Обещанные выше фоны (узоры, паттерны):
Note: справа на картинке указаны числовые значения “цвета основы узора”.
После разархивирования (7zip) можно сразу же открыть “demo/page.htm” и посмотреть, как будет выглядеть конкретный узор в GUI Qtenv.
Для смены узора я просто использую “инструментарий разработчика” (Ctrl+Shift+I). Путь к нужному узору указывается в стиле (“style
”; второй фон в свойстве “background
”) единственного “div
”. Этим же способом можно посмотреть, как будет выглядеть любая картинка в качестве фона. Например, мне понравились фракталы “Julia Pattern Map”, в частности “Cognitive Architecture (11)” и “Patchwork Design (24)” (он напоминает мне Куб1,2):
Я брал ¼ этих изображений, и смотрел, как они будут выглядеть в GUI. Результат – слишком привлекали внимание на себя, т.е. отвлекали.
В итоге, я выбрал “print-26.png”.
Небольшой опрос. Первый вопрос поможет мне лучше определить время для публикации следующей части. Второй – улучшить статью. Остальные вопросы – чистое любопытство.