本文根据官方教程 Conceptual Overview 一章写成。在这章中,教程通过一个代码示例,演示了 ns-3 的几大基本概念及其基本使用方法。但是个人觉得顺序比较难懂,所以对其进行了一定程度的重新组织。
下文中代码经过一点点修改,原代码见1,代码均以 GPLv2 发布。
概念
节点 Node
计算机网络可以抽象为一个图,而一个节点就是其中的一个网络设备。该抽象不考虑网络设备的内部结构与功能,所以路由器(3 层)、交换机(2 层)、计算机均抽象为节点。因此,节点本身没有任何功能,也不能直接连接任何信道,仅代表一个能收发数据的事物,功能需要作为应用实现,信道通过设备连接。
节点使用 ns3::Node
类表示,并由 ns3::NodeContainer
类管理。要创建指定数量的节点,可以用 Create
实例方法。以下代码创建了两个节点,并将其存储在 nodes
中:
|
|
要使用下标访问 NodeContainer
中的节点,可以用 Get
实例方法。以下代码访问了第一个节点(下标从 0 开始):
|
|
上述代码中的
Ptr<T>
是 ns-3 提供的智能指针,能够自动解引用,效果类似于std::shared_ptr<T>
。类型T
必须支持Ref()
和Unref()
方法。关于 ns-3 的内存管理机制,请看 这一段。
为了使光秃秃的节点具备一些基本的网络通信功能(实际上属于应用),可以使用 ns3::InternetStackHelper
安装网络栈(包含 TCP、UDP、IP 等)。一个 helper 可以一次安装一整个 NodeContainer
,如以下代码将 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
:
|
|
此处使用了多个隐式转换,从
const char*
即字符串描述的地址到Ipv4Address
和Ipv4Mask
。如果不希望从 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
:
|
|
属性(attribute)相比于普通的成员变量,具有更多的控制,如可以设置默认值(包括编译期和运行期)、边界检查等。关于更多属性信息,请看 这一段。
点对点信道限制输入的
NodeContainer
必须有且仅有两个节点,否则运行时会抛出异常。这里设置的网络设备属性和信道属性,应该参考具体的网络设备3和信道类4的文档。
信道的延迟,就是 propagation delay。
应用 Application
节点的实际功能均由应用实现。这是一个庞大的概念,不仅仅包含应用层的内容,也同时包含了传输层、网络层等内容。它描述了某个节点的行为。
应用使用 ns3::Application
类表示,并由 ApplicationContainer
类管理。该 container 的具体创建由具体的应用程序实现。使用 Start
和 Stop
实例方法,可以控制应用的开始结束时间,仿真程序在所有应用运行结束之前不会主动停止:
|
|
在示例代码中,具体使用了 ns3::UdpEchoClientApplication
和 ns3::UdpEchoServerApplication
类,作用应该不言而喻。UdpEchoServerApplication
使用 ns3::UdpEchoServerHelper
类创建,如以下代码在第二个节点上安装,设置端口为 9,并在 1-10s 运行:
|
|
这里包含一个隐式转换。
nodes.Get(1)
返回的是Ptr<Node>
,而Install
方法接受的是NodeContainer
,但是Ptr<Node>
可以隐式转换为NodeContainer
。
负责主动发送数据的 UdpEchoClientApplication
与上文的 helper 类似,但有更多的参数可供自定义。如以下代码,将第二个节点的端口 9 作为目标,设置发送间隔为 1s、包大小为 1024B、发送总数为 15,安装到第一个节点上,并在 2-10s 运行:
|
|
代码示例
使用 ns-3 编写的仿真器程序是声明式而非命令式的,这有点类似于前端工程中兴起的类似概念。
头文件
ns-3 提供了一些大粒度的头文件,可以一定程度上免去繁琐的找头文件过程。下面头文件的内容,基本就是示例代码中用到的模块。
|
|
日志
ns-3 的日志是可以按照模块自定义的,当然这也需要自行为代码划分模块。如示例代码中的这一行 macro 就是将当前源文件划分到 FirstScriptExample
模块:
|
|
如果需要控制日志的详细程度,可以使用(如将 UdpEchoClientApplication
的日志级别设为 INFO
)
|
|
命名空间
正如上文提到的,本代码示例中使用的类几乎均在 ns3
命名空间下。
模拟器
当上文模拟器所需执行的行为全部描述完毕后,就需要调用模拟器进行模拟了。这个示例代码的模拟是有穷尽的,所以可以等待模拟自行结束,然后直接销毁:
|
|
但是,如果模拟永远不会结束,或是希望自定义一个结束时间,则需要在 Run()
之前调用 Stop()
,如以下代码将模拟时间限制在 10s:
|
|
编译
教程中提到的 ./ns3 build
大概率是错的,个人猜测是之前 ./waf
直接查找替换后的产物6。现在不必将文件移至 scratch/
中,如本例子可以直接执行:
|
|
-
https://gitlab.com/nsnam/ns-3-dev/-/blob/61750bbd89ee258a423a7ec095b13896eeab47c5/examples/tutorial/first.cc ↩︎
-
https://www.nsnam.org/docs/doxygen/d5/d4f/classns3_1_1_ipv4_address_helper.html#acf7b16dd25bac67e00f5e25f90a9a035 ↩︎
-
https://www.nsnam.org/docs/doxygen/dc/d89/classns3_1_1_point_to_point_net_device.html ↩︎
-
https://www.nsnam.org/docs/doxygen/db/d4a/classns3_1_1_point_to_point_channel.html ↩︎
-
https://www.nsnam.org/docs/doxygen/d4/d0c/classns3_1_1_udp_echo_client.html ↩︎
-
https://gitlab.com/nsnam/ns-3-dev/-/commit/3c604d5b2e57850c3db611709c0c04be8c59c044?page=3#085fdec380970f024611fd48e61bfbcdfa826a5d_846_846 ↩︎