sábado, 23 de março de 2013

Simulando Redes com NS-3 e Python/C++

Recentemente, em uma das aulas do Mestrado na PUCRS, comecei a brincar com simulação de redes de computadores utilizando o software NS-3, um simulador de eventos de redes focado em pesquisas e uso educacional.

Pretendo compartilhar aqui parte do aprendizado e das descobertas feitas com o uso desta ferramenta.

Vou apresentar uma rápida visão do processo de instalação do ns-3 e dar uma passada no primeiro exemplo que acompanha o pacote do simulador.


Instalação



O primeiro passo é baixar o arquivo ns-allinone-3.16.tar.bz2 do site oficial:

http://www.nsnam.org/ns-3-16/download/

Em seguida, descompacte o pacote em qualquer diretório onde você realmente deseje acomodar a aplicação.
O ns-3 se organizará em uma estrutura de diretórios conforme abaixo. Destaco somente os diretórios que nos interessarão no momento.
- ns-allinone-3.16
  - ns-3.16
    - examples
      + tutorial
    + scratch
    + src
...

Para começar a testar o simulador, é necessário compilar seu código, o que pode ser feito através do script build.py do diretório raiz. Vamos adicionar dois parâmetros para poder trabalhar com os testes oferecidos pelo ns-3.

# ./build.py --enable-examples --enable-tests

Execute o comando acima a partir de um terminal. Até este ponto, independe se você está utilizando Linux ou Mac OS. No meu caso, testei em um Ubuntu 12 e em um Mac OS 10.8.

No Mac foi redondinho, tudo instalou perfeitamente. No Ubuntu da faculdade, os compiladores de C++ e Python não estavam instalados. Isso foi acusado logo no início dos logs de saída da execução do script. Acontecendo isso, o script pode ser interrompito com Ctrl+C sem problema algum. Na faculdade, instalamos via apt-get os seguintes pacotes:

g++
python-dev


Depois disso, executando novamente o script build.py, tudo funcionou normalmente. Ao final do log de compilação é normal que alguns módulos não tenham sido instalados por necessitarem de detalhes adicionais. O final do meu log ficou assim:

Modules built:
antenna                aodv                   applications             
bridge                 buildings              config-store             
core                   csma                   csma-layout              
dsdv                   dsr                    energy                   
flow-monitor           internet               lte                      
mesh                   mobility               mpi                      
netanim (no Python)    network                nix-vector-routing       
olsr                   point-to-point         point-to-point-layout    
propagation            spectrum               stats                    
test (no Python)       tools                  topology-read            
uan                    virtual-net-device     wifi                     
wimax                    

Modules not built (see ns-3 tutorial for explanation):
brite                  click                  emu                      
openflow               tap-bridge             visualizer       


Executando o primeiro exemplo



No diretório ns-3.16/examples/tutorial constam os arquivos de exemplos de simulação do tutorial. Todos escritos em C++, e o primeiro escrito também em Python (first.cc e first.py).

# cd ns-3.16/examples/tutorial
# ls
examples-to-run.py fourth.cc  third.cc
fifth.cc  hello-simulator.cc waf
first.cc  second.cc  wscript
first.py  sixth.cc


Sem medo, você pode abrir qualquer um destes arquivos com o seu editor favorito e analisar os códigos construídos, bem como a equivalência entre as versões C++ e Python.

O próximo passo é executar para ver o teste funcionando e a resposta resultante no console.

Por enquanto, vamos analisar o arquivo first.py. Poderia ser em C++, mas achei mais empolgante trabalhar com a versão em Python!

Para preservar o original, faça uma cópia do arquivo first.py para o diretório ns-3.16/scratch, a partir deste ponto permaneça no diretório ns-3.16. Observe que, se sua preferência for Python, os arquivo com extensão py devem ser copiados em lugar dos cc, mas não há problema em copiar ambos.

Estando no diretório ns-3.16, o próximo recurso que utilizaremos é o waf, um framework desenvolvido em Python para configuração, compilação e instalação de aplicações, que já vem embutido ao ns-3.16.

Se você quiser analisar um código C++, execute o comando ./waf e o diretório scratch será lido e todo o seu conteúdo será compilado. Essa etapa não é necessária se o código a ser rodado for em Python. A partir daí, já é possível executar nossos exemplos com os comandos abaixo:

./waf --run scratch/first (Para C++)
./waf --pyrun scratch/first.py (Para Python)

