一、适当的tracing sources
在官方文档中查找或者通过grep命令在终端查找,可以发现ns3::TcpSocketBase这个跟踪源。
find . -name '*.cc' | xargs grep -i tcp
该跟踪源有如下两个属性attributes ,由此可以判定,这就是我们要跟踪tcp拥塞窗口变化的跟踪源了。
其回调函数为ns3::TracedValueCallback::Uint32,格式为:
typedef void(* ns3::TracedValueCallback::Int32)(int32_t oldValue, int32_t newValue)
二、动态追踪源
任何 ns-3 脚本都有三个基本执行阶段。第一个阶段称为 "配置时间 "或 "设置时间",存在于脚本的main函数运行期间,但在调用Simulator::Run之前。第二阶段有时称为 "模拟时间",存在于Simulator::Run运行事件的时间段内。完成模拟执行后,Simulator::Run 会将控制权交还给主函数。此时,脚本将进入所谓的 "拆卸阶段",也就是将设置过程中创建的结构和对象拆卸并释放的阶段。
在尝试使用跟踪系统的过程中,一个常见的错误是假定在仿真期间动态构建的实体在配置期间也是可用的。特别是,ns-3 Socket 是应用程序经常创建的动态对象,用于节点之间的通信。ns-3 应用程序总是有与之相关的 "开始时间 "和 "停止时间"。在绝大多数情况下,应用程序在某个 "开始时间 "调用 StartApplication 方法之前,不会尝试创建动态对象。这是为了确保在应用程序尝试做任何事情之前,模拟已完全配置完毕。因此,在配置阶段,如果在仿真过程中动态创建了跟踪源,则无法将跟踪源连接到跟踪汇。
解决这一难题有两个办法:
1. 创建一个在动态对象创建后运行的模拟器事件,并在执行该事件时挂钩跟踪;
2. 在配置时创建动态对象,然后将其挂钩,并在模拟时将该对象交给系统使用。
在 fifth.cc 示例中,我们采用了第二种方法,因此创建 TutorialApp 应用程序,其全部目的就是将 Socket 作为参数。
三、解析fifth.cc
3.1 拓扑图
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
//
通过拓扑图可以明显看出这是两个节点通过TCP连接搭建PPP链路。
想查看 ns-3 TCP 拥塞窗口中的变化,需要启动一个流并将 CongestionWindow 属性挂接到发送方的socket上。 通常,使用on-off应用程序来生成流,但这有几个问题。 首先,在应用程序启动时间之前不会创建开关应用程序的套接字,因此我们无法在配置时挂接套接字。 其次,即使我们可以在开始时间之后安排呼叫,套接字也不是公开的,所以我们无法接通。
因此,我们可以制作一个简单的开关应用程序(TutorialApp)。首先,我们创建一个套接字并在其上进行跟踪连接;然后,我们将这个套接字传递到简单应用程序的构造函数中,将其安装在源节点中。
3.2 简单on-off应用程序TutorialApp
当前版本的 ns-3 将其移至一个单独的头文件(``tutorial-app.h'')和实现文件(``tutorial-app.cc'')中。这个简单的应用程序允许在配置时创建 "Socket"。
以下是tutorial-app.cc的内容,它的目的是让模拟器自动调用application,告诉它们何时启动、何时停止。(官方文档中对于开始和停止application的原理有一个深层次的解释,感兴趣可以自行查看)
#include "tutorial-app.h"
#include "ns3/applications-module.h"
using namespace ns3;
TutorialApp::TutorialApp()
: m_socket(nullptr), //要传输的socket
m_peer(), //目的地址
m_packetSize(0), //包的大小
m_nPackets(0), //包的数量
m_dataRate(0), //传输速率
m_sendEvent(), //发送事件
m_running(false), //是否正在运行
m_packetsSent(0) //包是否发完
{
}
TutorialApp::~TutorialApp()
{
m_socket = nullptr;
}
/* static */
TypeId
TutorialApp::GetTypeId()
{
static TypeId tid = TypeId("TutorialApp")
.SetParent<Application>()
.SetGroupName("Tutorial")
.AddConstructor<TutorialApp>();
return tid;
}
void
TutorialApp::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;
}
void
TutorialApp::StartApplication() //控制开始application的函数
{
m_running = true;
m_packetsSent = 0;
m_socket->Bind();
m_socket->Connect(m_peer);
SendPacket();
}
void
TutorialApp::StopApplication() //控制停止application的函数
{
m_running = false;
if (m_sendEvent.IsRunning())
{
Simulator::Cancel(m_sendEvent);
}
if (m_socket)
{
m_socket->Close();
}
}
void
TutorialApp::SendPacket() //发包函数
{
Ptr<Packet> packet = Create<Packet>(m_packetSize);
m_socket->Send(packet);
if (++m_packetsSent < m_nPackets)
{
ScheduleTx();
}
}
void
TutorialApp::ScheduleTx() // 调度下一个发送事件
{
if (m_running)
{
Time tNext(Seconds(m_packetSize * 8 / static_cast<double>(m_dataRate.GetBitRate())));
m_sendEvent = Simulator::Schedule(tNext, &TutorialApp::SendPacket, this);
}
}
介绍完原理与TutorialApp部分后,接下来进入fifth.cc中的代码部分,包含trace sinks和main函数。
3.3 trace sinks
/**
* Congestion window change callback
*
* \param oldCwnd Old congestion window.
* \param newCwnd New congestion window.
*/
static void
CwndChange(uint32_t oldCwnd, uint32_t newCwnd)
{
NS_LOG_UNCOND(Simulator::Now().GetSeconds() << "\t" << newCwnd);
}
CwndChange函数输出当前仿真的时间与新接收到的窗口值。
此外,还有一个新的trace sink,用来显示丢弃数据包的位置。这是因为作者在这段代码中添加一个错误模型(error model),想演示一下出现错误(丢包)时窗口会怎么变。内容如下:
/**
* Rx drop callback
*
* \param p The dropped packet.
*/
static void
RxDrop(Ptr<const Packet> p)
{
NS_LOG_UNCOND("RxDrop at " << Simulator::Now().GetSeconds());
}
该函数的具体原理:
该trace sink连接到PPP NetDevice 的 "PhyRxDrop "跟踪源。当数据包被 NetDevice 的物理层丢弃时,该跟踪源就会触发。若查看源代码(src/point-to-point/model/point-to-point-net-device.cc),就会发现该跟踪源指向 PointToPointNetDevice::m_phyRxDropTrace。如果在 src/point-to-point/model/point-to-point-net-device.h 中查找该成员变量,会发现它被声明为 TracedCallback<Ptr<const Packet>>。这说明回调目标应该是一个返回 void 的函数,并且只接受一个 Ptr<const Packet> 的参数(假设使用的是 ConnectWithoutContext),这正是上面所使用的函数。
3.4 main函数
3.4.1 TCP的基础配置
主函数首先将 TCP 类型配置为使用传统的 NewReno 拥塞控制变体,以及所谓的经典 TCP 丢失恢复机制。 在最初编写本教程程序时,这些是默认的 TCP 配置,但随着时间的推移,|ns3| TCP 已发展为使用当前 Linux TCP 默认的 "Cubic "和 "Prr "损失恢复机制。
// In the following three lines, TCP NewReno is used as the congestion
// control algorithm, the initial congestion window of a TCP connection is
// set to 1 packet, and the classic fast recovery algorithm is used. Note
// that this configuration is used only to demonstrate how TCP parameters
// can be configured in ns-3. Otherwise, it is recommended to use the default
// settings of TCP in ns-3.
Config::SetDefault("ns3::TcpL4Protocol::SocketType", StringValue("ns3::TcpNewReno"));
Config::SetDefault("ns3::TcpSocket::InitialCwnd", UintegerValue(1));
Config::SetDefault("ns3::TcpL4Protocol::RecoveryType",
TypeIdValue(TypeId::LookupByName("ns3::TcpClassicRecovery")));
3.4.2 创建节点与PPP链路,设置传输速率和延时,安装应用
NodeContainer nodes;
nodes.Create(2);
PointToPointHelper pointToPoint;
pointToPoint.SetDeviceAttribute("DataRate", StringValue("5Mbps"));
pointToPoint.SetChannelAttribute("Delay", StringValue("2ms"));
NetDeviceContainer devices;
devices = pointToPoint.Install(nodes);
3.4.3 配置错误模型
Ptr<RateErrorModel> em = CreateObject<RateErrorModel>();
em->SetAttribute("ErrorRate", DoubleValue(0.00001));
devices.Get(1)->SetAttribute("ReceiveErrorModel", PointerValue(em));
错误模型是让脚本在运行过程中出现丢包,重复ACK导致重传。
上述代码实例化了一个 RateErrorModel 对象,并将 "ErrorRate "属性设置为0.00001。然后,我们将实例化后的 RateErrorModel 设置为点对点 NetDevice 使用的错误模型。
3.4.4 安装协议栈,为设备创建接口和分配 IP 地址
InternetStackHelper stack;
stack.Install(nodes);
Ipv4AddressHelper address;
address.SetBase("10.1.1.0", "255.255.255.252");
Ipv4InterfaceContainer interfaces = address.Assign(devices);
由于使用的是 TCP,因此需要在目标节点上安装一些设备来接收 TCP 连接和数据。为此,ns-3 中通常使用 PacketSink 应用程序。(这一部分与UDP大不相同)
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.));
PacketSinkHelper通过使用 ns3::TcpSocketFactory 类创建套接字。该类实现了一种名为 "对象工厂"(object factory)的设计模式,是一种常用的机制,用于指定一个以抽象方式创建对象的类。可以不必自己创建对象,而是向 PacketSinkHelper 提供一个字符串,该字符串指定了用于创建对象的 TypeId 字符串,然后可反过来用于创建由工厂创建的对象的实例。
其余参数将告诉应用程序应绑定到哪个地址和端口。
3.4.5 创建socket
通过代码可以看到是通过TcpSocketFactory::GetTypeId和已经创建的node来创建,这个调用比上面的 PacketSinkHelper 调用级别稍低,使用的是显式 C++ 类型,而不是字符串引用的类型。除此之外,两者在概念上是相同的。
创建 TcpSocket 并将其连接到节点0后,我们就可以使用 TraceConnectWithoutContext 将拥塞窗口trace source连接到我们的trace sink。
Ptr<Socket> ns3TcpSocket = Socket::CreateSocket(nodes.Get(0), TcpSocketFactory::GetTypeId());
ns3TcpSocket->TraceConnectWithoutContext("CongestionWindow", MakeCallback(&CwndChange));
3.4.6 实例化两个trace sink
其中TutorialApp用来管理application的开与停,连接到节点0;RxDrop用来丢包,连接到设备1。
Ptr<TutorialApp> app = CreateObject<TutorialApp>();
app->Setup(ns3TcpSocket, sinkAddress, 1040, 1000, DataRate("1Mbps"));
nodes.Get(0)->AddApplication(app);
app->SetStartTime(Seconds(1.));
app->SetStopTime(Seconds(20.));
devices.Get(1)->TraceConnectWithoutContext("PhyRxDrop", MakeCallback(&RxDrop));
3.4.7 simulation的启停
Simulator::Stop(Seconds(20));
Simulator::Run();
Simulator::Destroy();
return 0;
四、实验结果
实验结果很长,以上是第一页的截图,可以看到实验结果非常不明显,因此进行改进。
将输出重定向到一个名为 cwnd.dat 的文件: (由于输出结果中含有丢包时间行,因此要在脚本中注释掉NS_LOG_UNCOND命令行)
保存脚本后运行如下代码,可以得到cwnd.dat文件,打开该文件,并删除前两行配置行,就可以得到各个时刻的窗口值。
./ns3 run fifth > cwnd.dat 2>&1
接下来通过gnuplot画图软件画出窗口变化图。
安装gnulpot
sudo apt-get install gnuplot
然后在终端依次输入(下面代码中的gnuplot> 是终端自带的,输入后面的就行)
gnuplot
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
输入完成后可以在主文件夹下看到cwnd.png图片