[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

7.3 A Real Example

Let’s do an example taken from one of the best-known books on TCP around. “TCP/IP Illustrated, Volume 1: The Protocols,” by W. Richard Stevens is a classic. I just flipped the book open and ran across a nice plot of both the congestion window and sequence numbers versus time on page 366. Stevens calls this, “Figure 21.10. Value of cwnd and send sequence number while data is being transmitted.” Let’s just recreate the cwnd part of that plot in ns-3 using the tracing system and gnuplot.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

7.3.1 Are There Trace Sources Available?

The first thing to think about is how we want to get the data out. What is it that we need to trace? The first thing to do is to consult “The list of all trace sources” to see what we have to work with. Recall that this is found in the ns-3 Doxygen in the “Core” Module section. If you scroll through the list, you will eventually find:

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

It turns out that the ns-3 TCP implementation lives (mostly) in the file src/internet-stack/tcp-socket-impl.cc. If you don’t know this a priori, you can use the recursive grep trick:

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

You will find page after page of instances of tcp pointing you to that file.

If you open src/internet-stack/tcp-socket-impl.cc in your favorite editor, you will see right up at the top of the file, the following declarations:

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

This should tell you to look for the declaration of m_cWnd in the header file src/internet-stack/tcp-socket-impl.h. If you open this file in your favorite editor, you will find:

  TracedValue<uint32_t> m_cWnd; //Congestion window

You should now understand this code completely. If we have a pointer to the TcpSocketImpl, we can TraceConnect to the “CongestionWindow” trace source if we provide an appropriate callback target. This is the same kind of trace source that we saw in the simple example at the start of this section, except that we are talking about uint32_t instead of int32_t.

We now know that we need to provide a callback that returns void and takes two uint32_t parameters, the first being the old value and the second being the new value:

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

[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

7.3.2 What Script to Use?

It’s always best to try and find working code laying around that you can modify, rather than starting from scratch. So the first order of business now is to find some code that already hooks the “CongestionWindow” trace source and see if we can modify it. As usual, grep is your friend:

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

This will point out a couple of promising candidates: examples/tcp/tcp-large-transfer.cc and src/test/ns3tcp/ns3tcp-cwnd-test-suite.cc.

We haven’t visited any of the test code yet, so let’s take a look there. You will typically find that test code is fairly minimal, so this is probably a very good bet. Open src/test/ns3tcp/ns3tcp-cwnd-test-suite.cc in your favorite editor and search for “CongestionWindow.” You will find,

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

This should look very familiar to you. We mentioned above that if we had a pointer to the TcpSocketImpl, we could TraceConnect to the “CongestionWindow” trace source. That’s exactly what we have here; so it turns out that this line of code does exactly what we want. Let’s go ahead and extract the code we need from this function (Ns3TcpCwndTestCase1::DoRun (void)). If you look at this function, you will find that it looks just like an ns-3 script. It turns out that is exactly what it is. It is a script run by the test framework, so we can just pull it out and wrap it in main instead of in DoRun. Rather than walk through this, step, by step, we have provided the file that results from porting this test back to a native ns-3 script – examples/tutorial/fifth.cc.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

7.3.3 A Common Problem and Solution

The fifth.cc example demonstrates an extremely important rule that you must understand before using any kind of Attribute: you must ensure that the target of a Config command exists before trying to use it. This is no different than saying an object must be instantiated before trying to call it. Although this may seem obvious when stated this way, it does trip up many people trying to use the system for the first time.

Let’s return to basics for a moment. There are three basic time periods that exist in any ns-3 script. The first time period is sometimes called “Configuration Time” or “Setup Time,” and is in force during the period when the main function of your script is running, but before Simulator::Run is called. The second time period is sometimes called “Simulation Time” and is in force during the time period when Simulator::Run is actively executing its events. After it completes executing the simulation, Simulator::Run will return control back to the main function. When this happens, the script enters what can be called “Teardown Time,” which is when the structures and objects created during setup and taken apart and released.

Perhaps the most common mistake made in trying to use the tracing system is assuming that entities constructed dynamically during simulation time are available during configuration time. In particular, an ns-3 Socket is a dynamic object often created by Applications to communicate between Nodes. An ns-3 Application always has a “Start Time” and a “Stop Time” associated with it. In the vast majority of cases, an Application will not attempt to create a dynamic object until its StartApplication method is called. This is to ensure that the simulation is completely configured before the app tries to do anything (what would happen if it tried to connect to a node that didn’t exist yet during configuration time). The answer to this issue is to 1) create a simulator event that is run after the dynamic object is created and hook the trace when that event is executed; or 2) create the dynamic object at configuration time, hook it then, and give the object to the system to use during simulation time. We took the second approach in the fifth.cc example. This decision required us to create the MyApp Application, the entire purpose of which is to take a Socket as a parameter.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