Após algumas eventuais informações de compilação exibidas no console, aparecerá o resultado da simulação que deve ser algo como:

At time 2s client sent 1024 bytes to 10.1.1.2 port 9
At time 2.00369s server received 1024 bytes from 10.1.1.1 port 49153
At time 2.00369s server sent 1024 bytes to 10.1.1.1 port 49153
At time 2.00737s client received 1024 bytes from 10.1.1.2 port 9


Dissecando o primeiro exemplo



Vamos agora dar uma analisada no código desse primeiro exemplo. Minha preferência, como mencionei antes, foi pelo código em Python, que segue abaixo:
import ns.applications
import ns.core
import ns.internet
import ns.network
import ns.point_to_point

ns.core.LogComponentEnable("UdpEchoClientApplication", ns.core.LOG_LEVEL_INFO)
ns.core.LogComponentEnable("UdpEchoServerApplication", ns.core.LOG_LEVEL_INFO)

nodes = ns.network.NodeContainer()
nodes.Create(2)

pointToPoint = ns.point_to_point.PointToPointHelper()
pointToPoint.SetDeviceAttribute("DataRate", ns.core.StringValue("5Mbps"))
pointToPoint.SetChannelAttribute("Delay", ns.core.StringValue("2ms"))

devices = pointToPoint.Install(nodes)

stack = ns.internet.InternetStackHelper()
stack.Install(nodes)

address = ns.internet.Ipv4AddressHelper()
address.SetBase(ns.network.Ipv4Address("10.1.1.0"), ns.network.Ipv4Mask("255.255.255.0"))

interfaces = address.Assign (devices)

echoServer = ns.applications.UdpEchoServerHelper(9)

serverApps = echoServer.Install(nodes.Get(1))
serverApps.Start(ns.core.Seconds(1.0))
serverApps.Stop(ns.core.Seconds(10.0))

echoClient = ns.applications.UdpEchoClientHelper(interfaces.GetAddress(1), 9)
echoClient.SetAttribute("MaxPackets", ns.core.UintegerValue(1))
echoClient.SetAttribute("Interval", ns.core.TimeValue(ns.core.Seconds (1.0)))
echoClient.SetAttribute("PacketSize", ns.core.UintegerValue(1024))

clientApps = echoClient.Install(nodes.Get(0))
clientApps.Start(ns.core.Seconds(2.0))
clientApps.Stop(ns.core.Seconds(10.0))

ns.core.Simulator.Run()
ns.core.Simulator.Destroy()



Nas primeiras linhas, são importados os módulos que serão utilizados na simulação.
Em seguida, nas linhas 7 e 8, é definido o nível de log para as aplicações que serão simuladas, neste caso, UdpEchoClientApplication e UdpEchoServerApplication.

Nas linhas 10 e 11, está a utilização do conceito mais básico do ns-3: os nós, ou Nodes. Um Node representa nada mais nada menos que uma máquina, um computador, sem nada de rede (nem a própria rede, nem dispositivo de rede).
Tudo começa a partir de um NodeContainer (linha 10), que guardará as referências para os Nodes criados. A quantidade de Nodes a serem criados é definida em um simples parâmetro (linha 11). E pronto! Temos os Nodes!

Na linha 13, temos o primeiro contato com o conceito de Topology Helper, que serve como um assistente para abstrair a criação de uma topologia de rede. Nessa linha temos o Helper para a criação de um topologia Peer-to-peer. Dependendo da topologia, o conjunto de parâmetros de configuração pode mudar. Nesse caso, nas linhas 14 e 15, configuramos a taxa de transmissão do dispositivo de rede e o atraso (delay) do cabo (Channel).

Na linha 17 começa uma parte bem interessante da mágica, chamando o método Install(nodes) simulando a instalação real da rede, ou seja, cabos, placas de rede, etc. A partir deste momento, nossos Nodes não são mais somente computadores isolados, mas possuem dispositivo de rede e uma rede fisicamente configurada. Após executar suas operações, o método Install(nodes) retorna uma referência aos dispositivos de rede recém instalados.

Nas linhas 19 e 20, vemos outro Helper, o InternetStackHelper, que vai dar um pouco mais de "vida" para nossos dispositivos de rede, adicionando a eles o reconhecimento de protocolos de internet (TCP, UDP, IP, etc).

Ainda falta um importante detalhe: Nossos micros não possuem IP! Para resolver isso, mais um Helper: Ipv4AddressHelper. Nas linhas 22 e 23 o helper é criado e são configurados o IP de rede e a máscara, através dos quais serão calculados automticamete os IPs para nossos dispositivos de rede.

