32 The ns-3 Network Simulator - An Introduction To Computer Networks, Desktop Edition 2.0.10
32 The ns-3 Network Simulator - An Introduction To Computer Networks, Desktop Edition 2.0.10
In this chapter we take a somewhat cursory look at the ns-3 simulator, intended as a replacement for ns-2. The project is
managed by the NS-3 Consortium, and all materials are available at www.nsnam.org.
Ns-3 represents a rather sharp break from ns-2. Gone is the Tcl programming interface; instead, ns-3 simulation programs
are written in the C++ language, with extensive calls to the ns-3 library, although they are often still referred to as simulation
“scripts”. As the simulator core itself is also written in C++, this in some cases allows improved interaction between
configuration and execution. However, configuration and execution are still in most cases quite separate: at the end of the
simulation script comes a call Simulator::Run() – akin to ns-2’s $ns run – at which point the user-written C++ has done its
job and the library takes over.
To configure a simple simulation, an ns-2 Tcl script had to create nodes and links, create network-connection “agents”
attached to nodes, and create traffic-generating applications attached to agents. Much the same applies to ns-3, but in
addition each node must be configured with its network interfaces, and each network interface must be assigned an IP
address.
The first step is to unzip the tar file; this should leave a directory named ns-allinone-3.nn, where nn reflects the version
number (20 in the author’s installation as of this 2014 writing). This directory is the root of the ns-3 system; it contains a
build.py (python) script and the primary ns-3 directory ns-3.nn. All that is necessary is to run the build.py script:
./build.py
Considerable configuration and then compiler output should ensue, hopefully terminating with a list of “Modules built” and
“Modules not built”.
From this point on, most ns-3 work will take place in the subdirectory ns-3.nn, that is, in ns-allinone-3.nn/ns-3.nn. This
development directory contains the source directory src, the script directory scratch, and the execution script waf.
The development directory also contains a directory examples containing a rich set of example scripts. The scripts in
examples/tutorial are described in depth in the ns-3 tutorial in doc/tutorial.
In fact, every uncompiled program in the scratch directory is compiled, meaning that projects in progress that are not yet
compilable must be kept elsewhere. One convenient strategy is to maintain multiple project directories, and link them
symbolically to scratch as needed.
The ns-3 system includes support for command-line options; the following example illustrates the passing by command line
of the value 3 for the variable nCsma:
to
./waf configure
./waf build
/*
Network topology:
A----R----B
static void
TraceCwnd () // Trace changes to the congestion window
{
AsciiTraceHelper ascii;
Ptr<OutputStreamWrapper> stream = ascii.CreateFileStream (fileNameRoot + ".cwnd");
Config::ConnectWithoutContext ("/NodeList/0/$ns3::TcpL4Protocol/SocketList/0/CongestionWindow", MakeBoundCallb
}
The function TraceCwnd() arranges for tracing of cwnd; the function CwndChange is a callback, invoked by the ns-3 system
whenever cwnd changes. Such callbacks are common in ns-3.
The parameter string beginning /NodeList/0/... is an example of the configuration namespace. Each ns-3 attribute can be
accessed this way. See 32.2.2 The Ascii Tracefile below.
The use of Config::SetDefault() allows us to configure objects that will not exist until some later point, perhaps not until the
ns-3 simulator is running. The first parameter is an attribute string, of the form ns3::class::attribute. A partial list of
attributes is at https://www.nsnam.org/docs/release/3.19/doxygen/group___attribute_list.html. Attributes of a class can also
be determined by a command such as the following:
The advantage of the Config::SetDefault mechanism is that often objects are created indirectly, perhaps by “helper” classes,
and so direct setting of class properties can be problematic.
It is perfectly acceptable to issue some Config::SetDefault calls, then create some objects (perhaps implicitly), and then
change the defaults (again with Config::SetDefault) for creation of additional objects.
We pick the TCP congesion-control algorithm by setting ns3::TcpL4Protocol::SocketType. Options are TcpRfc793 (no
congestion control), TcpTahoe, TcpReno, TcpNewReno and TcpWestwood. TCP Cubic and SACK TCP are not supported natively
(though they are available if the Network Simulation Cradle is installed).
Setting the DelAckCount attribute to 0 disables delayed ACKs. Setting the MinRTO value to 500 ms avoids some unexpected
hard timeouts. We will return to both of these below in 32.2.3 Unexpected Timeouts and Other Phenomena.
Next comes our local variables and command-line-option processing. In ns-3 the latter is handled via the CommandLine object,
which also recognized the --PrintAttributes option above. Using the --PrintHelp option gives a list of variables that can be
set via command-line arguments.
CommandLine cmd;
// Here, we define command line options overriding some of the above.
cmd.AddValue ("runtime", "How long the applications should send data", runtime);
cmd.AddValue ("delayRB", "Delay on the R--B link, in ms", delayRB);
cmd.AddValue ("queuesize", "queue size at R", queuesize);
cmd.AddValue ("tcpSegmentSize", "TCP segment size", tcpSegmentSize);
Next we create three nodes, illustrating the use of smart pointers and CreateObject().
Class Ptr is a “smart pointer” that manages memory through reference counting. The template function CreateObject acts as
the ns-3 preferred alternative to operator new. Parameters for objects created this way can be supplied via
Config::SetDefault, or by some later method call applied to the Ptr object. For Node objects, for example, we might call A ->
AddDevice(...).
A convenient alternative to creating nodes individually is to create a container of nodes all at once:
NodeContainer allNodes;
allNodes.Create(3);
Ptr<Node> A = allNodes.Get(0);
...
After the nodes are in place we create our point-to-point links, using the PointToPointHelper class. We also create
NetDeviceContainer objects; we don’t use these here (we could simply call AR.Install(A,R)), but will need them below when
assigning IPv4 addresses.
Next we hand out IPv4 addresses. The Ipv4AddressHelper class can help us with individual LANs (eg A–R and R–B), but it is up
to us to make sure our two LANs are on different subnets. If we attempt to put A and B on the same subnet, routing will
simply fail, just as it would if we were to do this with real network nodes.
InternetStackHelper internet;
internet.Install (A);
internet.Install (R);
internet.Install (B);
// Assign IP addresses
Ipv4AddressHelper ipv4;
ipv4.SetBase ("10.0.0.0", "255.255.255.0");
Ipv4InterfaceContainer ipv4Interfaces;
ipv4Interfaces.Add (ipv4.Assign (devAR));
ipv4.SetBase ("10.0.1.0", "255.255.255.0");
ipv4Interfaces.Add (ipv4.Assign(devRB));
Ipv4GlobalRoutingHelper::PopulateRoutingTables ();
Next we print out the addresses assigned. This gives us a peek at the GetObject template and the ns-3 object-aggregation
model. The original Node objects we created earlier were quite generic; they gained their Ipv4 component in the code above.
Now we retrieve that component with the GetObject<Ipv4>() calls below.
In general, A->GetObject<T> returns the component of type T that has been “aggregated” to Ptr<Object> A; often this
aggregation is invisible to the script programmer but an understanding of how it works is sometimes useful. The aggregation
is handled by the ns-3 Object class, which contains an internal list m_aggregates of aggregated companion objects. At most
one object of a given type can be aggregated to another, making GetObject<T> unambiguous. Given a Ptr<Object> A, we can
obtain an iterator over the aggregated companions via A->GetAggregateIterator(), of type Object::AggregateIterator. From
each Ptr<const Object> B returned by this iterator, we can call B->GetInstanceTypeId().GetName() to get the class name of
B.
The GetAddress() calls take two parameters; the first specfies the interface (a value of 0 gives the loopback interface) and the
second distinguishes between multiple addresses assigned to the same interface (which is not happening here). The call A4-
>GetAddress(1,0) returns an Ipv4InterfaceAddress object containing, among other things, an IP address, a broadcast
address and a netmask; GetLocal() returns the first of these.
Next we create the receiver on B, using a PacketSinkHelper. A receiver is, in essense, a read-only form of an application
server.
// create a sink on B
uint16_t Bport = 80;
Address sinkAaddr(InetSocketAddress (Ipv4Address::GetAny (), Bport));
PacketSinkHelper sinkA ("ns3::TcpSocketFactory", sinkAaddr);
ApplicationContainer sinkAppA = sinkA.Install (B);
sinkAppA.Start (Seconds (0.01));
// the following means the receiver will run 1 min longer than the sender app.
sinkAppA.Stop (Seconds (runtime + 60.0));
Now comes the sending application, on A. We must configure and create a BulkSendApplication, attach it to A, and arrange
for a connection to be created to B. The BulkSendHelper class simplifies this.
If we did not want to use the helper class here, the easiest way to create the BulkSendApplication is with an ObjectFactory.
We configure the factory with the type we want to create and the relevant configuration parameters, and then call
factory.Create(). (We could have used the Config::SetDefault() mechanism and CreateObject() as well.)
ObjectFactory factory;
factory.SetTypeId ("ns3::BulkSendApplication");
factory.Set ("Protocol", StringValue ("ns3::TcpSocketFactory"));
factory.Set ("MaxBytes", UintegerValue (maxBytes));
factory.Set ("SendSize", UintegerValue (tcpSegmentSize));
factory.Set ("Remote", AddressValue (sinkAddr));
Ptr<Object> bulkSendAppObj = factory.Create();
Ptr<Application> bulkSendApp = bulkSendAppObj -> GetObject<Application>();
bulkSendApp->SetStartTime(Seconds(0.0));
bulkSendApp->SetStopTime(Seconds(runtime));
A->AddApplication(bulkSendApp);
The above gives us no direct access to the actual TCP connection. Yet another alternative is to start by creating the TCP socket
and connecting it:
However, there is then no mechanism for creating a BulkSendApplication that uses a pre-existing socket. (For a workaround,
see the tutorial example fifth.cc.)
Before beginning execution, we set up tracing; we will look at the tracefile format later. We use the AR PointToPointHelper
class here, but both ascii and pcap tracing apply to the entire A–R–B network.
// Set up tracing
AsciiTraceHelper ascii;
std::string tfname = fileNameRoot + ".tr";
AR.EnableAsciiAll (ascii.CreateFileStream (tfname));
// Setup tracing for cwnd
Simulator::Schedule(Seconds(0.01),&TraceCwnd); // this Time cannot be 0.0
basic1-0-0.pcap
basic1-1-0.pcap
basic1-1-1.pcap
basic1-2-0.pcap
The first number refers to the node (A=0, R=1, B=2) and the second to the interface. A packet arriving at R but dropped there
will appear in the second .pcap file but not the third. These files can be viewed with WireShark.
Finally we are ready to start the simulator! The BulkSendApplication will stop at time runtime, but traffic may be in progress.
We allow it an additional 60 seconds to clear. We also, after the simulation has run, print out the number of bytes received by
B.
Simulator::Stop (Seconds (runtime+60));
Simulator::Run ();
Compare this graph to that in 31.2.1 Graph of cwnd v time produced by ns-2. The slow-start phase earlier ended at around
2.0 and now ends closer to 3.0. There are several modest differences, including the halving of cwnd just before T=1 and the
peak around T=2.6; these were not apparent in the ns-2 graph.
After slow-start is over, the graphs are quite similar; cwnd ranges from 10 to 21. The period before was 1.946 seconds; here it
is 2.0548; the difference is likely due to a more accurate implementation of the recovery algorithm.
One striking difference is the presence of the near-vertical line of dots just after each peak. What is happening here is that
ns-3 implements the cwnd inflation/deflation algorithm outlined at the tail end of 19.4 TCP Reno and Fast Recovery. When
three dupACKs are received, cwnd is set to cwnd/2 + 3, and is then allowed to increase to 1.5×cwnd. See the end of 19.4 TCP
Reno and Fast Recovery.
As with ns-2, the first letter indicates the action: r for received, d for dropped, + for enqueued, - for dequeued. For Wi-Fi
tracefiles, t is for transmitted. The second field represents the time.
The third field represents the name of the event in the configuration namespace, sometimes called the configuration path
name. The NodeList value represents the node (A=0, etc), the DeviceList represents the interface, and the final part of the
name repeats the action: Drop, MacRx, Enqueue, Dequeue.
After that come a series of class names (eg ns3::Ipv4Header, ns3::TcpHeader), from the ns-3 attribute system, followed in
each case by a parenthesized list of class-specific trace information.
In the output above, the final three records all refer to node B (/NodeList/2/). Packet 258 has just arrived (Seq=258001), and
ACK 259001 is then enqueued and sent.
If we comment out the line disabling delayed ACKs, little changes in our graph, except that the spacing between consecutive
TCP teeth now almost doubles to 3.776. This is because with delayed ACKs the receiver sends only half as many ACKs, and
the sender does not take this into account when incrementing cwnd (that is, the sender does not implement the suggestion of
RFC 3465 mentioned in 19.2.1 TCP Reno Per-ACK Responses).
If we leave out the MinRTO adjustment, and set tcpSegmentSize to 960, we get a more serious problem: the graph now looks
something like this:
We can enable ns-3’s internal logging in the TcpReno class by entering the commands below, before running the script. (In
some cases, as with WifiHelper::EnableLogComponents(), logging output can be enabled from within the script.) Once
enabled, logging output is written to stderr.
NS_LOG=TcpReno=level_info
export NS_LOG
But then, despite Fast Recovery proceding normally, we get a hard timeout:
8.71463 [node 0] RTO. Reset cwnd to 960, ssthresh to 14400, restart from seqnum 510721
What is happening here is that the RTO interval was just a little too short, probably due to the use of the “awkward” segment
size of 960.
Shortly thereafter, at T=8.98, cwnd is reset to 3360, in accordance with the Fast Recovery rules.
The overall effect is that cwnd is reset, not to 10, but to about 3.4 (in packets). This significantly slows down throughput.
In recovering from the hard timeout, the sequence number is reset to Seq=510721 (packet 532), as this was the last packet
acknowledged. Unfortunately, several later packets had in fact made it through to B. By looking at the tracefile, we can see
that at T=8.7818, B received Seq=538561, or packet 561. Thus, when A begins retransmitting packets 533, 534, etc after the
timeout, B’s response is to send the ACK the highest packet it has received, packet 561 (Ack=539521).
This scenario is not what the designers of Fast Recovery had in mind; it is likely triggered by a too-conservative timeout
estimate. Still, exactly how to fix it is an interesting question; one approach might be to ignore, in Fast Recovery, triple
dupACKs of packets now beyond what the sender is currently sending.
32.3 Wireless
We next present the wireless simulation of 31.6 Wireless Simulation. The full script is at wireless.cc; the animation output for
the netanim player is at wireless.xml. As before, we have one mover node moving horizontally 150 meters above a row of five
fixed nodes spaced 200 meters apart. The limit of transmission is set to be 250 meters, meaning that a fixed node goes out
of range of the mover node just as the latter passes over directly above the next fixed node. As before, we use Ad hoc On-
demand Distance Vector (AODV) as the routing protocol. When the mover passes over fixed node N, it goes out of range of
fixed node N-1, at which point AODV finds a new route to mover through fixed node N.
As in ns-2, wireless simulations tend to require considerably more configuration than point-to-point simulations. We now
review the source code line-by-line. We start with two callback functions and the global variables they will need to access.
Ptr<ConstantVelocityMobilityModel> cvmm;
double position_interval = 1.0;
std::string tracebase = "scratch/wireless";
// two callbacks
void printPosition()
{
Vector thePos = cvmm->GetPosition();
Simulator::Schedule(Seconds(position_interval), &printPosition);
std::cout << "position: " << thePos << std::endl;
}
void stopMover()
{
cvmm -> SetVelocity(Vector(0,0,0));
}
The phyMode string represents the Wi-Fi data rate (and modulation technique). DSSS rates are DsssRate1Mbps, DsssRate2Mbps,
DsssRate5_5Mbps and DsssRate11Mbps. Also available are ErpOfdmRate constants to 54 Mbps and OfdmRate constants to 150
Mbps with a 40 MHz band-width (GetOfdmRate150MbpsBW40MHz). All these are defined in src/wifi/model/wifi-phy.cc.
Next are the variables that determine the layout and network behavior. The factor variable allows slowing down the speed of
the mover node but correspondingly extending the runtime (though the new-route-discovery time is not scaled):
There are some niceties in calculating the packet transmission interval above; if we do it instead as
1000000*packetsize*8/bitrate then we sometimes run into 32-bit overflow problems or integer-division-roundoff problems.
Here we create the mover node with CreateObject<Node>(), but the fixed nodes are created via a NodeContainer, as is more
typical with larger simulations
// Create nodes
NodeContainer fixedpos;
fixedpos.Create(bottomrow);
Ptr<Node> lowerleft = fixedpos.Get(0);
Ptr<Node> mover = CreateObject<Node>();
Now we put together a set of “helper” objects for more Wi-Fi configuration. We must configure both the PHY (physical) and
MAC layers.
// The below set of helpers will help us to put together the desired Wi-Fi behavior
WifiHelper wifi;
wifi.SetStandard (WIFI_PHY_STANDARD_80211b);
wifi.SetRemoteStationManager ("ns3::AarfWifiManager"); // Use AARF rate control
The AARF rate changes can be viewed by enabling the appropriate logging with, at the shell level before ./waf,
NS_LOG=AarfWifiManager=level_debug. We are not otherwise interested in rate scaling (4.2.2 Dynamic Rate Scaling) here,
though.
The PHY layer helper is YansWifiPhyHelper. The YANS project (Yet Another Network Simulator) was an influential precursor to
ns-3; see [LH06]. Note the AddPropagationLoss configuration, where we set the Wi-Fi range to 250 meters. The MAC layer
helper is NqosWifiMacHelper; the “nqos” means “no quality-of-service”, ie no use of Wi-Fi PCF (4.2.7 Wi-Fi Polling Mode).
At this point the basic Wi-Fi configuration is done! The next step is to work on the positions and motion. First we establish
the positions of the fixed nodes.
Next we set up the mover node. ConstantVelocityMobilityModel is a subclass of MobilityModel. At the end we print out a
couple things just for confirmation.
AodvHelper aodv;
OlsrHelper olsr;
Ipv4ListRoutingHelper listrouting;
//listrouting.Add(olsr, 10); // generates less traffic
listrouting.Add(aodv, 10); // fastest to find new routes
Uncommenting the olsr line (and commenting out the last line) is all that is necessary to change to OLSR routing. OLSR is
slower to find new routes, but sends less traffic.
Now we set up the IP addresses. This is straightforward as all the nodes are on a single subnet.
InternetStackHelper internet;
internet.SetRoutingHelper(listrouting);
internet.Install (fixedpos);
internet.Install (mover);
Ipv4AddressHelper ipv4;
NS_LOG_INFO ("Assign IP Addresses.");
ipv4.SetBase ("10.1.1.0", "255.255.255.0"); // there is only one subnet
Ipv4InterfaceContainer i = ipv4.Assign (devices);
Now we create a receiving application UdpServer on node mover, and a sending application UdpClient on the lower-left node.
These applications generate their own sequence numbers, which show up in the ns-3 tracefiles marked with
ns3::SeqTsHeader. As in 32.2 A Single TCP Sender, we use Config::SetDefault() and CreateObject<>() to construct the
applications.
Ptr<Ipv4> m4 = mover->GetObject<Ipv4>();
Ipv4Address Maddr = m4->GetAddress(1,0).GetLocal();
std::cout << "IPv4 address of mover: " << Maddr << std::endl;
Address moverAddress (InetSocketAddress (Maddr, port));
Config::SetDefault("ns3::UdpClient::MaxPackets", UintegerValue(packetcount));
Config::SetDefault("ns3::UdpClient::PacketSize", UintegerValue(packetsize));
Config::SetDefault("ns3::UdpClient::Interval", TimeValue (MicroSeconds (interval)));
We now set up tracing. The first, commented-out line enables pcap-format tracing, which we do not need here. The
YansWifiPhyHelper object supports tracing only of “receive” (r) and “transmit” (t) records; the PointtoPointHelper of 32.2 A
Single TCP Sender also traced enqueue and drop records.
AsciiTraceHelper ascii;
wifiPhyHelper.EnableAsciiAll (ascii.CreateFileStream (tracebase + ".tr"));
If we view the animation with netanim, the moving node’s motion is clear. The mover node, however, sometimes appears to
transmit back to both the fixed-row node below left and the fixed-row node below right. These transmissions represent the
Wi-Fi link-layer ACKs; they appear to be sent to two fixed-row nodes because what netanim is actually displaying with its blue
links is transmission every other node in range.
We can also “view” the motion in text format by uncommenting the first line below.
//Simulator::Schedule(Seconds(position_interval), &printPosition);
Simulator::Schedule(Seconds(endtime), &stopMover);
Finally it is time to run the simulator, and print some final output.
Simulator::Stop(Seconds (endtime+60));
Simulator::Run ();
Simulator::Destroy ();
return 0;
}
listrouting.Add(aodv, 10);
to
listrouting.Add(dsdv, 10);
we find that the loss count goes from 4 packets out of 2000 to 398 out of 2000; for OLSR routing the loss count is 426. As we
discussed in 31.6 Wireless Simulation, the loss of one data packet triggers the AODV implementation to look for a new
route. The DSDV and OLSR implementations, on the other hand, only look for new routes at regularly spaced intervals.
32.4 Exercises
In preparation.