ns-3 入门 2:概念与第一个示例

本文根据官方教程 Conceptual Overview 一章写成。在这章中,教程通过一个代码示例,演示了 ns-3 的几大基本概念及其基本使用方法。但是个人觉得顺序比较难懂,所以对其进行了一定程度的重新组织。

下文中代码经过一点点修改,原代码见1,代码均以 GPLv2 发布。

概念

节点 Node

计算机网络可以抽象为一个图,而一个节点就是其中的一个网络设备。该抽象不考虑网络设备的内部结构与功能,所以路由器(3 层)、交换机(2 层)、计算机均抽象为节点。因此,节点本身没有任何功能,也不能直接连接任何信道,仅代表一个能收发数据的事物,功能需要作为应用实现,信道通过设备连接。

节点使用 ns3::Node 类表示,并由 ns3::NodeContainer 类管理。要创建指定数量的节点,可以用 Create 实例方法。以下代码创建了两个节点,并将其存储在 nodes 中:

cpp
1
2
NodeContainer nodes;
nodes.Create(2);

要使用下标访问 NodeContainer 中的节点,可以用 Get 实例方法。以下代码访问了第一个节点(下标从 0 开始):

cpp
1
Ptr<Node> node = nodes.Get(0);

上述代码中的 Ptr<T> 是 ns-3 提供的智能指针,能够自动解引用,效果类似于 std::shared_ptr<T>。类型 T 必须支持 Ref()Unref() 方法。关于 ns-3 的内存管理机制,请看 这一段

为了使光秃秃的节点具备一些基本的网络通信功能(实际上属于应用),可以使用 ns3::InternetStackHelper 安装网络栈(包含 TCP、UDP、IP 等)。一个 helper 可以一次安装一整个 NodeContainer,如以下代码将 nodes 中的所有节点安装网络栈:

cpp
1
2
InternetStackHelper stack;
stack.Install(nodes);

由于并没有变量上的依赖关系,缺少网络栈并不会造成编译失败,但以示例代码为例,缺少网络栈会导致运行时创建更上层的应用失败,从而直接抛出 SIGABRT。

网络设备 Net Device

正如电脑要上网就得插网卡(NIC)一样,节点要连接在一起,网络设备是不可或缺的。ns-3 中的网络设备抽象包含了网卡的硬件功能和驱动程序功能,每个节点可以连接多个网络设备,用于连接多个不同的信道。

网络设备使用 ns3::NetDevice 类表示,并使用 ns3::NetDeviceContainer 进行管理。同样正如网卡分为有线和无线网卡一样,实际使用时多选择其子类,如以太网使用的 ns3::CsmaNetDevice 类,以及 Wi-Fi 使用的 ns3::WifiNetDevice 类。在该篇示例代码中,由 helper 总揽网络设备和信道的创建,此处按下不表。

给网络设备分配 IPv4 地址后,可以将其看作一个接口。使用 ns3::Ipv4AddressHelper 类,可以为一个网络设备分配一个 IPv4 地址。以下代码设置了 IP 地址分配范围和掩码,并将其按顺序分配给一个 NetDeviceContainer 中的设备,返回的是一个接口容器 ns3::Ipv4InterfaceContainer

cpp
1
2
3
Ipv4AddressHelper address;
address.SetBase("10.1.1.0", "255.255.255.0");
Ipv4InterfaceContainer interfaces = address.Assign(devices);

此处使用了多个隐式转换,从 const char* 即字符串描述的地址到 Ipv4AddressIpv4Mask。如果不希望从 10.1.1.1 开始分配,可以使用第三个参数 base,如 “0.0.0.3” 表示从 10.1.1.3 开始分配2

信道 Channel

信道代表节点之间的连接,比如 RJ45 双绞线,比如 Wi-Fi,都可以抽象为信道。

信道使用 ns3::Channel 类表示,并由 ns3::ChannelContainer 类管理。同样,信道也有多种类型,如以太网使用的 ns3::CsmaChannel 类,Wi-Fi 使用的 ns3::WifiChannel 类。但是在本篇示例代码中,并没有明确地使用 Channel,而是借助了网络设备和信道的紧密联系,由 helper 类统一创建。比如在下面的代码中,新建了一个点对点信道,设置两端网络设备传输速率为 5 Mbps,信道延迟 2 ms,并将其安装到一个 NodeContainer 中,最终返回创建完成的网络设备 NetDeviceContainer

cpp
1
2
3
4
PointToPointHelper pointToPoint;
pointToPoint.SetDeviceAttribute("DataRate", StringValue("5Mbps"));
pointToPoint.SetChannelAttribute("Delay", StringValue("2ms"));
auto devices = pointToPoint.Install(nodes);