A linha 25 é a que associa então os novos IPs aos dispositivos de rede com a chamada address.Assign(devices).


Tudo pronto! Mas nada acontece, pois não há nada a simular, a menos que adicionemos as aplicações (software) que vão fazer os dispositivos da rede interagirem entre si.

Para efetivamente executar algo em cima da rede simulada, vemos a criação de uma aplicação cliente/servidor para transmissão de pacotes UDP, onde, na linha 27, é instanciada a aplicação para rodar no servidor na porta 9, e atribuída ao segundo Node (índice 1) na linha 29. Nas linhas 30 e 31 são definidos tempos (no contexto da simulação) em que a aplicação do servidor será iniciada e interrompida.

A partir da linha 33, o mesmo procedimento acontece para a aplicação que rodará no lado cliente. Nessa parte, três atributos são definidos, estabelecendo o número de pacotes a receber (linha 34), o intervalo de transmissão (linha 35) e o tamanho máximo do pacote (linha 36). Na linha 38 a aplicação é instalada no primeiro Node de nossa rede (índice 0). Por fim, igualmente à configuração a aplicação do lado servidor, define-se os tempos em que a execução iniciará e será interrompida.



As duas linhas finais são as que fazem o processo todo de simulação ser iniciado e, após sua execução, ser finalizado.


Até aqui foi a explicação do exemplo first.py que já vem com o ns-3. A partir daqui vou sugerir algumas customizações possíveis que tornam as simulações mais versáteis e também promovem um entendimento maior para a criação posterior de suas próprias simulações.



Customizando o exemplo



O tutorial oficial do ns-3, já mencionado antes, segue os exemplos em C++. Começa explicando em maior detalhe o exemplo first.cc (equivalente ao first.py que expliquei aqui), depois propõe mudanças que serão também as mesmas que vou propor na sequencia, mas para nosso exemplo em Python.

Abaixo, o novo código:

import ns.applications
import ns.core
import ns.internet
import ns.network
import ns.point_to_point

def main(argv):

  cmd = ns.core.CommandLine()
  
  cmd.nPackets = 1
  cmd.AddValue("nPackets", "Number of packets to echo")

  cmd.Parse(argv)

  ns.core.LogComponentEnable("UdpEchoClientApplication", ns.core.LOG_LEVEL_INFO)
  ns.core.LogComponentEnable("UdpEchoServerApplication", ns.core.LOG_LEVEL_INFO)

  nodes = ns.network.NodeContainer()
  nodes.Create(2)

  pointToPoint = ns.point_to_point.PointToPointHelper()
  # pointToPoint.SetDeviceAttribute("DataRate", ns.core.StringValue("5Mbps"))
  # pointToPoint.SetChannelAttribute("Delay", ns.core.StringValue("2ms"))

  devices = pointToPoint.Install(nodes)

  stack = ns.internet.InternetStackHelper()
  stack.Install(nodes)

  address = ns.internet.Ipv4AddressHelper()
  address.SetBase(ns.network.Ipv4Address("10.1.1.0"), ns.network.Ipv4Mask("255.255.255.0"))

  interfaces = address.Assign (devices)

  echoServer = ns.applications.UdpEchoServerHelper(9)

  serverApps = echoServer.Install(nodes.Get(1))
  serverApps.Start(ns.core.Seconds(1.0))
  serverApps.Stop(ns.core.Seconds(10.0))

  echoClient = ns.applications.UdpEchoClientHelper(interfaces.GetAddress(1), 9)
  echoClient.SetAttribute("MaxPackets", ns.core.UintegerValue(int(cmd.nPackets)))
  echoClient.SetAttribute("Interval", ns.core.TimeValue(ns.core.Seconds (1.0)))
  echoClient.SetAttribute("PacketSize", ns.core.UintegerValue(1024))

  clientApps = echoClient.Install(nodes.Get(0))
  clientApps.Start(ns.core.Seconds(2.0))
  clientApps.Stop(ns.core.Seconds(10.0))

  ns.core.Simulator.Run()
  ns.core.Simulator.Destroy()

if __name__ == '__main__':
  import sys
  main(sys.argv)


Método main - A primeira mudança já pode ser percebida com a criação de um método main para comportar nosso código anterior. Ao contrário do que pode parecer em uma primeira olhada, este método não será chamado automaticamente (como um main de linguagens C-like), mas é necessário invocá-lo explicitamente, como pode ser conferido nas três últimas linhas do arquivo, onde também mostra como podemos informar ao método os valores eventualmente informados na linha de comando.

