A Discrete-Event Network Simulator
Rastreamento

Rastreamento

Introdução

Como abordado na seção Usando o Sistema de Rastreamento, o objetivo principal de uma simulação no ns-3 é a geração de saída para estudo. Há duas estratégias básicas: usar mecanismos predefinidos de saída e processar o conteúdo para extrair informações relevantes; ou desenvolver mecanismos de saída que resultam somente ou exatamente na informação pretendida.

Usar mecanismos predefinidos de saída possui a vantagem de não necessitar modificações no ns-3, mas requer programação. Geralmente, as mensagens de saída do pcap ou NS_LOG são coletadas durante a execução da simulação e processadas separadamente por códigos (scripts) que usam grep, sed ou awk para reduzir e transformar os dados para uma forma mais simples de gerenciar. Há o custo do desenvolvimento de programas para realizar as transformações e em algumas situações a informação de interesse pode não estar contida em nenhuma das saídas, logo, a abordagem falha.

Se precisarmos adicionar o mínimo de informação para os mecanismos predefinidos de saída, isto certamente pode ser feito e se usarmos os mecanismos do ns-3, podemos ter nosso código adicionado como uma contribuição.

O ns-3 fornece outro mecanismo, chamado Rastreamento (Tracing), que evita alguns dos problemas associados com os mecanismos de saída predefinidos. Há várias vantagens. Primeiro, redução da quantidade de dados para gerenciar (em simulações grandes, armazenar toda saída no disco pode gerar gargalos de Entrada/Saída). Segundo, o formato da saída pode ser controlado diretamente evitando o pós-processamento com códigos sed ou awk. Se desejar, a saída pode ser processada diretamente para um formato reconhecido pelo gnuplot, por exemplo. Podemos adicionar ganchos (“hooks”) no núcleo, os quais podem ser acessados por outros usuários, mas que não produzirão nenhuma informação exceto que sejam explicitamente solicitados a produzir. Por essas razões, acreditamos que o sistema de rastreamento do ns-3 é a melhor forma de obter informações fora da simulação, portanto é um dos mais importantes mecanismos para ser compreendido no ns-3.

Métodos Simples

Há várias formas de obter informação após a finalização de um programa. A mais direta é imprimir a informação na saída padrão, como no exemplo,

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

Ninguém impedirá que editemos o núcleo do ns-3 e adicionemos códigos de impressão. Isto é simples de fazer, além disso temos controle e acesso total ao código fonte do ns-3. Entretanto, pensando no futuro, isto não é muito interessante.

Conforme aumentarmos o número de comandos de impressão em nossos programas, ficará mais difícil tratar a grande quantidade de saídas. Eventualmente, precisaremos controlar de alguma maneira qual a informação será impressa; talvez habilitando ou não determinadas categorias de saídas, ou aumentando ou diminuindo a quantidade de informação desejada. Se continuarmos com esse processo, descobriremos depois de um tempo que, reimplementamos o mecanismo NS_LOG. Para evitar isso, utilize o próprio NS_LOG.

Como abordado anteriormente, uma maneira de obter informação de saída do ns-3 é processar a saída do NS_LOG, filtrando as informações relevantes. Se a informação não está presente nos registros existentes, pode-se editar o núcleo do ns-3 e adicionar ao fluxo de saída a informação desejada. Claro, isto é muito melhor que adicionar comandos de impressão, desde que seguindo as convenções de codificação do ns-3, além do que isto poderia ser potencialmente útil a outras pessoas.

Vamos analisar um exemplo, adicionando mais informações de registro ao socket TCP do arquivo tcp-socket-base.cc, para isto vamos acrescentando uma nova mensagem de registro na implementação. Observe que em TcpSocketBase::ReceivedAck() não existem mensagem de registro para casos sem o ACK, então vamos adicionar uma da seguinte forma:

/** Processa o mais recente ACK recebido */
void
TcpSocketBase::ReceivedAck (Ptr<Packet> packet, const TcpHeader& tcpHeader)
{
  NS_LOG_FUNCTION (this << tcpHeader);

  // ACK Recebido. Compara o número ACK com o mais alto seqno não confirmado
  if (0 == (tcpHeader.GetFlags () & TcpHeader::ACK))
    { // Ignora se não há flag ACK
    }
  ...

para adicionar um novo NS_LOG_LOGIC na sentença apropriada:

/** Processa o mais recente ACK recebido */
void
TcpSocketBase::ReceivedAck (Ptr<Packet> packet, const TcpHeader& tcpHeader)
{
  NS_LOG_FUNCTION (this << tcpHeader);

  // ACK Recebido. Compara o número ACK com o mais alto seqno não confirmado
  if (0 == (tcpHeader.GetFlags () & TcpHeader::ACK))
    { // Ignora se não há flag ACK
      NS_LOG_LOGIC ("TcpSocketBase " << this << " sem flag ACK");
    }
  ...

Isto pode parecer simples e satisfatório a primeira vista, mas lembre-se que nós escreveremos código para adicionar ao NS_LOG e para processar a saída com a finalidade de isolar a informação de interesse. Isto porque o controle é limitado ao nível do componente de registro.

Se cada desenvolvedor adicionar códigos de saída para um módulo existente, logo conviveremos com a saída que outro desenvolvedor achou interessante. É descobriremos que para obter uma pequena quantidade de informação, precisaremos produzir uma volumosa quantidade de mensagens sem nenhuma relevância (devido aos comandos de saída de vários desenvolvedores). Assim seremos forçados a gerar arquivos de registros gigantescos no disco e processá-los para obter poucas linhas de nosso interesse.

Como não há nenhuma garantia no ns-3 sobre a estabilidade da saída do NS_LOG, podemos descobrir que partes do registro de saída, que dependíamos, desapareceram ou mudaram entre versões. Se dependermos da estrutura da saída, podemos encontrar outras mensagens sendo adicionadas ou removidas que podem afetar seu código de processamento.

Por estas razões, devemos considerar o uso do std::cout e as mensagens NS_LOG como formas rápidas e porém sujas de obter informação da saída no ns-3.

Na grande maioria dos casos desejamos ter um mecanismo estável, usando APIs que permitam acessar o núcleo do sistema e obter somente informações interessantes. Isto deve ser possível sem que exista a necessidade de alterar e recompilar o núcleo do sistema. Melhor ainda seria se um sistema notificasse o usuário quando um item de interesse fora modificado ou um evento de interesse aconteceu, pois o usuário não teria que constantemente vasculhar o sistema procurando por coisas.

O sistema de rastreamento do ns-3 é projetado para trabalhar seguindo essas premissas e é integrado com os subsistemas de Atributos (Attribute) e Configuração (Config) permitindo cenários de uso simples.

Visão Geral

O sistema de rastreamento do ns-3 é baseado no conceito independente origem do rastreamento e destino do rastreamento. O ns-3 utiliza um mecanismo uniforme para conectar origens a destinos.

As origens do rastreamento (trace source) são entidades que podem assinalar eventos que ocorrem na simulação e fornecem acesso a dados de baixo nível. Por exemplo, uma origem do rastreamento poderia indicar quando um pacote é recebido por um dispositivo de rede e prove acesso ao conteúdo do pacote aos interessados no destino do rastreamento. Uma origem do rastreamento pode também indicar quando uma mudança de estado ocorre em um modelo. Por exemplo, a janela de congestionamento do modelo TCP é um forte candidato para uma origem do rastreamento.

A origem do rastreamento não são úteis sozinhas; elas devem ser conectadas a outras partes de código que fazem algo útil com a informação provida pela origem. As entidades que consomem a informação de rastreamento são chamadas de destino do rastreamento (trace sinks). As origens de rastreamento são geradores de eventos e destinos de rastreamento são consumidores. Esta divisão explícita permite que inúmeras origens de rastreamento estejam dispersas no sistema em locais que os autores do modelo acreditam ser úteis.

Pode haver zero ou mais consumidores de eventos de rastreamento gerados por uma origem do rastreamento. Podemos pensar em uma origem do rastreamento como um tipo de ligação de informação ponto-para-multiponto. Seu código buscaria por eventos de rastreamento de uma parte específica do código do núcleo e poderia coexistir com outro código que faz algo inteiramente diferente com a mesma informação.

Ao menos que um usuário conecte um destino do rastreamento a uma destas origens, nenhuma saída é produzida. Usando o sistema de rastreamento, todos conectados em uma mesma origem do rastreamento estão obtendo a informação que desejam do sistema. Um usuário não afeta os outros alterando a informação provida pela origem. Se acontecer de adicionarmos uma origem do rastreamento, seu trabalho como um bom cidadão utilizador de código livre pode permitir que outros usuários forneçam novas utilidades para todos, sem fazer qualquer modificação no núcleo do ns-3.

Um Exemplo Simples de Baixo Nível

Vamos gastar alguns minutos para entender um exemplo de rastreamento simples. Primeiramente precisamos compreender o conceito de callbacks para entender o que está acontecendo no exemplo.

Callbacks

O objetivo do sistema de Callback, no ns-3, é permitir a uma parte do código invocar uma função (ou método em C++) sem qualquer dependência entre módulos. Isto é utilizado para prover algum tipo de indireção – desta forma tratamos o endereço da chamada de função como uma variável. Esta variável é denominada variável de ponteiro-para-função. O relacionamento entre função e ponteiro-para-função não é tão diferente que de um objeto e ponteiro-para-objeto.

Em C, o exemplo clássico de um ponteiro-para-função é um ponteiro-para-função-retornando-inteiro (PFI). Para um PFI ter um parâmetro inteiro, poderia ser declarado como,

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

O código descreve uma variável nomeada como “pfi” que é inicializada com o valor 0. Se quisermos inicializar este ponteiro com um valor significante, temos que ter uma função com uma assinatura idêntica. Neste caso, poderíamos prover uma função como,

int MyFunction (int arg) {}

Dessa forma, podemos inicializar a variável apontando para uma função:

pfi = MyFunction;

Podemos então chamar MyFunction indiretamente, usando uma forma mais clara da chamada,

int result = (*pfi) (1234);

É uma forma mais clara, pois é como se estivéssemos dereferenciando o ponteiro da função como dereferenciamos qualquer outro ponteiro. Tipicamente, todavia, usa-se uma forma mais curta pois o compilador sabe o que está fazendo,

int result = pfi (1234);

Esta forma é como se estivessemos chamando uma função nomeada “pfi”, mas o compilador reconhece que é uma chamada indireta da função MyFunction por meio da variável pfi.

Conceitualmente, é quase exatamente como o sistema de rastreamento funciona. Basicamente, uma origem do rastreamento é um callback. Quando um destino do rastreamento expressa interesse em receber eventos de rastreamento, ela adiciona a callback para a lista de callbacks mantida internamente pela origem do rastreamento. Quando um evento de interesse ocorre, a origem do rastreamento invoca seu operator() provendo zero ou mais parâmetros. O operator() eventualmente percorre o sistema e faz uma chamada indireta com zero ou mais parâmetros.

Uma diferença importante é que o sistema de rastreamento adiciona para cada origem do rastreamento uma lista interna de callbacks. Ao invés de apenas fazer uma chamada indireta, uma origem do rastreamento pode invocar qualquer número de callbacks. Quando um destino do rastreamento expressa interesse em notificações de uma origem, ela adiciona sua própria função para a lista de callback.

Estando interessado em mais detalhes sobre como é organizado o sistema de callback no ns-3, leia a seção Callback do manual.

Código de Exemplo

Analisaremos uma implementação simples de um exemplo de rastreamento. Este código está no diretório do tutorial, no arquivo 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;

A maior parte deste código deve ser familiar, pois como já abordado, o sistema de rastreamento faz uso constante dos sistemas Objeto (Object) e Atributos (Attribute), logo é necessário incluí-los. As duas primeiras inclusões (include) declaram explicitamente estes dois sistemas. Poderíamos usar o cabeçalho (header) do módulo núcleo, este exemplo é simples.

O arquivo traced-value.h é uma declaração obrigatória para rastreamento de dados que usam passagem por valor. Na passagem por valor é passada uma cópia do objeto e não um endereço. Com a finalidade de usar passagem por valor, precisa-se de um objeto com um construtor de cópia associado e um operador de atribuição. O conjunto de operadores predefinidos para tipos de dados primitivos (plain-old-data) são ++, —, +, ==, etc.

Isto significa que somos capazes de rastrear alterações em um objeto C++ usando estes operadores.

Como o sistema de rastreamento é integrado com Atributos e este trabalham com Objetos, deve obrigatoriamente existir um Object ns-3 para cada origem do rastreamento. O próximo código define e declara um Objeto.

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

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

As duas linhas mais importantes com relação ao rastreamento são .AddTraceSource e a declaração TracedValue do m_myInt.

O método .AddTraceSource provê a “ligação” usada para conectar a origem do rastreamento com o mundo externo, por meio do sistema de configuração. A declaração TracedValue provê a infraestrutura que sobrecarrega os operadores abordados anteriormente e gerencia o processo de callback.

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

Esta é a definição do destino do rastreamento. Isto corresponde diretamente a função de callback. Uma vez que está conectada, esta função será chamada sempre que um dos operadores sobrecarregados de TracedValue é executado.

Nós temos a origem e o destino do rastreamento. O restante é o código para conectar a origem ao destino.

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

  myObject->m_myInt = 1234;
}

Criamos primeiro o Objeto no qual está a origem do rastreamento.

No próximo passo, o TraceConnectWithoutContext conecta a origem ao destino do rastreamento. Observe que a função MakeCallback cria o objeto callback e associa com a função IntTrace. TraceConnectWithoutContext faz a associação entre a sua função e o operator(), sobrecarregado a variável rastreada referenciada pelo Atributo "MyInteger". Depois disso, a origem do rastreamento “disparará” sua função de callback.

O código para fazer isto acontecer não é trivial, mas a essência é a mesma que se a origem do rastreamento chamasse a função pfi() do exemplo anterior. A declaração TracedValue<int32_t> m_myInt; no Objeto é responsável pela mágica dos operadores sobrecarregados que usarão o operator() para invocar o callback com os parâmetros desejados. O método .AddTraceSource conecta o callback ao sistema de configuração, e TraceConnectWithoutContext conecta sua função a fonte de rastreamento, a qual é especificada por um nome Atributo.

Vamos ignorar um pouco o contexto.

Finalmente a linha,

myObject->m_myInt = 1234;

deveria ser interpretada como uma invocação do operador = na variável membro m_myInt com o inteiro 1234 passado como parâmetro.

Por sua vez este operador é definido (por TracedValue) para executar um callback que retorna void e possui dois inteiros como parâmetros — um valor antigo e um novo valor para o inteiro em questão. Isto é exatamente a assinatura da função para a função de callback que nós fornecemos — IntTrace.

Para resumir, uma origem do rastreamento é, em essência, uma variável que mantém uma lista de callbacks. Um destino do rastreamento é uma função usada como alvo da callback. O Atributo e os sistemas de informação de tipo de objeto são usados para fornecer uma maneira de conectar origens e destinos do rastreamento. O ação de “acionar” uma origem do rastreamento é executar um operador na origem, que dispara os callbacks. Isto resulta na execução das callbacks dos destinos do rastreamento registrados na origem com os parâmetros providos pela origem.

Se compilarmos e executarmos este exemplo,

./waf --run fourth

observaremos que a saída da função IntTrace é processada logo após a execução da origem do rastreamento:

Traced 0 to 1234

Quando executamos o código, myObject->m_myInt = 1234; a origem do rastreamento disparou e automaticamente forneceu os valores anteriores e posteriores para o destino do rastreamento. A função IntTrace então imprimiu na saída padrão, sem maiores problemas.

Usando o Subsistema de Configuração para Conectar as Origens de Rastreamento

A chamada TraceConnectWithoutContext apresentada anteriormente é raramente usada no sistema. Geralmente, o subsistema Config é usado para selecionar uma origem do rastreamento no sistema usando um caminho de configuração (config path). Nós estudamos um exemplo onde ligamos o evento “CourseChange”, quando estávamos brincando com third.cc.

Nós definimos um destino do rastreamento para imprimir a informação de mudança de rota dos modelos de mobilidade de nossa simulação. Agora está mais claro o que está função realizava.

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

Quando conectamos a origem do rastreamento “CourseChange” para o destino do rastreamento anteriormente, usamos o que é chamado de caminho de configuração (“Config Path”) para especificar a origem e o novo destino do rastreamento.

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

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

Para entendermos melhor o código, suponha que o número do nó retornado por GetId() é “7”. Neste caso, o caminho seria,

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

O último segmento de um caminho de configuração deve ser um Atributo de um Objeto. Na verdade, se tínhamos um ponteiro para o Objeto que tem o Atributo “CourseChange” , poderíamos escrever como no exemplo anterior. Nós sabemos que guardamos tipicamente ponteiros para outros nós em um ``NodeContainer. No exemplo third.cc, os nós de rede de interesse estão armazenados no wifiStaNodes NodeContainer. De fato enquanto colocamos o caminho junto usamos este contêiner para obter um Ptr<Node>, usado na chamada GetId(). Poderíamos usar diretamente o Ptr<Node> para chamar um método de conexão.

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

No exemplo third.cc, queremos um “contexto” adicional para ser encaminhado com os parâmetros do callback (os quais são explicados a seguir) então podemos usar o código equivalente,

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

Acontece que o código interno para Config::ConnectWithoutContext e Config::Connect permite localizar um Ptr<Object> e chama o método TraceConnect, no nível mais baixo.

As funções Config aceitam um caminho que representa uma cadeia de ponteiros de Objetos. Cada segmento do caminho corresponde a um Atributo Objeto. O último segmento é o Atributo de interesse e os seguimentos anteriores devem ser definidos para conter ou encontrar Objetos. O código Config processa o caminho até obter o segmento final. Então, interpreta o último segmento como um Atributo no último Objeto ele encontrou no caminho. Então as funções Config chamam o método TraceConnect ou TraceConnectWithoutContext adequado no Objeto final.

Vamos analisar com mais detalhes o processo descrito.

O primeiro caractere “/” no caminho faz referência a um namespace. Um dos namespaces predefinidos no sistema de configuração é “NodeList” que é uma lista de todos os nós na simulação. Itens na lista são referenciados por índices , logo “/NodeList/7” refere-se ao oitavo nó na lista de nós criados durante a simulação. Esta referência é um Ptr<Node>, por consequência é uma subclasse de um ns3::Object.

Como descrito na seção Modelo de Objeto do manual ns-3, há suporte para Agregação de Objeto. Isto permite realizar associação entre diferentes Objetos sem qualquer programação. Cada Objeto em uma Agregação pode ser acessado a partir de outros Objetos.

O próximo segmento no caminho inicia com o carácter “$”. O cifrão indica ao sistema de configuração que uma chamada GetObject deveria ser realizada procurando o tipo especificado em seguida. É diferente do que o MobilityHelper usou em third.cc gerenciar a Agregação, ou associar, um modelo de mobilidade para cada dos nós de rede sem fio. Quando adicionamos o “$”, significa que estamos pedindo por outro Objeto que tinha sido presumidamente agregado anteriormente. Podemos pensar nisso como ponteiro de comutação do Ptr<Node> original como especificado por “/NodeList/7” para os modelos de mobilidade associados — quais são do tipo “$ns3::MobilityModel”. Se estivermos familiarizados com GetObject, solicitamos ao sistema para fazer o seguinte:

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

Estamos no último Objeto do caminho e neste verificamos os Atributos daquele Objeto. A classe MobilityModel define um Atributo chamado “CourseChange”. Observando o código fonte em src/mobility/model/mobility-model.cc e procurando por “CourseChange”, encontramos,

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

o qual parece muito familiar neste momento.

Se procurarmos por declarações semelhantes das variáveis rastreadas em mobility-model.h encontraremos,

TracedCallback<Ptr<const MobilityModel> > m_courseChangeTrace;

A declaração de tipo TracedCallback identifica m_courseChangeTrace como um lista especial de callbacks que pode ser ligada usando as funções de Configuração descritas anteriormente.

A classe MobilityModel é projetada para ser a classe base provendo uma interface comum para todas as subclasses. No final do arquivo, encontramos um método chamado NotifyCourseChange():

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

Classes derivadas chamarão este método toda vez que fizerem uma alteração na rota para suportar rastreamento. Este método invoca operator() em m_courseChangeTrace, que invocará todos os callbacks registrados, chamando todos os trace sinks que tem interesse registrado na origem do rastreamento usando a função de Configuração.

No exemplo third.cc nós vimos que sempre que uma mudança de rota é realizada em uma das instâncias RandomWalk2dMobilityModel instaladas, haverá uma chamada NotifyCourseChange() da classe base MobilityModel. Como observado, isto invoca operator() em m_courseChangeTrace, que por sua vez, chama qualquer destino do rastreamento registrados. No exemplo, o único código que registrou interesse foi aquele que forneceu o caminho de configuração. Consequentemente, a função CourseChange que foi ligado no Node de número sete será a única callback chamada.

A peça final do quebra-cabeça é o “contexto”. Lembre-se da saída de third.cc:

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

A primeira parte da saída é o contexto. É simplesmente o caminho pelo qual o código de configuração localizou a origem do rastreamento. No caso, poderíamos ter qualquer número de origens de rastreamento no sistema correspondendo a qualquer número de nós com modelos de mobilidade. É necessário uma maneira de identificar qual origem do rastreamento disparou o callback. Uma forma simples é solicitar um contexto de rastreamento quando é usado o Config::Connect.

Como Localizar e Conectar Origens de Rastreamento, e Descobrir Assinaturas de Callback

As questões que inevitavelmente os novos usuários do sistema de Rastreamento fazem, são:

  1. “Eu sei que existem origens do rastreamento no núcleo da simulação, mas como eu descubro quais estão disponíveis para mim?”
  2. “Eu encontrei uma origem do rastreamento, como eu defino o caminho de configuração para usar quando eu conectar a origem?”
  3. “Eu encontrei uma origem do rastreamento, como eu defino o tipo de retorno e os argumentos formais da minha função de callback?”
  4. “Eu fiz tudo corretamente e obtive uma mensagem de erro bizarra, o que isso significa?”

Quais Origens de Rastreamento são Disponibilizadas

A resposta é encontrada no Doxygen do ns-3. Acesse o sítio Web do projeto, ns-3 project, em seguida, “Documentation” na barra de navegação. Logo após, “Latest Release” e “API Documentation”.

Acesse o item “Modules” na documentação do NS-3. Agora, selecione o item “C++ Constructs Used by All Modules”. Serão exibidos quatro tópicos extremamente úteis:

  • The list of all trace sources
  • The list of all attributes
  • The list of all global values
  • Debugging

Estamos interessados em “the list of all trace sources” - a lista de todas origens do rastreamento. Selecionando este item, é exibido uma lista com todas origens disponíveis no núcleo do ns-3.

Como exemplo, ns3::MobilityModel, terá uma entrada para

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

No caso, esta foi a origem do rastreamento usada no exemplo third.cc, esta lista será muito útil.

Qual String eu uso para Conectar?

A forma mais simples é procurar na base de código do ns-3 por alguém que já fez uso do caminho de configuração que precisamos para ligar a fonte de rastreamento. Sempre deveríamos primeiro copiar um código que funciona antes de escrever nosso próprio código. Tente usar os comandos:

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

e poderemos encontrar um código operacional que atenda nossas necessidades. Por exemplo, neste caso, ./ns-3-dev/examples/wireless/mixed-wireless.cc tem algo que podemos usar:

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

Se não localizamos nenhum exemplo na distribuição, podemos tentar o Doxygen do ns-3. É provavelmente mais simples que percorrer o exemplo “CourseChanged”.

Suponha que encontramos a origem do rastreamento “CourseChanged” na “The list of all trace sources” e queremos resolver como nos conectar a ela. Você sabe que está usando um ns3::RandomWalk2dMobilityModel. Logo, acesse o item “Class List” na documentação do ns-3. Será exibida a lista de todas as classes. Selecione a entrada para ns3::RandomWalk2dMobilityModel para exibir a documentação da classe.

Acesse a seção “Member Function Documentation” e obterá a documentação para a função GetTypeId. Você construiu uma dessas em um exemplo anterior:

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

Como abordado, este código conecta os sistemas Config e Atributos à origem do rastreamento. É também o local onde devemos iniciar a busca por informação sobre como conectar.

Você está observando a mesma informação para RandomWalk2dMobilityModel; e a informação que você precisa está explícita no Doxygen:

This object is accessible through the following paths with Config::Set
              and Config::Connect:

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

A documentação apresenta como obter o Objeto RandomWalk2dMobilityModel. Compare o texto anterior com o texto que nós usamos no código do exemplo:

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

A diferença é que há duas chamadas GetObject inclusas no texto da documentação. A primeira, para $ns3::MobilityModel solicita a agregação para a classe base. A segunda, para $ns3::RandomWalk2dMobilityModel é usada como cast da classe base para a implementação concreta da classe.

Analisando ainda mais o GetTypeId no Doxygen, temos

No TraceSources defined for this type.
TraceSources defined in parent class ns3::MobilityModel:

CourseChange: The value of the position and/or velocity vector changed
Reimplemented from ns3::MobilityModel

Isto é exatamente o que precisamos saber. A origem do rastreamento de interesse é encontrada em ns3::MobilityModel. O interessante é que pela documentação não é necessário o cast extra para obter a classe concreta, pois a origem do rastreamento está na classe base. Por consequência, o GetObject adicional não é necessário e podemos usar o caminho:

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

que é idêntico ao caminho do exemplo:

/NodeList/7/$ns3::MobilityModel

Quais são os Argumentos Formais e o Valor de Retorno?

A forma mais simples é procurar na base de código do ns-3 por um código existente. Você sempre deveria primeiro copiar um código que funciona antes de escrever seu próprio. Tente usar os comandos:

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

e você poderá encontrar código operacional. Por exemplo, neste caso, ./ns-3-dev/examples/wireless/mixed-wireless.cc tem código para ser reaproveitado.

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

como resultado, MakeCallback indicaria que há uma função callback que pode ser usada. Para reforçar:

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

Acredite em Minha Palavra

Se não há exemplos, pode ser desafiador descobrir por meio da análise do código fonte.

Antes de aventurar-se no código, diremos algo importante: O valor de retorno de sua callback sempre será void. A lista de parâmetros formais para uma TracedCallback pode ser encontrada no lista de parâmetro padrão na declaração. Recorde do exemplo atual, isto está em mobility-model.h, onde encontramos:

TracedCallback<Ptr<const MobilityModel> > m_courseChangeTrace;

Não há uma correspondência de um-para-um entre a lista de parâmetro padrão na declaração e os argumentos formais da função callback. Aqui, há um parâmetro padrão, que é um Ptr<const MobilityModel>. Isto significa que precisamos de uma função que retorna void e possui um parâmetro Ptr<const MobilityModel>. Por exemplo,

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

Isto é tudo que precisamos para Config::ConnectWithoutContext. Se você quer um contexto, use Config::Connect e uma função callback que possui como um parâmetro uma string de contexto, seguido pelo argumento.

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

Para garantir que CourseChangeCallback seja somente visível em seu arquivo, você pode adicionar a palavra chave static, como no exemplo:

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

exatamente o que é usado no exemplo third.cc.

A Forma Complicada

Esta seção é opcional. Pode ser bem penosa para aqueles que conhecem poucos detalhes de tipos parametrizados de dados (templates). Entretanto, se continuarmos nessa seção, mergulharemos em detalhes de baixo nível do ns-3.

Vamos novamente descobrir qual assinatura da função de callback é necessária para o Atributo “CourseChange”. Isto pode ser doloroso, mas precisamos fazê-lo apenas uma vez. Depois de tudo, você será capaz de entender um TracedCallback.

Primeiro, verificamos a declaração da origem do rastreamento. Recorde que isto está em mobility-model.h:

TracedCallback<Ptr<const MobilityModel> > m_courseChangeTrace;

Esta declaração é para um template. O parâmetro do template está entre <>, logo estamos interessados em descobrir o que é TracedCallback<>. Se não tem nenhuma ideia de onde pode ser encontrado, use o utilitário grep.

Estamos interessados em uma declaração similar no código fonte do ns-3, logo buscamos no diretório src. Então, sabemos que esta declaração tem um arquivo de cabeçalho, e procuramos por ele usando:

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

Obteremos 124 linhas, com este comando. Analisando a saída, encontramos alguns templates que podem ser úteis.

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

Observamos que todas linhas são do arquivo de cabeçalho traced-callback.h, logo ele parece muito promissor. Para confirmar, verifique o arquivo mobility-model.h e procure uma linha que corrobore esta suspeita.

#include "ns3/traced-callback.h"

Observando as inclusões em mobility-model.h, verifica-se a inclusão do traced-callback.h e conclui-se que este deve ser o arquivo.

O próximo passo é analisar o arquivo src/core/model/traced-callback.h e entender sua funcionalidade.

Há um comentário no topo do arquivo que deveria ser animador:

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.

Isto deveria ser familiar e confirma que estamos no caminho correto.

Depois deste comentário, encontraremos

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

Isto significa que TracedCallback é uma classe genérica (templated class). Possui oito possíveis tipos de parâmetros com valores padrões. Retorne e compare com a declaração que você está tentando entender:

TracedCallback<Ptr<const MobilityModel> > m_courseChangeTrace;

O typename T1 na declaração da classe corresponde a Ptr<const MobilityModel> da declaração anterior. Todos os outros parâmetros são padrões. Observe que o construtor não contribui com muita informação. O único lugar onde há uma conexão entre a função callback e o sistema de rastreamento é nas funções Connect e ConnectWithoutContext. Como mostrado a seguir:

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);
}

