Creando un nuevo módulo en NS3

[Actualizado a cmake, versión 3.36.1 (documentación oficial ns3.36)]
Crear un nuevo protocolo o algoritmo en NS3 es un paso que implica la creación de un nuevo módulo. Obviamente, a partir del tipo de protocolo o algoritmo a implementar, los pasos a seguir y los protocolos que nos servirán como referencia cambiarán. En cualquier caso, siempre necesitaremos generar un nuevo módulo e incluirlo en la compilación de NS3 para poder hacer simulaciones con posterioridad.

Para crear un nuevo módulo, NS3 nos proporciona un script python, create-module.py, dentro de la carpeta src de nuestra instalación de ns3.
Si por ejemplo queremos crear un nuevo protocolo inalámbrico, pongamos el protocolo weightlessp, si ejecutamos:

$utils/create-module.py weightless
Creating module /home/felix/tools/ns-3-dev-git/contrib/weightless
Successfully created new modules
Run './ns3 configure' to include them in the build

Se nos crea la plantilla dentro de src:

$cd contrib/weightless/
felix@homer-esi:~/tools/ns-3-dev-git/contrib/weightless$ ls
CMakeLists.txt doc examples helper model test

donde vemos las carpetas habituales para meter la documentación, los ejemplos, los asistentes, el modelo propiamente dicho y los test. También me genera el archivo CMakeList.txt que utiliza ns3 para compilar los módulos. Para ver que se incluye correctamente en el proceso de compilación:

$./ns3 configure --enable-example --enable-test
$./ns3 build
$./test.py -s weightlessp
.
PASS: TestSuite weightlessp
1 of 1 tests passed (1 passed, 0 skipped, 0 failed, 0 crashed, 0 valgrind errors)
.

Ahora falta, obviamente, lo difícil, que es programar el modelo propiamente dicho identificando cada una de las partes. Lo más recomendable es estudiar cuidadosamente un protocolo existente de la misma capa que el que queremos programar y copiar su estructura adaptando el comportamiento a la funcionalidad deseada.

Simulando consumo de energía en NS3 (II)

Seguimos explicando el modelo de simulación del consumo de energía. Ya explicamos en la primera entrada relativa a la energía los conceptos básicos modelados en NS3. Seguimos trabajando con el ejemplo básico examples/energy/energy-model-example.cc

Si observamos el ejemplo, existen dos funciones añadidas justo antes de la función principal, RemainingEnergy y TotalEnergy:

/// Trace function for remaining energy at node.
void
RemainingEnergy (double oldValue, double remainingEnergy)
{
NS_LOG_UNCOND (Simulator::Now ().GetSeconds ()
<< "s Current remaining energy = " << remainingEnergy << "J");
}

/// Trace function for total energy consumption at node.
void
TotalEnergy (double oldValue, double totalEnergy)
{
NS_LOG_UNCOND (Simulator::Now ().GetSeconds ()
<< "s Total energy consumed by radio = " << totalEnergy << "J");
}

Estas funciones, como podemos ver, imprimen la energía que queda en la fuente de energía y el total de energía consumida por la radio.

Para invocar estas funciones cuando existe un cambio en la energía restante y/o energía consumida, debemos enlazar las funciones anteriores a los cambios de estado de la fuente de energía y del modelo de energía consumido respectivamente. De esta forma, obtenemos primero una referencia a la fuente de energía con ID 1. A continuación, mediante la función TraceConnectWithoutContext, vinculamos el cambio de estado en RemainingEnergy a una llamada a la misma función, mediante MakeCallBack y un puntero a la función deseada, en este caso RemainingEnergy también:

Ptr basicSourcePtr = DynamicCast (sources.Get (1));
basicSourcePtr->TraceConnectWithoutContext ("RemainingEnergy", MakeCallback (&RemainingEnergy));

A continuación se sigue el mismo proceso obteniendo el modelo de consumo de energía de un dispositivo:

// device energy model
Ptr basicRadioModelPtr =
basicSourcePtr->FindDeviceEnergyModels ("ns3::WifiRadioEnergyModel").Get (0);
NS_ASSERT (basicRadioModelPtr != NULL);
basicRadioModelPtr->TraceConnectWithoutContext ("TotalEnergyConsumption", MakeCallback (&TotalEnergy));

De esta forma se tracean los cambios de estado en las fuentes de energía, que simulan baterías, modelos de energía, que simulan el consumo de las tarjetas de red, y los recolectores de energía que simularían paneles solares, viento, etc.