属性(attribute)相比于普通的成员变量,具有更多的控制,如可以设置默认值(包括编译期和运行期)、边界检查等。关于更多属性信息,请看 这一段

点对点信道限制输入的 NodeContainer 必须有且仅有两个节点,否则运行时会抛出异常。

这里设置的网络设备属性和信道属性,应该参考具体的网络设备3和信道类4的文档。

信道的延迟,就是 propagation delay。

应用 Application

节点的实际功能均由应用实现。这是一个庞大的概念,不仅仅包含应用层的内容,也同时包含了传输层、网络层等内容。它描述了某个节点的行为。

应用使用 ns3::Application 类表示,并由 ApplicationContainer 类管理。该 container 的具体创建由具体的应用程序实现。使用 StartStop 实例方法,可以控制应用的开始结束时间,仿真程序在所有应用运行结束之前不会主动停止:

cpp
1
2
3
ApplicationContainer serverApps = xxx;
serverApps.Start(Seconds(1.0));
serverApps.Stop(Seconds(10.0));

在示例代码中,具体使用了 ns3::UdpEchoClientApplicationns3::UdpEchoServerApplication 类,作用应该不言而喻。UdpEchoServerApplication 使用 ns3::UdpEchoServerHelper 类创建,如以下代码在第二个节点上安装,设置端口为 9,并在 1-10s 运行:

cpp
1
2
3
4
UdpEchoServerHelper echoServer(9);
ApplicationContainer serverApps = echoServer.Install(nodes.Get(1));
serverApps.Start(Seconds(1.0));
serverApps.Stop(Seconds(10.0));

这里包含一个隐式转换。nodes.Get(1) 返回的是 Ptr<Node>,而 Install 方法接受的是 NodeContainer,但是 Ptr<Node> 可以隐式转换为 NodeContainer

负责主动发送数据的 UdpEchoClientApplication 与上文的 helper 类似,但有更多的参数可供自定义。如以下代码,将第二个节点的端口 9 作为目标,设置发送间隔为 1s、包大小为 1024B、发送总数为 15,安装到第一个节点上,并在 2-10s 运行:

cpp
1
2
3
4
5
6
7
UdpEchoClientHelper echoClient(interfaces.GetAddress(1), 9);
echoClient.SetAttribute("MaxPackets", UintegerValue(1));
echoClient.SetAttribute("Interval", TimeValue(Seconds(1.0)));
echoClient.SetAttribute("PacketSize", UintegerValue(1024));
ApplicationContainer clientApps = echoClient.Install(nodes.Get(0));
clientApps.Start(Seconds(2.0));
clientApps.Stop(Seconds(10.0));

代码示例

使用 ns-3 编写的仿真器程序是声明式而非命令式的,这有点类似于前端工程中兴起的类似概念。

头文件

ns-3 提供了一些大粒度的头文件,可以一定程度上免去繁琐的找头文件过程。下面头文件的内容,基本就是示例代码中用到的模块。

cpp
1
2
3
4
5
#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"

日志

ns-3 的日志是可以按照模块自定义的,当然这也需要自行为代码划分模块。如示例代码中的这一行 macro 就是将当前源文件划分到 FirstScriptExample 模块:

cpp
1
ns3::NS_LOG_COMPONENT_DEFINE("FirstScriptExample");

如果需要控制日志的详细程度,可以使用(如将 UdpEchoClientApplication 的日志级别设为 INFO

cpp
1
ns3::LogComponentEnable("UdpEchoClientApplication", LOG_LEVEL_INFO);

命名空间

正如上文提到的,本代码示例中使用的类几乎均在 ns3 命名空间下。

模拟器

当上文模拟器所需执行的行为全部描述完毕后,就需要调用模拟器进行模拟了。这个示例代码的模拟是有穷尽的,所以可以等待模拟自行结束,然后直接销毁:

cpp
1
2
Simulator::Run();
Simulator::Destroy();

但是,如果模拟永远不会结束,或是希望自定义一个结束时间,则需要在 Run() 之前调用 Stop(),如以下代码将模拟时间限制在 10s:

cpp
1
2
3
Simulator::Stop(Seconds(10.0));
Simulator::Run();
Simulator::Destroy();

编译

教程中提到的 ./ns3 build 大概率是错的,个人猜测是之前 ./waf 直接查找替换后的产物6。现在不必将文件移至 scratch/ 中,如本例子可以直接执行:

bash
1
./ns3 run examples/tutorial/first.cc