Você está no olho do furação. Quando o template é instanciado pela declaração anterior, o compilador substitui T1 por Ptr<const MobilityModel>.

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

Podemos observar a implementação de tudo que foi explicado até este ponto. O código cria uma callback do tipo adequado e atribui sua função para ela. Isto é equivalente a pfi = MyFunction discutida anteriormente. O código então adiciona a callback para a lista de callbacks para esta origem. O que não observamos ainda é a definição da callback. Usando o utilitário grep podemos encontrar o arquivo ./core/callback.h e verificar a definição.

No arquivo há muito código incompreensível. Felizmente há algum em Inglês.

This class template implements the Functor Design Pattern.
It is used to declare the type of a Callback:
 - the first non-optional template argument represents
   the return type of the callback.
 - the second optional template argument represents
   the type of the first argument to the callback.
 - the third optional template argument represents
   the type of the second argument to the callback.
 - the fourth optional template argument represents
   the type of the third argument to the callback.
 - the fifth optional template argument represents
   the type of the fourth argument to the callback.
 - the sixth optional template argument represents
   the type of the fifth argument to the callback.

Nós estamos tentando descobrir o que significa a declaração

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

Agora entendemos que o primeiro parâmetro, void, indica o tipo de retorno da callback. O segundo parâmetro, Ptr<const MobilityModel> representa o primeiro argumento da callback.

A callback em questão é a sua função que recebe os eventos de rastreamento. Logo, podemos deduzir que precisamos de uma função que retorna void e possui um parâmetro Ptr<const MobilityModel>. Por exemplo,

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

Isto é tudo que precisamos no Config::ConnectWithoutContext. Se você quer um contexto, use Config::Connect e uma função callback que possui como um parâmetro uma string de contexto, seguido pelo argumento.

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

Se queremos garantir que CourseChangeCallback é visível somente em seu arquivo, você pode adicionar a palavra chave static, como no exemplo:

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

o que é exatamente usado no exemplo third.cc. Talvez seja interessante reler a seção (Acredite em Minha Palavra).

Há mais detalhes sobre a implementação de callbacks no manual do ns-3. Elas estão entre os mais usados construtores das partes de baixo-nível do ns-3. Em minha opinião, algo bastante elegante.

E quanto a TracedValue?

No início desta seção, nós apresentamos uma parte de código simples que usou um TracedValue<int32_t> para demonstrar o básico sobre código de rastreamento. Nós desprezamos os métodos para encontrar o tipo de retorno e os argumentos formais para o TracedValue. Acelerando o processo, indicamos o arquivo src/core/model/traced-value.h e a parte relevante do código:

template <typename T>
class TracedValue
{
public:
  ...
  void Set (const T &v) {
    if (m_v != v)
      {
      m_cb (m_v, v);
      m_v = v;
      }
  }
  ...
private:
  T m_v;
  TracedCallback<T,T> m_cb;
};

Verificamos que TracedValue é uma classe parametrizada. No caso simples do início da seção, o nome do tipo é int32_t. Isto significa que a variável membro sendo rastreada (m_v na seção privada da classe) será int32_t m_v. O método Set possui um argumento const int32_t &v. Você deveria ser capaz de entender que o código Set dispará o callback m_cb com dois parâmetros: o primeiro sendo o valor atual do TracedValue; e o segundo sendo o novo valor.

A callback m_cb é declarada como um TracedCallback<T, T> que corresponderá a um TracedCallback<int32_t, int32_t> quando a classe é instanciada.

Lembre-se que o destino da callback de um TracedCallback sempre retorna void. Lembre também que há uma correspondência de um-para-um entre a lista de parâmetros polimórfica e os argumentos formais da função callback. Logo, a callback precisa ter uma assinatura de função similar a:

void
MyCallback (int32_t oldValue, int32_t newValue)
{
  ...
}

Isto é exatamente o que nós apresentamos no exemplo simples abordado anteriormente.

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

Um Exemplo Real

Vamos fazer um exemplo retirado do livro “TCP/IP Illustrated, Volume 1: The Protocols” escrito por W. Richard Stevens. Localizei na página 366 do livro um gráfico da janela de congestionamento e números de sequência versus tempo. Stevens denomina de “Figure 21.10. Value of cwnd and send sequence number while data is being transmitted.” Vamos recriar a parte cwnd daquele gráfico em ns-3 usando o sistema de rastreamento e gnuplot.

Há Fontes de Rastreamento Disponibilizadas?

Primeiro devemos pensar sobre como queremos obter os dados de saída. O que é que nós precisamos rastrear? Consultamos então “The list of all trace sources” para sabermos o que temos para trabalhar. Essa seção encontra-se na documentação na seção “Module”, no item “C++ Constructs Used by All Modules”. Procurando na lista, encontraremos:

ns3::TcpNewReno
CongestionWindow: The TCP connection's congestion window

A maior parte da implementação do TCP no ns-3 está no arquivo src/internet/model/tcp-socket-base.cc enquanto variantes do controle de congestionamento estão em arquivos como src/internet/model/tcp-newreno.cc. Se não sabe a priori dessa informação, use:

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

