Ejemplo básico NS3: IoT y sensores

Como ya hemos comentado en las primeras secciones, hay numerosos ejemplos y test que te sirven de ayuda y como punto de partida, vamos a realizar un ejemplo básico intentando simular una red de sensores que mandan información a una pasarela (Gateway) o nodo central. El objetivo de este ejemplo es ir completándolo con otras funciones muy necesarias en simulación (Log, trazas, configuración, varias tecnologías, energía, etc.) en entradas posteriores. En el repositorio del tutorial, el archivo de inicio es basic-iot-sensors.cc.
Lo primero que vamos a realizar es crear 3 nodos (variable numsensors) y posicionarlos en un punto determinado (aleatorio). Utilizamos una función que crea un escenario con un radio (variable radio) determinado en metros y, a continuación, le pasamos el contenedor de nuestros nodos para que posicione, de forma aleatoria, dichos nodos;

NodeContainer nodes;
nodes.Create(numsensors);
MobilityHelper scenario = createscenario(radio);
scenario.Install (nodes);

Como ya indicamos en la entrada de topología , se crea un escenario mediante el MobilityHelper con la configuración deseada:

MobilityHelper createscenario(double radio)
{
MobilityHelper mobility;
mobility.SetPositionAllocator ("ns3::UniformDiscPositionAllocator",
"rho", DoubleValue (radio),
"X", DoubleValue (0.0),
"Y", DoubleValue (0.0),
"Z", DoubleValue (0.0));

mobility.SetMobilityModel ("ns3::ConstantPositionMobilityModel");
return mobility;
}



Del contenedor de nodos, creamos un centro de la estrella donde posicionaremos el primer nodo, que de ahora en adelante representará la pasarela:

MobilityHelper gatewayposition = createstarcenter();
gatewayposition.Install (nodes.Get(0));

Una vez creados los dispositivos y emplazados (la pasarela en la posición x,y,z a 0,0,0 y el resto de forma aleatoria), vamos a seguir configurando nuestros nodos, configurando una interfaz wifi en cada uno de los nodos. La tecnología inalámbrica WIFI está de sobra probada en NS3 y nos sirve de punto de partida para nuestro ejemplo.
Para la creación de NetDevice por cada Nodo e instalarlo en cada uno de los nodos, usaremos los asistentes, nos crearemos un asistente para la capa física (wifiPhy), la capa Mac (wifiMac) y el objeto que modela el canal inalámbrico (wifiChannel) y al cual deberemos conectar todos los objetos que modelan la capa física.

WifiHelper wifi;
YansWifiPhyHelper wifiPhy = YansWifiPhyHelper::Default ();
YansWifiChannelHelper wifiChannel = YansWifiChannelHelper::Default ();
WifiMacHelper wifiMac;
wifiMac.SetType ("ns3::AdhocWifiMac");
wifiPhy.SetChannel(wifiChannel.Create());
NetDeviceContainer devices = wifi.Install (wifiPhy, wifiMac, nodes);

En este caso para el modelado de la capa física y el canal utilizamos el modelo Yans. Es el modelo por defecto que se utiliza pero, obviamente, el El módulo wifi es uno de los más completos y estudiados con varios modelos disponibles.
El tipo de MAC es ns3::AdhocWifiMac que modela relaciones punto a punto entre dos dispositivos con Wifi. A continuación establecemos el canal en el asistente wifiPhy para que todos los objetos creados de la capa física compartan el canal y se puedan comunicar entre ellos. Estas configuraciones, en la parte wifi, son las configuraciones por defecto. Por último, instalamos usando el asistente general del wifi la capa física y la MAC en todos los nodos. El asistente de wifi nos devuelve un contenedor con los NetDevices creados (y asociados a los nodos):

NetDeviceContainer devices = wifi.Install (wifiPhy, wifiMac, nodes);

Hemos usado la configuración por defecto, este es uno de los puntos más importantes en cuanto a una simulación, tienes que definir claramente las métricas en las cuales estás interesados y modelar de forma simple el resto de elementos. Por ejemplo, si estás interesado en un protocolo de comunicaciones de la capa de aplicación y quieres validar su funcionalidad, la capa física/MAC que utilices debe ser modelada de la forma más simple posible.

Si por contra, estás interesado en modelar un nuevo algoritmo de control de flujo de 802.11ax, es la capa de aplicación la que debe modelar lo mínimo para generar el tráfico necesario para probar la funcionalidad.

Hasta aquí hemos creado el «Hardware» de nuestra simulación, vamos a instalar y configurar el resto de elementos.

Pasemos a la parte de configuración de la pila de protocolos, en este caso vamos a instalar y configurar una pila de protocolos TCP/IP, para ello creamos un asistente y lo instalamos en todos los nodos:

InternetStackHelper internet;
internet.Install (nodes);

Una vez instalada, configuramos la dirección de red y le asignamos direcciones IP a las interfaces de red instaladas en los nodos:

