根据官方教程 Building Topologies 一章写成。原示例代码12和文中代码均以GPLv2发布。
总线拓扑
第二篇示例代码1描述了一个简单的例子,默认共有5个节点,$n_0$ 和 $n_1$ 通过点对点链路相连,网段为10.1.1.0/24;$n_1$ 到 $n_{i+1}$ 共 ${i+1}$ 个节点($i$ 为 nCsma 参数的值,默认为3)通过共享介质的CSMA网络连接(有点类似Ethernet),使用10.1.2.0/24网段。其中 $n_1$ 同时连接了两个网络。之后在 $n_0$ 上设置示例代码1一样的echo client,在 $n_{i+1}$ 上设置对应的echo server。
代码大多跟之前的第一个示例没什么不同,只是有了更多的节点和设备。然而还是有一些需要注意的地方:
- 一个节点可以同时属于多个
NodeContainer,如 $n_1$,这样就方便在同一个节点上设置多个设备了。 NodeContainer.Create函数会在当前container的基础上增加指定数量的节点,而非用指定数量初始化。- 由于不在同一个网段内,两个网段自然是不互通的,于是需要使用
Ipv4StaticRoutingHelper::PopulateRoutingTables()让所有节点都能充当路由器的功能,并填充路由表。类似于Linux的sysctl -w net.ipv4.ip_forward=1。 - 对于
CsmaChannel之类可以同时连接多个设备的信道,可以在某一节点上启用混杂(promiscuous)模式(如代码113行),这样就可以监听到所有经过该信道的数据包了。
程序一共录制了三个pcap文件。可以看到,在 second-0-0.pcap 和 second-1-0.pcap 中,只有基于点对点协议的UDP包,但在 second-2-0.pcap 中,不光变成了网段不同、基于Ethernet的UDP包,还出现了ARP包,以进行IP到MAC地址的查找。
为了更高的可自定义度,EnablePcap 不仅可以接受网络设备指针(nd: Ptr<NetDevice>),也可以接受节点 ID(nodeid: uint32_t)和设备ID(deviceid: uint32_t)。换而言之,以下两行代码是等效的:
|
|
Get中输入的参数为节点在该容器中的索引,而非节点ID。节点ID是全局共享、递增的。
模型、属性和现实
在官方教程的对应章节中,主要是想提醒使用者须清楚认识到建模并不一定能涵盖真实情况的所有方面。比如上一节中 CsmaChannel 相对于真实Ethernet3 来说并没有碰撞检测特性,而这是很容易被使用者忽略的。
另外作者还提到了不同网络标准中的不同属性。比如Ethernet的常见最大包大小为1518字节,而由于Ethernet II(DIX)标准的封装方式比IEEE 802.2(LLC/SNAP) 的协议开销少8个字节4,所以前者允许的最大MTU可以高一点。在ns-3的 CsmaNetDevice 中,MTU和封装方式是两个单独的属性,若采用了后者的封装方式而忘了更改默认为1500的MTU,虽然模拟能够正常运行,但可能会偏离实际情况。
这里不是说MTU=1500同时采用LLC/SNAP一定是错的,现代部分网络甚至允许MTU=9000的Jumbo Frame,最终一切还是要看具体情况。
构建无线拓扑
这一段的示例代码2在第二篇代码中增加了一个无线网络,并连接到 $n_0$,Wi-Fi使用802.11a标准(有点远古),网段为10.1.3.0/24。随附示例代码中描述拓扑的字符画:
|
|
在上面总线拓扑的基础上,又添加了 YansWifiChannel,相对来说更复杂一点,但从始至终都是围着链路层和物理层工作。
- $n_0$ 作为AP,其他节点作为STA,分到两个
NodeContainer中。 - 使用
YansWifiPhyHelper来设置物理层属性,如频率、速率等。它承担了类似CsmaHelper的功能,只不过不能直接包办建立起信道和安装网络设备了。 - 通过
WifiMacHelper区分AP和STA,并设置同一个SSID来确保连接同一个网络。 - AP和STA的网络设备也有所不同,所以也包含在两个
NetDeviceContainer中,但可以使用同一个WifiHelper(用于安装网络设备)。 - 通过
MobilityHelper模拟STA节点走来走去,而AP节点则固定在原地。 - 用同一个
Ipv4AddressHelper在同一次Setbase之后多次Assign来保证AP和STA在同一个网段内(相当于使用固定IP,不考虑复杂的DHCP)。
在STA节点的链路层设置中(WifiMacHelper),可以看到设置了 ActiveProbing 属性,这使得STA节点会一直搜寻AP,导致仿真永远不会结束。因此,还需要在开始前手动设置10s结束仿真。
并不需要对Wi-Fi网络启用混杂模式,因为Wi-Fi网络的数据包都是广播的,所以所有节点都能收到。换而言之,混杂模式是自动开启的。
如果加上 --trace 参数,会记录下来4个文件,其中 third-0-1.pcap 对应的是Wi-Fi的流量。使用工具读取,可以看到相比Ethernet,Wi-Fi的通信更加复杂,还有beacon frame之类的控制信息。
记录STA移动轨迹
之前在介绍跟踪时,并未明确跟踪接收器的设计办法,使用的也是helper自带的连接方式。而在这个示例代码中,为了追踪某个STA的移动轨迹并打印到日志中,需要自行设计跟踪接收器(其实就是一个函数),并设置回调。这里的跟踪接收器也只是个函数而已:
|
|
其实功能非常简单,就是当调用这个函数的时候,打印出对应的事件名(context),以及STA节点的位置。为了让节点位置变化时调用这个函数,需要在 Simulator::Run() 之前设置回调:
|
|
Config::Connect() 的第一个参数就是事件名,也就是跟踪源的路径,这里仅指定了一个STA。第二个参数就是一个回调对象。
MakeCallback的作用是将函数指针包装成一个Callback对象。Callback类是一个模板类,有一个强制参数(对应返回值,void也算一个)和至多五个可选参数(对应参数列表)。MakeCallback会根据函数指针的类型自动推断参数列表。这样,就可以直接使用类似ret = cb(arg1, arg2, ...)的方法来调用回调了。如果需要设置对象的成员函数回调,或者希望预先设定某几个参数,可以参考 手册的对应部分。
这样,当STA位置变化时,就会在日志中打印当前位置,如:
|
|
ns-3中的队列
和很多人理解的不同,数据包并不是排一个队就能直接发出去了,而在实际情况中,有多层队列,排完这个再排下一个,用来处理不同的事情。在ns-3中虽然并没有实际操作系统中那么复杂,但也“将IP层或流量控制层与设备层分开”,前者,即所谓的流量控制层,处理QoS和AQM;而后者在 NetDevice 中,处理设备层的队列,与连接类型有关(Ethernet,Wi-Fi等)。如果设备层的队列并没有被填满,那么流量控制层队列基本等效于透明,毕竟这时并没有控制流量的必要。
官方教程中有 [不同队列类型详解]。在分配IP地址时,流量控制层默认启用 pfifo_fast 队列(容量为1000个包)并可以自行指定,而设备层队列由设备自身决定,不同的网络设备有不同的队列类型。
若要自行指定流量控制层的队列,有两种办法。如果还未安装网络设备,可以借助网络设备的helper实现:
|
|
如果已经安装了网络设备,可以使用 TrafficControlHelper 来设置根队列:
|
|