Haverá páginas de respostas apontando para aquele arquivo.

No início do arquivo src/internet/model/tcp-newreno.cc há as seguintes declarações:

TypeId
TcpNewReno::GetTypeId ()
{
  static TypeId tid = TypeId("ns3::TcpNewReno")
    .SetParent<TcpSocketBase> ()
    .AddConstructor<TcpNewReno> ()
    .AddTraceSource ("CongestionWindow",
                     "The TCP connection's congestion window",
                     MakeTraceSourceAccessor (&TcpNewReno::m_cWnd))
    ;
  return tid;
}

Isto deveria guiá-lo para localizar a declaração de m_cWnd no arquivo de cabeçalho src/internet/model/tcp-newreno.h. Temos nesse arquivo:

TracedValue<uint32_t> m_cWnd; //Congestion window

Você deveria entender este código. Se nós temos um ponteiro para TcpNewReno, podemos fazer TraceConnect para a origem do rastreamento “CongestionWindow” se fornecermos uma callback adequada. É o mesmo tipo de origem do rastreamento que nós abordamos no exemplo simples no início da seção, exceto que estamos usando uint32_t ao invés de int32_t.

Precisamos prover uma callback que retorne void e receba dois parâmetros uint32_t, o primeiro representando o valor antigo e o segundo o novo valor:

void
CwndTrace (uint32_t oldValue, uint32_t newValue)
{
  ...
}

Qual código Usar?

É sempre melhor localizar e modificar um código operacional que iniciar do zero. Portanto, vamos procurar uma origem do rastreamento da “CongestionWindow” e verificar se é possível modificar. Para tal, usamos novamente o grep:

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

Encontramos alguns candidatos: examples/tcp/tcp-large-transfer.cc e src/test/ns3tcp/ns3tcp-cwnd-test-suite.cc.

Nós não visitamos nenhum código de teste ainda, então vamos fazer isto agora. Código de teste é pequeno, logo é uma ótima escolha. Acesse o arquivo src/test/ns3tcp/ns3tcp-cwnd-test-suite.cc e localize “CongestionWindow”. Como resultado, temos

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

Como abordado, temos uma origem do rastreamento “CongestionWindow”; então ela aponta para TcpNewReno, poderíamos alterar o TraceConnect para o que nós desejamos. Vamos extrair o código que precisamos desta função (Ns3TcpCwndTestCase1::DoRun (void)). Se você observar, perceberá que parece como um código ns-3. E descobre-se exatamente que realmente é um código. É um código executado pelo framework de teste, logo podemos apenas colocá-lo no main ao invés de DoRun. A tradução deste teste para um código nativo do ns-3 é apresentada no arquivo examples/tutorial/fifth.cc.

Um Problema Comum e a Solução

O exemplo fifth.cc demonstra um importante regra que devemos entender antes de usar qualquer tipo de Atributo: devemos garantir que o alvo de um comando Config existe antes de tentar usá-lo. É a mesma ideia que um objeto não pode ser usado sem ser primeiro instanciado. Embora pareça óbvio, muitas pessoas erram ao usar o sistema pela primeira vez.

Há três fases básicas em qualquer código ns-3. A primeira é a chamada de “Configuration Time” ou “Setup Time” e ocorre durante a execução da função main, mas antes da chamada Simulator::Run. O segunda fase é chamada de “Simulation Time” e é quando o Simulator::Run está executando seus eventos. Após completar a execução da simulação, Simulator::Run devolve o controle a função main. Quando isto acontece, o código entra na terceira fase, o “Teardown Time”, que é quando estruturas e objetos criados durante a configuração são analisados e liberados.

Talvez o erro mais comum em tentar usar o sistema de rastreamento é supor que entidades construídas dinamicamente durante a fase de simulação estão acessíveis durante a fase de configuração. Em particular, um Socket ns-3 é um objeto dinâmico frequentemente criado por Aplicações (Applications) para comunicação entre nós de redes. Uma Aplicação ns-3 tem um “Start Time” e “Stop Time” associado a ela. Na maioria dos casos, uma Aplicação não tentar criar um objeto dinâmico até que seu método StartApplication é chamado em algum “Start Time”. Isto é para garantir que a simulação está completamente configurada antes que a aplicação tente fazer alguma coisa (o que aconteceria se tentasse conectar a um nó que não existisse durante a fase de configuração). A resposta para esta questão é:

  1. criar um evento no simulador que é executado depois que o objeto dinâmico é criado e ativar o rastreador quando aquele evento é executado; ou
  2. criar o objeto dinâmico na fase de configuração, ativá-lo, e passar o objeto para o sistema usar durante a fase de simulação.

Nós consideramos a segunda abordagem no exemplo fifth.cc. A decisão implicou na criação da Aplicação MyApp, com o propósito de passar um Socket como parâmetro.

Analisando o exemplo fifth.cc

Agora, vamos analisar o programa exemplo detalhando o teste da janela de congestionamento. Segue o código do arquivo localizado em examples/tutorial/fifth.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");

Todo o código apresentado já foi discutido. As próximas linhas são comentários apresentando a estrutura da rede e comentários abordando o problema descrito com o Socket.

// ===========================================================================
//
//         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.
// ===========================================================================
//

A próxima parte é a declaração da Aplicação MyApp que permite que o Socket seja criado na fase de configuração.

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;
};

A classe MyApp herda a classe Application do ns-3. Acesse o arquivo src/network/model/application.h se estiver interessado sobre detalhes dessa herança. A classe MyApp é obrigada sobrescrever os métodos StartApplication e StopApplication. Estes métodos são automaticamente chamado quando MyApp é solicitada iniciar e parar de enviar dados durante a simulação.

Como Aplicações são Iniciadas e Paradas (Opcional)

Nesta seção é explicado como eventos tem início no sistema. É uma explicação mais detalhada e não é necessária se não planeja entender detalhes do sistema. É interessante, por outro lado, pois aborda como partes do ns-3 trabalham e mostra alguns detalhes de implementação importantes. Se você planeja implementar novos modelos, então deve entender essa seção.

A maneira mais comum de iniciar eventos é iniciar uma Aplicação. Segue as linhas de um código ns-3 que faz exatamente isso:

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

O código do contêiner aplicação (src/network/helper/application-container.h) itera pelas aplicações no contêiner e chama,

app->SetStartTime (startTime);

como um resultado da chamada apps.Start e

app->SetStopTime (stopTime);

como um resultado da chamada apps.Stop.

O último resultado destas chamadas queremos ter o simulador executando chamadas em nossa Applications para controlar o inicio e a parada. No caso MyApp, herda da classe Application e sobrescreve StartApplication e StopApplication. Estas são as funções invocadas pelo simulador no momento certo. No caso de MyApp, o MyApp::StartApplication faz o Bind e Connect no socket, em seguida, inicia o fluxo de dados chamando MyApp::SendPacket. MyApp::StopApplication interrompe a geração de pacotes cancelando qualquer evento pendente de envio e também fechando o socket.

Uma das coisas legais sobre o ns-3 é que podemos ignorar completamente os detalhes de implementação de como sua Aplicação é “automaticamente” chamada pelo simulador no momento correto. De qualquer forma, detalhamos como isso acontece a seguir.

Se observarmos em src/network/model/application.cc, descobriremos que o método SetStartTime de uma Application apenas altera a variável m_startTime e o método SetStopTime apenas altera a variável m_stopTime.

Para continuar e entender o processo, precisamos saber que há uma lista global de todos os nós no sistema. Sempre que você cria um nó em uma simulação, um ponteiro para aquele nó é adicionado para a lista global NodeList.

Observe em src/network/model/node-list.cc e procure por NodeList::Add. A implementação public static chama uma implementação privada denominada NodeListPriv::Add. Isto é comum no ns-3. Então, observe NodeListPriv::Add e encontrará,

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

Isto significa que sempre que um Node é criado em uma simulação, como uma implicação, uma chamada para o método Start do nó é agendada para que ocorra no tempo zero. Isto não significa que o nó vai iniciar fazendo alguma coisa, pode ser interpretado como uma chamada informacional no Node dizendo a ele que a simulação teve início, não uma chamada para ação dizendo ao Node iniciar alguma coisa.

Então, o NodeList::Add indiretamente agenda uma chamada para Node::Start no tempo zero, para informar ao novo nó que a simulação foi iniciada. Se olharmos em src/network/model/node.h não acharemos um método chamado Node::Start. Acontece que o método Start é herdado da classe Object. Todos objetos no sistema podem ser avisados que a simulação iniciou e objetos da classe Node são exemplos.

Observe em seguida src/core/model/object.cc. Localize por Object::Start. Este código não é tão simples como você esperava desde que Objects ns-3 suportam agregação. O código em Object::Start então percorre todos os objetos que estão agregados e chama o método DoStart de cada um. Este é uma outra prática muito comum em ns-3. Há um método pública na API, que permanece constante entre implementações, que chama um método de implementação privada que é herdado e implementado por subclasses. Os nomes são tipicamente algo como MethodName para os da API pública e DoMethodName para os da API privada.

Logo, deveríamos procurar por um método Node::DoStart em src/network/model/node.cc. Ao localizar o método, descobrirá um método que percorre todos os dispositivos e aplicações no nó chamando respectivamente device->Start e application->Start.

As classes Device e Application herdam da classe Object, então o próximo passo é entender o que acontece quando Application::DoStart é executado. Observe o código em src/network/model/application.cc:

void
Application::DoStart (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::DoStart ();
}

Aqui finalizamos nosso detalhamento. Ao implementar uma Aplicação do ns-3, sua nova aplicação herda da classe Application. Você sobrescreve os métodos StartApplication e StopApplication e provê mecanismos para iniciar e finalizar o fluxo de dados de sua nova Application. Quando um Node é criado na simulação, ele é adicionado a uma lista global NodeList. A ação de adicionar um nó na lista faz com que um evento do simulador seja agendado para o tempo zero e que chama o método Node::Start do Node recentemente adicionado para ser chamado quando a simulação inicia. Como um Node herda de Object, a chamada invoca o método Object::Start no Node, o qual, por sua vez, chama os métodos DoStart em todos os Objects agregados ao Node (pense em modelos móveis). Como o Node Object tem sobrescritos DoStart, o método é chamado quando a simulação inicia. O método Node::DoStart chama o método Start de todas as Applications no nó. Por sua vez, Applications são também Objects, o que resulta na invocação do Application::DoStart. Quando Application::DoStart é chamada, ela agenda eventos para as chamadas StartApplication e StopApplication na Application. Estas chamadas são projetadas para iniciar e parar o fluxo de dados da Application.

Após essa longa jornada, você pode entende melhor outra parte do ns-3.

A Aplicação MyApp

A Aplicação MyApp precisa de um construtor e um destrutor,

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;
}

O código seguinte é a principal razão da existência desta Aplicação.

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;
}

Neste código inicializamos os atributos da classe. Do ponto de vista do rastreamento, a mais importante é Ptr<Socket> socket que deve ser passado para a aplicação durante o fase de configuração. Lembre-se que vamos criar o Socket como um TcpSocket (que é implementado por TcpNewReno) e associar sua origem do rastreamento de sua “CongestionWindow” antes de passá-lo no método Setup.

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

Este código sobrescreve Application::StartApplication que será chamado automaticamente pelo simulador para iniciar a Application no momento certo. Observamos que é realizada uma operação Socket Bind. Se você conhece Sockets de Berkeley isto não é uma novidade. É responsável pelo conexão no lado do cliente, ou seja, o Connect estabelece uma conexão usando TCP no endereço m_peer. Por isso, precisamos de uma infraestrutura funcional de rede antes de executar a fase de simulação. Depois do Connect, a Application inicia a criação dos eventos de simulação chamando SendPacket.