Ipv4AddressHelper ipv4;
ipv4.SetBase ("10.1.1.0", "255.255.255.0");
Ipv4InterfaceContainer i = ipv4.Assign (devices);

en este caso, usamos la dirección de red 10.1.1.0 con máscara 255.255.255.0 y se les asigna, de forma consecutiva, a cada uno de los NetDevices en el container «devices» creando un contenedor de interfaces ya configuradas.

El último paso de creación de nuestro escenario propiamente dicho es la creación de aplicaciones que creen tráfico. Vamos a instalar un servidor UDP en la pasarela (nodo 0) y lo configuramos para que escuche en el puerto 2000 :

uint16_t port = 2000;
UdpServerHelper server(port);
ApplicationContainer gatewayapps = server.Install(nodes.Get(0));
gatewayapps.Start(Seconds(1.0));
gatewayapps.Stop(Seconds(11.0));

Como vemos en el código de arriba, creamos un asistente de servidor en el puerto deseado y lo instalamos en el nodo 0. Por último indicamos los momentos en que arrancamos y paramos el servidor en este caso sobre el contenedor que se ha creado. Un paso similar hay que hacer en los clientes, debemos crear clientes software configurados para enviar una cantidad de tráfico al servidor que acabamos de instalar en el todo 0 (y que obtenemos con nodes.Get(0)).
Obtenemos la dirección IP del servidor (la 0) del contenedor de interfaces, establecemos el tamaño en 32 bytes, que para la información generada por un sensor es suficiente. En principio generaremos como mucho 10 paquetes:

Address gatewayAddress = Address(i.GetAddress (0));
uint32_t packetSize = 32;
uint32_t maxPacketCount = 10;

Con esta configuración, se crea un asistente de cliente, se establece el intervalo de envío y se configura el cliente:

UdpClientHelper client (gatewayAddress, port);
Time interPacketInterval = Seconds (0.5);
client.SetAttribute("PacketSize", UintegerValue(packetSize));
client.SetAttribute ("Interval", TimeValue (interPacketInterval));
client.SetAttribute ("MaxPackets", UintegerValue(maxPacketCount));

Hay que resaltar cómo se configura, mediante el método SetAttribute, cada una de las características, indicando su nombre y valor, este mecanismo es muy utilizado por los objetos NS3 para la configuración de parámetros.
Por último, instalamos en los nodos un cliente (incluyendo la pasarela que actuará como sensor/servidor) creándose otro contenedor de aplicaciones que debemos arrancar y parar. Hay que asegurarse que el servidor esté arrancado antes que los clientes, por eso arrancamos en el segundo 2 y paramos mas tarde para no perder información.

ApplicationContainer apps = client.Install(nodes);
apps.Start(Seconds(2.0));
apps.Stop(Seconds(10.0));

Por último, arrancamos el simulador:

Simulator::Stop (Hours (24));
Simulator::Run ();
Simulator::Destroy ();

Los detalles del simulador lo veremos en otra entrada.

Con esto terminamos nuestra simulación, colocando el archivo en el directorio scratch lo ejecutamos:

/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.747s)

Que compila y ejecuta nuestra simulación. Realmente no vemos ningún flujo de información por lo que no sabemos si todo ha ido bien, en próximas entradas veremos cómo obtener información, mediante log y trazas, de qué está pasando en nuestra simulación.

NS3: La clase NetDevice (Wifi)

La clase NetDevice representa una interfaz de red que va asociada a una tecnología. Es una clase abstracta de la cual heredamos para crear una interfaz de red de una tecnología asociada. Básicamente necesitamos tener en cuenta la capa MAC, la capa física y el canal.

Vamos a ver un ejemplo con la conocida tecnología WiFi. Cada módulo de tecnología de comunicación se estructura de forma distinta atendiendo al estándar, en el caso del módulo WiFi implementa muchas de las tecnologías del estándar IEEE 802.11 (802.11a, 802.11b, etc.) incluyendo los últimos estándares 802.11ac y 802.11ax así como algunas versiones especiales como 802.11p para redes vehiculares.

La clase YansWifiPhy implementa la capa física del estándar 802.11a en la cual tienes parámetros configurables como la frecuencia, la anchura del canal, el número del canal, configuraciones MIMO, etc. que son las propiedades de la clase.

Cabe destacar las variables TxGain y RxGain que expresan la ganancia en decibelios a la hora de transmitir y recibir respectivamente. Con estas variables podemos simular la antena de dicha interfaz.

Para conectar objetos de la clase YansWifiPhy necesitas un canal que simule cómo se comporta una transmisión WiFi con los parámetros que hayas configurado. La clase YansWifiChannel tiene este propósito. Realmente lo que hace este canal es configurar un ns3::PropagationLossModel y un ns3::PropagationDelayModel y aplicarlo a toda transmisión/recepción de un objeto YansWifiPhy. Como su propio nombre indica, esas clases modelan las perdidas y el retardo para la transmisión configurada. Es responsabilidad del programador configurar el canal con los modelos apropiados.

