Simulating real NDN applications¶
The version of ndn-cxx library bundled with ndnSIM includes a modified version of ndn::Face to directly send and receive Interest and Data packets to and from the simulated instances of NFD. With this modification, ndnSIM enables support to simulate real NDN applications written against the ndn-cxx, if they satisfy requirements listed in this guide or can be modified to satisfy these requirements.
Requirements¶
Source code of the application must be available
The application (parts of the application) needs to be compiled against the ndnSIM version of ndn-cxx library.
Source code should separate
main
function from the functional components of the application that will be simulatedThe entry point to the application (its functional component) will be NS-3 application class, which should be able to create and destroy an instance of the simulated component when scheduled by the scenario.
The application should not use global variables, if they define a state for the application instance
ndnSIM should be able to create multiple different instances of the application, e.g., for each simulated node.
Exception to this requirement is ndn::Scheduler: its implementation has been rewired to use NS-3’s scheduling routines.
The application MUST NOT contain any GUI or command-line terminal interactions
The application SHOULD NOT use disk operations, unless application instances access unique parts of the file system
In the simulated environment, all application instances will be accessing the same local file system, which can result in undefined behavior if not properly handled.
The application MUST use a subset of ndn::Face API:
If the application create ndn::Face, it MUST BE created either with a default constructor or constructor that accepts a single
boost::asio::io_service
parameter.// Supported ndn::Face face1(); ndn::Face face2(ioService); // Not supported in ndnSIM ndn::Face face4(host_name, port_number) ndn::Face face3(transport); // and others
ndn::Face::getIoService() should be used only to obtain a reference to
boost::asio::io_service
. Application MUST NOT use any methods ofboost::asio::io_service
, otherwise the simulation will crash.ndn::Face face; ... // Supported (the rewired Scheduler implementation does not access io_service methods) Scheduler scheduler(face.getIoService()); // Not supported in ndnSIM and will result in crash face.getIoService().stop();
Application should avoid use of Face::processEvents() or use it with caution
In real applications, processEvents blocks until some data is received or the timeout callback is called. In this case, any variables created before calling this method will still exist after the method returns. However, in ndnSIM, such an assumption cannot be made, since the scope of a variable is local.
void foo { ndn::Face face; face.expressInterest(...); face.setInterestFilter(...); // ndnSIM version of processEvents will not block! face.processEvents(); } // after existing foo scope, face variable is deallocated and all scheduled operations // will be canceled
Application (simulated component) MUST NOT create instances of
boost::asio::io_service
and use their methodsboost::asio::io_service
is inherently incompatible with NS-3, as both are providing mechanisms for asynchronous event processing.We also recommend that functional part of the application accepts reference to the KeyChain instance, instead of creating instance itself.
When simulating non-security aspects of the application, in simulation scenario it will be possible to use a dummy implementation of the KeyChain that does not perform crypto operations, but signs Data and Interests with fake signatures.
For example, this can be achieved by enabling the constructor of the real application to accept a reference to the KeyChain:
// Real applications should accept a reference to the KeyChain instance RealApp::RealApp(KeyChain& keyChain) : m_keyChain(keyChain) { }
How to simulate real applications using ndnSIM¶
To simulate a real application, the simulation scenario should contain a class derived from
ns3::Application
. This class needs to create an instance of the ndn::Face and/or real
application in the overloaded StartApplication
method. This class also need to ensure that the
created instance is not deallocated until StopApplication
method is called.
For example, if the functional class of the real application looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | #include <ndn-cxx/face.hpp>
#include <ndn-cxx/interest.hpp>
#include <ndn-cxx/security/key-chain.hpp>
#include <ndn-cxx/util/scheduler.hpp>
#include <iostream>
namespace app {
class RealApp
{
public:
RealApp(ndn::KeyChain& keyChain)
: m_keyChain(keyChain)
, m_faceProducer(m_faceConsumer.getIoService())
, m_scheduler(m_faceConsumer.getIoService())
{
// register prefix and set interest filter on producer face
m_faceProducer.setInterestFilter("/hello", std::bind(&RealApp::respondToAnyInterest, this, _2),
std::bind([]{}), std::bind([]{}));
// use scheduler to send interest later on consumer face
m_scheduler.scheduleEvent(ndn::time::seconds(2), [this] {
m_faceConsumer.expressInterest(ndn::Interest("/hello/world"),
std::bind([] { std::cout << "Hello!" << std::endl; }),
std::bind([] { std::cout << "NACK!" << std::endl; }),
std::bind([] { std::cout << "Bye!.." << std::endl; }));
});
}
void
run()
{
m_faceConsumer.processEvents(); // ok (will not block and do nothing)
// m_faceConsumer.getIoService().run(); // will crash
}
private:
void
respondToAnyInterest(const ndn::Interest& interest)
{
auto data = std::make_shared<ndn::Data>(interest.getName());
m_keyChain.sign(*data);
m_faceProducer.put(*data);
}
private:
ndn::KeyChain& m_keyChain;
ndn::Face m_faceConsumer;
ndn::Face m_faceProducer;
ndn::Scheduler m_scheduler;
};
|
The corresponding NS-3 “entry point” application class can be like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | #include "ns3/ndnSIM/helper/ndn-stack-helper.hpp"
#include "ns3/application.h"
namespace ns3 {
// Class inheriting from ns3::Application
class RealAppStarter : public Application
{
public:
static TypeId
GetTypeId()
{
static TypeId tid = TypeId("RealAppStarter")
.SetParent<Application>()
.AddConstructor<RealAppStarter>();
return tid;
}
protected:
// inherited from Application base class.
virtual void
StartApplication()
{
// Create an instance of the app, and passing the dummy version of KeyChain (no real signing)
m_instance.reset(new app::RealApp(ndn::StackHelper::getKeyChain()));
m_instance->run(); // can be omitted
}
virtual void
StopApplication()
{
// Stop and destroy the instance of the app
m_instance.reset();
}
private:
std::unique_ptr<app::RealApp> m_instance;
};
} // namespace ns3
|
Note
There is a requirement that ndn::Face MUST BE created within the context of a specific
ns3::Node
. In simple words this means that ndn::Face constructor must be called
somewhere within the overloaded StartApplication
method.
Attempt to create a ndn::Face outside ns3::Node
(e.g., if the example included
member variable Face m_face
in RealAppStarter
class) will result in simulation crash.
The final step is to actually write a simulation scenario that defines network topology, routing information between nodes, on which nodes the application should be installed and when it should be started and stopped.
For the trivial example, let us assume that we have only one simulation node and we want to start the application at time moment 6.5 seconds. This scenario can look like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | #include "ndn-cxx-simple/real-app.hpp"
#include "ndn-cxx-simple/real-app-starter.hpp"
#include "ns3/core-module.h"
#include "ns3/network-module.h"
#include "ns3/ndnSIM-module.h"
namespace ns3 {
NS_OBJECT_ENSURE_REGISTERED(RealAppStarter);
int
main(int argc, char* argv[])
{
CommandLine cmd;
cmd.Parse(argc, argv);
Ptr<Node> node = CreateObject<Node>();
ndn::StackHelper ndnHelper;
ndnHelper.Install(node);
ndn::AppHelper appHelper("RealAppStarter");
appHelper.Install(node)
.Start(Seconds(6.5));
Simulator::Stop(Seconds(20.0));
Simulator::Run();
Simulator::Destroy();
return 0;
}
} // namespace ns3
int
main(int argc, char* argv[])
{
return ns3::main(argc, argv);
}
|
Example of a real application simulation¶
To demonstrate functionality of ndnSIM in a more complex and realistic case, we will use the NDN ping application included as part of NDN Essential Tools.
For this example, we used a scenario template repository as a base to write simulation-specific extensions and define scenarios, and the final version of the scenario is available in GitHub.
The following lists steps we did to simulate ndnping and ndnpingserver apps on a simple three-node topology:
forked scenario template repository
imported the latest version of NDN Essential Tools source code as a git submodule
updated the build script (
wscript
) to compile the source code ofndnping
andndnpingserver
(with the exception of compilation units that containmain
function) against ndnSIMdefined
PingClient
andPingServer
classes to hold state of application instancesdefined
PingClientApp
andPingServerApp
NS-3 applications, that create and destroy instances ofPingClient
andPingServer
per NS-3 logic.defined a simple scenario that creates a three node topology, installs NDN stacks, and installs
PingClientApp
andPingServerApp
applications on different simulation nodes.
After all these steps, the repository is ready to run the simulation (see README.md for more details).
Note
The listed steps did not include any modification of NDN Essential Tools source code. However, this was not the case when we initially attempted to run the simulation, as the source code was violating a few requirements of this guide. The changes that we made are an example of how to adapt the source code to be compatible with ndnSIM simulations.