void
MyApp::StopApplication (void)
{
  m_running = false;

  if (m_sendEvent.IsRunning ())
    {
      Simulator::Cancel (m_sendEvent);
    }

  if (m_socket)
    {
      m_socket->Close ();
    }
}

A todo instante um evento da simulação é agendado, isto é, um Event é criado. Se o Event é uma execução pendente ou está executando, seu método IsRunning retornará true. Neste código, se IsRunning() retorna verdadeiro (true), nós cancelamos (Cancel) o evento, e por consequência, é removido da fila de eventos do simulador. Dessa forma, interrompemos a cadeia de eventos que a Application está usando para enviar seus Packets. A Aplicação não enviará mais pacotes e em seguida fechamos (Close) o socket encerrando a conexão TCP.

O socket é deletado no destrutor quando m_socket = 0 é executado. Isto remove a última referência para Ptr<Socket> que ocasiona o destrutor daquele Objeto ser chamado.

Lembre-se que StartApplication chamou SendPacket para iniciar a cadeia de eventos que descreve o comportamento da Application.

void
MyApp::SendPacket (void)
{
  Ptr<Packet> packet = Create<Packet> (m_packetSize);
  m_socket->Send (packet);

  if (++m_packetsSent < m_nPackets)
    {
      ScheduleTx ();
    }
}

Este código apenas cria um pacote (Packet) e então envia (Send).

É responsabilidade da Application gerenciar o agendamento da cadeia de eventos, então, a chamada ScheduleTx agenda outro evento de transmissão (um SendPacket) até que a Application decida que enviou o suficiente.

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);
    }
}

Enquanto a Application está executando, ScheduleTx agendará um novo evento, que chama SendPacket novamente. Verifica-se que a taxa de transmissão é sempre a mesma, ou seja, é a taxa que a Application produz os bits. Não considera nenhuma sobrecarga de protocolos ou canais físicos no transporte dos dados. Se alterarmos a taxa de transmissão da Application para a mesma taxa dos canais físicos, poderemos ter um estouro de buffer.

Destino do Rastreamento

O foco deste exercício é obter notificações (callbacks) do TCP indicando a modificação da janela de congestionamento. O código a seguir implementa o destino do rastreamento.

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

Esta função registra o tempo de simulação atual e o novo valor da janela de congestionamento toda vez que é modificada. Poderíamos usar essa saída para construir um gráfico do comportamento da janela de congestionamento com relação ao tempo.

Nós adicionamos um novo destino do rastreamento para mostrar onde pacotes são perdidos. Vamos adicionar um modelo de erro.

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

Este destino do rastreamento será conectado a origem do rastreamento “PhyRxDrop” do NetDevice ponto-a-ponto. Esta origem do rastreamento dispara quando um pacote é removido da camada física de um NetDevice. Se olharmos rapidamente src/point-to-point/model/point-to-point-net-device.cc verificamos que a origem do rastreamento refere-se a PointToPointNetDevice::m_phyRxDropTrace. E se procurarmos em src/point-to-point/model/point-to-point-net-device.h por essa variável, encontraremos que ela está declarada como uma TracedCallback<Ptr<const Packet> >. Isto significa que nosso callback deve ser uma função que retorna void e tem um único parâmetro Ptr<const Packet>.

O Programa Principal

O código a seguir corresponde ao início da função principal:

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);

São criados dois nós ligados por um canal ponto-a-ponto, como mostrado na ilustração no início do arquivo.

Nas próximas linhas, temos um código com algumas informações novas. Se nós

rastrearmos uma conexão que comporta-se perfeitamente, terminamos com um

janela de congestionamento que aumenta monoliticamente. Para observarmos um

comportamento interessante, introduzimos erros que causarão perda de pacotes,

duplicação de ACK’s e assim, introduz comportamentos mais interessantes a

janela de congestionamento.

O ns-3 provê objetos de um modelo de erros (ErrorModel) que pode ser adicionado aos canais (Channels). Nós usamos o RateErrorModel que permite introduzir erros no canal dada uma taxa.

Ptr<RateErrorModel> em = CreateObjectWithAttributes<RateErrorModel> (
  "RanVar", RandomVariableValue (UniformVariable (0., 1.)),
  "ErrorRate", DoubleValue (0.00001));
devices.Get (1)->SetAttribute ("ReceiveErrorModel", PointerValue (em));

O código instancia um objeto RateErrorModel. Para simplificar usamos a função CreateObjectWithAttributes que instancia e configura os Atributos. O Atributo “RanVar” foi configurado para uma variável randômica que gera uma distribuição uniforme entre 0 e 1. O Atributo “ErrorRate” também foi alterado. Por fim, configuramos o modelo erro no NetDevice ponto-a-ponto modificando o atributo “ReceiveErrorModel”. Isto causará retransmissões e o gráfico ficará mais interessante.

InternetStackHelper stack;
stack.Install (nodes);

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

Neste código configura a pilha de protocolos da internet nos dois nós de rede, cria interfaces e associa endereços IP para os dispositivos ponto-a-ponto.

Como estamos usando TCP, precisamos de um nó de destino para receber as conexões e os dados. O PacketSink Application é comumente usado no ns-3 para este propósito.

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.));

Este código deveria ser familiar, com exceção de,

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

Este código instancia um PacketSinkHelper e cria sockets usando a classe ns3::TcpSocketFactory. Esta classe implementa o padrão de projeto “fábrica de objetos”. Dessa forma, em vez de criar os objetos diretamente, fornecemos ao PacketSinkHelper um texto que especifica um TypeId usado para criar um objeto que, por sua vez, pode ser usado para criar instâncias de Objetos criados pela implementação da fábrica de objetos.

O parâmetro seguinte especifica o endereço e a porta para o mapeamento.

As próximas duas linhas do código criam o socket e conectam a origem do rastreamento.

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

A primeira declaração chama a função estática Socket::CreateSocket e passa um Node e um TypeId para o objeto fábrica usado para criar o socket.

Uma vez que o TcpSocket é criado e adicionado ao Node, nós usamos TraceConnectWithoutContext para conectar a origem do rastreamento “CongestionWindow” para o nosso destino do rastreamento.

Codificamos uma Application então podemos obter um Socket (durante a fase de configuração) e usar na fase de simulação. Temos agora que instanciar a Application. Para tal, segue os passos:

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.));

A primeira linha cria um Objeto do tipo MyApp – nossa Application. A segunda linha especifica o socket, o endereço de conexão, a quantidade de dados a ser enviada em cada evento, a quantidade de eventos de transmissão a ser gerados e a taxa de produção de dados para estes eventos.

Next, we manually add the MyApp Application to the source node and explicitly call the Start and Stop methods on the Application to tell it when to start and stop doing its thing.

Depois, adicionamos a MyApp Application para o nó origem e chamamos os métodos Start e Stop para dizer quando e iniciar e parar a simulação.

Precisamos agora fazer a conexão entre o receptor com nossa callback.

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

Estamos obtendo uma referência para o Node NetDevice receptor e conectando a origem do rastreamento pelo Atributo “PhyRxDrop” do dispositivo no destino do rastreamento RxDrop.

Finalmente, dizemos ao simulador para sobrescrever qualquer Applications e parar o processamento de eventos em 20 segundos na simulação.

  Simulator::Stop (Seconds(20));
  Simulator::Run ();
  Simulator::Destroy ();

  return 0;
}

Lembre-se que quando Simulator::Run é chamado, a fase de configuração termina e a fase de simulação inicia. Todo o processo descrito anteriormente ocorre durante a chamada dessa função.

Após o retorno do Simulator::Run, a simulação é terminada e entramos na fase de finalização. Neste caso, Simulator::Destroy executa a tarefa pesada e nós apenas retornamos o código de sucesso.

Executando fifth.cc

O arquivo fifth.cc é distribuído no código fonte, no diretório examples/tutorial. Para executar:

./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.20919 1072
1.21511 1608
1.22103 2144
...
1.2471  8040
1.24895 8576
1.2508  9112
RxDrop at 1.25151
...

Podemos observar o lado negativo de usar “prints” de qualquer tipo no rastreamento. Temos mensagens waf sendo impressas sobre a informação relevante. Vamos resolver esse problema, mas primeiro vamos verificar o resultado redirecionando a saída para um arquivo cwnd.dat:

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

Removemos as mensagens do waf e deixamos somente os dados rastreados. Pode-se também comentar as mensagens de “RxDrop...”.

Agora podemos executar o gnuplot (se instalado) e gerar um gráfico:

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

Devemos obter um gráfico da janela de congestionamento pelo tempo no arquivo “cwnd.png”, similar ao gráfico 7.1:

figure:: figures/cwnd.png

Gráfico da janela de congestionamento versus tempo.

Usando Auxiliares Intermediários

Na seção anterior, mostramos como adicionar uma origem do rastreamento e obter informações de interesse fora da simulação. Entretanto, no início do capítulo foi comentado que imprimir informações na saída padrão não é uma boa prática. Além disso, comentamos que não é interessante realizar processamento sobre a saída para isolar a informação de interesse. Podemos pensar que perdemos muito tempo em um exemplo que apresenta todos os problemas que propomos resolver usando o sistema de rastreamento do ns-3. Você estaria correto, mas nós ainda não terminamos.

Uma da coisas mais importantes que queremos fazer é controlar a quantidade de saída da simulação. Nós podemos usar assistentes de rastreamento intermediários fornecido pelo ns-3 para alcançar com sucesso esse objetivo.

Fornecemos um código que separa em arquivos distintos no disco os eventos de modificação da janela e os eventos de remoção. As alterações em cwnd são armazenadas em um arquivo ASCII separadas por TAB e os eventos de remoção são armazenados em um arquivo pcap. As alterações para obter esse resultado são pequenas.

Analisando sixth.cc

Vamos verificar as mudanças do arquivo fifth.cc para o sixth.cc. Verificamos a primeira mudança em CwndChange. Notamos que as assinaturas para o destino do rastreamento foram alteradas e que foi adicionada uma linha para cada um que escreve a informação rastreada para um fluxo (stream) representando um arquivo

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);
}

Um parâmetro “stream” foi adicionado para o destino do rastreamento CwndChange. Este é um objeto que armazena (mantém seguramente vivo) um fluxo de saída em C++. Isto resulta em um objeto muito simples, mas que gerência problemas no ciclo de vida para fluxos e resolve um problema que mesmo programadores experientes de C++ tem dificuldades. Resulta que o construtor de cópia para o fluxo de saída (ostream) é marcado como privado. Isto significa que fluxos de saída não seguem a semântica de passagem por valor e não podem ser usados em mecanismos que necessitam que o fluxo seja copiado. Isto inclui o sistema de callback do ns-3. Além disso, adicionamos a seguinte linha:

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

que substitui std::cout por *stream->GetStream ()

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

Isto demostra que o Ptr<OutputStreamWrapper> está apenas encapsulando um std::ofstream, logo pode ser usado como qualquer outro fluxo de saída.

Uma situação similar ocorre em RxDrop, exceto que o objeto passado (Ptr<PcapFileWrapper>) representa um arquivo pcap. Há uma linha no trace sink para escrever um marcador de tempo (timestamp) eo conteúdo do pacote perdido para o arquivo pcap.

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

É claro, se nós temos objetos representando os dois arquivos, precisamos criá-los em algum lugar e também passá-los aos trace sinks. Se observarmos a função main, temos o código:

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));