La función TraceconnectWithoutContext conecta una fuente de trazas (es decir, algo que representa un estado dentro de NS3 y que puede cambiar) con una función. Identificaremos estas fuentes de trazas por que son variables de la plantilla TracedValue, por ejemplo, puedes ver en la definición BasicEnergySource tiene una variable TracedValue m_remainingEnergyJ; que es susceptible de ser traceada mediante una función asociada con TraceconnectWithoutContext asocíandole una función que tenga como argumentos el valor antiguo y nuevo. El tipo de argumentos en este caso será double. La documentación de cada clase proporciona qué TracedSources tiene para usar este mecanismo (e.g. BasicEnergySource).

El sistema de log de NS3

Un buen entorno de logging te permite depurar y entender qué está pasando en tu simulación, así como entender cómo está estructurado el simulador NS3 y sus diferentes módulos.
El sistema de logging de NS3 se usa mediante variables de entorno y mediante el propio código de tu simulación si quieres habilitar logging en tu propia simulación.

Se establecen siete niveles de log proporcionando de menos a mas información:

  1. NS_LOG_ERROR: mensajes de error
  2. NS_LOG_WARN: mensajes de aviso
  3. NS_LOG_DEBUG: mensajes específicos de depuración
  4. NS_LOG_INFO: mensajes de información genéricos
  5. NS_LOG_FUNCTION: mensajes de llamadas a funciones para la trazabilidad de llamadas
  6. NS_LOG_LOGIC: mensajes de log con el flujo lógico dentro de cada función
  7. NS_LOGIC_ALL: todos los mensajes

Adicionalmente hay un nivel incondicional que imprime la salida con independencia de los niveles de log activos o no. Este es NS_LOG_UNCOND
¿Cómo visualizamos la información de log en ns3?, bien, vamos a comenzar con nuestro ejemplo IoT básico donde, precisamente, uno de los problemas es que no generábamos ningún tipo de información acerca de la simulación.
Efectivamente si lo ejecutamos, habiendo colocado previamente el archivo basic-iot-sensors.cc en el directorio scratch:

$./waf --run basic-iot-sensors
Waf: Entering directory `/ns-allinone-3.30.1/ns-3.30.1/build'
Waf: Leaving directory `/ns-allinone-3.30.1/ns-3.30.1/build'
Build commands will be stored in build/compile_commands.json
'build' finished successfully (2.047s)

Vemos que no sale ningún tipo de información útil acerca de la simulación salvo que se ha compilado y ejecutado sin errores.
Vamos a estudiar la información que nos genera el módulo de YansWifiPhy, para ello:

$ export NS_LOG=YansWifiPhy
felix@homer:~/tools/ns-allinone-3.30.1/ns-3.30.1$ ./waf --run basic-iot-sensors
Waf: Entering directory `/ns-allinone-3.30.1/ns-3.30.1/build'
Waf: Leaving directory `//ns-allinone-3.30.1/ns-3.30.1/build'
Build commands will be stored in build/compile_commands.json
'build' finished successfully (1.701s)
+0.000000000s -1 YansWifiPhy:YansWifiPhy(0x5641614960f0)
+0.000000000s -1 YansWifiPhy:SetChannel(0x5641614960f0, 0x564161490400)
+0.000000000s -1 YansWifiPhy:YansWifiPhy(0x56416153d640)
+0.000000000s -1 YansWifiPhy:SetChannel(0x56416153d640, 0x564161490400)
+0.000000000s -1 YansWifiPhy:YansWifiPhy(0x564161541e90)
...

vemos que usamos la variable de entorno NS_LOG para fijar el módulo en el que estamos interesados. Si ponemos NS_LOG a un nombre de un módulo que no existe se imprimirá un listado de todos los módulos que NS3 tiene implementados. El nivel de log por defecto en la mayoría de los módulos será todos, podemos especificarlo también a la hora de definir el módulo. Por ejemplo vamos a definir el nivel info para el módulo UdpClient:

$ export NS_LOG=UdpClient=level_info
$ ./waf --run basic-iot-sensors
Waf: Entering directory `/ns-allinone-3.30.1/ns-3.30.1/build'
Waf: Leaving directory `/ns-allinone-3.30.1/ns-3.30.1/build'
Build commands will be stored in build/compile_commands.json
'build' finished successfully (1.480s)
TraceDelay TX 32 bytes to 10.1.1.1 Uid: 0 Time: 2
TraceDelay TX 32 bytes to 10.1.1.1 Uid: 1 Time: 2
TraceDelay TX 32 bytes to 10.1.1.1 Uid: 2 Time: 2
TraceDelay TX 32 bytes to 10.1.1.1 Uid: 33 Time: 2.5
....