Parâmetros da linha de comando - Essa é a segunda mudança. As quatro primeiras linhas dentro do método main tratam de ler o que foi informado a partir da linha de comando. Isso nos dá uma incrível flexibilidade na parametrização dos testes sem alteração de código. A declaração cmd.nPackets = 1 e a linha seguinte definem uma variável que pode ser informada por parâmetro ou, caso contrário, terá o valor padrão 1. O valor informado, por fim, será utilizado na linha 43, na definição do máximo de pacotes a ser processado pela aplicação cliente.

A linha cmd.Parse(argv) traduz as informações fornecidas via linha de comando, possibilitando inclusive configurar atributos das classes que utilizamos na simulação, como veremos a seguir. Para que os atributos sejam configurados via linha de comando, é importante não configurá-los no código, caso contrário, o código prevalecerá e a linha de comando não surtirá efeito algum. Observe as linhas 23 e 24 comentadas.


Iniciando a simulação com uma outra linha de comando:

./waf --pyrun "scratch/first.py --nPackets=1 --ns3::PointToPointNetDevice::DataRate=5Mbps --ns3::PointToPointChannel::Delay=2ms"

A chamada acima informa --nPackets=1 e configura os atributos ns3::PointToPointNetDevice::DataRate e ns3::PointToPointChannel::Delay a princípio com os mesmos valores que já tínhamos no código, mas a partir de agora você pode informar os valores que quiser e observar os diferentes resultados da simulação.



A linha abaixo chama --PrintHelp que ajuda a ver as opções disponíveis e inclui também os atributos definidos por nós (como o nPackets):

./waf --run "scratch/myfirst --PrintHelp"

Há outras possibilidades que podem ser exploradas pela linha de comando, como, por exemplo, imprimir na tela os atributos possíveis para determinada classe:

./waf --pyrun "scratch/first.py --PrintAttributes=ns3::PointToPointChannel

Até nossa variável nPackets não tem real necessidade de existir, salvo para propósitos didáticos, pois este atributo também pode ser configurado via linha de comando. Só não esqueça de remover sua atribuição no código, para não ocorrer de a linha de comando não surtir efeito, conforme expliquei sobre os atributos DataRate e Delay.

./waf --pyrun "scratch/first.py --ns3::PointToPointNetDevice::DataRate=5Mbps --ns3::PointToPointChannel::Delay=2ms --ns3::UdpEchoClient::MaxPackets=2"



Por hora é isso, mas pretendo fazer novos posts sobre o ns-3, pois estou curtindo bastante esta disciplina e já vi que há muitos outros conteúdos interessantes que ainda serão explorados.

Até a próxima!

6 comentários:

  1. Como exportar resultados?

    ResponderExcluir
    Respostas
    1. Olá! Peço desculpas pela demora em retornar e peço que confira minha resposta geral nos comentários. Obrigado!

      Excluir
  2. Olá Daniel, ótima postagem, procuro por um programador com solidos conhecimentos em NS3 para desenvolvimento de um pequeno projeto, você teria disponibilidade? ou alguma indicação? Obrigado

    ResponderExcluir
    Respostas
    1. Olá! Peço desculpas pela demora em retornar e peço que confira minha resposta geral nos comentários. Obrigado!

      Excluir
  3. Opa, pessoal! Peço desculpas pelo demora em responder. Há bastante tempo deixei de atualizar o blog, infelizmente. Sobre este post especificamente, escrevi durante um semestre do mestrado em que tive aulas sobre redes e trabalhamos com ns-3. Nossos experimentos foram mais superficiais, afinal a ferramenta era capaz de muito mais do que poderíamos explorar em um semestre. Além disso, hoje eu realmente não lembro nada mais, pois acabei focando e me especializando em outra área. Para quem quiser um auxílio ou até uma parceria para algum projeto que utilize o ns-3 eu recomendo fortemente entrar em contato com o Prof. Dr. Tiago Ferreto (http://www.inf.pucrs.br/ferreto). Se ele não puder ajudar diretamente, saberá indicar algum aluno que tenha continuado pesquisas nesta área. Peço desculpas novamente e agradeço pela compreensão. Forte abraço

    ResponderExcluir
  4. Olá! Peço desculpas pela demora em retornar e peço que confira minha resposta geral nos comentários. Obrigado!

    ResponderExcluir