Na primeira seção do código, criamos o arquivo de rastreamento ASCII e o objeto responsável para gerenciá-lo. Em seguida, usando uma das formas da função para criação da callback permitimos o objeto ser passado para o destino do rastreamento. As classes assistentes para rastreamento ASCII fornecem um vasto conjunto de funções para facilitar a manipulação de arquivos texto. Neste exemplo, focamos apenas na criação do arquivo para o fluxo de saída.

A função CreateFileStream() instancia um objeto std::ofstream e cria um novo arquivo. O fluxo de saída ofstream é encapsulado em um objeto do ns-3 para gerenciamento do ciclo de vida e para resolver o problema do construtor de cópia.

Então pegamos o objeto que representa o arquivo e passamos para MakeBoundCallback(). Esta função cria um callback como MakeCallback(), mas “associa” um novo valor para o callback. Este valor é adicionado ao callback antes de sua invocação.

Essencialmente, MakeBoundCallback(&CwndChange, stream) faz com que a origem do rastreamento adicione um parâmetro extra “fluxo” após a lista formal de parâmetros antes de invocar o callback. Esta mudança está de acordo com o apresentado anteriormente, a qual inclui o parâmetro Ptr<OutputStreamWrapper> stream.

Na segunda seção de código, instanciamos um PcapHelper para fazer a mesma coisa para o arquivo de rastreamento pcap. A linha de código,

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

cria um arquivo pcap chamado “sixth.pcap” no modo “w” (escrita). O parâmetro final é o “tipo da ligação de dados” do arquivo pcap. As opções estão definidas em bpf.h. Neste caso, DLT_PPP indica que o arquivo pcap deverá conter pacotes prefixado com cabeçalhos ponto-a-ponto. Isto é verdade pois os pacotes estão chegando de nosso driver de dispositivo ponto-a-ponto. Outros tipos de ligação de dados comuns são DLT_EN10MB (10 MB Ethernet) apropriado para dispositivos CSMA e DLT_IEEE802_11 (IEEE 802.11) apropriado para dispositivos sem fio. O arquivo src/network/helper/trace-helper.h" define uma lista com os tipos. As entradas na lista são idênticas as definidas em bpf.h, pois foram duplicadas para evitar um dependência com o pcap.

Um objeto ns-3 representando o arquivo pcap é retornado de CreateFile e usado em uma callback exatamente como no caso ASCII.

É importante observar que ambos objetos são declarados de maneiras muito similares,

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

Mas os objetos internos são inteiramente diferentes. Por exemplo, o Ptr<PcapFileWrapper> é um ponteiro para um objeto ns-3 que suporta Attributes e é integrado dentro do sistema de configuração. O Ptr<OutputStreamWrapper>, por outro lado, é um ponteiro para uma referência para um simples objeto contado. Lembre-se sempre de analisar o objeto que você está referenciando antes de fazer suposições sobre os “poderes” que o objeto pode ter.

Por exemplo, acesse o arquivo src/network/utils/pcap-file-wrapper.h e observe,

class PcapFileWrapper : public Object

que a classe PcapFileWrapper é um Object ns-3 por herança. Já no arquivo src/network/model/output-stream-wrapper.h, observe,

class OutputStreamWrapper : public SimpleRefCount<OutputStreamWrapper>

que não é um Object ns-3, mas um objeto C++ que suporta contagem de referência.

A questão é que se você tem um Ptr<alguma_coisa>, não necessariamente significa que “alguma_coisa” é um Object ns-3, no qual você pode modificar Attributes, por exemplo.

Voltando ao exemplo. Se compilarmos e executarmos o exemplo,

./waf --run sixth

Veremos as mesmas mensagens do “fifth”, mas dois novos arquivos aparecerão no diretório base de sua distribuição do ns-3.

sixth.cwnd  sixth.pcap

Como “sixth.cwnd” é um arquivo texto ASCII, você pode visualizar usando cat ou um editor de texto.

1.20919 536     1072
1.21511 1072    1608
...
9.30922 8893    8925
9.31754 8925    8957

Cada linha tem um marcador de tempo, o valor da janela de congestionamento e o valor da nova janela de congestionamento separados por tabulação, para importar diretamente para seu programa de plotagem de gráficos. Não há nenhuma outra informação além da rastreada, logo não é necessário processamento ou edição do arquivo.

Como “sixth.pcap” é um arquivo pcap, você pode visualizar usando o tcpdump ou wireshark.

reading from file ../../sixth.pcap, link-type PPP (PPP)
1.251507 IP 10.1.1.1.49153 > 10.1.1.2.8080: . 17689:18225(536) ack 1 win 65535
1.411478 IP 10.1.1.1.49153 > 10.1.1.2.8080: . 33808:34312(504) ack 1 win 65535
...
7.393557 IP 10.1.1.1.49153 > 10.1.1.2.8080: . 781568:782072(504) ack 1 win 65535
8.141483 IP 10.1.1.1.49153 > 10.1.1.2.8080: . 874632:875168(536) ack 1 win 65535

Você tem um arquivo pcap com os pacotes que foram descartados na simulação. Não há nenhum outro pacote presente no arquivo e nada mais para dificultar sua análise.

Foi uma longa jornada, mas agora entendemos porque o sistema de rastreamento é interessante. Nós obtemos e armazenamos importantes eventos da implementação do TCP e do driver de dispositivo. E não modificamos nenhuma linha do código do núcleo do ns-3, e ainda fizemos isso com apenas 18 linhas de código:

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));

Usando Classes Assistentes para Rastreamento

As classes assistentes (trace helpers) de rastreamento do ns-3 proveem um ambiente rico para configurar, selecionar e escrever diferentes eventos de rastreamento para arquivos. Nas seções anteriores, primeiramente em “Construindo Topologias”, nós vimos diversas formas de métodos assistentes para rastreamento projetados para uso dentro de outras classes assistentes.

Segue alguns desses métodos já estudados:

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

O que não parece claro é que há um modelo consistente para todos os métodos relacionados à rastreamento encontrados no sistema. Apresentaremos uma visão geral desse modelo.

Há dois casos de uso primários de classes assistentes em ns-3: Classes assistentes de dispositivo e classes assistentes de protocolo. Classes assistentes de dispositivo tratam o problema de especificar quais rastreamentos deveriam ser habilitados no domínio do nó de rede. Por exemplo, poderíamos querer especificar que o rastreamento pcap deveria ser ativado em um dispositivo particular de um nó específico. Isto é o que define o modelo conceitual de dispositivo no ns-3 e também os modelos conceituais de várias classes assistentes de dispositivos. Baseado nisso, os arquivos criados seguem a convenção de nome <prefixo>-<nó>-<dispositivo>.

As classes assistentes de protocolos tratam o problema de especificar quais rastreamentos deveriam ser ativados no protocolo e interface. Isto é definido pelo modelo conceitual de pilha de protocolo do ns-3 e também pelos modelos conceituais de classes assistentes de pilha de rede. Baseado nisso, os arquivos criados seguem a convenção de nome <prefixo>-<protocolo>-<interface>.

As classes assistentes consequentemente encaixam-se em uma taxinomia bi-dimensional. Há pequenos detalhes que evitam todas as classes comportarem-se da mesma forma, mas fizemos parecer que trabalham tão similarmente quanto possível e quase sempre há similares para todos métodos em todas as classes.

                                                   | pcap | ascii |
---------------------------------------------------+------+-------|
Classe Assistente de Dispositivo (*Device Helper*)   |      |       |
---------------------------------------------------+------+-------|
Classe Assistente de Protocolo (*Protocol Helper*)   |      |       |
---------------------------------------------------+------+-------|

Usamos uma abordagem chamada mixin para adicionar funcionalidade de rastreamento para nossas classes assistentes. Uma mixin é uma classe que provê funcionalidade para aquela que é herdada por uma subclasse. Herdar de um mixin não é considerado uma forma de especialização mas é realmente uma maneira de colecionar funcionalidade.

Vamos verificar rapidamente os quatro casos e seus respectivos mixins.

Classes Assistentes de Dispositivo para Rastreamento Pcap

O objetivo destes assistentes é simplificar a adição de um utilitário de rastreamento pcap consistente para um dispositivo ns-3. Queremos que opere da mesma forma entre todos os dispositivos, logo os métodos destes assistentes são herdados por classes assistentes de dispositivo. Observe o arquivo src/network/helper/trace-helper.h para entender a discussão do código a seguir.

A classe PcapHelperForDevice é um mixin que provê a funcionalidade de alto nível para usar rastreamento pcap em um dispositivo ns-3. Todo dispositivo deve implementar um único método virtual herdado dessa classe.

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

A assinatura deste método reflete a visão do dispositivo da situação neste nível. Todos os métodos públicos herdados da classe PcapUserHelperForDevice são reduzidos a chamada da implementação deste simples método dependente de dispositivo. Por exemplo, o nível mais baixo do método pcap,

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

chamaremos diretamente a implementação do dispositivo de EnablePcapInternal. Todos os outros métodos de rastreamento pcap públicos desta implementação são para prover funcionalidade adicional em nível de usuário. Para o usuário, isto significa que todas as classes assistentes de dispositivo no sistema terão todos os métodos de rastreamento pcap disponíveis; e estes métodos trabalharão da mesma forma entre dispositivos se o dispositivo implementar corretamente EnablePcapInternal.

Métodos da Classe Assistente de Dispositivo para Rastreamento 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);

Em cada método apresentado existe um parâmetro padrão chamado promiscuous que é definido para o valor “false”. Este parâmetro indica que o rastreamento não deveria coletar dados em modo promíscuo. Se quisermos incluir todo tráfego visto pelo dispositivo devemos modificar o valor para “true”. Por exemplo,

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

ativará o modo de captura promíscuo no NetDevice especificado por nd.

Os dois primeiros métodos também incluem um parâmetro padrão chamado explicitFilename que será abordado a seguir.

É interessante procurar maiores detalhes dos métodos da classe PcapHelperForDevice no Doxygen; mas para resumir ...

Podemos ativar o rastreamento pcap em um par nó/dispositivo-rede específico provendo um Ptr<NetDevice> para um método EnablePcap. O Ptr<Node> é implícito, pois o dispositivo de rede deve estar em um Node. Por exemplo,

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

Podemos ativar o rastreamento pcap em um par nó/dispositivo-rede passando uma std::string que representa um nome de serviço para um método EnablePcap. O Ptr<NetDevice> é buscado a partir do nome da string. Novamente, o Ptr<Node> é implícito pois o dispositivo de rede deve estar em um Node.

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

Podemos ativar o rastreamento pcap em uma coleção de pares nós/dispositivos usando um NetDeviceContainer. Para cada NetDevice no contêiner o tipo é verificado. Para cada dispositivo com o tipo adequado, o rastreamento será ativado. Por exemplo,

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

Podemos ativar o rastreamento em uma coleção de pares nó/dispositivo-rede usando um NodeContainer. Para cada Node no NodeContainer seus NetDevices são percorridos e verificados segundo o tipo. Para cada dispositivo com o tipo adequado, o rastreamento é ativado.

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

Podemos ativar o rastreamento pcap usando o número identificador (ID) do nó e do dispositivo. Todo Node no sistema tem um valor inteiro indicando o ID do nó e todo dispositivo conectado ao nó tem um valor inteiro indicando o ID do dispositivo.

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

Por fim, podemos ativar rastreamento pcap para todos os dispositivos no sistema, desde que o tipo seja o mesmo gerenciado pela classe assistentes de dispositivo.

helper.EnablePcapAll ("prefix");

Seleção de um Nome de Arquivo para o Rastreamento Pcap da Classe Assistente de Dispositivo

Implícito nas descrições de métodos anteriores é a construção do nome de arquivo por meio do método da implementação. Por convenção, rastreamento pcap no ns-3 usa a forma “<prefixo>-<id do nó>-<id do dispositivo>.pcap”