Vemos que ahora sale la información de las aplicaciones UDP instaladas en los nodos IoT enviando información. Substituye level_info por level_all en el primer comando para que veas como sale mucha más información.
Si algún módulo no se usa en tú simulación y pones NS_LOG a ese módulo no saldrá ninguna información adicional.
Si quieres indicar que el nombre de la función que genera el mensaje también se imprima, puedes hacer un OR con prefix_fund. En bash necesitarás comillas:

$export 'NS_LOG=UdpClient=level_all|prefix_func'

Para habilitar varios módulos, puedes encadenar usando : varios módulos.

$ export 'NS_LOG=UdpClient:UdpServer'

Vamos a crear un módulo para nuestra simulación y a sacar nuestro mensaje de log. Para ello, edita el archivo basic-iot-sensors.cc y añade estas dos líneas, por ejemplo, justo debajo de donde se inicia la función main:

int main (int argc, char *argv[]){
NS_LOG_COMPONENT_DEFINE ("IotEjemplo");
NS_LOG_INFO ("Creando la simulación");

Aunque ya estaba incluido en dicho archivo, si lo utilizas en tu simulación acuerdate incluir los archivos de cabezera de log («ns3/log.h»).
En la primera función definimos el módulo IotEjemplo y en la segunda sacamos nuestro primer mensaje. Si ahora queremos ver esa información:

$export 'NS_LOG=IotEjemplo'
$ ./waf --run basic-iot-sensors
Waf: Entering directory `/ns-allinone-3.30.1/ns-3.30.1/build'
[2876/2963] Compiling scratch/basic-iot-sensors.cc
[2921/2963] Linking build/scratch/basic-iot-sensors
Waf: Leaving directory `/ns-allinone-3.30.1/ns-3.30.1/build'
Build commands will be stored in build/compile_commands.json
'build' finished successfully (6.339s)
IotEjemplo:main(): [INFO ] Creando la simulación

Donde, como podemos ver, se muestra nuestro mensaje.

Es buena idea usar el mecanismo de log en cualquier simulación de ns3 pero se hace imprescindible si aspiras a crear un nuevo módulo para el simulador destinado a ser usado por la comunidad.

Configuración del entorno (VScode) y ejecución de ejemplos en NS3

Actualizado a la versión 3.36 (Mayo 2022)
Mi recomendación antes de nada, una vez instalado NS3 y comprobado que funciona mediante el paso de los correspondientes test (un paso imprescindible), es que configures un entorno de desarrollo adecuado para maximizar tu productividad. Un buen editor de código fuente es Visual Studio Code

En Linux, si abres el editor dentro del directorio de ns3 (en mi caso ns-3.36):

~/tools/ns-allinone-3.36/ns-3.36/$code .

se crea una carpeta .vscode con un archivo c_cpp_properties.json. En ese archivo tenemos que añadir la ruta donde están los .h, el auto completado se habilitará:

{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/build/include/ns3/**",
"/home/felix/tools/ns-allinone-3.36/ns-3.36/build"
],
"defines": [],
"compilerPath": "/usr/bin/clang-11",
"cStandard": "c17",
"cppStandard": "c++14",
"intelliSenseMode": "linux-clang-x64",
"compileCommands": "${workspaceFolder}/cmake-cache/compile_commands.json"
}
],
"version": 4
}

Una buena forma de empezar es ejecutar aquellos ejemplos que más te interesen del directorio examples.
Por ejemplo (En negrita los comandos ejecutados):

ns-3.36$ ./ns3 run wifi-simple-adhoc
...
Testing 1 packets sent with receiver rss -80
Received one packet!

Si editas los ejemplos, ahora podrás ver qué métodos y argumentos tiene cada objeto que manipules en tu simulación. Si no lo ves es que tienes las sugerencias deshabilitadas. Puedes habilitarlas siguiendo estas instrucciones

Tus trabajos se harán en el directorio scratch. En el caso de los ejemplos (dentro del directorio examples) que vienen con NS3 no es necesario copiarlos en el directorio scratch y se pueden ejecutar directamente.

El script ns3 es una herramienta de configuración y compilación escrita en python y que es usado en el proyecto NS3 para hacer de interface con el sistema cmake. Se puede usar para configurar y construir el propio NS3.

De momento, nosotros solo nos interesa para compilar y ejecutar nuestros ejemplos. Dentro del directorio de ns3 (~/tools/ns-allinone-3.36/ns-3.36 en mi caso), la sintaxis del comando a ejecutar es:

ns-3.36$./ns3 run ejemplo

Si no es un ejemplo de los que vienen con ns3, buscará ejemplo.cc en el directorio scratch, lo compilará y lo ejecutará.

Programando en NS3

NS3 puede tener una curva de aprendizaje considerable dado que es un simulador de eventos discretos centrado en redes y, por lo tanto, tiene mucha variedad de combinaciones

Para programar cualquier ejemplo, ten en cuenta los siguientes consejos para acortar el tiempo de aprendizaje:

  • Por supuesto, el tutorial oficial es el mejor punto de partida
  • Los ejemplos (en el directorio examples) y los test (en el directorio test de cada módulo en el directorio src) del propio ns3 son la mejor fuente de información y puedes utilizarlos como plantillas para comenzar a programar
  • Para muchas de las tareas de configuración existen clases asistentes (terminada en *helper). MobilityHelper, BasicEnergySourceHelper, OlsrHelper, etc.
  • Genera los resultados en algún estándar como pcap y podrás usar herramientas externas para analizar tus resultados.
  • Utiliza un objeto de la clase NodeContainer para guardar todos tus nodos y luego utilizarlo para aplicar las mismas operaciones a todos tus nodos. Por ejemplo, poner un mismo tipo de netdevice a 100 nodos
  • A la hora de depurar, puede ser interesante habilitar el sistema de log de los diversos componentes para obtener mas información: por ejemplo LogComponentEnable («UdpClient», LOG_LEVEL_INFO); habilita los mensajes del componente UdpClient al nivel de info. Hay varios niveles de log. Pero ten cuidado, a mas log mas lenta va la simulación
  • Estructura tu simulación de forma que los parámetros que quieras cambiar entre simulación y simulación puedan ser pasados como argumento. Aquí tienes un ejemplo de cómo hacerlo.
  • Separa la generación de resultados, del procesamiento y análisis de los resultados. De esta forma, no tendrás que ejecutar toda la simulación de nuevo para cambiar algo del análisis
  • Para crear un nuevo modelo, lee cuidadosamente la parte del manual de soporte
  • Aunque hay muchos módulos, generalmente solo hay que configurar los módulos comunes y estructurar tu escenario de simulación. ¡Puedes ser productivo en un tiempo relativamente corto!

    Topología de los nodos en NS3

    Sobre todo en simulaciones que tienen nodos inalámbricos, después de crear los nodos debes posicionarlos para formar tu topología. Topologías habituales en simulaciones son en forma de matriz cuadrada, en línea, en estrella con el nodo que hace de pasarela en el centro, etc.

    El emplazamiento de un nodo (X,Y,Z) generalmente se hace con la ayuda de un objeto de la clase MobilityHelper que permite crear escenarios con posiciones donde luego emplazamos nuestros nodos (almacenados en un contenedor del tipo NodeContainer).
    Es decir, configuramos una topología concreta con el MobilityHelper y luego instalamos en esa topología los nodos creados y configurados de acuerdo a los requisitos de la simulación.
    Una configuración sencilla en matriz 2D sería:

    MobilityHelper mobility;
    mobility.SetPositionAllocator ("ns3::GridPositionAllocator",
    "MinX", DoubleValue (0.0),
    "MinY", DoubleValue (0.0),
    "DeltaX", DoubleValue (distanceX),
    "DeltaY", DoubleValue (distanceY),
    "GridWidth", UintegerValue (nodesgridwidth),
    "LayoutType", StringValue ("RowFirst"));
    mobility.SetMobilityModel ("ns3::ConstantPositionMobilityModel");

    En primer lugar se le indica que vamos a crear puntos de acuerdo a ns3::GridPositionAllocator . Esto es importante por que el resto de argumentos que viene a continuación va en función de este primer argumento. El resto indica las coordenadas iniciales (MinX, MinY) en 0,0 , con una separación determinada, en este caso indicado por distanceX y distanceY, con una anchura indicado por la variable nodesgridwidth empezando por las filas (RowFirst). Por último indicamos que los emplazamientos son fijos «ns3::ConstantPositionMobilityModel».

    En el repositorio de este tutorial podemos ver algunos ejemplos de base para topologías habituales. Los parámetros de entrada son configurables por línea de comandos de cara a permitir la simulación automática de varias topologías.

    También en simulaciones inalámbricas, una vez emplazados, es posible que quieras mover tus nodos, para lo cual tendrás que configurar el escenario de acuerdo a tu modelo de movilidad

    Otra posibilidad que te permite ns3 es la de leer topologías de un archivo donde has almacenado la posición de algunos nodos.Los inputs readers te permiten este tipo de lecturas ad-hoc y generar topologías personalizadas y/o concretas.

    Simulaciones

    Una simulación es una representación simplificada de la realidad ejecutada en un computador. Esta simulación se hace con el ánimo de estudiar y analizar un aspecto concreto que, con el análisis adecuado, se puede exportar a cómo se comportará la realidad.

    ¿Para qué usamos las simulaciones?, bueno, a menudo para ahorrarnos tiempo y dinero en estudiar el comportamiento de diversos parámetros de la vida real. Otras veces por que sencillamente, no podemos probar algo en la vida real pero queremos ver qué pasaría.

    En redes de computadores es habitual simular redes para probar nuevos protocolos, estudiar la viabilidad de nuevas topologías, estudiar que pasa si se rompe un enlace, estudiar las prestaciones de algoritmos, y un largo etc.

    Mapa conceptual simulación

    La simulación de redes de computadores pretende simular topologías y tráfico de red sin tener que desplegar dichas redes físicamente.

    Tal y como podemos ver en el mapa conceptual, abajo a la derecha en naranja, cuando queremos simular algo el primer paso es hacer las preguntas correctas. Esto es importantísimo por que no podemos simularlo todo con mucha precisión por lo que debemos simplificar aquellos aspectos que no son relevantes. Por ejemplo, si queremos analizar las prestaciones de un algoritmo de enrutado en cuanto a si encuentra rutas óptimas o no, el canal de comunicación puede ser muy simple y nunca perder paquetes si no es relevante para lo que queremos analizar. Simular un canal de comunicación sin errores siempre es mas simple y sencillo que no un canal de comunicación mas realista y con errores.

    Cuando tenemos claro qué vamos a probar/mejorar o qué queremos estudiar, necesitamos hacer un modelo, este modelo puede ser teórico (es decir, ecuaciones) o simulado (es decir, simulación). El modelo simulado lo podemos simular y producto de esta simulación obtendremos unos resultados en crudo que debemos analizar e interpretar.

    Los resultados de la simulación se deben validar, es decir debemos asegurarnos que son correctos, comparándolos con resultados experimentales de parámetros reales y/o con los resultados de un modelo teórico. Solo después de esta validación podremos decir que nuestro modelo de simulación se acerca a la realidad y es válido.

    Conceptos básicos NS3

    El simulador ns3 es un simulador de eventos discretos utilizado ampliamente en investigación y en docencia.
    Se pueden desarrollar todo tipo de simulaciones relativas a tecnologías, protocolos y aplicaciones atendiendo a la configuración que hagamos en un archivo C++.

    Los elementos básicos de una simulación vienen representados por clases c++ que simulan los elementos básicos hardware/software de un escenario real, son los siguientes:

    1. Node: representa un dispositivo (computador, servidor, teléfono móvil, portátil, sensor, cámara, etc.)
    2. Channel: representa un canal de comunicación y su comportamiento (cable, inalámbrico, punto a punto, etc.)
    3. Net Device : Dispositivo de red, es una tarjeta de red inalámbrica (e.j wifi) o cableada (e.j ethernet)
    4. Application: es una aplicación software. Desde el punto de vista de una simulación es un generador/consumidor
      de paquetes de información. Un servidor web, un navegador, etc. son ejemplos de aplicaciones
    5. Protocol stack: es una pila de protocolos de comunicación. El mas conocido, la pila de protocolos TCP/IP que hace posible la comunicación en Internet
    6. El simulador propiamente dicho: se encarga de lanzar y gestionar la simulación.

    Con estos cinco elementos podemos simular cualquier escenario de red. En nuestro archivo de C++ debemos tener estos elementos configurados apropiadamente para simular un escenario real.
    Cuando analizemos un escenario ns3 veremos que en C++ vamos realizando los mismos pasos que haríamos en un escenario real.

    1. Creamos los objetos C++ que representen a los nodos, el canal, la aplicación y la pila de protocolos que necesitemos
    2. A los nodos, les «instalamos» los dispositivos de red (Net Devices)
    3. «Conectamos» el channel a los dispositivos de red
    4. Instalamos en cada nodo la pila de protocolos y la configuramos apropiadamente. En TCP/IP le damos la dirección IP, máscara de subred, etc.
    5. Instalamos en cada nodo la aplicación y la configuramos
    6. Configuramos y lanzamos la simulación

    Hay elementos secundarios que nos facilitan la labor de gestionar simulaciones complejas (argumentos, log, configuración de muchos elementos) con
    muchos nodos y/o aplicaciones.