Pull to refresh

Учебник по симулятору сети ns-3. Глава 7

Reading time80 min
Views2.2K
Original author: https://www.nsnam.org/documentation/
image

[главы 1,2]
[глава 3]
[глава 4]
[глава 5]
[глава 6]

Глава 7 Трассировка

7.1 История вопроса

7.1.1 Тупые инструменты

7.2 Обзор

7.2.1 Простой пример

7.2.2 Подключение через Config

7.2.3 Поиск источников

7.2.4 Доступные источники

7.2.5 Config-пути

7.2.6 Сигнатуры обратных вызовов

7.2.7 TracedValue

7.3 Реальный пример

7.3.1 Доступные источники

7.3.2 Поиск примеров

7.3.3 Источники динамической трассировки

7.3.4 Разбор fifth.cc

Запуск/останов приложений

Приложение MyApp

Приемник трассировки

Основная программа

7.3.5 Запуск fith.cc

7.3.6 Использование помощников среднего уровня

7.4 Помощники трассировки

7.4.1 Помощники устройств

PCAP

ASCII

7.4.2 Помощники протоколов

7.5 Резюме


Глава 7 Трассировка


7.1 История вопроса


Как упоминалось в разделе 5.3, весь смысл симуляции ns-3 заключается в генерации выходных данных для изучения. У вас есть две основные стратегии получения выходных данных от ns-3: использование общих предопределенных механизмов и анализ содержимого их массового вывода для извлечения интересующей информации или разработать некий механизм вывода, который передаст только ту информацию, которую вы хотели.

Использование предопределенных механизмов массового вывода имеет то преимущество, что не требует каких-либо изменений в ns-3, но может потребовать написания сценариев для анализа и фильтрации интересующих данных. Часто, выходные сообщения PCAP или NS_LOG, собранные во время симуляции, дополнительно прогоняются через скрипты, которые используют grep, sed или awk для разбора сообщений, уменьшения, преобразования данных в удобную форму. Для выполнения преобразований должны быть написаны программы, так что это не проходит без затрат. Вывод NS_LOG не считается частью API ns-3 и может меняться от релиза к релизу без предупреждения. Кроме того, вывод NS_LOG доступен только в отладочных сборках, поэтому его использование влечет за собой снижение производительности. Более того, этот подход не сработает, если интересующей информации нет ни в одном из предопределенных механизмах вывода.

Если вам нужно добавить немного информации к предопределенным механизмам, это, безусловно, можно сделать и если Вы используете один из механизмов ns-3, то можете добавить свой код как контрибутор.

Ns-3 предоставляет другой механизм, называемый трассировкой, который позволяет избежать некоторых проблем, присущих механизмам массового вывода. У него есть несколько важных преимуществ. Во-первых, вы можете уменьшить объем данных, которыми нужно управлять, отслеживать только интересующие вас события (выгрузка всех данных на диск, для последующей обработки, операции ввода/вывода могут стать узким местом для больших симуляций). Во-вторых, если вы используете этот метод, то можете напрямую управлять форматом вывода, избавляя себя от необходимости постобработки с использованием сценариев sed, awk, perl или python. По вашему желанию вывод может быть отформатирован сразу, например, в форму, приемлемую для gnuplot (см. также 8.3). Вы можете добавить в ядро ns-3 крючки (хуки), которые затем могут быть доступны для других пользователей, но они не будут давать никакой информации, пока их об этом прямо не попросят. По этой причине мы считаем, что система трассировки ns-3 — это лучший способ извлечения информации из моделирования, и поэтому один из самых важных для понимания механизмов в ns-3.

7.1.1 «Тупые» инструменты


Есть много способов получить информацию из программы. Самый простой способ — это распечатать информацию непосредственно в стандартный вывод, как здесь:

#include <iostream>
...
void
SomeFunction (void)
{
  uint32_t x = SOME_INTERESTING_VALUE;
  ...
  std::cout << "The value of x is " << x << std::endl;
  ...
}

Никто не помешает вам углубиться в ядро ns-3 и добавить операторы печати. Это безумно легко сделать, и, в конце концов, у вас есть полный контроль над собственной веткой ns-3. Хотя это, вероятно, не окупится в долгосрочной перспективе.

По мере увеличения числа операторов печати в ваших программах, задача обработки большого количества выходных данных будет становиться все более и более сложной. В конце концов, вы можете почувствовать необходимость управлять тем, какую информацию и как печатать, возможно, путем включения и выключения определенных категорий трасс, или увеличения⁄уменьшения объема нужной информации. Если вы продолжите идти по этому пути, то обнаружите, что повторно реализовали механизм NS_LOG (см. 5.1). Чтобы избежать этого, одной из первых вещей, которые вы могли бы сделать, это пользоваться NS_LOG.

Мы упоминали выше, что один из способов получить информацию из ns-3 — это проанализировать существующий вывод NS_LOG на предмет интересующей информации. Если вы обнаружите, что некоторая нужная вам информация отсутствует в существующем выводе журнала, вы можете отредактировать ядро ns-3 и просто направить интересующую информацию в поток вывода. Это, конечно, лучше чем добавление ваших собственных операторов печати, поскольку оно следует соглашениям о кодировании ns-3 и потенциально может быть полезна для других людей как патч к существующему ядру.

Давайте возьмем случайный пример. Если вы хотите добавить больше журналирования в TCP-сокет ns-3 (tcp-socket-base.cc) вы можете просто добавить в реализацию новое сообщение. Обратите внимание, что в TcpSocketBase::ProcessEstablished() нет сообщения журнала на получение SYN + ACK в состоянии ESTABLISHED. Вы можете его легко добавить, изменив код. Вот оригинал:

/* Received a packet upon ESTABLISHED state. This function is mimicking the
role of tcp_rcv_established() in tcp_input.c in Linux kernel. */
void
TcpSocketBase::ProcessEstablished (Ptr<Packet> packet,
const TcpHeader& tcpHeader)
{
  NS_LOG_FUNCTION (this << tcpHeader);
  ...
  else if (tcpflags == (TcpHeader::SYN | TcpHeader::ACK))
  { 
  // No action for received SYN+ACK, it is probably a duplicated packet
  }
...

Чтобы добавить возможность журналирования SYN + ACK, вы можете добавить новый NS_LOG_LOGIC в тело оператора if:

/* Received a packet upon ESTABLISHED state. This function is mimicking the
role of tcp_rcv_established() in tcp_input.c in Linux kernel. */
void
TcpSocketBase::ProcessEstablished (Ptr<Packet> packet, const TcpHeader& tcpHeader)
{
  NS_LOG_FUNCTION (this << tcpHeader);
  ...
  else if (tcpflags == (TcpHeader::SYN | TcpHeader::ACK))
  { // No action for received SYN+ACK, it is probably a duplicated packet
    NS_LOG_LOGIC ("TcpSocketBase " << this << " ignoring SYN+ACK");
  }
...

На первый взгляд это может показаться довольно просто и достаточно, но нужно учитывать, что вы будете писать код для добавления операторов NS_LOG, а для разбора журнала также придется написать код (как в сценариях grep, sed или awk), чтобы извлечь вашу информацию. Это потому, что хоть у вас и есть некоторый контроль над тем, что выводится в журнал, но этот контроль не ниже уровня компонента журнала, который обычно представляет собой целый файл исходного кода. Если вы добавляете код в существующий модуль, ваш вывод в журнал будет сосуществовать с тем выводом, который разработчик счел интересным добавить. Вы можете обнаружить, что для получения небольшого количества необходимой информации вам, возможно, придется пробираться через огромное количество посторонних неинтересующих сообщений. Всякий раз когда вам понадобится что-то сделать, вы будете вынуждены сохранять на диск огромные файлы журнала и обрабатывать их ради нескольких строк.

Поскольку в ns-3 нет никаких гарантий относительно стабильности вывода NS_LOG, вы также можете обнаружить, что важные для вас фрагменты вывода журнала, исчезают или меняются при смене релиза. Если вы зависите от структуры вывода, вы может найти другие сообщения, добавляемые или удаляемые, которые могут повлиять на код вашего синтаксического анализа.

Наконец, вывод NS_LOG доступен только в отладочных сборках, вы не можете получить вывод журнала из оптимизированных сборок, которые запускаются примерно в два раза быстрее. Использование NS_LOG влечет за собой снижение производительности.

По этим причинам мы считаем, что печать в сообщения std::cout и NS_LOG — быстрый и грязный способ получить больше информация из ns-3, но он не подходит для серьезной работы.

Желательно иметь стабильное средство, использующее стабильные API, которые позволяют проникнуть в основную систему и получить только требуемую информацию. Желательно иметь возможность сделать это без необходимости изменять и перекомпилировать основную систему. Было бы более интересно, если бы система уведомляла пользовательский код, когда интересующий объект изменился или произошло интересующее событие, тогда пользователю не понадобится активно копаться в системе чтобы получить эту информацию.

Система трассировки ns-3 разработана для работы в этом направлении и хорошо интегрирована подсистемами атрибутов (Attribute) и настроек (Config), позволяющими сравнительно легко использовать сценарии.

7.2 Обзор


Система трассировки ns-3 построена на принципах независимых источников и приемников трассировки, наряду с унифицированным механизмом подключения источников к приемникам. Источники трассировки — это объекты, которые могут сигнализировать о событиях, происходящих в симуляции, и предоставлять доступ к интересующим базовым данным. Например, источник трассировки может сигнализировать, о приеме сетевым устройством пакета и предоставлять доступ к содержимому этого пакета для заинтересованных приемников трассировки. Источник трассировки также может указывать, когда происходит интересующее изменение состояния модели. Например, окно заторов в модели TCP является основным кандидатом в источники трассировки. Каждый раз, когда окно заторов изменяется, подключенные приемники трассировки информируются об изменении с помощью отправки им старого и нового значения.

Источники трассировки сами по себе не имеют ценности, пока они не связаны с другими частями кода, которые реально делают с информацией, предоставленной источником, что-то полезное. Объекты, использующие информацию трассировки, называются приемниками трассировки. Источники трассировки являются генераторами данных, а приемники трассировки — потребителями. Такое явное разделение позволяет размещать большое количество источников по всей системе, там где авторы моделей считают их полезными. Вставка источников трассировки лишь немного увеличивает накладные расходы при выполнении.

Потребителей событий, сгенерированных источником трассировки, может быть ноль или более. Источник трассировки выступает как элемент информационного линка «один ко многим». Ваш код, просматривающий события трассировки из определенного фрагмента кода ядра может успешно сосуществовать с другим кодом, делающим что-то совершенно иное с той же информацией.

Если пользователь не подключит приемник трассировки к одному из этих источников, то выводить приемнику будет нечего. Используя систему трассировки, вы и другие пользователи, подключенные к тому же источнику трассировки, получите из системы только то, что вам нужно. Никто из вас, изменяя информацию, выводимую системой не повлияет на других пользователей. Если вы, как добропорядочный гражданин open source, добавите источник трассировки, то ваша работа, может открыть возможность другим пользователям без внесения каких-либо изменений в ядро ns-3, создать новые инструменты, которые возможно, окажутся очень полезными для всех в целом.

7.2.1 Простой пример


Давайте уделим несколько минут и пройдемся по простому примеру трассировки. Чтобы понять, что происходит в примере, нам понадобится некоторое представление об обратных вызовах (обратный вызов — callback), поэтому мы должны сделать небольшой обзор прямо сейчас.

Обратные вызовы (Callbacks)


Задача системы обратных вызовов в ns-3 состоит в том, чтобы позволить одному коду вызывать функцию (или метод в C++) другого кода без какой-либо специфической межмодульной зависимости. В конечном итоге это означает, что вам необходим какой-то из вариантов косвенности — чтобы рассматривать адрес вызываемой функции как переменную. Эта переменная называется переменной-указателем на функцию. Отношение между функцией и указателем на функцию на самом деле ничем не отличаются от отношения объекта и указателя на объект.

В Си каноническим примером указателя на функцию является указатель на функцию возвращающую целое число (pointer-to-function-returning-integer или />
PFI). PFI, принимающая один параметр int, может быть объявлена как,

int (*pfi)(int arg) = 0;

(Но прежде чем писать такой код, прочитайте Section 33 C++ — FAQ:

(www.parashift.com/c++-faq/pointers-to-members.html))

После этого объявления вы просто получаете переменную с именем pfi, которая инициализирована значением 0. Если вы хотите инициализировать этот указатель чем-то значимым, то вам нужно иметь функцию с совпадающей сигнатурой. Для этого вы могли бы использовать функцию, которая выглядит следующим образом:

int MyFunction (int arg) {}

Если у вас есть такая функция, вы можете инициализировать переменную так, чтобы она указала на вашу функцию:

pfi = MyFunction;

Теперь вы можете вызвать MyFunction косвенно, используя более наглядную форму вызова:

int result = (*pfi) (1234);

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

int result = pfi (1234);

Выглядит так, как будто, вы вызываете функцию с именем pfi, но компилятор достаточно умен, чтобы догадаться, что через переменную pfi нужно косвенно вызвать функцию MyFunction.

Концептуально, система трассировки работает точно также. По сути, приемник трассировки является функцией обратного вызова. Когда приемник выражает интерес к получению событий трассировки, он добавляет себя в качестве обратного вызова в список обратных вызовов, хранящийся в источнике трассировки. Когда происходит интересующее событие, источник трассировки вызывает свой operator(...), подставляя ноль или больше аргументов. В конце концов operator(...) заходит в систему и делает нечто похожее на косвенный вызов, который вы только что видели, предоставляя ноль или более параметров, так же как выше вызов pfi передал бы целевой функции MyFunction один параметр.

Важное отличие, которое добавляет система трассировки, состоит в том, что для каждого источника трассировки существует внутренний список обратных вызовов. Вместо того, чтобы просто сделать один косвенный вызов, источник трассировки может выполнять несколько обратных вызовов. Когда приемник трассировки выражает интерес к уведомлениям от источника трассировки, он в по сути просто добавляет свою собственную функцию в его список обратных вызовов.

Если вам нужна более подробная информации о том, как это на самом деле устроено в ns-3, не стесняйтесь просматривать раздел «Обратный вызов» руководства ns-3.

Прогулка по fourth.cc


Мы предоставили некоторый код для реализации того, что действительно является самым простым примером трассировки, который можно собрать. Вы можете найти этот код в директории examples/tutorial как fourth.cc. Давайте пройдемся по нему:

/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
/*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation;
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "ns3/object.h"
#include "ns3/uinteger.h"
#include "ns3/traced-value.h"
#include "ns3/trace-source-accessor.h"
#include <iostream>
using namespace ns3;

По большей части, этот код должен быть вам знаком. Как уже упоминалось выше, система трассировки интенсивно использует системы Object и Attribute, поэтому вам нужно будет их подключить. Первые два include явно включают в себя декларации для этих систем. Вы можете использовать заголовок основного модуля, чтобы подключить все одним файлом, но мы делаем здесь так, чтобы проиллюстрировать, насколько все просто на самом деле.

Файл traced-value.h содержит требуемые объявления для трассировки данных, которые подчиняются семантике значений(Если объект можно копировать и присваивать, то о нем говорят, что он обладает семантикой значений.).
Вообще, семантика значения просто означает, что вы можете передавать сам объект, а не передавать адрес объекта. На самом деле все это означает, что вы сможете простым путём отслеживать все изменения, внесенные в TracedValue.

Поскольку система трассировки интегрирована с атрибутами (Atrrribute), а атрибуты работают с объектами Object, то должен существовать объект ns-3 со встроенным источником трассировки. Следующий фрагмент кода объявляет и определяет простой объект, с которым мы сможем работать.

class MyObject : public Object
{
  public:
  static TypeId GetTypeId (void)
 {
    static TypeId tid = TypeId ("MyObject")
    .SetParent (Object::GetTypeId ())
    .SetGroupName ("MyGroup")
    .AddConstructor<MyObject> ()
    .AddTraceSource ("MyInteger",
    "An integer value to trace.",
    MakeTraceSourceAccessor (&MyObject::m_myInt),
    "ns3::TracedValueCallback::Int32");
    return tid;
 }

  MyObject () {}
  TracedValue<int32_t> m_myInt;
};

Выше приведены две важные в отношении трассировки строки кода .AddTraceSource и объявление TracedValue m_myInt.

AddTraceSource предоставляет «крючки» (хуки), используемые для подключения источника трассировки к внешнему миру через систему Config. Первый аргумент — это имя данного источника трассировки, которое делает его видимым в системе Config. Второй аргумент — это строка справки. Теперь посмотрим на третий аргумент, а точнее на аргумент третьего аргумента: &MyObject::m_myInt. Это TracedValue, который будучи добавленным в класс, всегда становится членом данных класса. Последний аргумент — это имя typedef для типа TracedValue в текстовом виде. Это используется для корректной генерации в документации сигнатуры функции обратного вызова, что особенно полезно для более общих типов обратных вызовов.

Объявление TracedValue<> предоставляет инфраструктуру, которая управляет обработкой обратного вызова. Каждый раз, когда «подопечная» переменная изменяется, механизм TracedValue будет предоставлять как старое, так и новое значение этой переменной, в данном случае значение int32_t. Функция приемника трассировки traceSink для такого TracedValue должна иметь сигнатуру

void (* traceSink)(int32_t oldValue, int32_t newValue);

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

Продолжая просматривать fourth.cc, мы видим:

void
IntTrace (int32_t oldValue, int32_t newValue)
{
  std::cout << "Traced " << oldValue << " to " << newValue << std::endl;
}

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

Мы посмотрели на источник и приемник трасс. Остается код для подключения источника к приемнику, который расположен в main():

int
main (int argc, char *argv[])
{
  Ptr<MyObject> myObject = CreateObject<MyObject> ();
  myObject->TraceConnectWithoutContext ("MyInteger", MakeCallback(&IntTrace));
  myObject->m_myInt = 1234;
}

Здесь мы сначала создаем экземпляр MyObject, в котором имеется источник трассировки.

Следующий шаг, TraceConnectWithoutContext, формирует соединение между источником трассировки и и приемником. Первый аргумент — это просто имя источника трассировки «MyInteger», которое мы видели выше. Обратите внимание на шаблонную функцию MakeCallback. Эта функция реализует магию, необходимую для создания базового объекта обратного вызова ns-3 и связывания его с функцией IntTrace. TraceConnect устанавливает связь между предоставленной функцией и перегруженным operator() в трассируемой переменной, на которую ссылается атрибут «MyInteger». После того, как эта связь установлена, источник трассировки будет запускать предоставленную вами функцию обратного вызова.

Код для выполнения всего этого, конечно, нетривиален, но суть в том, что вы делаете что-то похожее на приведенный выше пример pfi(), который вызывается источником трассировки. Объявление TracedValue <int32_t> m_myInt; в самом объекте создает магию, необходимую для обеспечения перегрузки оператора присваивания, которые будут использовать operator() для фактического обратного вызова с нужными параметрами. Строка .AddTraceSource ворожит соединение обратного вызова с системой Config, а TraceConnectWithoutContext колдует над подключением вашей функции к источнику трассировки, который определен именем атрибута.

Давайте пока не будем говорить о контексте.

Наконец, строку, присваивающую значение m_myInt:

myObject->m_myInt = 1234;

следует понимать как вызов operator= для переменной-члена m_myInt с целым числом 1234 передаваемым в качестве параметра.

Поскольку m_myInt является TracedValue, этот оператор определен для выполнения обратного вызова, который возвращает void и принимает два целочисленные значения в качестве параметров — старое и новое значения для рассматриваемого целого числа. Это функция с той же сигнатурой, что и у определенной нами функции обратного вызова — IntTrace.

Подводя итог, можно сказать, что источник трассировки — это, по сути, переменная, которая хранит список обратных вызовов. Функция трассировки используется как цель обратного вызова. Информационные системы типов Attribute и Object используются для обеспечения возможности подключения источников трассировки к приемникам трассировки. Акт «воздействия» на источник трассировки — это выполнение оператора источника трассировки, который запускает обратные вызовы. Они используют данные переданные источником как параметры обратного вызова.

Если вы сейчас скомпилируете и запустите этот пример,

$ ./waf --run fourth

вы увидите, что выходные данные из функции IntTrace будут выведены сразу после доступа к источнику трассировки:

Traced 0 to 1234

Когда мы выполнили код, источник трассировки сработал и автоматически передал для трассировки значения переменной до и после. Функция IntTrace затем распечатывает это в стандартный вывод.

myObject-> m_myInt = 1234;


7.2.2 Подключение через Config


Вызов TraceConnectWithoutContext, показанный выше в простом примере, на самом деле очень редко используется в системе. В более общем случае для выбора источника трассировки используется подсистема Config с применением так называемого Config‑пути. Мы видели пример этого в предыдущем разделе, где при эксперименте с third.cc мы подключили событие «CourseChange».

Напомним, что при моделировании мы определили приемник трассировки для печати информации об изменении курса в модели мобильности. Теперь должно быть намного понятнее, что делает эта функция:

void
CourseChange (std::string context, Ptr<const MobilityModel> model)
{
  Vector position = model->GetPosition ();
  NS_LOG_UNCOND (context <<
  " x = " << position.x << ", y = " << position.y);
}

Когда мы подключали источник трассировки «CourseChange» к указанному выше приемнику, то при организации соединения между источником трассировки по умолчанию и новым приемником использовали Config-путь для указания источника:

std::ostringstream oss;
oss << "/NodeList/"
<< wifiStaNodes.Get (nWifi - 1)->GetId ()
<< "/$ns3::MobilityModel/CourseChange";

Config::Connect (oss.str (), MakeCallback (&CourseChange));

Давайте попробуем разобраться в том, что может вызвать затруднение в понимании этого кода. Предположим, что номер узла, возвращаемый функцией GetId (), равен «7». В этом случае показанный выше путь будет содержать

"/NodeList/7/$ns3::MobilityModel/CourseChange"

Последний сегмент Config-пути должен быть атрибутом объекта. На самом деле, если у вас был указатель на Object который имеет атрибут «CourseChange», вы можете написать так же, как мы это делали в предыдущем примере. Вам к настоящему времени известно, что мы обычно храним указатели на наши узлы в NodeContainer. В примере third.cc интересующие узлы хранятся в wifiStaNodes NodeContainer. На самом деле, когда мы выстраивали путь, то использовали этот контейнер, чтобы получить Ptr <Node>, который мы использовали для вызова GetId (). Мы могли бы использовать этот Ptr <Node> для вызова. Подключите метод напрямую:

Ptr<Object> theObject = wifiStaNodes.Get (nWifi - 1);
theObject->TraceConnectWithoutContext ("CourseChange", MakeCallback (&CourseChange));

В примере third.cc мы действительно хотели, чтобы дополнительный «контекст» был доставлен вместе с параметрами обратным вызова (которые будут объяснены ниже), чтобы мы могли использовать следующий эквивалентный код:

Ptr<Object> theObject = wifiStaNodes.Get (nWifi - 1);
theObject->TraceConnect ("CourseChange", MakeCallback (&CourseChange));

Получается, что внутренний код для Config::ConnectWithoutContext и Config::Connect на самом деле находится в Ptr <Object> и вызывает на самом низком уровне соответствующий метод TraceConnect.

Функционал Config принимает путь, представляющий цепочку указателей объектов. Каждый элемент пути соответствует объекту Attribute. Последний элемент является интересующим атрибутом, чтобы его можно было найти должны быть указаны предыдущие элементы. Код Config анализирует и «идет» по этому пути, пока не дойдет до последнего элемента. Затем интерпретирует его в качестве атрибута последнего объекта, который он нашел при движении по пути.

Функционал Config затем вызывает для конечного объекта соответствующий метод TraceConnect или TraceConnectWithoutContext. Давайте посмотрим на происходящее более подробно. Ведущий символ «/» в пути относится к так называемому пространству имен. Одно из предопределенных пространств имен в конфигурации системы это «NodeList», которое представляет собой список всех узлов в симуляции. Элементы в списке обозначены индексами (вспомните индексы начинаются с 0), поэтому «/NodeList/7» относится к восьмому узлу в списке узлов, созданных во время моделирования. Эта ссылка на самом деле является Ptr <Node> и, следовательно, является подклассом ns3::Object.

Как описано в разделе «Объектная модель» руководства ns-3, мы широко используем агрегацию объектов. Этот позволяет нам формировать ассоциации между различными объектами без создания сложного дерева наследования или предопределения какие объекты будут частью узла. Каждый объект в агрегации может быть достигнут из других объектов.

В нашем примере следующий пройденный сегмент пути начинается с символа «$». Это указывает на систему конфигурации, и на то, что сегмент является именем типа объекта, поэтому для поиска этого типа должен выполняться вызов GetObject. Получается, что MobilityHelper, используемый в third.cc, объединяет или связывает модель мобильности с каждым беспроводным узлом. Когда вы добавляете «$», вы запрашиваете другой объект, который предположительно был ранее агрегирован. Вы можете понимать это как переключение указателей с исходного Ptr <Node>, как указано в «/NodeList/7», на связанная с ним модель мобильности, которая имеет тип ns3::MobilityModel. Если вы знакомы с GetObject, мы попросили систему сделать следующее:

Ptr<MobilityModel> mobilityModel = node->GetObject<MobilityModel> ()

Теперь мы находимся на последнем объекте пути, поэтому мы обращаем наше внимание на атрибуты этого объекта. Класс MobilityModel определяет атрибут под названием «CourseChange». Вы можете увидеть это, посмотрев на исходный код в src/mobility/model/mobility-model.cc и поискав «CourseChange» в вашем любимом редакторе. Вы должны найти код

.AddTraceSource ("CourseChange",
"The value of the position and/or velocity vector changed",
MakeTraceSourceAccessor (&MobilityModel::m_courseChangeTrace),
"ns3::MobilityModel::CourseChangeCallback")

который должен выглядеть очень знакомым на данный момент. Если вы поищете соответствующее объявление базовой трассируемой переменной в mobility-model.h, то увидите

TracedCallback<Ptr<const MobilityModel> > m_courseChangeTrace;

Объявление типа TracedCallback идентифицирует m_courseChangeTrace как специальный список обратных вызовов, которые можно подключить с помощью функций Config, описанных выше. В заголовочном файле также есть typedef для сигнатуры функции обратного вызова:

typedef void (* CourseChangeCallback)(Ptr<const MobilityModel> * model);

Класс MobilityModel разработан как базовый класс, обеспечивающий общий интерфейс для всех создаваемых подклассов. Если вы выполните поиск до конца файла, вы увидите метод с именем NotifyCourseChange ():

void
MobilityModel::NotifyCourseChange (void) const
{
  m_courseChangeTrace(this);
}

Производные классы будут вызывать этот метод всякий раз, когда они изменяют курс, чтобы поддержать трассировку. Этот метод вызывает operator() в базовом m_courseChangeTrace, который, в свою очередь, вызовет все зарегистрированные обратные вызовы, всех приемников трассировки, которые, путем вызова функции Config, зарегистрировали свой интерес к источнику.

Итак, в примере third.cc, который мы рассмотрели, всякий раз, когда в одном из установленных экземпляров RandomWalk2dMobilityModel, будет вызов NotifyCourseChange(), который вызывает в базовом классе MobilityModel. Как видно выше, он вызывает operator() для m_courseChangeTrace, который, в свою очередь, вызывает любые зарегистрированные потоки трассировки. В этом примере единственным кодом, регистрирующим интерес, был код, который предоставил Config-путь. Поэтому функция CourseChange, которая была подключена с узла номер семь, будет единственным обратным вызовом.

Последний кусок пазла — «контекст». Напомним, что мы видели результат, похожий на следующий из third.cc:

/NodeList/7/$ns3::MobilityModel/CourseChange x = 7.27897, y =
2.22677

Первая часть вывода — это контекст. Это просто путь, по которому в конфигурационном коде расположен источник трассировки. В рассматриваемом нами случае в системе может быть любое количество источников на любое количество узлов с моделями мобильности. Должен быть какой-то способ определить, какой источник трассировки запустил обратный вызов на самом деле. Самый простой способ — подключиться с помощью Config::Connect вместо Config::ConnectWithoutContext.

7.2.3 Поиск источников


У новых пользователей системы трассировки неизбежно возникают вопросы.

Первый вопрос:
«Хорошо, я знаю, что в ядре симуляции должны быть источники трассировки, но как мне узнать, какие из них мне доступны?»
Второй вопрос:
«Хорошо, я нашел источник трассировки, как мне определить Config-путь, который нужно использовать для подключения к нему?»
Третий вопрос:
«Хорошо, я нашел источник трассировки и Config-путь, как мне выяснить, каковы должны быть у моей функции обратного вызова возвращаемый тип и формальные аргументы?»
Четвертый вопрос:
«Хорошо, я собрал все это и получил невероятно странное сообщение об ошибке, что оно означает?"

Мы рассмотрим каждый из них по очереди.

7.2.4 Доступные источники


Хорошо, я знаю, что в ядре симуляции должны быть источники трассировки, но как мне узнать, какие из них мне доступны?

Ответ на первый вопрос можно найти в документации по API ns-3. Если вы зайдете на сайт проекта ns-3, в панели навигации вы найдете ссылку «Documentation». Если вы выберете эту ссылку, то попадете на наш страницу документации. Там есть ссылка на «Latest Release», который приведет вас к документации для последней стабильной версии релиза ns-3. Если вы кликните ссылку «Documentation API», то окажетесь на странице документации к API ns-3. На боковой панели вы должны увидеть иерархию, которая начинается так:

  • ns-3
  • ns-3 Documentation
  • All TraceSources
  • All Attributes
  • All GlobalValues

В этом списке нас интересует «All TraceSources» («Все источники трассировки»). Идите дальше и выберите эту ссылку. Вы увидите, как и следовало ожидать, список всех доступных в ns-3 источников трассировки. Например, прокрутив вниз до ns3::MobilityModel, вы найдете запись

CourseChange: The value of the position and/or velocity vector changed

Вы должны узнать в ней источник трассировки, который мы использовали в примере third.cc. Будет полезно просмотреть весь список.

7.2.5 Config-пути


Хорошо, я нашел источник трассировки, как мне определить Config-путь, который нужно использовать для подключения к нему?

Если вы знаете, какой объект вас интересует, то для этого класса в разделе «Detailed Description» («Подробное описание») будут перечислены все доступные источники трассировки. Например, начиная со списка «All TraceSources», нажмите на ссылку ns3::MobilityModel, который приведет вас к документации для класса MobilityModel. Почти на самом верху страницы находится строка с кратким описанием класса, заканчивающаяся ссылкой «More…» («Далее…»). Нажмите на эту ссылку, чтобы пропустить сводку API и перейти к “Detailed Description” («Подробное описание») класса. В конце описания будет три (или менее) списка:

  • Config Paths: список типичных Config-путей для этого класса.
  • Attributes: список всех атрибутов, предоставленных этим классом.
  • TraceSources: список всех источников трассировки, доступных из этого класса.

Обсудим сначала Config-пути. Предположим, что вы только что нашли источник трассировки «CourseChange» в списке «All TraceSources», и вы хотите выяснить, как подключиться к нему. Вы знаете (опять же из примера third.cc), что используется ns3::RandomWalk2dMobilityModel. Поэтому либо нажмите на имя класса в списке «All TraceSources», либо найдите ns3::RandomWalk2dMobilityModel в “Class List” («Список классов»). В любом случае, теперь вы должны увидеть страницу «ns3::RandomWalk2dMobilityModel Class».

Если вы теперь прокрутите вниз до раздела «Detailed Description» («Подробное описание»), после сводного списка методов и атрибутов класса (или просто нажмите на ссылку «More…» («Далее…») в конце краткого описания класса в верхней части страницы), вы увидите общую документацию класса. Продолжая прокручивать страницу вниз, найдите список «Config Paths»(«Config-пути»):

Config-пути


Модель ns3::RandomWalk2dMobilityModel доступна по путям Config::Set и Config::Connect:

  • «/ NodeList / [i] / $ ns3 :: MobilityModel / $ ns3 :: RandomWalk2dMobilityModel»

В документации рассказывается, как добраться до объекта

RandomWalk2dMobilityModel. Сравните строку выше со строкой, которую мы фактически использовали в примере кода:

"/NodeList/7/$ns3::MobilityModel"

Разница заключается в том, что в строке, найденной в документации, подразумеваются два вызова GetObject. Первый запросит агрегацию базового класса для $ns3::MobilityModel. Второй используется для приведения базового класса $ns3::RandomWalk2dMobilityModel к конкретной реализации класса. Документация показывает вам обе эти операции. Оказывается, что искомый источник трассировки находится фактически в базовом классе.

Просматривая далее список источников трассировки в разделе «Detailed Description» («Подробное описание») вы обнаружите текст:

«No TraceSources are defined for this type.» (Источники трассировки для этого типа не определены .)

TraceSources, определенны в родительском классе ‘‘ns3::MobilityModel‘‘

• CourseChange: значение положения и / или вектора скорости изменилось.

Сигнатура обратного вызова:
 ns3::MobilityModel::CourseChangeCallback



Это именно то, что вам нужно было узнать. Интересующий вас источник трассировки находится в ns3::MobilityModel (которую вы знаете в любом случае). Интересно, что этот фрагмент API-документации говорит вам о том, что вам не нужен Config-путь выше, чтобы добраться до конкретного класса, поскольку источник трассировки фактически находится в базовом классе. Следовательно дополнительный GetObject не требуется, и вы просто используете путь:

"/NodeList/[i]/$ns3::MobilityModel"

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

"/NodeList/7/$ns3::MobilityModel"

Кроме того, еще один способ получить Config-путь — это поискать в базе кода ns-3 у того, кто уже догадался как это сделать. Вы всегда должны пытаться скопировать чужой рабочий код, прежде чем начать писать свой собственный. Попробуйте что-то вроде:

$ find . -name '*.cc' | xargs grep CourseChange | grep Connect

и вы можете найти свой ответ вместе с рабочим кодом. Например, в этом случае src/mobility/examples/ В main-random-topology.cc есть то, что только и ждет, чтобы вы его использовали:

Config::Connect ("/NodeList/*/$ns3::MobilityModel/CourseChange",
MakeCallback (&CourseChange));

Мы скоро вернемся к этому примеру.

7.2.6 Сигнатуры обратных вызовов


Хорошо, я нашел источник трассировки и Config-путь, как мне выяснить, возвращаемый тип и какие формальные аргументы должны быть у моей функции обратного вызова?

Самый простой способ — изучить сигнатуру обратного вызова typedef, которая указана в «сигнатуре обратного вызова» трассировки. Источник в «Detailed Description» для класса, как показано выше.

Повторяя запись источника трассировки «CourseChange» из

ns3::RandomWalk2dMobilityModel, мы имеем:

  • CourseChange: значение положения и / или вектора скорости изменилось.

Сигнатура обратного вызова:
ns3::MobilityModel::CourseChangeCallback



Сигнатура обратного вызова дается как ссылка на соответствующий typedef, где мы находим

typedef void (* CourseChangeCallback) (std :: string context,
        Ptr <const MobilityModel> * model);

cигнатур для уведомлений об изменении курса TracedCallback.

Если обратный вызов подключен с использованием ConnectWithoutContext, то пропустите аргумент context из сигнатуры.

Параметры:

[in] context Строка контекста, предоставляемая источником трассировки.

[in] model модель MobilityModel, которая меняет курс.

Как и ранее, чтобы увидеть пример использования, примените grep к кодовой базе ns-3. Пример выше, из src/mobility/examples/main-random-topology.cc, соединяет источник трассировки CourseChange с функцией CourseChange в том же файле:

static void
CourseChange (std::string context, Ptr<const MobilityModel> model)
{
  ...
}

Обратите внимание, что эта функция:

  • Принимает строковый аргумент «context», который мы опишем через минуту. (Если обратный вызов подключен с использованием функции ConnectWithoutContext аргумент контекста будет пропущен.)
  • Имеет MobilityModel как последний аргумент (или как единственный аргумент, если используется ConnectWithoutContext).
  • Возвращает void.

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

Прежде чем приступить к запуску кода, я скажу вам простой способ выяснить это: возвращаемое значение вашего обратного вызова всегда будет void. Формальный список параметров для TracedCallback можно найти в объявлении списке параметров шаблона. Напомним, что для нашего текущего примера он в mobility-model.h, где мы ранее нашли:

TracedCallback<Ptr<const MobilityModel> > m_courseChangeTrace;

Существует однозначное соответствие между списком параметров шаблона в объявлении и формальными аргументами функции обратного вызова. Здесь есть один параметр шаблона, которым является Ptr <const MobilityModel>. Этот говорит вам, что нужна функция, которая возвращает void и принимает Ptr <const MobilityModel>. Например:

void
CourseChange (Ptr<const MobilityModel> model)
{
  ...
}

Это все, что нужно, если вы хотите использовать

Config::ConnectWithoutContext. Если вам необходим контекст, то нужен Config::Connect и используйте функцию обратного вызова, которая принимает строковый контекст, и с ним аргументы шаблона:

void
CourseChange (std::string context, Ptr<const MobilityModel> model)
{
  ...
}

Если вы хотите быть уверенным, что функция CourseChangeCallback видна только в вашем локальном файле, то можете добавить ключевое слово static:

static void
CourseChange (std::string path, Ptr<const MobilityModel> model)
{
  ...
}

это именно то, что мы использовали в примере third.cc.

Реализация


Этот раздел не является обязательным. Это будет нелегкое путешествие, особенно для тех, кто не знаком с деталями шаблонов. Однако, если вы справитесь с ним, у вас будет очень хорошее понимание многих низкоуровневых идиом ns-3.

Итак, еще раз, давайте выясним, какая сигнатура функции обратного вызова требуется для источника трассировки «CourseChange». Этот будет мучительно, но вам нужно сделать это только один раз. После того, как вы пройдете через это, вы сможете просто посмотреть на TracedCallback и все понять.

Первое, на что нам нужно обратить внимание — это объявление источника трассировки. Напомним, что оно в mobility-model.h, где мы ранее нашли:

TracedCallback<Ptr<const MobilityModel> > m_courseChangeTrace;

Это объявление для шаблона. Параметр шаблона находится внутри угловых скобок, поэтому мы действительно заинтересованы выяснить, что это за TracedCallback <>. Если вы абсолютно не знаете, где это может быть найдено, то grep вам в помощь.

Возможно, нас заинтересуют некоторые объявления в исходнике ns-3, поэтому сначала перейдем в src директорию. Затем мы знаем, что это объявление должно быть в каком-то заголовочном файле, поэтому просто запустите grep, используя:

$ find . -name '*.h' | xargs grep TracedCallback

Вы увидите, как пролетят 303 строки (чтобы узнать, насколько это ужасно, я перенаправил вывод в wc). Хотя это может показаться слишком много, но на самом деле это не много. Просто пропустите вывод через more и начните просматривать через него. В содержимом первой страницы вы увидите подозрительно похожие на шаблоны строки.

TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::TracedCallback ()
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::ConnectWithoutContext (c ...
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::Connect (const CallbackB ...
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::DisconnectWithoutContext ...
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::Disconnect (const Callba ...
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::operator() (void) const ...
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::operator() (T1 a1) const ...
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::operator() (T1 a1, T2 a2 ...
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::operator() (T1 a1, T2 a2 ...
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::operator() (T1 a1, T2 a2 ...
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::operator() (T1 a1, T2 a2 ...
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::operator() (T1 a1, T2 a2 ...
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::operator() (T1 a1, T2 a2 ...

Оказывается, все это извлечено из заголовочного файла traced-callback.h, который называется очень многообещающе. Вы можете затем заглянуть в mobility-model.h и увидите, что есть строка, которая подтверждает эту догадку:

#include "ns3/traced-callback.h"

Конечно, вы могли бы подойти к этому с другой стороны и начать с рассмотрения включений в mobility-model.h и заметив включение traced-callback.h сделать вывод, что это как раз нужный файл.

В любом случае, следующий шаг — взглянуть на src/core/model/traced-callback.h в вашем любимом редакторе, чтобы узнать, что происходит.

Вы увидите комментарий в верхней части файла, который должен утешить:



An ns3::TracedCallback has almost exactly the same API as a normal ns3::Callback but instead of forwarding calls to a single function (as an ns3::Callback normally does), it forwards calls to a chain of ns3::Callback.



Перевод:


Ns3::TracedCallback имеет почти тот же API, что и обычный ns3::Callback, но вместо переадресации вызова одной функции (как это обычно делает ns3::Callback), она перенаправляет вызовы в цепочку состоящую из ns3::Callback.




Это должно звучать очень знакомо и показать, что вы на правильном пути.

Сразу после этого комментария вы обнаружите

template<typename T1 = empty, typename T2 = empty,
         typename T3 = empty, typename T4 = empty,
         typename T5 = empty, typename T6 = empty,
         typename T7 = empty, typename T8 = empty>
class TracedCallback
{
...
Это говорит о том, что TracedCallback является шаблонным классом. Он имеет восемь возможных типов параметров со значениями по умолчанию. Вернитесь и сравните это с объявлением, которое вы пытаетесь понять:

TracedCallback<Ptr<const MobilityModel> > m_courseChangeTrace;

Имя типа
typename T1

в шаблонном объявлении класса соответствует Ptr <const MobilityModel> в декларации выше. Все остальные параметры типа остаются по умолчанию. Изучение конструктора расскажет вам немного. Единственное место, где вы видели связь между вашей функцией обратного вызова и системой трассировки находится в функциях Connect и ConnectWithoutContext. Если вы прокрутите вниз, вы увидите метод

ConnectWithoutContext:

template<typename T1, typename T2,
         typename T3, typename T4,
         typename T5, typename T6,
         typename T7, typename T8>
void
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::ConnectWithoutContext ...
{
  Callback<void,T1,T2,T3,T4,T5,T6,T7,T8> cb;
  cb.Assign (callback);
  m_callbackList.push_back (cb);
}

Вы сейчас в логове зверя. Когда объявленный выше шаблон инстанциируется, компилятор заменяет T1 на Ptr <const MobilityModel>.

void
TracedCallback<Ptr<const MobilityModel>::ConnectWithoutContext ... cb
{
  Callback<void, Ptr<const MobilityModel> > cb;
  cb.Assign (callback);
  m_callbackList.push_back (cb);
}

Теперь вы можете увидеть реализацию всего, о чем мы говорили. Код создает Callback нужного типа и присваивает ему указатель на вашу функцию. Это эквивалент pfi = MyFunction, которую мы обсуждали в начале этого раздела. Затем код добавляет функцию обратного вызова в список обратных вызовов для этого источника. Осталось только посмотреть на определение обратного вызова. Используя тот же трюк с grep, который мы использовали для поиска TracedCallback, вы сможете найти файл ./core/callback.h, который нам нужен.

Если вы посмотрите в конец файла, вы увидите много, возможно, почти непонятного шаблонного кода. Вы можете в конце концов, обратиться к API документации для шаблонного класса Callback. К счастью, там есть кое-какие слова:

Шаблонный класс обратного вызова Callback.

Этот шаблонный класс реализует паттерн проектирования Functor. Он используется для объявления типа обратного вызова:

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

Попытаемся выяснить, что означает объявление:

Callback<void, Ptr<const MobilityModel> > cb;

Сейчас мы в состоянии понять, что первый (обязательный) аргумент шаблона, void, представляет тип возвращаемый обратным вызовом. Второй (необязательный) аргумент шаблона Ptr <const MobilityModel> представляет тип первого аргумента для обратного вызова.

Рассматриваемый обратный вызов — ваша функция для получения событий трассировки. Отсюда можно сделать вывод, что вам нужна функция который возвращает void и принимает Ptr <const MobilityModel>. Например,

void
CourseChangeCallback (Ptr<const MobilityModel> model)
{
  ...
}

Это все, что вам понадобиться, если вы хотите применить Config::ConnectWithoutContext. Если вам нужен контекст, то вам нужно применить

Config::Connect и использовать функцию обратного вызова, которая принимает строковый контекст. Вот почему, функция Connect предоставит вам контекст. Вам понадобиться:

void
CourseChangeCallback (std::string context, Ptr<const MobilityModel> model)
{
  ...
}

Если вы хотите, чтобы ваш CourseChangeCallback был виден только в вашем локальном файле, то можете добавить ключевое слово
static

:

static void
CourseChangeCallback (std::string path, Ptr<const MobilityModel> model)
{
  ...
}

это именно то, что мы использовали в примере third.cc. Возможно, теперь вы должны вернуться и перечитать предыдущий раздел (поверьте мне на слово). Если вы заинтересованы в более подробной информации о реализации Callbacks, не стесняйтесь взглянуть на руководство по эксплуатации ns-3. Они являются одной из наиболее часто используемых конструкций в низкоуровневых частях ns-3. Это, на мой взгляд, довольно элегантная вещь.

7.2.7 TracedValue


Ранее, в этом разделе мы представили простой фрагмент кода, который использовал TracedValue <int32_t> для демонстрации базового трассирующего кода. Мы разобрались, что такое TracedValue и как найти тип возвращаемого значения и формальные аргументы для обратного вызова. Как мы уже упоминали, файл traced-value.h содержит необходимые объявления для трассировки данных, которые подчиняются семантике значения. В общем случае семантика значений означает, что вы можете передавать сам объект, а не передавать адрес объекта. Мы расширяем это требование, чтобы включить полный набор операторов в стиле присваивания, которые предопределены для типов простых старых данных (plain old data — POD):

оператор= (присвоение)
оператор*=
оператор/=
оператор+=
оператор-=
оператор++ (и префикс и постфикс)
оператор-- (и префикс и постфикс)
оператор<<=
оператор>>=
оператор&=
оператор|=
оператор%=
оператор^=



На самом деле все это означает, что в объекте C++, который обладает семантикой значения, вы сможете отслеживать все изменения, сделанные с помощью этих операторов.

Объявление TracedValue<>, которое мы видели выше, обеспечивает инфраструктуру, которая перегружает упомянутые выше операторы и управляет процессом обратного вызова. При использовании любого из вышеперечисленных операторов с TracedValue он обеспечит как старое и так новое значение этой переменной, в данном случае значение int32_t. При результату инспекции объявления TracedValue мы знаем, что функция приемника трассировки будет иметь аргументы (int32_t oldValue, int32_t newValue). Тип возвращаемый функцией обратного вызова TracedValue всегда void, поэтому ожидаемая сигнатура функции обратного вызова для приемника traceSink будет:

void (* traceSink)(int32_t oldValue, int32_t newValue);

.AddTraceSource в методе GetTypeId предоставляет «ловушки», используемые для подключения источника трассировки к внешнему миру через систему Config. Мы уже обсуждали первые три аргумента AddTraceSource: имя атрибута для системы Config, строка справки и адрес члена данных класса TracedValue. Последний строковый аргумент, «ns3::TracedValueCallback::Int32» в примере, является именем typedef для сигнатуры функции обратного вызова. Мы требуем, чтобы эти подписи были определены, и даем полное имя типа AddTraceSource, поэтому документация API может связать источник трассировки с сигнатурой функции. Для TracedValue сигнатура простая; для TracedCallbacks, как мы убедились, действительно помогает документация к API.

7.3 Реальный пример


Давайте рассмотрим пример из одной из самых известных книг по TCP. «Иллюстрированный TCP/IP, том 1: Протоколы» У. Ричарда Стивенса — это классика. Я просто открыл книгу и наткнулся на стр. 366 на хороший график зависимости окна заторов и порядковых номеров от времени. Стивенс называет его так: «Рисунок 21.10. Значение cwnd и порядковый номер отправки при передаче данных». Давайте просто воссоздадим cwnd-часть этого графика в ns-3, используя систему трассировки и gnuplot.

7.3.1 Доступные источники


Первое, о чем нужно подумать, так это о способе которым мы хотим получить данные. Что нам нужно оттрассировать? Итак, давайте посмотрим «All Trace Sources», чтобы увидеть, с чем нам нужно работать. Напомним, что это можно найти в документации API ns-3. Если вы прокрутите список, то в конечном итоге найдете:

ns3 :: TcpSocketBase



  • CongestionWindow: окно заторов TCP-соединения
  • SlowStartThreshold: порог медленного запуска TCP (в байтах)

Оказывается, что реализация TCP ns-3 живет (в основном) в файле

src/internet/model/tcp-socket-base.cc, а варианты управления заторами находятся в таких файлах, как src/internet/model/tcp-bic.cc. Если вы не знаете об этом априори, вы можете использовать рекурсивный трюк с grep:

$ find . -name '*.cc' | xargs grep -i tcp

За страницей с экземплярами TCP, вы найдете страницу указывающую на этот файл. Обратившись к документации класса TcpSocketBase и пролистав до списка TraceSources, вы найдете

TraceSources

CongestionWindow: окно заторов TCP-соединения

Сигнатура обратного вызова: ns3::TracedValueCallback::Uint32

Нажав на ссылку typedef обратного вызова, мы увидим сигнатуру, с которой вы уже знакомы:

typedef void(* ns3::TracedValueCallback::Int32)(int32_t oldValue, int32_t newValue)

Теперь вы должны полностью понять этот код. Если у нас есть указатель на объект TcpSocketBase, и мы предоставим соответствующую функцию обратного вызова, то к источнику трассировки «CongestionWindow» можно применить TraceConnect. Это тот же источник трассировки, который мы видели в простом примере в начале этого раздела, за исключением того, что мы говорим о uint32_t вместо int32_t. И мы знаем, что должны предоставить функцию обратного вызова с такой сигнатурой.

7.3.2 Поиск примеров


При написании кода всегда плохо начинать с нуля, лучше попытаться найти рабочий код, который вы сможете затем модифицировать. Так, первым делом нужно найти некий код, который уже перехватывает источник трассировки «CongestionWindow» и посмотреть как мы можем изменить его. Как обычно, grep ваш друг:

$ find . -name '*.cc' | xargs grep CongestionWindow

Это укажет на пару многообещающих кандидатов:

examples/tcp/tcp-large-transfer.cc

и

src/test/ns3tcp/ns3tcp-cwnd-test-suite.cc.

Мы еще ни разу не заглядывали в тестовый код, поэтому давайте посмотрим на него. Обычно вы обнаружите, что тестовый код достаточно минималистичен, так что, вероятно, это хорошая мысль начать с него. Откройте файл src/test/ns3tcp/ns3tcp-cwnd-test-suite.cc в вашем любимом редакторе и поищите «CongestionWindow». Вы найдете,

ns3TcpSocket->TraceConnectWithoutContext ("CongestionWindow",
MakeCallback (&Ns3TcpCwndTestCase1::CwndChange, this));

Это должно выглядеть для вас очень знакомо. Мы упоминали выше, что если бы у нас был указатель на TcpSocketBase, мы могли бы к источнику трассировки «CongestionWindow» применить TraceConnect. Это именно то, что мы видим здесь. Получается, что эта строка кода делает именно то, что нам нужно. Давайте продолжим и извлечем из этой функции (Ns3TcpCwndTestCase1::DoRun (void)) нужный нам код. Если вы посмотрите на эту функцию, то обнаружите, что она выглядит так же, как скрипт ns-3. Оказывается, что так оно есть. Это скрипт, запускаемый тестовой средой, поэтому мы можем просто вытащить его и обернуть в main, вместо DoRun. Чтобы не проходить через это шаг за шагом, мы предоставили файл examples/tutorial/fivth.cc с результатами переноса этого теста обратно в родной скрипт ns-3.

7.3.3 Источники динамической трассировки


Пример fifth.cc демонстрирует чрезвычайно важное правило, которое вы должны понять перед использованием любого вида источника трассировки: прежде чем пытаться использовать команду Config::Connect вы должны убедиться, что для неё существует цель. Это тоже самое, что сказать, что объект должен быть создан перед попыткой обращения к нему. Хотя это может показаться на словах очевидным, но сбивает с толку многих людей, пытающихся использовать систему впервые.

Давайте на мгновение вернемся к основам. В любом сценарии ns-3 есть три основных фазы выполнения. Первая фаза иногда называется «Время конфигурации» (“Configuration Time”) или «Время настройки» (“Setup Time”) и длится в течение периода, пока главная функция вашего скрипта работает до вызова Simulator::Run. Вторая фаза иногда называется «Время моделирования» (“Simulation Time”) и существует в период времени, когда Simulator::Run активно выполняет свои события. После завершения выполнения симуляции, Simulator::Run вернет управление обратно в главную функцию. Когда это происходит, сценарий входит в так называемую «Фазу демонтажа» (“Teardown Phase”), когда структуры и объекты созданные во время фазы настройки разбираются и освобождаются.

Возможно, самая распространенная ошибка, допускаемая при попытке использовать систему трассировки, таится в предположении, что сущности, создаваемые динамически во время моделирования, доступны в фазе конфигурации. В частности, Socket ns-3 является динамическим объектом, часто создаваемым приложениями для связи между узлами (Nodes). Приложение ns-3 всегда имеет «Время начала» (“Start Time”) и «Время завершения» (“Stop Time”), связанные с ним. В подавляющем большинстве случаев приложение не будет пытаться создавать динамический объект, пока не будет вызван его метод StartApplication в некоторое «время начала». Это должно гарантировать, что симуляция полностью настроена до того, как приложение попытается что-либо сделать (что произошло бы, если оно во время фазы настройки попытается подключиться к узлу, который еще не существует?). В результате, в фазе настройки вы не сможете подключить приемник к источнику трассировки, если один из них создается динамически в фазе моделирования.

Существуют два решения этого ребуса:

  1. Создать событие симулятора, которое запускается после создания динамического объекта, и запустить трассировку, по этому событию;
  2. Создать динамический объект во время конфигурации, затем подключить его и передать системе для использования во время симуляции.

В примере fifth.cc мы использовали второй подход. Это решение потребовало от нас создания MyApp Application, целью которого является использование Socket в качестве параметра.

7.3.4 Разбор fifth.cc


Теперь давайте взглянем на пример программы, которую мы создали, проанализировав тест окна заторов. Открываем examples/tutorial/fif.cc в вашем любимом редакторе. Вы должны увидеть знакомый код:

/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
/*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation;
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Include., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include <fstream>
#include "ns3/core-module.h"
#include "ns3/network-module.h"
#include "ns3/internet-module.h"
#include "ns3/point-to-point-module.h"
#include "ns3/applications-module.h"
using namespace ns3;
NS_LOG_COMPONENT_DEFINE ("FifthScriptExample");

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

// ================================================================
//
// node 0 node 1
// +----------------+ +----------------+
// | ns-3 TCP | | ns-3 TCP |
// +----------------+ +----------------+
// | 10.1.1.1 | | 10.1.1.2 |
// +----------------+ +----------------+
// | point-to-point | | point-to-point |
// +----------------+ +----------------+
// | |
// +---------------------+
// 5 Mbps, 2 ms
//
//
// We want to look at changes in the ns-3 TCP congestion window. We need
// to crank up a flow and hook the CongestionWindow attribute on the socket
// of the sender. Normally one would use an on-off application to generate a
// flow, but this has a couple of problems. First, the socket of the on-off
// application is not created until Application Start time, so we wouldn't be
// able to hook the socket (now) at configuration time. Second, even if we
// could arrange a call after start time, the socket is not public so we
// couldn't get at it.
//
// So, we can cook up a simple version of the on-off application that does what
// we want. On the plus side we don't need all of the complexity of the on-off
// application. On the minus side, we don't have a helper, so we have to get
// a little more involved in the details, but this is trivial.
//
// So first, we create a socket and do the trace connect on it; then we pass
// this socket into the constructor of our simple application which we then
// install in the source node.
// ================================================================
//

Перевод комментария:
Мы хотим посмотреть на изменения в окне заторов TCP ns-3. Нам нужно запустить поток и подключиться к атрибуту CongestionWindow сокета отправителя. Обычно, для генерации потока отправитель использует on-off приложение, но это имеет несколько проблем. Во-первых, сокет on-off приложения создается только после старта приложения, поэтому у нас не будет возможности подключить сокет (сейчас) во время фазы настройки. Во-вторых, даже если мы сможем организовать вызов после момента старта, сокет не является открытым членом класса, поэтому мы не сможем получить доступ к нему.
Таким образом, мы можем создать простую версию приложения, которое делает то, что на нужно. С положительной стороны нам не нужна вся сложность on-off приложения. С другой стороны, у нас нет помощника, поэтому мы должны быть немного больше вовлечены в детали, но это тривиально.
Итак, сначала мы создаем сокет и подключаем к нему трассировку, затем мы передаем этот сокет в конструктор нашего простого приложения, которое мы установим на узле источнике.



Это также должно быть самоочевидно.

Следующая часть — это объявление приложения MyApp, которое мы соорудили для создания сокета во время фазы настройки.

class MyApp : public Application
{
  public:
  MyApp ();
  virtual ~MyApp();
  void Setup (Ptr<Socket> socket, Address address, uint32_t packetSize,
  uint32_t nPackets, DataRate dataRate);

  private:
  virtual void StartApplication (void);
  virtual void StopApplication (void);

  void ScheduleTx (void);
  void SendPacket (void);
  
  Ptr<Socket> m_socket;
  Address m_peer;
  uint32_t m_packetSize;
  uint32_t m_nPackets;
  DataRate m_dataRate;
  EventId m_sendEvent;
  bool m_running;
  uint32_t m_packetsSent;
};

Вы можете видеть, что этот класс наследуется от класса Application ns-3. Если вам интересно, что унаследовано — взгляните на

src/network/model/application.h.

Класс MyApp обязан переопределить методы StartApplication и StopApplication. Эти методы вызываются автоматически, когда MyApp требуется начать или остановить отправку данных во время моделирования.

Запуск/останов приложений


Стоит потратить немного времени на объяснение того, как на самом деле инициируются события в системе. Это еще одно довольно глубокое объяснение, и его можно проигнорировать, если вы не планируете углубляться в суть системы. Это, тем не менее полезно. В обсуждении затрагивается вопрос о том, как работают некоторые очень важные части ns-3 и раскрываются некоторые важные идиомы. Если вы планируете реализацию новых моделей, то, вероятно, захотите вникнуть в этот раздел.

Самый распространенный способ начать прокручивание событий — запустить приложение. Это происходит в результате следующих (надеюсь) знакомых строк скрипта ns-3:

ApplicationContainer apps = ...
apps.Start (Seconds (1.0));
apps.Stop (Seconds (10.0));

Код контейнера приложения (см. src/network/helper/application-container.h, если вам интересно) перебирает содержащиеся в нем приложения и в результате вызова apps.Start вызывает:

app->SetStartTime (startTime);

и в результате вызова apps.Stop:

app->SetStopTime (stopTime);

Конечным результатом этих вызовов является то, чего мы и хотим — симулятор автоматически выполняет вызовы наших приложений (Application), чтобы сказать им, когда запускаться или останавливаться. В случае MyApp, он наследуется от класса Application и переопределяет StartApplication и StopApplication. Это функции, которые будут вызываться симулятором в нужный момент. В MyApp вы обнаружите, что MyApp::StartApplication делает начальный Bind и Connect сокета, а затем запускает поток данных, вызывая MyApp::SendPacket. Генерацию пакетов останавливает MyApp::StopApplication, отменяя все ожидающие события отправки, а затем закрывает сокет.

Одна из приятных особенностей ns-3 состоит в том, что вы можете полностью игнорировать детали реализации того, как ваше приложение «автоволшебно» вызывается симулятором в правильное время. Но так как мы уже отважились углубиться в ns-3, то давайте приступим.

Если вы посмотрите на src/network/model/application.cc, то обнаружите, что метод SetStartTime приложения просто устанавливает переменную-член m_startTime, а метод SetStopTime банально устанавливает m_stopTime. Здесь, без каких-либо намеков, след оборвется.

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

Взгляните на src/network/model/node-list.cc и найдите NodeList::Add. Открытая (public) статическая реализация вызывает закрытую (private) реализацию с именем NodeListPriv::Add. Это относительно распространенная в ns-3 идиома. Итак, взгляните на NodeListPriv::Add. Там вы найдете,

Simulator::ScheduleWithContext (index, TimeStep (0), &Node::Initialize, node);

Это говорит о том, что всякий раз, когда в симуляции создается узел (Node), имеется побочный эффект, состоящий в том, что на нулевой момент времени будет запланирован вызов метода Initialize этого узла. При этом действия метода Initialize не совсем соответствуют его названию. Он не запускает какие либо действия узла, а всего лишь информирует его о начале симуляции.

Итак, NodeList::Add косвенно планирует вызов Node::Initialize в нулевое время, чтобы сообщить новому узлу, что симуляция началась. Если вы посмотрите в src/network/model/node.h, вы не найдете метод с называнием Node::Initialize. Оказывается, метод Initialize унаследован от класса Object. Все объекты в системе могут быть уведомлены о начале симуляции, а объекты класса Node являются лишь одним из типов таких объектов.

Далее посмотрите на src/core/model/object.cc и найдите Object::Initialize. Этот код не так прост, как вы могли ожидать, поскольку объекты ns-3 поддерживают агрегацию. Код в Object::Initialize последовательно проходит по всем объектам, которые были агрегированы вместе, и вызывает их метод DoInitialize. Это еще одна идиома, которая очень распространена в ns-3, иногда ее называют «паттерн шаблонного проектирования»: открытый не виртуальный метод API, который остается постоянным в разных реализациях и вызывает закрытый виртуальный метод реализации, который наследуется и реализуется подклассами. Имена, как правило, что-то вроде MethodName для открытого API и DoMethodName для закрытого API.

Это говорит нам о том, для продолжения поиска мы должны заглянуть в src/network/model/node.cc, чтобы обнаружить метод Node::DoInitialize. Если вы найдете код, то увидите что метод, перебирает все устройства в узле, а затем все приложения в узле, вызывая device->Initialize и application->Initialize соответственно.

Возможно, вы уже знаете, что классы Device и Application наследуются от класса Object, поэтому следующим шагом будет изучить, что происходит, когда вызывается Application::DoInitialize. Взгляните на src/network/model/application.cc и вы найдете:

void
Application::DoInitialize (void)
{
  m_startEvent = Simulator::Schedule (m_startTime, &Application::StartApplication, this);
  if (m_stopTime != TimeStep (0))
  {
    m_stopEvent = Simulator::Schedule (m_stopTime, &Application::StopApplication, this);
  }
  Object::DoInitialize ();
}

Здесь мы наконец дошли до конца следа. Если при реализации ns-3 приложения, вы в точности используете этот код, то ваше новое приложение будет наследником класса Application. Переопределяя методы StartApplication и StopApplication, вы обеспечиваете механизмы запуска и остановки потока данных, исходящего из вашего нового приложения. Когда в симуляции создается узел (Node), он добавляется в глобальный список узлов. Акт добавления узла в NodeList заставляет симулятор запланировать на нулевое время событие, которое при запуске симуляции вызывает метод Node::Initialize «свежедобавленного» узла. Так как Node наследуется от Object, то для него вызывается метод Object::Initialize, который, в свою очередь, вызывает методы DoInitialize на всех объектах, агрегированных в узле (вспомните модель мобильности). Поскольку объект Node имеет переопределенный DoInitialize, он и вызывается при запуске симуляции. Метод Node::DoInitialize вызывает методы Initialize всех приложений на узле. Поскольку приложения также являются объектами Object, то вызываются Application::DoInitialize. Когда вызывается Application::DoInitialize, он планирует события для вызовов методов StartApplication и StopApplication приложения. Эти вызовы предназначены для запуска и остановки потока данных из приложения.

Это был еще один довольно долгий путь, но его нужно пройти только один раз, зато теперь вы понимаете еще один очень глубокий кусок ns-3.

Приложение MyApp


Приложению MyApp, конечно же, необходимы конструктор и деструктор:

MyApp::MyApp ()
: m_socket (0),
m_peer (),
m_packetSize (0),
m_nPackets (0),
m_dataRate (0),
m_sendEvent (),
m_running (false),
m_packetsSent (0)
{
}

MyApp::~MyApp()
{
  m_socket = 0;
}

Наличие следующего фрагмента кода — это единственная причина, по которой мы изначально написали это приложение.

void
MyApp::Setup (Ptr<Socket> socket, Address address, uint32_t packetSize,
uint32_t nPackets, DataRate dataRate)
{
  m_socket = socket;
  m_peer = address;
  m_packetSize = packetSize;
  m_nPackets = nPackets;
  m_dataRate = dataRate;
}

Этот код должен быть довольно понятным. Мы просто инициализируем переменные-члены. Важный для перспективы трассировки элемент, который нам нужно было предоставить приложению во время время настройки — это сокет Ptr<Socket>. Напомним, что мы собираемся создать Socket как TcpSocket (который реализуется TcpSocketBase) и подключить его источник трассировки «CongestionWindow», перед передачей его в метод Setup.

void
MyApp::StartApplication (void)
{
  m_running = true;
  m_packetsSent = 0;
  m_socket->Bind ();
  m_socket->Connect (m_peer);
  SendPacket ();
}

Приведенный выше код представляет собой переопределенную реализацию Application::StartApplication, которая будет автоматически вызывается симулятором, чтобы запустить наше приложение в нужное время. Вы можете видеть, что он делает операцию привязки сокета (Socket Bind). Если вы знакомы с сокетами Беркли, это не должно быть сюрпризом. Это, как вы могли ожидать, требует выполнения на локальной стороне соединения. Последующий Connect сделает все необходимое для установления TCP соединения с адресом m_peer. Теперь должно быть понятно, почему мы отложили привязку до фазы моделирования, так как для завершения действий Connect требуется полностью функционирующая сеть. После установления подключения приложение начинает создавать события моделирования, вызывая SendPacket.

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

void
MyApp::StopApplication (void)
{
  m_running = false;
  if (m_sendEvent.IsRunning ())
  {
     Simulator::Cancel (m_sendEvent);
  }
  if (m_socket)
  {
     m_socket->Close ();
  }
}

Каждый раз, когда запланировано событие симуляции, создается Event. Если Event ожидает выполнения или выполняется, его метод IsRunning вернет true. В этом коде, если IsRunning() возвращает true, мы вызываем метод Cancel, который удаляет событие из очереди событий симулятора. Делая это, мы разрываем цепь событий, которая поддерживает приложение в состоянии постоянной отправки своих пакетов, и приложение умолкает. После того как мы угомонили приложение, мы закрываем сокет, который разрывает TCP-соединение.

Сокет фактически удаляется в деструкторе при выполнении
m_socket = 0

. Это удаляет последнюю ссылку на указатель Ptr <Socket>, который вызывает деструктор этого объекта.

Напомним, что StartApplication вызывал SendPacket для запуска цепочки событий, которая описывает поведение приложения.

void
MyApp::SendPacket (void)
{
  Ptr<Packet> packet = Create<Packet> (m_packetSize);
  m_socket->Send (packet);
  if (++m_packetsSent < m_nPackets)
  {
    ScheduleTx ();
  }
}

Здесь вы видите, что SendPacket делает именно это. Он создает пакет, а затем выполняет отправку, которая, если вы знаете, сокеты Беркли, выглядит именно так, как вы ожидали.

Приложение ответственно за планирование цепочки событий, поэтому в следующих строках он вызывает ScheduleTx, чтобы запланировать новое событие передачи (SendPacket) пока приложение не решит, прекратить оправку.

void
MyApp::ScheduleTx (void)
{
  if (m_running)
  {
    Time tNext (Seconds (m_packetSize * 8 / static_cast<double>  (m_dataRate.GetBitRate ())));
    m_sendEvent = Simulator::Schedule (tNext, &MyApp::SendPacket, this);
  }
}

Здесь вы видите, как ScheduleTx это делает. Если приложение работает (если StopApplication не вызывался) будет запланировано новое событие, которое снова вызовет SendPacket. Внимательный читатель заметит, что это может сбить с толку новых пользователей. Дело в скорости передачи данных приложения. Это не имеет никакого отношения к скорости передачи данных в канале передачи (Channel). Это скорость, с которой приложение создает биты. Она не учитывает какие-либо издержки для различных протоколов или каналов, которые используются для передачи данных. Если вы установите скорость передачи данных приложения (Application) такую же, что и у Channel, вы в конечном итоге получите переполнение буфера.

Приемник трассировки


Весь смысл этого упражнения — получить обратные вызовы трассировки из TCP, указывающие, что окно заторов было обновлено. Следующий фрагмент кода реализует соответствующий приемник трассировки:

static void
CwndChange (uint32_t oldCwnd, uint32_t newCwnd)
{
  NS_LOG_UNCOND (Simulator::Now ().GetSeconds () << "\t" << newCwnd);
}

Это должно быть очень знакомо вам, поэтому мы не будем останавливаться на деталях. Эта функция только регистрирует текущее время симуляции и новое значение окна заторов каждый раз, когда оно изменяется. Вы, вероятно, можете себе представить, что могли бы загрузить полученный вывод в графическую программу (gnuplot или Excel) и сразу же увидеть симпатичный график поведения окна заторов в зависимости от времени.

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

static void
RxDrop (Ptr<const Packet> p)
{
  NS_LOG_UNCOND ("RxDrop at " << Simulator::Now ().GetSeconds ());
}

Этот приемник трассировки будет подключен к источнику трассировки PhyRxDrop устройства точка-точка NetDevice. Источник трассировки срабатывает, когда физический уровень устройства NetDevice отбрасывает пакет. Если вы сделаете небольшой крюк и обратитесь к источнику (src/point-to-point/model/point-to-point-net-device.cc) то увидите, что источник трассировки ссылается на PointToPointNetDevice::m_phyRxDropTrace. Если вы посмотрите в src/point-to-point/model/point-to-point-net-device.h, то обнаружите, что эта переменная-член объявлена как TracedCallback <Ptr <const Packet>>. Это подскажет вам, что целью обратного вызова должна быть функция, которая возвращает void и принимает единственный параметр, которым является Ptr <const Packet> (при условии, что мы используем ConnectWithoutContext) — именно то, что мы имеем выше.

Основная программа


Следующий код должен быть вам уже знаком:

int
main (int argc, char *argv[])
{
  NodeContainer nodes;
  nodes.Create (2);
  PointToPointHelper pointToPoint;
  pointToPoint.SetDeviceAttribute ("DataRate", StringValue ("5Mbps"));
  pointToPoint.SetChannelAttribute ("Delay", StringValue ("2ms"));
  NetDeviceContainer devices;
  devices = pointToPoint.Install (nodes);

Он создает два узла с двухточечным каналом между ними, как показано на рисунке в начале файла. Следующие несколько строк кода показывают что-то новое. Если мы проследим соединение, которое ведет себя безупречно, то мы получим монотонно увеличивающееся окно заторов. Чтобы увидеть какое-нибудь интересное поведение, мы должны реально внести ошибки соединения, которые приведут к потере пакетов, вызовет дублирование ACK и создаст более интересные варианты поведения окна заторов. Ns-3 предоставляет объекты ErrorModel, которые могут быть присоединены к каналам. Мы используем RateErrorModel, что позволяет нам внести в канал ошибки с заданной интенсивностью.

Ptr<RateErrorModel> em = CreateObject<RateErrorModel> ();
em->SetAttribute ("ErrorRate", DoubleValue (0.00001));
devices.Get (1)->SetAttribute ("ReceiveErrorModel", PointerValue (em));

Приведенный выше код создает экземпляр объекта RateErrorModel, и мы устанавливаем для атрибута «ErrorRate» желаемую величину. Затем мы устанавливаем полученный экземпляр RateErrorModel в качестве модели ошибок, используемой устройством точка-точка NetDevice. Это даст нам несколько повторных передач и сделает наш сюжет немного более интересным.

InternetStackHelper stack;
stack.Install (nodes);
Ipv4AddressHelper address;
address.SetBase ("10.1.1.0", "255.255.255.252");
Ipv4InterfaceContainer interfaces = address.Assign (devices);

Код выше должен быть знакомым. Он устанавливает интернет-стеки на наших двух узлах, создает интерфейсы и назначает IP‑адреса для устройств точка-точка.

Поскольку мы используем TCP, нам нужно что-то на узле назначения для TCP-соединений и получения данных. Для этой цели в ns-3 обычно используется приложение PacketSink.

uint16_t sinkPort = 8080;
Address sinkAddress (InetSocketAddress(interfaces.GetAddress (1), sinkPort));
PacketSinkHelper packetSinkHelper ("ns3::TcpSocketFactory",
InetSocketAddress (Ipv4Address::GetAny (), sinkPort));
ApplicationContainer sinkApps = packetSinkHelper.Install (nodes.Get (1));
sinkApps.Start (Seconds (0.));
sinkApps.Stop (Seconds (20.));

Это все должно быть знакомо, за исключением,

PacketSinkHelper packetSinkHelper ("ns3::TcpSocketFactory",
InetSocketAddress (Ipv4Address::GetAny (), sinkPort));

Этот код создает экземпляр PacketSinkHelper и говорит ему создавать сокеты, используя класс ns3::TcpSocketFactory. Этот класс реализует шаблон проектирования под названием «фабрика объектов», который является широко используемый механизмом определения класса, используемого для абстрактного создания объектов. Здесь вместо того, чтобы создавая сами объекты, вы предоставляете PacketSinkHelper строку, которая задает TypeId, используемый при создании объекта, который затем может быть использован, в свою очередь, для создания экземпляров объектов, созданных фабрикой.

Оставшийся параметр сообщает приложению, к каким адресу и порту оно должно привязаться.

Следующие две строки кода создадут сокет и подключат источник трассировки.

Ptr<Socket> ns3TcpSocket = Socket::CreateSocket (nodes.Get (0),
TcpSocketFactory::GetTypeId ());
ns3TcpSocket->TraceConnectWithoutContext ("CongestionWindow",
MakeCallback (&CwndChange));

Первый оператор вызывает статическую функцию-член Socket::CreateSocket и предоставляет Node и явный TypeId для фабрики объектов, используемой для создания сокета. Это вызов более низкого уровня, чем вызов PacketSinkHelper выше и использует явный тип C++ вместо того, на который ссылается строка. В остальном это концептуально то же самое.

Как только TcpSocket создан и присоединен к узлу, мы можем использовать TraceConnectWithoutContext для подключения источника трассировки CongestionWindow к нашему приемнику трассировки.

Напомним, что мы написали приложение, чтобы иметь возможность создать сокет (в фазе настройки) и использовать его во время симуляции. Теперь нам нужно создать экземпляр этого приложения. Мы не озаботились созданием помощника для управления приложением, поэтому нам придется создавать и настраивать его «вручную». Это на самом деле довольно легко:

Ptr<MyApp> app = CreateObject<MyApp> ();
app->Setup (ns3TcpSocket, sinkAddress, 1040, 1000, DataRate ("1Mbps"));
nodes.Get (0)->AddApplication (app);
app->Start (Seconds (1.));
app->Stop (Seconds (20.));

Первая строка создает объект типа MyApp — наше приложение. Вторая строка сообщает приложению какой сокет использовать, к какому адресу подключаться, сколько данных отправлять по каждому событию отправки, сколько сгенерировать событий отправки и битрейт, с которым выдавать данные по этим событиям.

Затем мы вручную добавляем приложение MyApp в исходный узел и явно вызываем Start и Stop методы приложения, чтобы сказать ему, когда начинать и прекращать делать свою работу.

Нам нужно на самом деле подключить наш обратный вызов RxDrop к событию потери данных устройством-получателем точка-точка NetDevice.

devices.Get (1)->TraceConnectWithoutContext("PhyRxDrop", MakeCallback (&RxDrop));

Теперь должно быть очевидно, что мы получаем ссылку на принимающий узел NetDevice из его контейнера чтобы подключить источник трассировки, определенный на этом устройстве атрибутом «PhyRxDrop», к приемнику трассировки RxDrop. Наконец, мы говорим симулятору независимо от приложений просто прекратить обработку событий после 20 секунд моделирования.

  Simulator::Stop (Seconds(20));
  Simulator::Run ();
  Simulator::Destroy ();
  return 0;
}

Напомним, что как только вызывается Simulator::Run, фаза конфигурации заканчивается и начинается фаза моделирования. Вся работа, которую мы организовали, создав приложение и научив его подключаться и отправлять данные, на самом деле происходит во время вызова этой функции. Как только Simulator::Run вернет управление, симуляция завершится и мы вступаем в фазу демонтажа. В этом случае, Simulator::Destroy позаботится о мрачных деталях, и мы просто возвращаем код успеха после его завершения.

7.3.5 Запуск fith.cc


Так как мы предоставили вам файл fith.cc, то после сборки дистрибутива в отладочном режиме (поскольку он использует NS_LOG, в отличие от оптимизированной сборки, которая убирает NS_LOG), то скомпилированный пример будет будет ждать когда вы его запустите.

$ ./waf --run fifth
Waf: Entering directory `/home/craigdo/repos/ns-3-allinone-dev/ns-3-dev/build'
Waf: Leaving directory `/home/craigdo/repos/ns-3-allinone-dev/ns-3-dev/build'
'build' finished successfully (0.684s)
1 536
1.0093 1072
1.01528 1608
1.02167 2144
...
1.11319 8040
1.12151 8576
1.12983 9112
RxDrop at 1.13696
...

Возможно, вы сразу увидите обратную сторону использования печати любого рода в ваших трассировках. Мы получаем эти посторонние сообщения Waf напечатанные вперемешку с интересующей нас информацией — сообщениями RxDrop. Мы исправим это в ближайшее время, но я уверен, что вам не терпится увидеть результаты всей этой работы. Давайте перенаправим этот вывод в файл с именем cwnd.dat:

$ ./waf --run fifth > cwnd.dat 2>&1

Теперь отредактируйте «cwnd.dat» в вашем любимом редакторе и удалите статус сборки waf и пропустите строки, оставляя только данные трассировки (вы также можете закомментировать TraceConnectWithoutContext ("PhyRxDrop", MakeCallback (& RxDrop)); в сценарии так же легко избавиться от печати. Теперь вы можете запустить gnuplot (если он у вас установлен) и попросить его сгенерировать несколько красивых картинок:

$ gnuplot
gnuplot> set terminal png size 640,480
gnuplot> set output "cwnd.png"
gnuplot> plot "cwnd.dat" using 1:2 title 'Congestion Window' with linespoints
gnuplot> exit

Теперь у вас должен получиться график зависимости окна заторов от времени, который содержится в файле «cwnd.png», он выглядит следующим образом:




7.3.6 Использование помощников среднего уровня


В предыдущем разделе мы показали, как подключить источник трассировки и, как мы надеемся, получить из симуляции интересную информацию. Возможно, вы помните, что намного раньше в этой главе мы назвали запись в стандартный вывод с помощью std::cout «тупым» инструментом. Мы также писали о том, что возникла проблема с анализом вывода журнала, состоящая в том, как выделить интересующую информацию. Если вам пришло в голову, что мы просто потратили много времени на реализацию примера, показывающего все проблемы, которые мы намереваемся исправить с помощью системы трассировки ns-3! Вы будете наверное правы. Но, потерпите нас. Мы еще не закончили.

Одна из самых важных вещей, которой мы хотим добиться — это иметь возможность легко управлять количеством выводимой симуляцией информации, и мы также хотим сохранять эти данные в файл, чтобы можно было вернуться к нему позже. Чтобы сделать это и завершить картину, мы можем воспользоваться предоставляемые в ns-3 помощниками трассировки среднего уровня.

В примере fifth.cc мы предоставляем скрипт, который записывает изменения cwnd, и события потери пакетов, на диск в отдельные файлы. Изменения cwnd хранятся в виде файла ASCII, разделенного табуляцией, а события потери пакетов сохраняются в PCAP‑файл. Изменения, для того чтобы это заработало, довольно малы.

Разбор sixth.cc


Давайте посмотрим на изменения, необходимые для перехода от fifth.cc к sixth.cc. Откройте examples/tutorial/sixth.cc в вашем любимом редакторе. Вы можете увидеть первое изменение, выполнив поиск CwndChange. Вы обнаружите, что мы изменили сигнатуры для приемников трассировки и добавили в каждый приемник одну строку, которая записывает отслеживаемую информация в поток, представляющий файл.

static void
CwndChange (Ptr<OutputStreamWrapper> stream, uint32_t oldCwnd, uint32_t newCwnd)
{
  NS_LOG_UNCOND (Simulator::Now ().GetSeconds () << "\t" << newCwnd);
  *stream->GetStream () << Simulator::Now ().GetSeconds () << "\t" << oldCwnd << "\t" << newCwnd << std::endl;
}

static void
RxDrop (Ptr<PcapFileWrapper> file, Ptr<const Packet> p)
{
  NS_LOG_UNCOND ("RxDrop at " << Simulator::Now ().GetSeconds ());
  file->Write(Simulator::Now(), p);
}

Мы добавили в приемник трассировки CwndChange параметр stream. Это объект, который держит (поддерживает в живом состоянии) поток вывода C++. Оказывается, это очень простой объект, но он решает задачи жизненного цикла потока и проблему, с которой сталкиваются даже опытные пользователи C++. Оказывается, что копирующий конструктор для std::ostream объявлен как private. Это означает, что std::ostreams не подчиняется семантике значений и не может использоваться в любом механизме, который требует копирования потока. Это включает в себя систему обратного вызова ns-3, которая, как вы можете помнить, требует объектов, которые обладают семантикой значения. Далее обратите внимание, что мы добавили в реализацию приемника трассировки CwndChange следующую строку:

*stream->GetStream () << Simulator::Now ().GetSeconds () << "\t" << oldCwnd << "\t" << newCwnd << std::endl;

Это был бы очень знакомый код, если заменить
* stream-> GetStream ()

на
std :: cout

, как в:

std::cout << Simulator::Now ().GetSeconds () << "\t" << oldCwnd << "\t" << newCwnd << std::endl;

Это показывает, что Ptr <OutputStreamWrapper> действительно просто выдает std::ofstream для вас, и вы можете использовать его здесь, как любой другой выходной поток.

Аналогичная ситуация происходит в RxDrop за исключением того, что передаваемый объект (Ptr <PcapFileWrapper>) представляет файл PCAP. В приемнике трассировки есть одна строка для записи метки времени и содержимого пакета, сбрасываемого в файл PCAP:

file->Write(Simulator::Now(), p);

Конечно, если у нас есть объекты, представляющие два файла, нам нужно их где-то создать, а также передать их приемникам трассировки. Если вы заглянете в основную функцию, вы найдете новый код для этого:

AsciiTraceHelper asciiTraceHelper;
Ptr<OutputStreamWrapper> stream = asciiTraceHelper.CreateFileStream ("sixth.cwnd");
ns3TcpSocket->TraceConnectWithoutContext ("CongestionWindow", MakeBoundCallback (& CwndChange, stream));

...

PcapHelper pcapHelper;
Ptr<PcapFileWrapper> file = pcapHelper.CreateFile ("sixth.pcap", std::ios::out, PcapHelper::DLT_PPP);
devices.Get (1)->TraceConnectWithoutContext("PhyRxDrop",
MakeBoundCallback (&RxDrop, file));

В первой части фрагмента кода, приведенного выше, мы создаем объект управляющий файлом ASCII-трассировки и используем вариант функции создания обратного вызова, чтобы организовать передачу объекта в приемник. Помощники ASCII‑трассировки предоставляют широкий набор функций, облегчающих использование текстовых (ASCII) файлов. Мы просто намерены здесь проиллюстрировать использование функции создания файлового потока.

В своей основе, функция CreateFileStream создает экземпляр объекта std :: ofstream и новый файл (или усекает (удаляет содержимое) существующего файла). Этот std::ofstream упакован в объект ns-3, что решает вопросы управления временем жизни и реализации копирующего конструктора.

Затем мы берем этот объект ns-3, представляющий файл, и передаем его в MakeBoundCallback (). Эта функция создает обратный вызов, аналогичный MakeCallback (), но он «привязывает» новое значение к обратному вызову. Это значение добавляется как первый аргумент обратного вызова, перед его вызовом.

По сути, MakeBoundCallback (& CwndChange, stream) заставляет источник трассировки добавить дополнительный параметр «stream» в начало списка формальных параметров обратного вызова, перед обращением к нему. Это меняет необходимую сигнатуру приемника CwndChange, совпадающую с показанной выше, которая включает поток как дополнительный параметр Ptr <OutputStreamWrapper>.

Во второй части кода, фрагмента выше, мы создаем экземпляр PcapHelper, чтобы сделать для нашего файла PCAP‑трассировки то же, что делали при создании AsciiTraceHelper. Строка кода,

Ptr<PcapFileWrapper> file = pcapHelper.CreateFile ("sixth.pcap",
"w", PcapHelper::DLT_PPP);

создает файл PCAP с именем sixth.pcap в режиме «w». Это означает, что новый файл будет усечен (содержимое удалено) если найден существующий файл с таким именем. Последний параметр — это «тип данных канала передачи» нового PCAP‑файла. Он такой же, как типы линков данных библиотеки PCAP, что определены в bpf.h, если вы знакомы с PCAP. В данном случае DLT_PPP указывает, что файл PCAP будет содержать пакеты с префиксом заголовков точка-точка. Именно такие пакеты приходят от нашего драйвера устройства точка-точка. Другие распространенные типы каналов передачи данных: DLT_EN10MB (10 МБ Ethernet) для csma устройств и DLT_IEEE802_11 (IEEE 802.11) для wifi устройств. Это определено в src/network/helper/trace-helper.h, если вам интересно посмотреть список. Записи в списке совпадают с таковыми в bpf.h, но мы дублируем их, чтобы избежать зависимости от исходников PCAP.

Объект ns-3, возвращаемый CreateFile, представляет собой PCAP файл, и используется в соответствующем обратном вызове точно так же, как было сделано с ASCII.

Важное примечание: необходимо отметить, что, хотя оба этих объекта объявлены очень похожими способами,

Ptr<PcapFileWrapper> file ...
Ptr<OutputStreamWrapper> stream ...

Но базовые объекты совершенно разные. Например, Ptr<PcapFileWrapper> является умным указателем на Object ns-3, довольно тяжелый объект, который поддерживает атрибуты и интегрирован в систему Config. В то время как Ptr<OutputStreamWrapper>, является умным указателем на объект с подсчетом ссылок, который является очень легковесной сущностью. Не забудьте посмотреть на объект, на который вы ссылаетесь, прежде чем делать какие-либо предположения о «Полномочиях», которые может иметь этот объект.

Например, взгляните в дистрибутиве на src/network/utils/pcap-file-wrapper.h и обратите внимание,

class PcapFileWrapper : public Object

что этот класс PcapFileWrapper является наследником Object ns-3. Затем посмотрите на src/network/model/output-stream-wrapper.h и убедитесь,

class OutputStreamWrapper : public
SimpleRefCount<OutputStreamWrapper>

что этот объект вообще не является наследником Object ns-3, это «просто» объект C++, который поддерживает встроенный счетчик ссылок. Дело в том, что если вы видите Ptr <что-то>, это не обязательно означает, что «что-то» это ns-3 объект, на который вы можете например повесить атрибуты ns-3. Теперь вернемся к примеру. Если вы соберете и запустите этот пример,

$ ./waf --run sixth

то увидите те же сообщения, что и при запуске «fifth», но в директории верхнего уровня вашего дистрибутива ns-3 появятся два новых файла:

sixth.cwnd sixth.pcap

Так как «sixth.cwnd» является текстовым ASCII файлом, вы можете просмотреть его с помощью cat или вашего любимого просмотрщика файлов.

1 0     536
1.0093  536  1072
1.01528 1072 1608
1.02167 1608 2144
...
9.69256 5149 5204
9.89311 5204 5259

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

Поскольку «sixth.pcap» является файлом PCAP, вы можете просмотреть его с помощью tcpdump.

reading from file sixth.pcap, link-type PPP (PPP)
1.136956 IP 10.1.1.1.49153 > 10.1.1.2.8080: Flags [.], seq 17177:17681, ack 1, win 32768, options [TS val 1133 ecr 1127,eol], length 504
1.403196 IP 10.1.1.1.49153 > 10.1.1.2.8080: Flags [.], seq 33280:33784, ack 1, win 32768, options [TS val 1399 ecr 1394,eol], length 504

...

7.426220 IP 10.1.1.1.49153 > 10.1.1.2.8080: Flags [.], seq 785704:786240, ack 1, win 32768, options [TS val 7423 ecr 7421,eol], length 536
9.630693 IP 10.1.1.1.49153 > 10.1.1.2.8080: Flags [.], seq 882688:883224, ack 1, win 32768, options [TS val 9620 ecr 9618,eol], length 536

В PCAP файле будут пакеты, которые были потеряны в симуляции. В файле не присутствуют другие пакеты и нет чего либо, что усложнит жизнь.

Это был долгий путь, но мы сейчас находимся в той точке, где можем оценить систему трассировки ns-3. Мы вытащили важные события из сердцевины реализации TCP и драйвера устройства. Мы сохранили эти события прямо в файлы, понятные широко известным инструментам. Мы сделали это без изменения какого-либо основного кода, и всего лишь в 18 строках кода:

static void
CwndChange (Ptr<OutputStreamWrapper> stream, uint32_t oldCwnd, 
uint32_t newCwnd)
{
  NS_LOG_UNCOND (Simulator::Now ().GetSeconds () << "\t" << newCwnd);
  *stream->GetStream () << Simulator::Now ().GetSeconds () << "\t" << oldCwnd << "\t" << newCwnd << std::endl;
}

...

AsciiTraceHelper asciiTraceHelper;
Ptr<OutputStreamWrapper> stream = asciiTraceHelper.CreateFileStream ("sixth.cwnd");
ns3TcpSocket->TraceConnectWithoutContext ("CongestionWindow", MakeBoundCallback (& CwndChange, stream));

...

static void
RxDrop (Ptr<PcapFileWrapper> file, Ptr<const Packet> p)
{
  NS_LOG_UNCOND ("RxDrop at " << Simulator::Now ().GetSeconds ());
  file->Write(Simulator::Now(), p);
}

...

PcapHelper pcapHelper;
Ptr<PcapFileWrapper> file = pcapHelper.CreateFile ("sixth.pcap", "w", PcapHelper::DLT_PPP);
devices.Get (1)->TraceConnectWithoutContext("PhyRxDrop", MakeBoundCallback (&RxDrop, file));

7.4 Помощники трассировки


Помощники трассировки ns-3 предоставляют богатую среду для настройки и выбора различных событий трассировки и их записи в файлы. В предыдущих главах, в первую очередь в 6, мы увидели несколько разновидностей вспомогательных методов трассировки, предназначенных для использования внутри других (устройств) помощников.

Возможно, вы вспомните, увидев некоторые из этих вариантов:

pointToPoint.EnablePcapAll ("second");
pointToPoint.EnablePcap ("second", p2pNodes.Get (0)->GetId (), 0);
csma.EnablePcap ("third", csmaDevices.Get (0), true);
pointToPoint.EnableAsciiAll (ascii.CreateFileStream ("myfirst.tr"));

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

В настоящее время в ns-3 существуют два основных варианта использования помощников трассировки: помощники устройств и помощники протоколов. Помощники устройств решают вопрос описания, какие трассировки должны быть включены через пару (узел, устройство). Например, вы можете указать, что PCAP-трассировка должна быть включена на определенном устройстве на определенном узле. Это следует из концептуальной модели устройства ns-3, а также концептуальных моделей различных помощников устройств. Затем, исходя из этого, именование создаваемых файлы следуют соглашению <префикс> — <узел> — <устройство>.

Помощники протокола решают вопрос описания, какие трассировки должны быть разрешены для пары протокола и интерфейса. Это следует из концептуальной модели стека протоколов ns-3, а также концептуальных моделей помощников интернет стека. Естественно, файлы трассировки должны соответствовать соглашению об именах <префикс> — <протокол> — <устройства>.

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

PCAP
ASCII
Помощник устройства
V
V
Помощник протокола
V
V



Чтобы добавить функциональность трассировки в наши вспомогательные классы, мы используем подход под названием mixin (примесь). Миксин это класс, который обеспечивает функциональность, когда она наследуется подклассом. Наследование от миксина не считается формой специализации, но реальным способом коллекционировать функциональность.

Давайте кратко рассмотрим все четыре случая и соответствующие им миксины.

7.4.1 Помощники устройств



PCAP


Задача этих помощников — упростить добавление в устройство ns-3 средств полноценной PCAP-трассировки. Мы хотим, чтобы все возможные варианты PCAP-трассировки работали одинаково на всех устройствах, поэтому методы этих помощников наследуются помощниками устройств. Если вы хотите следить за обсуждением имея перед глазами реальный код, то посмотрите на src/network/helper/trace-helper.h, .

Класс PcapHelperForDevice представляет собой миксин, обеспечивающий высокоуровневую функциональность для использования PCAP-трассировки в устройстве ns-3. Каждое устройство должно реализовывать один виртуальный метод, унаследованный от этого класса.

virtual void EnablePcapInternal (std::string prefix, Ptr<NetDevice> nd, bool promiscuous, bool explicitFilename) = 0;

Сигнатура этого метода отражает «устройствоцентрический» взгляд на происходящее в этом уровне. Все публичные методы, унаследованные от класса PcapUserHelperForDevice, сводятся к вызову этого единственного, зависящего от реализации устройства, метода. Например, метод PCAP самого низкого уровня,

void EnablePcap (std::string prefix, Ptr<NetDevice> nd, bool promiscuous = false, bool explicitFilename = false);

будет напрямую вызывать EnablePcapInternal, реализацию в устройстве. Все другие общедоступные методы PCAP-трассировки опираются на эту реализацию, чтобы обеспечить дополнительную функциональность на уровне пользователя. Для пользователя это означает, что если устройство правильно реализует EnablePcapInternal, то у помощников всех устройств в системе будут доступны все методы PCAP-трассировки и они будут работать одинаково для всех устройств.

Методы



void EnablePcap (std::string prefix, Ptr<NetDevice> nd, bool promiscuous = false, bool explicitFilename = false);
void EnablePcap (std::string prefix, std::string ndName, bool promiscuous = false, bool explicitFilename = false);
void EnablePcap (std::string prefix, NetDeviceContainer d, bool promiscuous = false);
void EnablePcap (std::string prefix, NodeContainer n, bool promiscuous = false);
void EnablePcap (std::string prefix, uint32_t nodeid, uint32_t deviceid, bool promiscuous = false);
void EnablePcapAll (std::string prefix, bool promiscuous = false);

В каждом из методов, показанных выше, есть параметр по умолчанию, названный promiscuous («неразборчивый»), который по умолчанию равен false. Этот параметр указывает, что трассировка не должна выполняться для неразборчивого режима. Если вы хотите, чтобы ваши трассы включали весь трафик, видимый устройством (и если устройство поддерживает неразборчивый режим), просто добавьте true в любой из вызовов выше. Например,

Ptr<NetDevice> nd;
...
helper.EnablePcap ("prefix", nd, true);

активирует захват на NetDevice, указанном в nd в неразборчивом режиме.

Первые два метода также содержат параметр по умолчанию, который называется explicitFilename, который будет обсуждаться ниже.

Рекомендуется ознакомиться с документацией API для класса PcapHelperForDevice, чтобы узнать подробности этих методов, но подведу итог…

  • Вы можете включить PCAP-трассировку для конкретной пары узел/сетевое устройство, предоставив Ptr <NetDevice> методу EnablePcap. Указатель Ptr <Node> используется неявно, поскольку сетевое устройство должно принадлежать только одному узлу (Node)(Другими словами, поскольку существует однозначное соответствие узла и устройства, метод EnablePcap получает указатель узла извлекая его из полей объекта описывающего устройство.)
    . Например,

    Ptr<NetDevice> nd;
    ...
    helper.EnablePcap ("prefix", nd);

  • Вы можете включить PCAP-трассировку для конкретной пары узел/сетевое устройство, передав методу EnablePcap строку std::string, представленную сервисом имен объектов. Ptr <NetDevice> ищется по имени в строке. Опять же, <Node> неявный, поскольку указанное сетевое устройство должно принадлежать ровно одному узлу. Например,

    Names::Add ("server" ...);
    Names::Add ("server/eth0" ...);
    ...
    helper.EnablePcap ("prefix", "server/ath0");

  • Вы можете включить PCAP-трассировку для набора пар узел/сетевое устройство, предоставив NetDeviceContainer. Для каждого NetDevice в контейнере проверяется тип. Для каждого устройства соответствующего типа (того же типа, что передан помощнику устройства), включится трассировка. Опять же, <Node> неявный, так как найденное сетевое устройство должно принадлежать только одному узлу. Например,

    NetDeviceContainer d = ...;
    ...
    helper.EnablePcap ("prefix", d);

  • Вы можете включить PCAP-трассировку для набора пар узел/сетевое устройство, предоставив NodeContainer. Будут перебираться все NetDevice в узлах, содержащихся в NodeContainer. Для каждого из этих NetDevice, проверяется тип устройства. Трассировка будет включена для всех устройств, тип которых соответствует типу, переданному помощнику устройства).
    NodeContainer n; ... helper.EnablePcap ("prefix", n);

  • Вы можете включить PCAP-трассировку указав идентификаторы (ID) узла и устройства, а также с помощью явного указателя. Каждый узел в системе имеет целочисленный ID, и каждое устройство, подключенные к узлу, имеет целочисленный ID(ID устройств нумеруются относительно узла, они не глобальны.)
    .

    helper.EnablePcap ("prefix", 21, 1);

  • Наконец, вы можете включить PCAP-трассировку для всех устройств в системе того же типа, что и переданный помощнику устройства.

    helper.EnablePcapAll ("prefix");



Имена файлов


В описаниях методов, приведенных выше, подразумевается создание полного имени файла указанным ниже методом. По соглашению, имена файлов PCAP-трассировки в системе ns-3 имеют вид Как упоминалось ранее, каждый узел в системе будет иметь назначенный системой ID и каждое устройство будет иметь индекс интерфейса (также называемый ID устройства) относительно его узла. При включении трассировки на первом устройстве узла 21, по умолчанию создается файл PCAP-трассировки, с использованием префикса «prefix» его имя будет prefix-21-1.pcap.

 <префикс> - <идентификатор узла> - <идентификатор устройства> .pcap

Вы всегда можете использовать сервис имен объектов ns-3, чтобы сделать это более понятным. Например, если вы используете сервис имен объектов для присвоения имени «сервер» узлу 21, то в результате имя файла трассировки PCAP автоматически станет, prefix-server-1.pcap, и если вы также назначите устройству имя «eth0», имя вашего файла PCAP автоматически подхватит это изменение и станет называться prefix-server-eth0.pcap.

Наконец, два метода, из показанных выше,

void EnablePcap (std::string prefix, Ptr<NetDevice> nd, bool promiscuous = false, bool explicitFilename = false);
void EnablePcap (std::string prefix, std::string ndName, bool promiscuous = false, bool explicitFilename = false);

имеют параметр по умолчанию, который называется explicitFilename (явное имя файла). При значении true этот параметр отключает автоматический механизм генерации имени файла и позволяет вам задать имя файла явно. Эта опция доступна только в методах, которые включают PCAP‑трассировку на одном устройстве.

Например, чтобы помощник устройства создал PCAP файл с захватом в неразборчивом режиме с именем my-pcap-file.pcap, можно использовать:

Ptr<NetDevice> nd;
...
helper.EnablePcap ("my-pcap-file.pcap", nd, true, true);

Первый параметр true разрешает трассировку в неразборчивом режиме, а второй указывает помощнику интерпретировать параметр prefix как полное имя файла.

ASCII


Поведение вспомогательного миксина ASCII-трассировки в значительной степени сходно с версией для PCAP. Посмотрите на src/network/helper/trace-helper.h, если вы хотите следить за обсуждением, глядя на реальный код.

Класс AsciiTraceHelperForDevice добавляет функциональность высокого уровня для использования ASCII-трассировки в классе помощника устройства. Как и в случае PCAP, каждое устройство должно реализовывать один виртуальный метод, унаследованный от миксина ASCII-трассировки.

virtual void EnableAsciiInternal (Ptr<OutputStreamWrapper> stream,
std::string prefix,
Ptr<NetDevice> nd,
bool explicitFilename) = 0;

Сигнатура этого метода отражает «устройствоцентрический» взгляд на ситуацию в этом уровне, а также тот факт, что помощник может выполнять запись в общий поток вывода. Все открытые методы, связанные с ASCII-трассировкой, унаследованные от класса AsciiTraceHelperForDevice, сводятся к вызову этого единственного зависящего от реализации устройства метода. Например, методы ASCII‑трассировки самого низкого уровня,

void EnableAscii (std::string prefix, Ptr<NetDevice> nd, bool explicitFilename = false);
void EnableAscii (Ptr<OutputStreamWrapper> stream, Ptr<NetDevice> nd);

будут напрямую вызывать реализацию EnableAsciiInternal в устройстве, предоставляя либо реальный префикс, либо поток. Все другие общедоступные методы ASCII-трассировки будут основываться на этих низкоуровневых функциях для обеспечения дополнительного пользовательского уровня функциональности. Для пользователя это означает, что все помощники устройств в системе будут иметь все доступные методы ASCII-трассировки и все эти методы будут работать одинаково на всех устройствах, если устройства реализуют EnablAsciiInternal правильно.

Методы



void EnableAscii (std::string prefix, Ptr<NetDevice> nd, bool explicitFilename = false);
void EnableAscii (Ptr<OutputStreamWrapper> stream, Ptr<NetDevice> nd);
void EnableAscii (std::string prefix, std::string ndName, bool explicitFilename = false);
void EnableAscii (Ptr<OutputStreamWrapper> stream, std::string ndName);
void EnableAscii (std::string prefix, NetDeviceContainer d);
void EnableAscii (Ptr<OutputStreamWrapper> stream, NetDeviceContainer d);
void EnableAscii (std::string prefix, NodeContainer n);
void EnableAscii (Ptr<OutputStreamWrapper> stream, NodeContainer n);
void EnableAsciiAll (std::string prefix);
void EnableAsciiAll (Ptr<OutputStreamWrapper> stream);
void EnableAscii (std::string prefix, uint32_t nodeid, uint32_t deviceid, bool explicitFilename);
void EnableAscii (Ptr<OutputStreamWrapper> stream, uint32_t nodeid, uint32_t deviceid);

Рекомендуется ознакомиться с документацией API для класса

AsciiTraceHelperForDevice, чтобы узнать подробности из этих методов, но подведу итог…

  • Для ASCII-трассировки доступно в два раза больше методов, чем для PCAP-трассировки. Это потому, что в дополнение к модели в стиле PCAP, где трассы от каждой уникальной пары узел/устройство записываются в уникальный файл, мы поддерживаем модель, в которой трассировочная информация от многих пар узлов/устройств записывается в общий файл. Этот означает, что механизм генерации имени файла заменен механизмом ссылки на общий файл; и число методов API удваивается, чтобы разрешить все комбинации.

     <префикс> - <узел> - <устройство>

  • Как и в случае PCAP-трассировки, вы можете включить ASCII-трассировку для конкретной пары (узел, сетевое устройство), передав Ptr <NetDevice> в метод EnableAscii. Ptr <Node> неявный, так как сетевое устройство может принадлежать только одному узлу. Например,

    Ptr<NetDevice> nd;
    ...
    helper.EnableAscii ("prefix", nd);

  • Первые четыре метода также включают параметр по умолчанию, который называется explicitFilename, который работает аналогично эквивалентным параметрам в случае PCAP. Но теперь контексты трассировки не записываются в файл ASCII-трассировки, поскольку они будут избыточными. Система будет выбирать имя для создаваемого файла, используя те же правила, которые описаны в разделе PCAP, за исключением того, что файл будет иметь суффикс .tr вместо .pcap.
  • Если вы хотите включить ASCII-трассировку на более чем одном сетевом устройстве и отправить все трассировки в один файл, вы может сделать это с помощью объекта ссылки на единый файл. Мы уже видели это на примере cwnd выше: В этом случае контексты трассировки записываются в файл ASCII-трассировки, поскольку они необходимы для устранения неоднозначности трассировок от двух устройств. Обратите внимание, что, поскольку пользователь полностью указывает имя файла, строка должна включать для единообразия суффикс «.tr».

    Ptr<NetDevice> nd1;
    Ptr<NetDevice> nd2;
    ...
    Ptr<OutputStreamWrapper> stream = asciiTraceHelper.CreateFileStream ("trace-file-name.tr");
    ...
    helper.EnableAscii (stream, nd1);
    helper.EnableAscii (stream, nd2);

  • Вы можете включить ASCII-трассировку для конкретной пары (узел, сетевое устройство), предоставив методу EnablePcap строку std::string, представляющую строку от сервиса имен объектов. Ptr<NetDevice> ищется по имени из строки. Опять же, <Node> неявный, поскольку указанное сетевое устройство должно принадлежать ровно одному узлу. Например, Это приведет к созданию двух файлов с именами prefix-client-eth0.tr и prefix-server-eth0.tr с трассировками для каждого устройства в соответствующем файле трассировки. Поскольку все функции EnableAscii перегружены в обертке для работы с потоками, вы можете использовать такую форму:

    Names::Add ("client" ...);
    Names::Add ("client/eth0" ...);
    Names::Add ("server" ...);
    Names::Add ("server/eth0" ...);
    ...
    helper.EnableAscii ("prefix", "client/eth0");
    helper.EnableAscii ("prefix", "server/eth0");

    Names::Add ("client" ...);
    Names::Add ("client/eth0" ...);
    Names::Add ("server" ...);
    Names::Add ("server/eth0" ...);
    ...
    Ptr<OutputStreamWrapper> stream = asciiTraceHelper.CreateFileStream ("trace-file-name.tr");
    ...
    helper.EnableAscii (stream, "client/eth0");
    helper.EnableAscii (stream, "server/eth0");


Это приведет к созданию одного файла трассировки с именем trace-file-name.tr, который содержит все события трассировки для обоих устройств. События будут замещены строками контекста трассировки.

  • Вы можете включить ASCII-трассировку для набора пар (узел, сетевое устройство), предоставив NetDeviceContainer. Для каждого NetDevice в контейнере проверяется тип. Трассировка включится для каждого устройства соответствующего типа (того же типа, что и полученный помощником устройства). Опять же, <Node> является неявным, поскольку найденное сетевое устройство может принадлежать только одному узлу. Например, Это приведет к созданию нескольких файлов ASCII-трассировки, каждый из которых следует соглашению
    <префикс> - <идентификатор узла> - <идентификатор устройства>.tr

    .
    NetDeviceContainer d = ...;
    ...
    helper.EnableAscii ("prefix", d);

  • Объединение всех трасс в один файл выполняется аналогично приведенным выше примерам:

    NetDeviceContainer d = ...;
    ...
    Ptr<OutputStreamWrapper> stream = asciiTraceHelper.CreateFileStream ("trace-file-name.tr");
    ...
    helper.EnableAscii (stream, d);

  • Вы можете включить ASCII-трассировку для набора пар (узел, сетевое устройство), предоставив NodeContainer. Будут перебираться все NetDevice в узлах, содержащихся в NodeContainer. Для каждого из этих NetDevice, проверяется тип устройства. Трассировка будет включена для всех устройств, тип которых соответствует типу, переданному помощнику устройства).Это приведет к созданию нескольких файлов ASCII-трассировки, каждый из которых следует соглашению. Объединение всех трасс в один файл выполняется аналогично примерам выше.

    NodeContainer n;
    ...
    helper.EnableAscii ("prefix", n);

    <префикс>-<идентификатор узла>-<идентификатор устройства>.tr

  • Вы можете включить ASCII-трассировку на основе идентификаторов узла и устройства, а также с помощью явного указателя. У каждого узла в системе имеется целочисленный ID и каждое устройство, подключенное к узлу, тоже имеет целочисленный ID.Конечно, трассы могут быть объединены в один файл, как показано выше.
    helper.EnableAscii ("prefix", 21, 1);

  • Наконец, вы можете включить ASCII-трассировку для всех устройств в системе того же типа, что и переданный помощнику устройств.Это приведет к созданию нескольких файлов ASCII-трассировки, по одному на каждого устройство системы с типом переданным помощнику. Все эти файлы будут следовать соглашению. Объединение всех трасс в один файл выполняется аналогично приведенным выше примерам.
    helper.EnableAsciiAll ("prefix");

    <префикс>-<идентификатор узла>-<идентификатор устройства>.tr



Имена файлов


В приведенных выше описаниях методов в стиле префикса подразумевается создание полных имен файлов посредством показанного ниже метода. По соглашению ASCII-трассировки в системе ns-3 имеют вид .

<префикс>-<идентификатор узла>-<идентификатор устройства>.tr

Как упоминалось ранее, каждый узел в системе будет иметь назначенный системой ID узла и каждое устройство будет иметь индекс интерфейса (также называемый ID устройства) относительно его узла. По умолчанию в результате включения трассировки на первом устройстве узла 21 создается файл ASCII-трассировки с использованием префикса «prefix»: prefix-21-1.tr.

Вы всегда можете использовать сервис имен объектов ns-3, чтобы сделать это более понятным. Например, если вы используете сервис имен объектов для присвоения имени «server» узлу 21, результирующее имя файла ASCII-трассировки автоматически станет prefix-server-1.tr и если вы также назначите устройству имя «eth0», имя файла ASCII-трассировки автоматически подхватит его и станет называться prefix-server-eth0.tr.

Некоторые из методов имеют параметр по умолчанию, который называется explicitFilename. Когда его значение установлено в true, этот параметр отключает механизм автоматического завершения имени файла и позволяет вам создать явное имя файла. Эта опция доступно только в тех методах, которые принимают префикс и включают трассировку на одном устройстве.

7.4.2 Помощники протоколов



PCAP


Цель этих миксинов — упростить добавление в протоколы согласованного средства PCAP-трассировки. Мы хотим, чтобы любые варианты PCAP-трассировки работали одинаково для всех протоколов, поэтому методы этих помощников наследуются помощниками стека. Если вы хотите следить за обсуждением глядя на реальный код, посмотрите на src/network/helper/trace-helper.h.

В этом разделе мы будем иллюстрировать методы применительно к протоколу Ipv4. Для трассировки в аналогичных протоколах, просто замените соответствующий тип. Например, используйте Ptr <Ipv6> вместо Ptr <Ipv4> и вызовите EnablePcapIpv6 вместо EnablePcapIpv4.

Класс PcapHelperForIpv4 обеспечивает функциональность высокого уровня для использования PCAP-трассировки в протоколе Ipv4. Каждый помощник протокола, разрешающий эти методы, должен реализовывать один виртуальный метод, унаследованный от этого класса. Там, например, будет отдельная реализация для Ipv6, но единственное отличие будет в именах методов и сигнатуре. Различные имена методов требуются для устранения из Ipv6 неоднозначности класса Ipv4, поскольку оба являются производными от класса Object и их методы, имеют одинаковую сигнатуру.

virtual void EnablePcapIpv4Internal (std::string prefix,
Ptr<Ipv4> ipv4,
uint32_t interface,
bool explicitFilename) = 0;

Сигнатура этого метода отражает протокол и «интерфейсориентированный» взгляд на ситуацию в этом уровне. Все открытые методы, унаследованные от класса PcapHelperForIpv4, сводятся к вызову этого единственного зависящего от реализации устройства метода. Например, метод PCAP самого низкого уровня,

void EnablePcapIpv4 (std::string prefix, Ptr<Ipv4> ipv4, uint32_t interface, bool explicitFilename = false);

будет напрямую вызывать реализациюEnablePcapIpv4Internal для устройства. Все другие общедоступные методы PCAP-трассировки опираются на эту реализацию, чтобы обеспечить дополнительную функциональность на уровне пользователя. Для пользователя это означает, что у любого помощника протокола в системе будут доступны все методы PCAP-трассировки и эти методы будут работать для протоколов одинаково, если помощник правильно реализует EnablePcapIpv4Internal.

Методы


Эти методы предназначены для того, чтобы один в один соответствовать Node- и NetDevice-ориентированными версиям устройства. Вместо ограничений пары Node и NetDevice мы используем ограничения протокола и интерфейса.

Обратите внимание, что, как и в версии устройства, есть шесть методов:

void EnablePcapIpv4 (std::string prefix, Ptr<Ipv4> ipv4, uint32_t interface, bool explicitFilename = false);
void EnablePcapIpv4 (std::string prefix, std::string ipv4Name, uint32_t interface, bool explicitFilename = false);
void EnablePcapIpv4 (std::string prefix, Ipv4InterfaceContainer c);
void EnablePcapIpv4 (std::string prefix, NodeContainer n);
void EnablePcapIpv4 (std::string prefix, uint32_t nodeid, uint32_t interface, bool explicitFilename);
void EnablePcapIpv4All (std::string prefix);

Рекомендуется ознакомиться с документацией API для класса PcapHelperForIpv4, чтобы узнать подробности этих методов, но подведу итог…

  • Вы можете включить PCAP-трассировку для конкретной пары протокол/интерфейс, указав Ptr <Ipv4> и интерфейс для метода EnablePcap. Например,

    Ptr<Ipv4> ipv4 = node->GetObject<Ipv4> ();
    ...
    helper.EnablePcapIpv4 ("prefix", ipv4, 0);

  • Вы можете включить PCAP-трассировку для конкретной пары узел/сетевое устройство, предоставив строку std::string, которую вернул сервис имен объектов, методу EnablePcap.Ptr <Ipv4>. Например,

    Names::Add ("serverIPv4" ...);
    ...
    helper.EnablePcapIpv4 ("prefix", "serverIpv4", 1);

  • Вы можете включить PCAP-трассировку для набора пар протокола/интерфейса, предоставив Ipv4InterfaceContainer. Для каждой пары Ipv4/интерфейс в контейнере проверяется тип протокола. Для каждого протокола соответствующего типа (того же типа, что был передан помощнику устройства) будет включена трассировка для соответствующего интерфейса. Например,

    NodeContainer nodes;
    ...
    NetDeviceContainer devices = deviceHelper.Install (nodes);
    ...
    Ipv4AddressHelper ipv4;
    ipv4.SetBase ("10.1.1.0", "255.255.255.0");
    Ipv4InterfaceContainer interfaces = ipv4.Assign (devices);
    ...
    helper.EnablePcapIpv4 ("prefix", interfaces);

  • Вы можете включить PCAP-трассировку для набора пар протокол/интерфейс, предоставив NodeContainer. Для каждого найденного в NodeContainer узла с соответствующим протоколом. Для каждого протокола перебираются его интерфейсы и на результирующих парах включается трассировка. Например,

    NodeContainer n;
    ...
    helper.EnablePcapIpv4 ("prefix", n);

  • Вы можете включить PCAP-трассировку указав ID узла и интерфейса. В этом случае идентификатор узла транслируется в Ptr <Node> и в узле разыскивается соответствующий протокол. Итоговый протокол и интерфейс используется для указания результирующего источника трассировки.

    helper.EnablePcapIpv4 ("prefix", 21, 1);

  • Наконец, вы можете включить PCAP-трассировку для всех интерфейсов в системе, у которых связанный протокол будет тот же, что был передан помощнику устройства.

    helper.EnablePcapIpv4All ("prefix");



Имена файлов


Во всех описанных выше методах подразумевается построение полных имен файлов посредством описанного ниже метода. По соглашению, PCAP-трассировки, полученные для устройств в системе ns-3, имеют вид. В случае трассировки протоколов существует однозначное соответствие между протоколами и узлами. Это потому, что объекты протокола агрегируются в объекты узла. Поскольку в системе нет глобального идентификатора протокола, мы используем в именовании файлов соответствующий идентификатор узла. Поэтому существует вероятность конфликтов имен файлов в автоматически выбранных именах файлов трассировки. По этой причине соглашение об именах файлов трассировки протоколов изменено.

<префикс> - <идентификатор узла> - <идентификатор устройства> .pcap

Как упоминалось ранее, каждый узел в системе будет иметь назначенный системой идентификатор. Так как между экземплярами протокола и экземплярами узла есть соответствие один-к-одному, мы используем идентификатор узла. Каждый интерфейс имеет идентификатор относительно его протокола. Для именования файлов трассировки в помощниках протокола мы используем соглашение. Поэтому при включении PCAP-трассировки на интерфейсе 1 протокола Ipv4 узла 21 по умолчанию создается файл трассировки, использующий префикс «prefix»: «prefix-n21-i1.pcap».
<префикс> -n <идентификатор узла> -i <идентификатор интерфейса> .pcap


Вы всегда можете воспользоваться сервисом имен объектов ns-3, чтобы сделать это более понятным. Например, если вы использовали сервис имен объектов для присвоения имени «serverIpv4» для Ptr <Ipv4> на узле 21, то результирующее имя файла PCAP-трассировки автоматически станет, «prefix-nserverIpv4-i1.pcap».

Некоторые из методов имеют параметр по умолчанию, который называется explicitFilename. Когда установлено значение true, этот параметр отключает механизм автоматического завершения имени файла и позволяет вам явно указать его имя. Эта опция доступно только в тех методах, которые принимают префикс и включают трассировку на одном устройстве.

ASCII


Поведение ASCII-трассировщиков по большей части похоже на случай PCAP. Если вы хотите следить за обсуждением, глядя на реальный код, то взгляните на

src/network/helper/trace-helper.h.

В этом разделе мы будем иллюстрировать методы применительно к протоколу Ipv4. Чтобы выбрать трассировку для аналогичного протокола, просто подставьте соответствующий тип. Например, используйте Ptr <Ipv6> вместо Ptr <Ipv4> и вызовите EnableAsciiIpv6 вместо EnableAsciiIpv4.

Класс AsciiTraceHelperForIpv4 добавляет в помощник протокола функциональность высокого уровня для использования ASCII-трассировки. Каждый протокол, который включает эти методы, должен реализовывать один виртуальный метод, унаследованный от этого класса.

virtual void EnableAsciiIpv4Internal (Ptr<OutputStreamWrapper> stream,
std::string prefix,
Ptr<Ipv4> ipv4,
uint32_t interface,
bool explicitFilename) = 0;

Сигнатура этого метода отражает протокол и «интерфейснориентированное» представление о ситуации в этом уровне, а также тот факт, что помощник может писать в общий поток вывода. Все открытые методы, унаследованные от класса PcapAndAsciiTraceHelperForIpv4 сводятся к вызову этого единственного, зависящего от реализации устройства, метода. Например, методы ASCII-трассировки самого низкого уровня,

void EnableAsciiIpv4 (std::string prefix, Ptr<Ipv4> ipv4, uint32_t interface, bool explicitFilename = false);
void EnableAsciiIpv4 (Ptr<OutputStreamWrapper> stream, Ptr<Ipv4> ipv4, uint32_t interface);

будут напрямую вызывать реализацию метода EnableAsciiIpv4Internal для устройства, предоставляя либо префикс, либо поток. Все другие общедоступные методы ASCII-трассировки будут основываться на этих низкоуровневых функциях для обеспечения дополнительной функциональности уровня пользователя. Для пользователя это означает, что все помощники устройств в системе будут иметь все доступные методы ASCII-трассировки и эти методы будут работать одинаково для всех протоколов, если протоколы корректно реализуют EnableAsciiIpv4Internal.

Методы



void EnableAsciiIpv4 (std::string prefix, Ptr<Ipv4> ipv4, uint32_t interface, bool explicitFilename = false);
void EnableAsciiIpv4 (Ptr<OutputStreamWrapper> stream, Ptr<Ipv4> ipv4, uint32_t interface);
void EnableAsciiIpv4 (std::string prefix, std::string ipv4Name, uint32_t interface, bool explicitFilename = false);
void EnableAsciiIpv4 (Ptr<OutputStreamWrapper> stream, std::string ipv4Name, uint32_t interface);
void EnableAsciiIpv4 (std::string prefix, Ipv4InterfaceContainer c);
void EnableAsciiIpv4 (Ptr<OutputStreamWrapper> stream, Ipv4InterfaceContainer c);
void EnableAsciiIpv4 (std::string prefix, NodeContainer n);
void EnableAsciiIpv4 (Ptr<OutputStreamWrapper> stream, NodeContainer n);
void EnableAsciiIpv4All (std::string prefix);
void EnableAsciiIpv4All (Ptr<OutputStreamWrapper> stream);
void EnableAsciiIpv4 (std::string prefix, uint32_t nodeid, uint32_t deviceid, bool explicitFilename);
void EnableAsciiIpv4 (Ptr<OutputStreamWrapper> stream, uint32_t nodeid, uint32_t interface);

Рекомендуется просмотреть документацию API для класса PcapAndAsciiHelperForIpv4, чтобы узнать подробности. из этих методов, но подведу итог…

  • Для ASCII-трассировки доступно в два раза больше методов, чем для PCAP-трассировки. Это потому, что в дополнение к модели в стиле PCAP, где трассы от каждой уникальной пары протокол/интерфейс записываются в уникальный файл, мы поддерживаем модель, в которой информация трассировки для многих пар протокол/интерфейс записывается в общий файл. Это означает, что механизм генерации имени файла <prefix> -n <node id> — <interface> заменяется механизмом обращения к общему файлу; и число методов API удваивается, чтобы разрешить все комбинации.
  • Как и в случае PCAP-трассировки, вы можете включить ASCII-трассировку для конкретной пары протокол/интерфейс, предоставив Ptr <Ipv4> и интерфейс для метода EnableAscii. Например, В этом случае контексты трассировки не записываются в файл ASCII-трассировки, поскольку они будут избыточными. Система будет выбирать имя файла, который будет создан, используя те же правила, которые описаны в разделе PCAP, за исключением того, что файл будет иметь суффикс «.tr» вместо «.pcap».

    Ptr<Ipv4> ipv4;
    ...
    helper.EnableAsciiIpv4 ("prefix", ipv4, 1);

  • Если вы хотите включить ASCII-трассировку на нескольких интерфейсах и отправить все трассировки в один файл, вы может сделать это также с помощью объекта для ссылки на один файл. У нас уже было что-то похожее на это в примере «cwnd» выше: В этом случае контексты трассировки записываются в файл ASCII‑трассировки, поскольку они необходимы для устранения неоднозначности трассировок из двух интерфейсов. Обратите внимание, что поскольку пользователь полностью указывает имя файла, строка должна включать для единообразия суффикс «.tr».

    Ptr<Ipv4> protocol1 = node1->GetObject<Ipv4> ();
    Ptr<Ipv4> protocol2 = node2->GetObject<Ipv4> ();
    ...
    Ptr<OutputStreamWrapper> stream = asciiTraceHelper.CreateFileStream ("trace-file-name.tr");
    ...
    helper.EnableAsciiIpv4 (stream, protocol1, 1);
    helper.EnableAsciiIpv4 (stream, protocol2, 1);

  • Вы можете включить ASCII-трассировку для определенного протокола, предоставив методу EnablePcap объект полученный от сервиса имен объектов по имени из строки std::string. Ptr <Ipv4> отыскивается по имени из строки. <Node> в именах результирующих файлов не указывается явно, так как существует однозначное соответствие между экземпляром протокола и узлом, например, Это приведет к созданию двух файлов с именами «prefix-nnode1Ipv4-i1.tr» и «prefix-nnode2Ipv4-i1.tr» с трассировками для каждого интерфейса в соответствующем файле. Поскольку все функции EnableAscii перегружены при реализации обертки для потоков, вы также можете использовать такую форму: Это приведет к созданию единого файла трассировки с именем «trace-file-name.tr», который содержит все события трассировки для обоих интерфейсов. События будут замещены строками контекста трассировки.

    Names::Add ("node1Ipv4" ...);
    Names::Add ("node2Ipv4" ...);
    ...
    helper.EnableAsciiIpv4 ("prefix", "node1Ipv4", 1);
    helper.EnableAsciiIpv4 ("prefix", "node2Ipv4", 1);

    Names::Add ("node1Ipv4" ...);
    Names::Add ("node2Ipv4" ...);
    ...
    Ptr<OutputStreamWrapper> stream = asciiTraceHelper.CreateFileStream ("trace-file-name.tr");
    ...
    helper.EnableAsciiIpv4 (stream, "node1Ipv4", 1);
    helper.EnableAsciiIpv4 (stream, "node2Ipv4", 1);

  • Вы можете включить ASCII-трассировку для набора пар протокол/интерфейс, предоставив Ipv4InterfaceContainer. Трассировка включится для соответствующего интерфейса, протокол которого соответствует заданному типу (типу, который передан помощнику устройств). Опять же, <Node> неявный, так как есть взаимно-однозначное соответствие между протоколом и его узлом. Например, Это приведет к созданию нескольких файлов ASCII-трассировки, каждый из которых следует соглашению:. Объединение всех трасс в один файл выполняется аналогично примеру выше:

    NodeContainer nodes;
    ...
    NetDeviceContainer devices = deviceHelper.Install (nodes);
    ...
    Ipv4AddressHelper ipv4;
    ipv4.SetBase ("10.1.1.0", "255.255.255.0");
    Ipv4InterfaceContainer interfaces = ipv4.Assign (devices);
    ...
    ...
    helper.EnableAsciiIpv4 ("prefix", interfaces);

    <префикс> -n <id> -i <интерфейс> .tr 

    NodeContainer nodes;
    ...
    NetDeviceContainer devices = deviceHelper.Install (nodes);
    ...
    Ipv4AddressHelper ipv4;
    ipv4.SetBase ("10.1.1.0", "255.255.255.0");
    Ipv4InterfaceContainer interfaces = ipv4.Assign (devices);
    ...
    Ptr<OutputStreamWrapper> stream = asciiTraceHelper.CreateFileStream ("trace-file-name.tr");
    ...
    helper.EnableAsciiIpv4 (stream, interfaces);

  • Вы можете включить ASCII-трассировку для набора пар протокол/интерфейс, предоставив NodeContainer. Для каждого узла в NodeContainer будет найден соответствующий протокол. Для каждого протокола будут перебираться его интерфейсы и будет включена и трассировка в результирующих парах. Например, Это приведет к созданию нескольких файлов ASCII-трассировки, каждый из которых следует соглашению. Объединение всех трасс в один файл выполняется аналогично примеру выше.

    NodeContainer n;
    ...
    helper.EnableAsciiIpv4 ("prefix", n);

    <префикс> - <ID узла> - <идентификатор устройства> .tr

  • Вы можете включить PCAP‑трассировку на основе идентификаторов узла и устройства. В этом случае идентификатор узла преобразуется в Ptr <Node>, и соответствующий протокол ищется в узле. Итоговый протокол и интерфейс используются для указания результирующего источника трассировки.Конечно, трассы могут быть объединены в один файл, как показано выше.
    helper.EnableAsciiIpv4 ("prefix", 21, 1);

  • Наконец, вы можете включить ASCII-трассировку для всех интерфейсов в системе, чей связанный протокол будет тот же, что вы передали помощнику устройств.Это приведет к созданию нескольких файлов ASCII-трассировки, по одному на каждый интерфейс в системе, соответствующих типу протокола, переданному помощнику. Все эти файлы будут следовать соглашению. Объединение всех трасс в единый файл выполняется аналогично приведенным выше примерам.

    helper.EnableAsciiIpv4All ("prefix");

    <префиксу> -n <ID узла> -i <interface>.tr



Имена файлов


В приведенных выше описаниях методов в стиле префикса подразумевается создание полных имен файлов посредством метода показанного ниже. По соглашению, ASCII-трассировки в системе ns-3 имеют вид

<префикс>-<ID узла>-<идентификатор-устройства>.tr

Как упоминалось ранее, каждый узел в системе будет иметь назначенный системой ID. Так как между протоколами и узлами есть соответствие один-к-одному, мы используем ID узла, чтобы идентифицировать идентификатор протокола. Каждый интерфейс для данного протокола будет иметь индекс (также называемый просто интерфейсом) относительно своего протокола. Тогда по умолчанию файл ASCII-трассировки, созданный в результате включения трассировки на первом устройстве узла 21 с использованием префикса «prefix», будет «prefix-n21-i1.tr». Когда в узле нескольких протоколов, используйте префикс для устранения неоднозначности.

Вы всегда можете использовать сервис имен объектов ns-3, чтобы сделать это более понятным. Например, если вы используете сервис имен объектов присвоения имени «serverIpv4» протоколу на узле 21, а также для указываете его интерфейс, результирующее имя файла ASCII-трассировки автоматически станет «prefix-nserverIpv4-1.tr».

Некоторые из методов имеют параметр по умолчанию, который называется explicitFilename. Когда установлено значение true, этот параметр отключает механизм автоматического завершения имени файла и позволяет вам указать имя файла явно. Эта опция доступно только в тех методах, которые принимают префикс и включают трассировку на одном устройстве.

7.5 Резюме


Ns-3 включает в себя чрезвычайно богатую среду, позволяющую пользователям на нескольких уровнях настраивать виды информации, которые могут быть извлечены из моделирования.

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

Это очень сложная система, и мы понимаем, что ее нужно долго переваривать, особенно новым пользователям или тем, кто не достаточно хорошо знаком с C++ и его идиомами. Мы считаем систему трассировки очень важной частью ns-3 и поэтому рекомендуем как можно лучше ознакомиться с ней. Вероятно, понимание остальной части системы ns-3 станет довольно простым, как только вы освоите систему трассировки.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 4: ↑2 and ↓20
Comments0

Articles