Como mencionado, todo nó no sistema terá um id de nó associado; e todo dispositivo terá um índice de interface (também chamado de id do dispositivo) relativo ao seu nó. Por padrão, então, um arquivo pcap criado como um resultado de ativar rastreamento no primeiro dispositivo do nó 21 usando o prefixo “prefix” seria “prefix-21-1.pcap”.

Sempre podemos usar o serviço de nome de objeto do ns-3 para tornar isso mais claro. Por exemplo, se você usa o serviço para associar o nome “server” ao nó 21, o arquivo pcap resultante automaticamente será, “prefix-server-1.pcap” e se você também associar o nome “eth0” ao dispositivo, seu nome do arquivo pcap automaticamente será denominado “prefix-server-eth0.pcap”.

Finalmente, dois dos métodos mostrados,

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);

tem um parâmetro padrão explicitFilename. Quando modificado para verdadeiro, este parâmetro desabilita o mecanismo automático de completar o nome do arquivo e permite criarmos um nome de arquivo abertamente. Esta opção está disponível nos métodos que ativam o rastreamento pcap em um único dispositivo.

Por exemplo, com a finalidade providenciar uma classe assistente de dispositivo para criar um único arquivo de captura pcap no modo promíscuo com um nome específico (“my-pcap-file.pcap”) em um determinado dispositivo:

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

O primeiro parâmetro true habilita o modo de rastreamento promíscuo e o segundo faz com que o parâmetro prefix seja interpretado como um nome de arquivo completo.

Classes Assistentes de Dispositivo para Rastreamento ASCII

O comportamento do assistente de rastreamento ASCII mixin é similar a versão do pcap. Acesse o arquivo src/network/helper/trace-helper.h para compreender melhor o funcionamento dessa classe assistente.

A classe AsciiTraceHelperForDevice adiciona funcionalidade em alto nível para usar o rastreamento ASCII para uma classe assistente de dispositivo. Como no caso do pcap, todo dispositivo deve implementar um método herdado do rastreador ASCII mixin.

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

A assinatura deste método reflete a visão do dispositivo da situação neste nível; e também o fato que o assistente pode ser escrito para um fluxo de saída compartilhado. Todos os métodos públicos associados ao rastreamento ASCII herdam da classe AsciiTraceHelperForDevice resumem-se a chamada deste único método dependente de implementação. Por exemplo, os métodos de rastreamento ASCII de mais baixo nível,

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

chamarão uma implementação de EnableAsciiInternal diretamente, passando um prefixo ou fluxo válido. Todos os outros métodos públicos serão construídos a partir destas funções de baixo nível para fornecer funcionalidades adicionais em nível de usuário. Para o usuário, isso significa que todos os assistentes de dispositivo no sistema terão todos os métodos de rastreamento ASCII disponíveis e estes métodos trabalharão do mesmo modo em todos os dispositivos se estes implementarem EnableAsciiInternal.

Métodos da Classe Assistente de Dispositivo para Rastreamento ASCII

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);

Para maiores detalhes sobre os métodos é interessante consultar a documentação para a classe AsciiTraceHelperForDevice; mas para resumir ...

Há duas vezes mais métodos disponíveis para rastreamento ASCII que para rastreamento pcap. Isto ocorre pois para o modelo pcap os rastreamentos de cada par nó/dispositivo-rede são escritos para um único arquivo, enquanto que no ASCII todo as as informações são escritas para um arquivo comum. Isto significa que o mecanismo de geração de nomes de arquivos <prefixo>-<nó>-<dispositivo> é substituído por um mecanismo para referenciar um arquivo comum; e o número de métodos da API é duplicado para permitir todas as combinações.

Assim como no rastreamento pcap, podemos ativar o rastreamento ASCII em um par nó/dispositivo-rede passando um Ptr<NetDevice> para um método EnableAscii. O Ptr<Node> é implícito pois o dispositivo de rede deve pertencer a exatamente um Node. Por exemplo,

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

Os primeiros quatro métodos também incluem um parâmetro padrão explicitFilename que opera similar aos parâmetros no caso do pcap.

Neste caso, nenhum contexto de rastreamento é escrito para o arquivo ASCII pois seriam redundantes. O sistema pegará o nome do arquivo para ser criado usando as mesmas regras como descritas na seção pcap, exceto que o arquivo terá o extensão ”.tr” ao invés de ”.pcap”.

Para habilitar o rastreamento ASCII em mais de um dispositivo de rede e ter todos os dados de rastreamento enviados para um único arquivo, pode-se usar um objeto para referenciar um único arquivo. Nós já verificamos isso no exemplo “cwnd”:

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

Neste caso, os contextos são escritos para o arquivo ASCII quando é necessário distinguir os dados de rastreamento de dois dispositivos. É interessante usar no nome do arquivo a extensão ”.tr” por motivos de consistência.

Podemos habilitar o rastreamento ASCII em um par nó/dispositivo-rede específico passando ao método EnableAscii uma std::string representando um nome no serviço de nomes de objetos. O Ptr<NetDevice> é obtido a partir do nome. Novamente, o <Node> é implícito pois o dispositivo de rede deve pertencer a exatamente um Node. Por exemplo,

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

Isto resultaria em dois nomes de arquivos - “prefix-client-eth0.tr” e “prefix-server-eth0.tr” - com os rastreamentos de cada dispositivo em seu arquivo respectivo. Como todas as funções do EnableAscii são sobrecarregadas para suportar um stream wrapper, podemos usar da seguinte forma também:

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");

Isto resultaria em um único arquivo chamado “trace-file-name.tr” que contém todosos eventos rastreados para ambos os dispositivos. Os eventos seriam diferenciados por strings de contexto.

Podemos habilitar o rastreamento ASCII em um coleção de pares nó/dispositivo-rede fornecendo um NetDeviceContainer. Para cada NetDevice no contêiner o tipo é verificado. Para cada dispositivo de um tipo adequado (o mesmo tipo que é gerenciado por uma classe assistente de dispositivo), o rastreamento é habilitado. Novamente, o <Node> é implícito pois o dispositivo de rede encontrado deve pertencer a exatamente um Node.

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

Isto resultaria em vários arquivos de rastreamento ASCII sendo criados, cada um seguindo a convenção <prefixo>-<id do nó>-<id do dispositivo>.tr.

Para obtermos um único arquivo teríamos:

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

Podemos habilitar o rastreamento ASCII em um coleção de pares nó/dispositivo-rede fornecendo um NodeContainer. Para cada Node no NodeContainer, os seus NetDevices são percorridos. Para cada NetDevice associado a cada nó no contêiner, o tipo do dispositivo é verificado. Para cada dispositivo do tipo adequado (o mesmo tipo que é gerenciado pelo assistente de dispositivo), o rastreamento é habilitado.

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

Isto resultaria em vários arquivos ASCII sendo criados, cada um seguindo a convenção <prefixo>-<id do nó>-<id do dispositivo>.tr.

Podemos habilitar o rastreamento pcap na base da ID do nó e ID do dispositivo tão bem como com um Ptr. Cada Node no sistema possui um número identificador inteiro associado ao nó e cada dispositivo conectado possui um número identificador inteiro associado ao dispositivo.

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

Os rastreamentos podem ser combinados em um único arquivo como mostrado acima.

Finalmente, podemos habilitar o rastreamento ASCII para todos os dispositivos no sistema.

helper.EnableAsciiAll ("prefix");

Isto resultaria em vários arquivos ASCII sendo criados, um para cada dispositivo no sistema do tipo gerenciado pelo assistente. Todos estes arquivos seguiriam a convenção <prefixo>-<id do nó>-<id do dispositivo>.tr.

Selecionando Nome de Arquivo para as Saídas ASCII

Implícito nas descrições de métodos anteriores é a construção do nome de arquivo por meio do método da implementação. Por convenção, rastreamento ASCII no ns-3 usa a forma “<prefixo>-<id do nó>-<id do dispositivo>.tr”.

Como mencionado, todo nó no sistema terá um id de nó associado; e todo dispositivo terá um índice de interface (também chamado de id do dispositivo) relativo ao seu nó. Por padrão, então, um arquivo ASCII criado como um resultado de ativar rastreamento no primeiro dispositivo do nó 21 usando o prefixo “prefix” seria “prefix-21-1.tr”.

Sempre podemos usar o serviço de nome de objeto do ns-3 para tornar isso mais claro. Por exemplo, se usarmos o serviço para associar o nome server ao nó 21, o arquivo ASCII resultante automaticamente será, prefix-server-1.tr e se também associarmos o nome eth0 ao dispositivo, o nome do arquivo ASCII automaticamente será denominado prefix-server-eth0.tr.

Diversos métodos tem um parâmetro padrão explicitFilename. Quando modificado para verdadeiro, este parâmetro desabilita o mecanismo automático de completar o nome do arquivo e permite criarmos um nome de arquivo abertamente. Esta opção está disponível nos métodos que possuam um prefixo e ativem o rastreamento em um único dispositivo.

Classes Assistentes de Protocolo para Rastreamento Pcap

O objetivo destes mixins é facilitar a adição de um mecanismo consistente para da facilidade de rastreamento para protocolos. Queremos que todos os mecanismos de rastreamento para todos os protocolos operem de mesma forma, logo os métodos dessas classe assistentes são herdados por assistentes de pilha. Acesse src/network/helper/trace-helper.h para acompanhar o conteúdo discutido nesta seção.

Nesta seção ilustraremos os métodos aplicados ao protocolo Ipv4. Para especificar rastreamentos em protocolos similares, basta substituir pelo tipo apropriado. Por exemplo, use um Ptr<Ipv6> ao invés de um Ptr<Ipv4> e chame um EnablePcapIpv6 ao invés de EnablePcapIpv4.

A classe PcapHelperForIpv4 provê funcionalidade de alto nível para usar rastreamento no protocolo Ipv4. Cada classe assistente de protocolo devem implementar um método herdado desta. Haverá uma implementação separada para Ipv6, por exemplo, mas a diferença será apenas nos nomes dos métodos e assinaturas. Nomes de métodos diferentes são necessário para distinguir a classe Ipv4 da Ipv6, pois ambas são derivadas da classe Object, logo os métodos compartilham a mesma assinatura.

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

A assinatura desse método reflete a visão do protocolo e interface da situação neste nível. Todos os métodos herdados da classe PcapHelperForIpv4 resumem-se a chamada deste único método dependente de dispositivo. Por exemplo, o método do pcap de mais baixo nível,

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

chamará a implementação de dispositivo de EnablePcapIpv4Internal diretamente. Todos os outros métodos públicos de rastreamento pcap são construídos a partir desta implementação para prover funcionalidades adicionais em nível do usuário. Para o usuário, isto significa que todas as classes assistentes de dispositivo no sistema terão todos os métodos de rastreamento pcap disponíveis; e estes métodos trabalharão da mesma forma entre dispositivos se o dispositivo implementar corretamente EnablePcapIpv4Internal.

Métodos da Classe Assistente de Protocolo para Rastreamento Pcap

Estes métodos são projetados para terem correspondência de um-para-um com o Node e NetDevice. Ao invés de restrições de pares Node e NetDevice, usamos restrições de protocolo e interface.

Note que como na versão de dispositivo, há seis métodos:

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);

Para maiores detalhes sobre estes métodos é interessante consultar na documentação da classe PcapHelperForIpv4, mas para resumir ...

Podemos habilitar o rastreamento pcap em um par protocolo/interface passando um Ptr<Ipv4> e interface para um método EnablePcap. Por exemplo,

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