7.3.4 A fifth.cc Walkthrough

Now, let’s take a look at the example program we constructed by dissecting the congestion window test. Open examples/tutorial/fifth.cc in your favorite editor. You should see some familiar looking code:

  /* -*- 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/common-module.h"
  #include "ns3/simulator-module.h"
  #include "ns3/node-module.h"
  #include "ns3/helper-module.h"
  
  using namespace ns3;
  
  NS_LOG_COMPONENT_DEFINE ("FifthScriptExample");

This has all been covered, so we won’t rehash it. The next lines of source are the network illustration and a comment addressing the problem described above with 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.
  // ===========================================================================
  //

This should also be self-explanatory.

The next part is the declaration of the MyApp Application that we put together to allow the Socket to be created at configuration time.

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

You can see that this class inherits from the ns-3 Application class. Take a look at src/node/application.h if you are interested in what is inherited. The MyApp class is obligated to override the StartApplication and StopApplication methods. These methods are called when the corresponding base class Start and Stop methods are called during simulation time.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

7.3.4.1 How Applications are Started and Stopped

It is worthwhile to spend a bit of time explaining how events actually get started in the system. The most common way to start pumping events is to start an Application. This is done as the result of the following (hopefully) familar lines of an ns-3 script:

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

The application container code (see src/helper/application-container.h if you are interested) loops through its contained applications and calls,

  app->Start (startTime);
  app->Stop (stopTime);

on each of them. The Start method of an Application calls Application::ScheduleStart (see src/helper/application-container.cc) which, in turn, schedules an event to start the Application:

  Simulator::Schedule (startTime, &Application::StartApplication, this);

Since MyApp inherits from Application and overrides StartApplication, this bit of code causes the simulator to execute something that is effectively like,

  this->StartApplication (startTime);

where the this pointer, if you have kept it all straight, is the pointer to the Application in the container. It is then expected that another event will be scheduled in the overridden StartApplication that will begin doing some application-specific function, like sending packets.

StopApplication operates in a similar manner and tells the Application to stop generating events.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

7.3.4.2 The MyApp Application

The MyApp Application needs a constructor and a destructor, of course:

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

The existence of the next bit of code is the whole reason why we wrote this Application in the first place.

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

This code should be pretty self-explanatory. We are just initializing member variables. The important one from the perspective of tracing is the Ptr<Socket> socket which we needed to provide to the application during configuration time. Recall that we are going to create the Socket as a TcpSocket (which is implemented by TcpSocketImpl) and hook its “CongestionWindow” trace source before passing it to the Setup method.

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

The above code is the overridden implementation Application::StartApplication that will be automatically called by the simulator to start our Application running. You can see that it does a Socket Bind operation. If you are familiar with Berkeley Sockets this shouldn’t be a surprise. It performs the required work on the local side of the connection just as you might expect. The following Connect will do what is required to establish a connection with the TCP at Address m_peer. It should now be clear why we need to defer a lot of this to simulation time, since the Connect is going to need a fully functioning network to complete. After the Connect, the Application then starts creating simulation events by calling SendPacket.

The next bit of code explains to the Application how to stop creating simulation events.

  void
  MyApp::StopApplication (void)
  {
    m_running = false;
  
    if (m_sendEvent.IsRunning ())
      {
        Simulator::Cancel (m_sendEvent);
      }
  
    if (m_socket)
      {
        m_socket->Close ();
      }
  }

Every time a simulation event is scheduled, an Event is created. If the Event is pending execution or executing, its method IsRunning will return true. In this code, if IsRunning() returns true, we Cancel the event which removes it from the simulator event queue. By doing this, we break the chain of events that the Application is using to keep sending its Packets and the Application goes quiet. After we quiet the Application we Close the socket which tears down the TCP connection.

The socket is actually deleted in the destructor when the m_socket = 0 is executed. This removes the last reference to the underlying Ptr<Socket> which causes the destructor of that Object to be called.

Recall that StartApplication called SendPacket to start the chain of events that describes the Application behavior.

  void
  MyApp::SendPacket (void)
  {
    Ptr<Packet> packet = Create<Packet> (m_packetSize);
    m_socket->Send (packet);
  
    if (++m_packetsSent < m_nPackets)
      {
        ScheduleTx ();
      }
  }

Here, you see that SendPacket does just that. It creates a Packet and then does a Send which, if you know Berkeley Sockets, is probably just what you expected to see.

It is the responsibility of the Application to keep scheduling the chain of events, so the next lines call ScheduleTx to schedule another transmit event (a SendPacket) until the Application decides it has sent enough.

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

Here, you see that ScheduleTx does exactly that. If the Applciation is running (if StopApplication has not been called) it will schedule a new event, which calls SendPacket again. The alert reader will spot something that also trips up new users. The data rate of an Application is just that. It has nothing to do with the data rate of an underlying Channel. This is the rate at which the Application produces bits. It does not take into account any overhead for the various protocols or channels that it uses to transport the data. If you set the data rate of an Application to the same data rate as your underlying Channel you will eventually get a buffer overflow.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

7.3.4.3 The Trace Sinks

The whole point of this exercise is to get trace callbacks from TCP indicating the congestion window has been updated. The next piece of code implements the corresponding trace sink:

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

This should be very familiar to you now, so we won’t dwell on the details. This function just logs the current simulation time and the new value of the congestion window every time it is changed. You can probably imagine that you could load the resulting output into a graphics program (gnuplot or Excel) and immediately see a nice graph of the congestion window behavior over time.

We added a new trace sink to show where packets are dropped. We are going to add an error model to this code also, so we wanted to demonstrate this working.

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

This trace sink will be connected to the “PhyRxDrop” trace source of the point-to-point NetDevice. This trace source fires when a packet is dropped by the physical layer of a NetDevice. If you take a small detour to the source (src/devices/point-to-point/point-to-point-net-device.cc) you will see that this trace source refers to PointToPointNetDevice::m_phyRxDropTrace. If you then look in src/devices/point-to-point/point-to-point-net-device.h for this member variable, you will find that it is declared as a TracedCallback<Ptr<const Packet> >. This should tell you that the callback target should be a function that returns void and takes a single parameter which is a Ptr<const Packet> – just what we have above.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

7.3.4.4 The Main Program

The following code should be very familiar to you by now:

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

This creates two nodes with a point-to-point channel between them, just as shown in the illustration at the start of the file.

The next few lines of code show something new. If we trace a connection that behaves perfectly, we will end up with a monotonically increasing congestion window. To see any interesting behavior, we really want to introduce link errors which will drop packets, cause duplicate ACKs and trigger the more interesting behaviors of the congestion window.

ns-3 provides ErrorModel objects which can be attached to Channels. We are using the RateErrorModel which allows us to introduce errors into a Channel at a given rate.

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

The above code instantiates a RateErrorModel Object. Rather than using the two-step process of instantiating it and then setting Attributes, we use the convenience function CreateObjectWithAttributes which allows us to do both at the same time. We set the “RanVar” Attribute to a random variable that generates a uniform distribution from 0 to 1. We also set the “ErrorRate” Attribute. We then set the resulting instantiated RateErrorModel as the error model used by the point-to-point NetDevice. This will give us some retransmissions and make our plot a little more interesting.

  InternetStackHelper stack;
  stack.Install (nodes);

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

The above code should be familiar. It installs internet stacks on our two nodes and creates interfaces and assigns IP addresses for the point-to-point devices.

Since we are using TCP, we need something on the destination node to receive TCP connections and data. The PacketSink Application is commonly used in ns-3 for that purpose.

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

This should all be familiar, with the exception of,

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

This code instantiates a PacketSinkHelper and tells it to create sockets using the class ns3::TcpSocketFactory. This class implements a design pattern called “object factory” which is a commonly used mechanism for specifying a class used to create objects in an abstract way. Here, instead of having to create the objects themselves, you provide the PacketSinkHelper a string that specifies a TypeId string used to create an object which can then be used, in turn, to create instances of the Objects created by the factory.

The remaining parameter tells the Application which address and port it should Bind to.

The next two lines of code will create the socket and connect the trace source.

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

The first statement calls the static member function Socket::CreateSocket and provides a Node and an explicit TypeId for the object factory used to create the socket. This is a slightly lower level call than the PacketSinkHelper call above, and uses an explicit C++ type instead of one referred to by a string. Otherwise, it is conceptually the same thing.

Once the TcpSocket is created and attached to the Node, we can use TraceConnectWithoutContext to connect the CongestionWindow trace source to our trace sink.

Recall that we coded an Application so we could take that Socket we just made (during configuration time) and use it in simulation time. We now have to instantiate that Application. We didn’t go to any trouble to create a helper to manage the Application so we are going to have to create and install it “manually.” This is actually quite easy:

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

The first line creates an Object of type MyApp – our Application. The second line tells the Application what Socket to use, what address to connect to, how much data to send at each send event, how many send events to generate and the rate at which to produce data from those events.

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.

We need to actually do the connect from the receiver point-to-point NetDevice to our callback now.

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

It should now be obvious that we are getting a reference to the receiving Node NetDevice from its container and connecting the trace source defined by the attribute “PhyRxDrop” on that device to the trace sink RxDrop.

Finally, we tell the simulator to override any Applications and just stop processing events at 20 seconds into the simulation.

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

    return 0;
  }

Recall that as soon as Simulator::Run is called, configuration time ends, and simulation time begins. All of the work we orchestrated by creating the Application and teaching it how to connect and send data actually happens during this function call.

As soon as Simulator::Run returns, the simulation is complete and we enter the teardown phase. In this case, Simulator::Destroy takes care of the gory details and we just return a success code after it completes.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

7.3.5 Running fifth.cc

Since we have provided the file fifth.cc for you, if you have built your distribution (in debug mode since it uses NS_LOG – recall that optimized builds optimize out NS_LOGs) it will be waiting for you to run.

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

You can probably see immediately a downside of using prints of any kind in your traces. We get those extraneous waf messages printed all over our interesting information along with those RxDrop messages. We will remedy that soon, but I’m sure you can’t wait to see the results of all of this work. Let’s redirect that output to a file called cwnd.dat:

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

Now edit up “cwnd.dat” in your favorite editor and remove the waf build status and drop lines, leaving only the traced data (you could also comment out the TraceConnectWithoutContext("PhyRxDrop", MakeCallback (&RxDrop)); in the script to get rid of the drop prints just as easily.

You can now run gnuplot (if you have it installed) and tell it to generate some pretty pictures:

  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

You should now have a graph of the congestion window versus time sitting in the file “cwnd.png” in all of its glory, that looks like:


figures/cwnd
[ < ] [ > ]   [ << ] [ Up ] [ >> ]

This document was generated on November 13, 2009 using texi2html 1.82.