De la capa MAC definida en el estándar se encarga la clase WifiMac. Cuando creas un objeto de la clase WifiMac debes configurarle un objeto de la clase WifiPhy con el que interacciona. Puedes establecer con WifiMac parámetros para estándares específicos. Con funciones privadas, existen funciones para configurar estándares precisos, por ejemplo ns3::WifiMac::Configure80211a o Configure80211ax_2_4Ghz. En la lista de métodos privados de WifiMac tienes la lista completa.

Una vez tenemos el objeto de WifiMac y WifiPhy (por ejemplo YansWifiPhy) con su canal asociado, podemos construir el equivalente a la interfaz de rede Wifi con un NetDevice. Lo normal es utilizar un asistente ya que generalmente, tendremos que crear un NetDevice por cada nodo e instalarlo en dicho nodo. El asistente para este proceso es la clase WifiHelper. Una de las formas más comunes de usarlo es llamar al método install con un objeto WifiPhy, un objeto WifiMac y un NodeContainer. Este método creará un contenedor de objetos DeviceNet (NetDeviceContainer) asociados a cada nodo en el NodeContainer y con los correspondientes objetos instalados.

La clase que hereda de la clase NetDevice para wifi es la clase WifiNetDevice
Si echamos un vistazo a sus métodos, tenemos los métodos de configuración:

void SetMac (const Ptr< WifiMac > mac)
bool SetMtu (const uint16_t mtu)
void SetNode (const Ptr< Node > node)
void SetPhy (const Ptr< WifiPhy > phy)

Como hemos dicho anteriormente, la clase WifiHelper nos ayuda con esta configuración cuando tenemos que hacerlo con varios nodos.
Resumiendo, el esquema de recepción de un paquete con esta configuración sería, por lo tanto:
Channel -> WifiPhy -> WifiMac -> WifiNetDevice -> Node

En otra entrada analizaremos un ejemplo simplificado para ver cómo se configura un escenario simple. No obstante en el directorio examples de la instalación de NS3 hay un directorio wireless con ejemplos de Wifi y otras tecnologías inalámbricas.

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á.

NS3: La clase Node

La clase Node y NetDevice, junto con el canal de comunicación, simulan el hardware de nuestra red. Un objeto de la clase Node podríamos identificarlo con un PC, un móvil, un sensor, etc. es decir cualquier dispositivo de cómputo donde vamos a instalar cosas. Al igual que los dispositivos de cómputo en la realidad, los objetos de la clase Node necesitan interfaces de red para comunicarse. Estas interfaces de red son de una tecnología específica (Bluetooth, Wifi, Ethernet, LTE, etc.). Al igual que en la realidad, un objeto Node puede tener mas de un interfaz de red. Las interfaces de red en NS3 vienen representadas por objetos de la clase específica de la tecnología que hereda de la clase abstracta NetDevice.

Por ejemplo una interfaz wifi viene representada por la clase WifiNetDevice que hereda de NetDevice.

El ejemplo mínimo de creación de un nodo lo podemos ver con el ejemplo node1.cc . Es un ejemplo que crea un único nodo y obtiene su ID. Cada nodo tiene un Id único. La variable n es, a todos los efectos, una caja vacía donde luego instalaremos y configuraremos el resto de cosas.

Si ponemos el archivo node1.cc en el directorio scratch y lo ejecutamos con NS_LOG="Node" ./waf --run node1 veremos la información del módulo Node (es lo que indicamos con NS_LOG="Node" antes del comando waf) y el identificador del nodo. Salvo que estes depurando, evita usar muchos LOG ya que ralentizan la simulación. Podemos habilitar el sistema de Log desde el código fuente tal y como veremos en otra entrada de blog.

El problema de crear los nodos de forma directa es que tienes que ocuparte de hacer todas las operaciones a todos los nodos. Para simplificar este proceso habitual en una simulación, NS3 dispone de la clase NodeContainer.

Si has echado un vistazo a la clase Node verás que es bastante sencilla, puedes instalar aplicaciones, puedes conectar interfaces de red y registrar manejadores o gestores de protocolos.

En vez de usar directamente la clase Node, usaremos la clase NodeContainer para este tipo de tareas. En NodeContainer.cc podemos ver que hay varias formas de crear un contenedor (añadiendo nodos ya creados, indicando el número de nodos a crear con Create, concatenando dos contenedores ya creados, etc.) con un aspecto importante a tener en cuenta, el número id de un nodo es único e irrepetible.

Al igual que otros asistentes que veremos, el NodeContainer es un contenedor inspirado en los contenedores STL.Como tal, se pueden iterar sobre sus elementos con un iterador (por ejemplo, la función imprimirContainerIds en NodeContainer.cc), concatenar contenedores (por ejemplo, el NodeContainer c del ejemplo), etc.

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.