Podemos ativar o rastreamento pcap em um par protocolo/interface passando uma std::string que representa um nome de serviço para um método EnablePcapIpv4. O Ptr<Ipv4> é buscado a partir do nome da string. Por exemplo,

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

Podemos ativar o rastreamento pcap em uma coleção de pares protocolo/interface usando um Ipv4InterfaceContainer. Para cada par``Ipv4``/interface no contêiner o tipo do protocolo é verificado. Para cada protocolo do tipo adequado, o rastreamento é ativado para a interface correspondente. Por exemplo,

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);

Podemos ativar o rastreamento em uma coleção de pares protocolo/interface usando um NodeContainer. Para cada Node no NodeContainer o protocolo apropriado é encontrado. Para cada protocolo, suas interfaces são enumeradas e o rastreamento é ativado nos pares resultantes. Por exemplo,

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

Pode ativar o rastreamento pcap usando o número identificador do nó e da interface. Neste caso, o ID do nó é traduzido para um Ptr<Node> e o protocolo apropriado é buscado no nó. O protocolo e interface resultante são usados para especificar a origem do rastreamento resultante.

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

Por fim, podemos ativar rastreamento pcap para todas as interfaces no sistema, desde que o protocolo seja do mesmo tipo gerenciado pela classe assistente.

helper.EnablePcapIpv4All ("prefix");

Seleção de um Nome de Arquivo para o Rastreamento Pcap da Classe Assistente de Protocolo

Implícito nas descrições de métodos anterior é a construção do nome de arquivo por meio do método da implementação. Por convenção, rastreamento pcap no ns-3 usa a forma <prefixo>-<id do nó>-<id do dispositivo>.pcap. No caso de rastreamento de protocolos, há uma correspondência de um-para-um entre protocolos e Nodes. Isto porque Objects de protocolo são agregados a Node Objects`. Como não há um id global de protocolo no sistema, usamos o ID do nó na nomeação do arquivo. Consequentemente há possibilidade de colisão de nomes quando usamos o sistema automático de nomes. Por esta razão, a convenção de nome de arquivo é modificada para rastreamentos de protocolos.

Como mencionado, todo nó no sistema terá um id de nó associado. Como há uma correspondência de um-para-um entre instâncias de protocolo e instâncias de nó, usamos o id de nó. Cada interface tem um id de interface relativo ao seu protocolo. Usamos a convenção “<prefixo>-n<id do nó>-i<id da interface>.pcap” para especificar o nome do arquivo de rastreamento para as classes assistentes de protocolo.

Consequentemente, por padrão, uma arquivo pcap criado como um resultado da ativação de rastreamento na interface 1 do protocolo ipv4 do nó 21 usando o prefixo prefix seria prefix-n21-i1.pcap.

Sempre podemos usar o serviço de nomes de objetos do ns-3 para tornar isso mais claro. Por exemplo, se usamos o serviço de nomes para associar o nome “serverIpv4” ao Ptr<Ipv4> no nó 21, o nome de arquivo resultante seria prefix-nserverIpv4-i1.pcap.

Diversos métodos tem um parâmetro padrão explicitFilename. Quando modificado para verdadeiro, este parâmetro desabilita o mecanismo automático de completar o nome do arquivo e permite criarmos um nome de arquivo abertamente. Esta opção está disponível nos métodos que ativam o rastreamento pcap em um único dispositivo.

Classes Assistentes de Protocolo para Rastreamento ASCII

O comportamento dos assistentes de rastreamento ASCII é similar ao do pcap. Acesse o arquivo src/network/helper/trace-helper.h para compreender melhor o funcionamento dessa classe assistente.

Nesta seção apresentamos os métodos aplicados ao protocolo Ipv4. Para protocolos similares apenas substitua para o tipo apropriado. Por exemplo, use um Ptr<Ipv6> ao invés de um Ptr<Ipv4> e chame EnableAsciiIpv6 ao invés de EnableAsciiIpv4.

A classe AsciiTraceHelperForIpv4 adiciona funcionalidade de alto nível para usar rastreamento ASCII para um assistente de protocolo. Todo protocolo que usa estes métodos deve implementar um método herdado desta classe.

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

A assinatura deste método reflete a visão central do protocolo e interface da situação neste nível; e também o fato que o assistente pode ser escrito para um fluxo de saída compartilhado. Todos os métodos públicos herdados desta classe PcapAndAsciiTraceHelperForIpv4 resumem-se a chamada deste único método dependente de implementação. Por exemplo, os métodos de rastreamento ASCII de mais baixo nível,

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);

chamarão uma implementação de EnableAsciiIpv4Internal diretamente, passando um prefixo ou fluxo válido. Todos os outros métodos públicos serão construídos a partir destas funções de baixo nível para fornecer funcionalidades adicionais em nível de usuário. Para o usuário, isso significa que todos os assistentes de protocolos no sistema terão todos os métodos de rastreamento ASCII disponíveis e estes métodos trabalharão do mesmo modo em todos os protocolos se estes implementarem EnableAsciiIpv4Internal.

Métodos da Classe Assistente de Protocolo para Rastreamento 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);

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);

Para maiores detalhes sobre os métodos consulte na documentação a classe PcapAndAsciiHelperForIpv4; mas para resumir ...

Há duas vezes mais métodos disponíveis para rastreamento ASCII que para rastreamento pcap. Isto ocorre pois para o modelo pcap os rastreamentos de cada par protocolo/interface são escritos para um único arquivo, enquanto que no ASCII todo as as informações são escritas para um arquivo comum. Isto significa que o mecanismo de geração de nomes de arquivos “<prefixo>-n<id do nó>-i<interface>” é substituído por um mecanismo para referenciar um arquivo comum; e o número de métodos da API é duplicado para permitir todas as combinações.

Assim, como no rastreamento pcap, podemos ativar o rastreamento ASCII em um par protocolo/interface passando um Ptr<Ipv4> e uma interface para um método EnableAsciiIpv4. Por exemplo,

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

Neste caso, nenhum contexto de rastreamento é escrito para o arquivo ASCII pois seriam redundantes. O sistema pegará o nome do arquivo para ser criado usando as mesmas regras como descritas na seção pcap, exceto que o arquivo terá o extensão .tr ao invés de .pcap.

Para habilitar o rastreamento ASCII em mais de uma interface e ter todos os dados de rastreamento enviados para um único arquivo, pode-se usar um objeto para referenciar um único arquivo. Nós já verificamos isso no exemplo “cwnd”:

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);

Neste caso, os contextos são escritos para o arquivo ASCII quando é necessário distinguir os dados de rastreamento de duas interfaces. É interessante usar no nome do arquivo a extensão .tr por motivos de consistência.

Pode habilitar o rastreamento ASCII em protocolo específico passando ao método EnableAsciiIpv4 uma std::string representando um nome no serviço de nomes de objetos. O Ptr<Ipv4> é obtido a partir do nome. O <Node> é implícito, pois há uma correspondência de um-para-um entre instancias de protocolos e nós. Por exemplo,

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

Isto resultaria em dois nomes de arquivos prefix-nnode1Ipv4-i1.tr e prefix-nnode2Ipv4-i1.tr, com os rastreamentos de cada interface em seu arquivo respectivo. Como todas as funções do EnableAsciiIpv4 são sobrecarregadas para suportar um stream wrapper, podemos usar da seguinte forma também:

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);

Isto resultaria em um único arquivo chamado trace-file-name.tr que contém todos os eventos rastreados para ambas as interfaces. Os eventos seriam diferenciados por strings de contexto.

Podemos habilitar o rastreamento ASCII em um coleção de pares protocolo/interface provendo um Ipv4InterfaceContainer. Para cada protocolo no contêiner o tipo é verificado. Para cada protocolo do tipo adequado (o mesmo tipo que é gerenciado por uma classe assistente de protocolo), o rastreamento é habilitado para a interface correspondente. Novamente, o <Node> é implícito, pois há uma correspondência de um-para-um entre protocolo e seu nó. Por exemplo,

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);

Isto resultaria em vários arquivos de rastreamento ASCII sendo criados, cada um seguindo a convenção <prefixo>-n<id do nó>-i<interface>.tr.

Para obtermos um único arquivo teríamos:

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);

Podemos habilitar o rastreamento ASCII em uma coleção de pares protocolo/interface provendo um NodeContainer`. Para cada Node no NodeContainer os protocolos apropriados são encontrados. Para cada protocolo, sua interface é enumerada e o rastreamento é habilitado nos pares. Por exemplo,

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

Podemos habilitar o rastreamento pcap usando o número identificador do nó e número identificador do dispositivo. Neste caso, o ID do nó é traduzido para um Ptr<Node> e o protocolo apropriado é procurado no nó de rede. O protocolo e interface resultantes são usados para especificar a origem do rastreamento.

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

Os rastreamentos podem ser combinados em um único arquivo como mostrado anteriormente.

Finalmente, podemos habilitar o rastreamento ASCII para todas as interfaces no sistema.

helper.EnableAsciiIpv4All ("prefix");

Isto resultaria em vários arquivos ASCII sendo criados, um para cada interface no sistema relacionada ao protocolo do tipo gerenciado pela classe assistente.Todos estes arquivos seguiriam a convenção <prefix>-n<id do node>-i<interface>.tr.

Seleção de Nome de Arquivo para Rastreamento ASCII da Classe Assistente de Protocolo

Implícito nas descrições de métodos anteriores é a construção do nome do arquivo por meio do método da implementação. Por convenção, rastreamento ASCII no sistema ns-3 são da forma <prefix>-<id node>-<id do dispositivo>.tr.

Como mencionado, todo nó no sistema terá um número identificador de nó associado. Como há uma correspondência de um-para-um entre instâncias de protocolo e instâncias de nó, usamos o ID de nó. Cada interface em um protocolo terá um índice de interface (também chamando apenas de interface) relativo ao seu protocolo. Por padrão, então, um arquivo de rastreamento ASCII criado a partir do rastreamento no primeiro dispositivo do nó 21, usando o prefixo “prefix”, seria prefix-n21-i1.tr. O uso de prefixo distingue múltiplos protocolos por nó.

Sempre podemos usar o serviço de nomes de objetos do ns-3 para tornar isso mais claro. Por exemplo, se usarmos o serviço de nomes para associar o nome “serverIpv4” ao Ptr<Ipv4> no nó 21, o nome de arquivo resultante seria prefix-nserverIpv4-i1.tr.

Diversos métodos tem um parâmetro padrão explicitFilename. Quando modificado para verdadeiro, este parâmetro desabilita o mecanismo automático de completar o nome do arquivo e permite criarmos um nome de arquivo abertamente. Esta opção está disponível nos métodos que ativam o rastreamento em um único dispositivo.

Considerações Finais

O ns-3 inclui um ambiente completo para permitir usuários de diversos níveis personalizar os tipos de informação para serem extraídas de suas simulações.

Existem funções assistentes de alto nível que permitem ao usuário o controle de um coleção de saídas predefinidas para uma granularidade mais fina. Existem funções assistentes de nível intermediário que permitem usuários mais sofisticados personalizar como as informações são extraídas e armazenadas; e existem funções de baixo nível que permitem usuários avançados alterarem o sistema para apresentar novas ou informações que não eram exportadas.

Este é um sistema muito abrangente e percebemos que é muita informação para digerir, especialmente para novos usuários ou aqueles que não estão intimamente familiarizados com C++ e suas expressões idiomáticas. Consideramos o sistema de rastreamento uma parte muito importante do ns-3, assim recomendamos que familiarizem-se o máximo possível com ele. Compreender o restante do sistema ns-3 é bem simples, uma vez que dominamos o sistema de rastreamento.

Tabela de Conteúdo

Tópico anterior

Construindo topologias

Próximo tópico

Conclusão

Esta Página