转自https://blog.csdn.net/Mary19920410/article/details/70332788
概念概述
在我们需要真正开始看或者写 ns-3 代码之前,首先要做的是解释系统中的几个核心概念和抽象(abstraction)。其中的大部分可能对一些人来说是显而易见的,但我们建议花点时间阅读本部分,以确保你从一个坚实的基础开始。
关键抽象
在本节中,我们将回顾在网络(networking)中普遍使用但在 ns-3 中有特定含义的术语。
节点(Node)
网络术语中,连接到网络的计算设备被称为主机(host)有时也被称为终端系统(end system)。由于 ns-3 是网络模拟器,而不是特指互联网模拟器,我们故意不使用主机这个词,因为它紧密地与互联网及其协议联系在一起。相反,我们使用一个更通用的源自图论的也被用于其他模拟器的术语——节点。
在 ns-3 中基本计算设备抽象被称作节点。这种抽象在 C++ 中用 Node
类表示。Node
类提供管理仿真中计算设备表现的方法。
你应该将一个 Node
当作一台你将要添加功能的计算机。可以添加应用程序、协议栈和(有相关驱动的)外围卡(peripheral card)使这台电脑能够做有用的工作。我们在 ns-3 的使用了相同的基本模型。
应用(Application)
通常,计算机软件分为两大类。系统软件根据一些计算模型组织各类计算机资源,例如内存、处理器周期、磁盘、网络等。系统软件通常不使用这些资源来完成直接有益于用户的任务。应用获取和使用由系统软件控制的资源来完成用户的一些目标。
通常,系统软件和应用软件的分界线在于操作系统受限时特权级别的改变。在 ns-3 中没有真正的操作系统的概念尤其没有特权级别或者系统调用的概念。然而,我们有应用程序的概念。正如“真实世界”中,软件应用在计算机上运行执各种行任务,在模拟世界,ns-3 应用程序在 ns-3 节点
上运行驱动模拟。
在 ns-3 中,对用户程序的基本抽象是应用。这种抽象在 C++ 中用 Application
类表示。Application
类提供管理在仿真中用户级应用的表现的方法。开发人员期望 Application
类能够在面向对象编程中定制以创建新的应用。 在本教程中,我们将使用定制的 Application
类被称为 UdpEchoClientApplication
和 UdpEchoServerApplication
类。 如你所料,这些应用程序组成一个客户端/服务器应用程序集,用于生成和回显模拟的网络数据包。
信道(Channel)
在现实世界中,它能够将计算机连接到网络。通常,在这些网络上数据流的介质称为信道。将以太网电缆连接到墙上的插头后,即可将计算机连接到以太网通信信道。在 ns-3 的模拟世界,节点连接到表示通信信道的对象上。 在这里,基本通信子网的抽象被称作信道在 C++ 中用 Channel
类表示。
Channel
类提供了管理通信子网对象和连接节点到它们之上的方法。信道也被开发者们用面向对象编程感定制。定制信道既可对简单如电线,复杂如大型以太网交换机建模。也可对三维空间中充满障碍物的无线网络建模。
在本教程中我们将使用的信道有 CsmaChannel
、PointToPointChannel
和 WifiChannel
。以 CsmaChannel
为例,建模了一个实现载波侦听多路访问
( carrier sense multiple access,CSMA)通信介质的通信子网。这带给我们类以太网的功能。
网络设备(Net Device)
曾经,如果你想将计算机连接到网络,你必须购买特定种类的网线和一种被称作外围卡(peripheral card)的硬件设备。如果外围卡能够实现一些网络功能,则它们被称为网络接口卡(Network Interface Card)或网卡(NIC)。今天,大多数计算机都带有内置的网络接口硬件,用户看不到这些构件。
如果没有软件驱动程序来控制硬件,网卡将无法工作。在 Unix(或 Linux),一块外设被归类为设备(device)。设备被设备驱动(device driver)所控制,网络设备(NIC)受网络设备驱动(network device driver)控制它们统称为 net device。 在 Unix 和 Linux 中,你通过诸如 eth0 之类的名字来查找这些设备。
在 ns-3 中,net device 抽象涵盖了软件驱动和仿真硬件。一个 net device 被“安装”到一个节点中,以便在仿真中节点能和其他节点通过信道通信。就像在真实计算机中,节点可以经由多个 NetDevice
被连接到多个信道
上。
Net device 的抽象在 C++ 中用 NetDevice
类表示。该 NetDevice
类提供用于管理 Node
和 Channel
对象连接的方法,并且可以由面向对象编程的开发者定制。在本教程中,我们将使用几个定制的 NetDevice
它们被称为 CsmaNetDevice
、PointToPointNetDevice
和 WifiNetDevice
。就像以太网网卡被设计成用于以太网网络,CsmaNetDevice
被设计成用于 CsmaChannel
、PointToPointNetDevice
被设计成用于 PointToPointChannel
、WifiNetNevice
被设计成用于 WifiChannel
。
拓扑助手(Topology Helper)
在真实的网络中,你会发现添加了(或内置)网卡的主机。在 ns-3 中,我们要说的是,你会发现附加了 NetDevice
的 Node
。在大型模拟网络中,你需要整理 Node
、NetDevice
、Channel
之间的各种连接。
因为 NetDevice
与 Node
之间的连接、NetDevice
与 Channel
之间的连接、分配 IP 地址等等都是 ns-3 中 常见的任务,所以我们提供了所谓的拓扑助手(Topology Helper)使这个尽可能容易。例如,创建 NetDevice、添加 MAC 地址、在节点上安装 Net Device、配置节点的协议栈,然后将 NetDevice连接到一个信道需要很多 ns-3 核心操作来完成。将多个设备连接到多点信道上,然后将各个网络连接在一起成为互连网络,甚至需要更多的操作才能完成。为了方便使用,我们提供了 Topology Helper 对象,将这些不同的操作组成一个易于使用的模型。
第一个 ns-3 程序
如果你遵循上述建议下载系统,你将会在你的主目录下的 repos
目录下发现一个 ns-3 发布版。切换到这个目录,你应该会发现类似下面的目录结构:
-
AUTHORS examples scratch utils waf
.bat*
-
bindings LICENSE
src utils
.py waf-tools
-
build ns3 test
.py* utils
.pyc wscript
-
CHANGES
.html README testpy-output VERSION wutils
.py
-
doc RELEASE_NOTES testpy
.supp waf* wutils
.pyc
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
切到 examples/tutorial
目录。你会发现一个名为 first.cc
的文件。这是一个脚本,将在两个节点之间创建一个简单的点对点(point-to-point)链接,并在节点之间回送(echo)单个数据包。让我们逐字逐句地分析这个脚本,现在就开始吧,用你喜欢的编辑器打开这个脚本。
样板文件(Boilerplate)
该脚本的第一行是 emacs 模式行。这告诉 emacs 我们在源代码中使用的格式约定(编码风格)。
/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
- 1
- 1
- 1
这总是一个有争议的话题,所以我们也会立即离开这个话题。ns-3 项目,就像大多数大型项目一样,采用了所有贡献代码都必须遵守的编码风格。如果你想为本项目贡献你的代码,你的代码必须符合 ns-3 编码标准,该标准的描述在文件 doc/codingstd.txt
中,在项目的网页上也有[描述][28]。
我们建议你要习惯 ns-3 代码的形式和感觉,并且只要使用我们的代码工作时都采用这一标准。开发团队和贡献者已经这样做了各种各样的抱怨。如果使用 emacs 编辑器,上面的 emacs 模式行更容易获得正确的格式。
ns-3 模拟器使用的是 GNU 通用公共许可证(GNU General Public License,GUN GPL)。在 ns-3 中,你会在每个文件的头部看到相应的 GNU 法律术语。通常,你会看到 ns-3 项目的版权声明。
-
/*
-
* 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, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-
*/
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
模块包含(Module Include)
代码适当地以多个 include 语句开始。
-
#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"
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
为了帮助高级脚本用户处理系统中存在的大量 include 文件,我们根据相关的大型模块来将 include 文件进行分组。我们提供了一个单独的 include 文件,它将递归地加载每个模块使用的所有 include 文件。而不必确切地知道你所需要的头文件,也不必为很多依赖关系头疼,我们允许你以大粒度加载一组文件。它虽然不是最有效的方法,但它确实使写脚本变得更容易。
件在构建过程中,每个 ns-3 include 文件都被放在了一个目录名为 ns3
下(构建目录下),以避免 include 文件名冲突。ns3/core-module.h
文件与 src/core
目录下的 ns-3 模块相对应。如果你列出这个目录,你会发现大量的头文件。当你做一个构建时,Waf 会将公共头文件放在一个 ns
目录下,根据你的配置适当地放置在 build/debug
或 build/optimized
下。Waf 也会自动生成一个模块包含文件(module include file)来加载所有公共头文件。
因为你是,理所当然地,虔诚地遵循着本教程,你将已经完成了
$ ./waf -d debug --enable-examples --enable-tests configure
- 1
- 1
- 1
为了让项目是包含样例与测试的 debug 版,你也完成了
$ ./waf
- 1
- 1
- 1
所以,如果现在你查看目录 ../../build/debug/ns3
你会发现上面显示的四个模块包含文件。如果你查看这些文件的内容,你会发现它们在各自的模块中包含了所有的公共 include 文件。
ns3 命名空间(Namespace)
脚本的下一行是命名空间声明。
using namespace ns3;
- 1
- 1
- 1
ns-3 项目是在一个名为 ns-3
的 C++ 命名空间中实现的。这会在全局命名空间之外在一定范围内将 ns-3 相关的声明进行分组,我们希望这能对其他代码的集成有帮助。C++ 的 using 语句将 ns-3 命名空间引入到当前(全局)声明区域。也就是说在这个声明之后,你就不必为了使用 ns-3 代码而在所有的代码前加上 ns3::
范围解析操作符了。如果你不熟悉命名空间,请查阅任何一本 C++ 教程,并将 ns3
命名空间和用法与 std
命名空间和(在对 cout
和 stream 的讨论中经常发现的) using namespace std:
语句进行比较。
日志(Logging)
脚本接下来的一行是:
NS_LOG_COMPONENT_DEFINE ("FirstScriptExample");
- 1
- 1
- 1
我们将借这个语句来谈一谈 Doxygen 文档系统。如果你看一下这个项目的网站,ns-3 project,你会发现导航栏中的链接,“文档”(Documentation)。如果你点击了这个链接,你将被带到我们的文档页。其中有一个链接“最新发布”(Latest Release),这将带你到 ns-3 最新稳定版的文档页。如果你选择了“API文档”(API Documentation)链接,你会被带到 ns-3 的 API 文档页。
在左侧,你会找到文档结构的图形表示。ns-3 导航树中的 NS3 Modules
是一个很好的开始的地方。如果展开 Modules
,你会看到 ns-3 模块文档的列表。在这里,模块的概念是和上面讨论的模块包括文件紧密相连的。ns-3 的日志记录子系统在 Using the Logging Module 部分有讨论,所以在本教材的后面我们会使用它,但你可以通过查看 Core
模块了解上面的语句,然后展开 Debugging tools
,选择 Logging
页。 点击 Logging
。
现在,你应该正在查看日志记录模块的 Doxygen 文档。在页面顶部的 Macros
列表中,你将看到 NS_LOG_COMPONENT_DEFINE 条目。在跳进去之前,查看日志模块的“详细描述”(Detailed Description)获得对整体操作的印象可能是极好的。你既可以通过向下滚动鼠标来完成,也可以通过选择协助图下的“更多…”(More…)链接来完成。
一旦你有了“发生了什么”的总体思路,那么请继续,看看具体的 NS_LOG_COMPONENT_DEFINE
文档。我不会在这里重复文件内容,但总的来说,这一行声明了一个叫 FirstScriptExample
的日志记录组件,它允许你通过引用这一名称,启用和禁用控制台消息日志。
主函数(Main Function)
脚本接下来一行是
-
int
-
main
(int argc, char *argv[])
-
{
- 1
- 2
- 3
- 1
- 2
- 3
- 1
- 2
- 3
这只是你程序(脚本)主函数的声明。和其他 C++ 程序一样,你需要定义一个主函数作为第一个运行的函数。 这没什么特别的。你的 ns-3 脚本只是一个 C++ 程序。
下一行将时间分辨率设置为 1 纳秒(nanosecond),这恰好是一个默认值:
Time::SetResolution (Time::NS);
- 1
- 1
- 1
分辨率是可以表示的最小时间值(也是两个时间值之间最小可表示差值)。你只可以更改一次分辨率。启用这种灵活的机制是有点消耗内存的,所以一旦明确设置了分辨率,我们就会释放内存,防止进一步更新(如果不明确设置分辨率,它将默认为 1 纳秒,并且当仿真开始时,内存会被释放)。
脚本接下两行用于启用在 Echo Client 和 Echo Server 应用中内置的两个日志组件:
-
LogComponentEnable(
"UdpEchoClientApplication", LOG_LEVEL_INFO);
-
LogComponentEnable(
"UdpEchoServerApplication", LOG_LEVEL_INFO);
- 1
- 2
- 1
- 2
- 1
- 2
如果你读完了日志记录组件的文档,你将看到有多个级别的日志冗余/详情你可以启用。这两行代码为 echo 客户端和服务器启用 INFO 级的调试日志记录。这将导致应用在仿真期间,发送和接收数据包时会打印输出日志消息。
现在,我们将直接进入创建拓扑和运行仿真的业务中去。我们使用拓扑助手(topology helper)对象使此工作尽可能的容易。
拓扑助手(Topology Helper)
NodeContainer(节点容器)
脚本接下来的两行实际上将创建 ns-3 节点
对象,其代表仿真中的计算机。
-
NodeContainer nodes;
-
nodes.
Create (
2);
- 1
- 2
- 1
- 2
- 1
- 2
在我们继续之前让我们找一找 NodeContainer
类的文档。进入一个给定类的文档的另一种方法是通过 Doxygen 页的 Classes
标签。如果你还在 Doxygen 页,只需向上滚动到页面顶部并选择 Classes
标签。你应该可以看到一组新的标签出现,其中有一个是 Classes List
。在该标签下你会看到一列包含所有 ns-3 类的的列表。继续向下滚动,寻找 ns3::NodeContainer
。当你找到了该类,选择它并阅读它的文档。
你可能还记得节点是关键抽象之一。它代表一个(我们要给其添加协议栈、应用和外围卡的)计算机。NodeContainer
拓扑助手提供了一个便捷的方法来创建、管理和访问任何 Node
对象。 上面的第一行仅仅声明了一个被称为 nodes
的 NodeContainer。 第二行调用 Node
对象的 Create 方法,要求该 Container 创建两个节点。正如 Doxygen 中的描述,container 进入 ns-3 系统合适的层调用,创建两个 Node
对象并存储指向那些对象内部的指针。
The nodes as they stand in the script do nothing.构建拓扑接下来的一步是将节点连接到一个网络中去。我们支持的最简单的网络形式是两个节点之间的单个点对点链路( point-to-point link)。我们将在这构建一个这样的链路。
PointToPointHelper(点对点助手)
我们正在构造一个点对点链路,并且在这个对你而言将变得非常熟悉的模式中,我们使用拓扑助手( topology helper)对象来完成将链路放在一起这一低级工作。回想一下,有两个关键抽象分别是 NetDevice
和 Channel
。在现实世界中,这些术语大致对应于外围卡和网络线缆。通常,这两个东西紧密地联系在一起,并且不能期望能够互换,如,以太网设备和无线信道。我们的拓扑助手遵循这一紧密耦合,因此你将使用 单独的 PointToPointHelper
来配置和连接 ns-3 的 PointToPointNetDevice
和 PointToPointChannel
对象。
接下来三行是,
-
PointToPointHelper pointToPoint;
-
pointToPoint.
SetDeviceAttribute (
"DataRate",
StringValue (
"5Mbps"));
-
pointToPoint.
SetChannelAttribute (
"Delay",
StringValue (
"2ms"));
- 1
- 2
- 3
- 1
- 2
- 3
- 1
- 2
- 3
第一行,
PointToPointHelper pointToPoint;
- 1
- 1
- 1
实例化栈上的 PointToPointHelper
对象。以一种高级角度来看下一行,
pointToPoint.SetDeviceAttribute ("DataRate", StringValue ("5Mbps"));
- 1
- 1
- 1
告诉 PointToPointHelper
对象当它创建一个 PointToPointNetDevice
对象时,使用值为“5Mbps”(每秒 5 兆比特)的“数据率”(DataRate)。
从更详细的角度来看,字符串“数据率”相当于我们所谓 PointToPointNetDevice
的 Attribute
(属性)。 如果你看一下类 ns3::PointToPointNetDevice
的 Doxygen 找到 GetTypeId
方法的文档,你会发现一列定义该设备的 Attribute
。其中就有“数据率”属性。大多数 ns-3 用户可见的对象都有一列相似的属性
。你接下来会看见,我们使用该机制轻松地配置而不必重编译。
类似于 PointToPointNetDevice
的数据率属性的是 PointToPointChannel
“延迟”(Delay)属性 。最后一行,
pointToPoint.SetChannelAttribute ("Delay", StringValue ("2ms"));
- 1
- 1
- 1
告诉 PointToPointHelper
使用“2ms”(两毫秒)作为(它随后创建的)每个点对点信道的传播延迟(propagation delay)值。
NetDeviceContainer(网络设备容器)
此时在脚本中,我们有一个包含两个节点的 NodeContainer
。我们有一个 PointToPointHelper
,并准备了 PointToPointNetDevices
和 PointToPointChannel
对象。正如在仿真中我们用 NodeContainer
拓扑助手来创建节点
,我们将要求 PointToPointHelper
来做这一工作,来创建、配置和安装设备。我们将需要一个所有我们所创建的 NetDevice 对象的列表,所以我们使用 NetDeviceContainer
来保存它们,就像我们使用 NodeContainer
来保存我们创建的节点一样。 下面两行代码,
-
NetDeviceContainer devices;
-
devices = pointToPoint.
Install (nodes);
- 1
- 2
- 1
- 2
- 1
- 2
将完成配置设备和通道。第一行声明上面提到的设备容器,第二行挑起重担。PointToPointHelper
的 Install
方法将 NodeContainer
作为参数。在内部创建了一个 NetDeviceContainer
。对于每个 NodeContainer
中的节点(对点对点链路而言必须是两个节点)都会创建一个 PointToPointNetDevice
并保存到设备容器中。创建一个 PointToPointChannel
附加两个 PointToPointNetDevices
。当对象被 PointToPointHelper
创建,以前在 helper 中设置的属性
被用来初始化被创建对象相应的属性
。
执行 pointToPoint.Install(nodes)
调用后,我们将有两个节点,每个节点都有一个已安装的点对点 net device,他们之间有一个单独点对点信道。而这两个设备将被配置为在两毫秒传输延迟的信道上以五兆比特每秒的速率传输数据。
InternetStackHelper(互联网协议栈助手)
我们现在已经配置了节点和设备,但在节点上并没有安装任何协议栈。接下来的两行代码会处理这个问题。
-
InternetStackHelper stack;
-
stack.
Install (nodes);
- 1
- 2
- 1
- 2
- 1
- 2
就像 PointToPointHelper
是针对点对点网络设备的,InternetStackHelper
则是针对互联网协议栈的。Install
方法采用 NodeContainer
作为参数。当它被执行时,它将为节点容器中的每个节点安装互联网协议栈(TCP、UDP、IP,等等)。
Ipv4AddressHelper(IPv4 地址助手)
接下来,我们需要将 IP 地址与节点上的设备相关联。为此我们提供了一个拓扑助手来管理 IP 地址的分配。这里的唯一用户可见 API 是用来在执行实际地址分配(在 helper 内以较低级操作完成)时设置 Base IP 地址和网络掩码。
在我们示例脚本的接下来两行,
-
Ipv4AddressHelper address;
-
address.
SetBase (
"10.1.1.0",
"255.255.255.0");
- 1
- 2
- 1
- 2
- 1
- 2
声明一个address helper 对象,并告诉它应该从网络 10.1.1.0 开始分配 IP 地址,使用掩码 255.255.255.0 来定义可分配的位。默认情况下分配的地址是从 1 开始单调递增的,所以从这个 base 分配的第一个地址是 10.1.1.1,其次是 10.1.1.2 等等。低级 ns-3 系统会记住分配的所有 IP 地址,如果你不小心生成相同的 IP 地址将会导致一个致命错误(顺便说一下,这是一个很难调试的错误)。
下一行代码,
Ipv4InterfaceContainer interfaces = address.Assign (devices);
- 1
- 1
- 1
执行实际上的地址分配。在 ns-3 中我们使用 IPv4Interface
关联 IP 地址和设备。正如我们有时候需要一列由 helper 创建的 net device 以备后用,我们有时也需要一列 Ipv4Interface
对象。而 Ipv4InterfaceContainer
提供了此功能。
我们已经建立了一个点对点网络,安装了协议栈并分配了IP地址。此时我们需要的是产生流量的应用。
应用(Application)
ns-3 系统的另一个核心抽象是应用 。在这个脚本中,我们将使用两个核心 ns-3 Application
类的定制,UdpEchoServerApplication
和 UdpEchoClientApplication
。 正如我们之前的解释,我们使用 helper 对象来帮助配置和管理底层对象。这里,我们使用 UdpEchoServerHelper
和 UdpEchoClientHelper
对象,使我们的工作更轻松。
UdpEchoServerHelper
下面几行我们示例程序,first.cc,中的代码是用来在我们先前创建的节点上设置 UDP echo server 应用的。
-
UdpEchoServerHelper echoServer (9);
-
-
ApplicationContainer serverApps = echoServer.
Install (nodes.
Get (
1));
-
serverApps.
Start (
Seconds (
1.0));
-
serverApps.
Stop (
Seconds (
10.0));
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
上述代码片段的第一行声明了 UdpEchoServerHelper
。像之前的一样,这不是应用本身,而是一个用来帮助我们创建实际应用的对象。我们的约定中有一个是将必须属性
放到 helper 构造函数中去。在这种情况下,helper 不能做任何有用的事除非它提供了(客户端也知道的)端口号。与其只挑一个寄希望于它们能够工作,不如要求端口号作为构造函数的参数。而构造函数反过来简单地对传入值执行 setAttribute
。如果你愿意,以后你可以用 setAttribute
将“端口”属性设置为其他值。
与许多其他的 helper 对象相似,UdpEchoServerHelper
对象也有一个 Install
方法。也正是此方法的执行,实际上导致了底层 echo 服务器应用被实例化并附加到一个节点上。有趣的是,Install
方法需要 一个 NodeContainter
作为参数,就像我们已经看过的其他的 Install
方法一样。这是实际上传递给方法的东西,即使看起来不是这样。这里有一个 C++ 隐式变换( implicit conversion)在起作用,获得 nodes.Get(1)
(其返回指向节点对象的智能指针 —— Ptr<Node>
)的结果并将之用到一个未命名的 NodeContainer
的构造函数中,然后将该 NodeContainer
传递给 Install
。如果你在 C++ 代码中找不到能够正常编译和运行的特定的方法签名(method signature),那么请查找这些种类的隐式转换。
我们现在看到 echoServer.Install
要在节点上安装一个 UdpEchoServerApplication
,其通过索引号来查找(我们用来管理节点的)NodeContainer
。Install
将返回保存了指向所有由 helper 创建的应用(一种情形是我们传了一个包含一个节点的 NodeContainer
)的指针的容器。
应用需要一个时刻来“开始”产生流量,并且可能需要一个可选的“停止”时刻。在这里我们两者都提供。这些时刻用 ApplicationContainer
的方法 Start
和 Stop
来设置。这些方法需要 Time
参数。在这种情况下,我们使用一个 explicit C++ 转换序列获取一个 C++ double 类型的 1.0 并用 Seconds
Cast 将其转换为 ns-3 Time 对象。请注意,转换规则可能是由模型作者控制的,并且 C++ 也有自己的规则,因此你不能总是假设参数会被转换成理想的形式。这两行,
-
serverApps.
Start (
Seconds (
1.0));
-
serverApps.
Stop (
Seconds (
10.0));
- 1
- 2
- 1
- 2
- 1
- 2
会导致 echo 服务器应用在 1 秒后开始
(启用自身),十秒钟后停止
(禁用自身)。基于我们已经声明了一个模拟器事件(应用停止
事件)将被执行十秒的事实,模拟器将至少持续十秒的时间。
UdpEchoClientHelper
echo 客户端应用的建立同服务器端的实质上相同。这里通过 UdpEchoClientHelper
管理底层 UdpEchoClientApplication
。
-
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));
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
但是,对于 echo 客户端我们需要设置五个不同的属性。前两个属性在构造 UdpEchoClientHelper
期间设置。我们传递的参数(传给内部 helper)是用来设置 RemoteAddress 和 RemotePort 属性
的,按照约定,将必须属性
参数放到 helper 构造函数中去。
回想一下,我们使用 Ipv4InterfaceContainer
来跟踪分配给设备的 IP 地址。接口
容器的第零接口将对应节点
容器第零节点的 IP 地址。接口
容器的第一接口对应于节点
容器第一节点的 IP 地址。因此,在第一行代码中(上面),我们创建了 helper ,并告诉它,将客户端的远程地址(remote address)设置为服务器所在节点的 IP 地址。我们还要告诉它安排在 9 号端口发送数据包。
MaxPackets 属性
告诉客户端在仿真过程中我们允许它发送数据包的最大数量。Interval 属性
告诉客户端数据包之间等待时间是多久,而 PACKETSIZE 属性
告诉客户其数据包有效载荷应该是多大。属性
的这种特殊的组合告诉客户端发送一个 1024 字节的数据包。
就像 echo 服务器的情况一样,我们告诉 echo 客户端 Start
和 Stop
,不同的是在这里我们在服务器启用 1 秒后启用客户端(进入仿真后 2 秒)。
模拟器
此时我们需要做的是运行仿真。这里是通过全局函数 Simulator::Run
来完成的。
Simulator::Run ();
- 1
- 1
- 1
之前我们调用方法时,
-
serverApps.
Start (
Seconds (
1.0));
-
serverApps.
Stop (
Seconds (
10.0));
-
...
-
clientApps.
Start (
Seconds (
2.0));
-
clientApps.
Stop (
Seconds (
10.0));
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
实际上我们在模拟器中在 1 秒,2 秒时两个在 10 秒时调度事件。当 Simulator::Run
被调用时,系统将开始检查调度事件列表并执行它们。首先它将执行在 1.0 秒钟的事件,这会启用 echo 服务器应用(此事件可能反过来调度许多其他事件)。然后它将执行 t=2.0 秒时的事件,这会启用 echo 客户端应用。当然,此事件也可能会调度更多事件。在 echo 客户端应用中实现的 start 事件将通过向服务器发送一个数据包开始仿真的数据传输阶段。
向服务器发送数据包这一动作将触发一系列事件,这些事件将在幕后自动调度,并根据我们在脚本中设置的各种计时参数执行数据包 echo 机制。
最后,因为我们只发送一个数据包(MaxPackets
属性
被设为 1),单一客户端的 echo 请求将触发逐渐停止的事件链,仿真将进入空闲状态。一旦发生这种情况,剩余的事件将是服务器和客户端的 Stop
的事件,当这些事件执行完,就没有进一步的事件要处理了,Simulator::Run
返回。仿真完成。
剩下的就是清理了。这将通过调用全局函数 Simulator::Destroy
完成。执行 helper 函数(或低级 ns-3 代码),hook 被插进模拟器以销毁我们创建的所有对象。你不必自己追踪这些对象——你需要做的仅仅是调用 Simulator::Destroy
并退出。ns-3 系统为你解决最难的部分。我们的第一个 ns-3 脚本——first.cc剩下的几行就是下面这些:
-
Simulator::
Destroy ();
-
return
0;
-
}
- 1
- 2
- 3
- 1
- 2
- 3
- 1
- 2
- 3
什么时候模拟器会终止?
ns-3 是一个离散事件(Discrete Event,DE)模拟器。在这种模拟器中,每个事件都与其执行时间相关联,并且模拟器按照时间顺序执行仿真。当然事件也可能导致将来的事件被调度(例如,计时器可能会重新规划自身在下一个时间间隔到期)。
初始事件通常由每个对象触发。例如,IPv6 将调度路由器通告(Router Advertisement,RA)、邻居请求(Neighbor Solicitation)等等。应用调度第一个数据包发送事件等。
当一个事件被处理,它可能会生成零个、一个或多个事件。当执行仿真时,事件被消耗,但可能更多的事件被生成。当事件队列中没有其他事件或者当发现特殊的 Stop
事件时,仿真将自动停止。Stop 事件通过 Simulator::Stop (stopTime);
函数创建。这里有一个 Simulator::Stop
作为终止仿真不可少的一部分的典型例子 :当它是自我维持(self-sustaining)事件。自我维持(或重复)事件是总是重新调度它们自身。因此,它们总是保持事件队列非空。
有许多包含重复事件的协议和模块,例如:
- FlowMonitor —— 定期检查丢失的数据包
- RIPng —— 路由表更新周期广播
- 等等
在这些情况下,需要使用 Simulator::Stop
来温和地结束仿真。另外,当 ns-3 处于仿真(emulation,注意与之前的 simulation 区分,除非特别说明接下来的仿真指的是 simulation)模式时,RealtimeSimulator
用来保持上述模拟时钟与机器时钟同步,并且用 Simulator::Stop
结束进程必不可少的。
在本教程中很多仿真程序都不是显式地调用 Simulator::Stop
,因为事件队列中的事件会自动退出。虽然这些程序也能接受对 Simulator::Stop
的调用。例如,在第一个示例程序中以下附加语句将在 11 秒调度一个显式的终止事件:
-
+ Simulator::
Stop (
Seconds (
11.0));
-
Simulator::
Run ();
-
Simulator::
Destroy ();
-
return
0;
-
}
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
上述代码并不会实际上改变这个程序的行为,因为这一特定的仿真会在 10 秒时自然结束。但是,如果将上述语句中的停止时间从 11 秒更改为 1 秒,你会注意到在有任何输出打印到屏幕之前仿真被终止(因为输出发生在仿真时间 2 秒左右)。
在调用 Simulator::Run
之前调用 Simulator::Stop
很重要,否则,Simulator::Run
可能永远不会将控制权返回给主程序以完成终止!
构建你的脚本
我们已经让构建脚本变得很简单了。你需要做的就是将你的脚本拖到 scratch 目录,然后运行 Waf 它就会自动的被构建好。那么,让我们试试看。在切回顶层目录之后复制 examples/tutorial/first.cc
到 scratch
目录。
-
$
cd ../..
-
$
cp examples/tutorial/first.cc scratch/myfirst.cc
- 1
- 2
- 1
- 2
- 1
- 2
现在使用waf构建你的第一个示例脚本:
$ ./waf
- 1
- 1
- 1
你应该会看到 myfirst
示例已成功生成的消息。
-
Waf: Entering directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build
'
-
[614/708] cxx: scratch/myfirst.cc -> build/debug/scratch/myfirst_3.o
-
[706/708] cxx_link: build/debug/scratch/myfirst_3.o -> build/debug/scratch/myfirst
-
Waf: Leaving directory `/home/craigdo/repos/ns-3-allinone/ns-3-dev/build'
-
'build' finished successfully (2.357s)
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
现在你可以运行示例了(注意,如果你在 scratch 目录下构建程序,则必须在 scratch 目录以外运行它):
$ ./waf --run scratch/myfirst
- 1
- 1
- 1
你会看到一些输出:
-
Waf: Entering directory `/home/craigdo/repos/ns-
3-allinone/ns-
3-dev/build
'
-
Waf: Leaving directory `/home/craigdo/repos/ns-
3-allinone/ns-
3-dev/build
'
-
'build' finished successfully (0.418s)
-
Sent
1024 bytes
to
10.1.
1.2
-
Received
1024 bytes
from
10.1.
1.1
-
Received
1024 bytes
from
10.1.
1.2
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
这里构建系统先进行检查以确保文件已经生成然后运行它。echo 客户端上的日志记录组件显示它已经发送一个 1024 字节数据包到 10.1.1.2 echo 服务器。echo 服务器上的日志记录组件说它已经从 10.1.1.1 接收到 1024 个字节。echo 服务器静静地 echo 数据包,echo 客户端日志记录到它已经收到了服务器返回的数据包。
NS-3 源代码
既然你已经用了一些 ns-3 helper,你可能想看看实现这些功能的源代码。最新的代码可以通过一下链接访问:http://code.nsnam.org/ns-3-dev 。在那,你能看到 ns-3 开发树的 Mercurial 摘要页。
在页面顶部,你将看到一些链接,
summary | shortlog | changelog | graph | tags | files
- 1
- 1
- 1
选择 files
链接。下面是我们大部分 repositorie 顶层目录的样子:
-
drwxr-xr-x [up]
-
drwxr-xr-x bindings python files
-
drwxr-xr-x doc files
-
drwxr-xr-x examples files
-
drwxr-xr-x ns3 files
-
drwxr-xr-x scratch files
-
drwxr-xr-x src files
-
drwxr-xr-x utils files
-
-rw-r--r-- 2009-07-01 12:47 +0200 560 .hgignore file | revisions | annotate
-
-rw-r--r-- 2009-07-01 12:47 +0200 1886 .hgtags file | revisions | annotate
-
-rw-r--r-- 2009-07-01 12:47 +0200 1276 AUTHORS file | revisions | annotate
-
-rw-r--r-- 2009-07-01 12:47 +0200 30961 CHANGES.html file | revisions | annotate
-
-rw-r--r-- 2009-07-01 12:47 +0200 17987 LICENSE file | revisions | annotate
-
-rw-r--r-- 2009-07-01 12:47 +0200 3742 README file | revisions | annotate
-
-rw-r--r-- 2009-07-01 12:47 +0200 16171 RELEASE_NOTES file | revisions | annotate
-
-rw-r--r-- 2009-07-01 12:47 +0200 6 VERSION file | revisions | annotate
-
-rwxr-xr-x 2009-07-01 12:47 +0200 88110 waf file | revisions | annotate
-
-rwxr-xr-x 2009-07-01 12:47 +0200 28 waf.bat file | revisions | annotate
-
-rw-r--r-- 2009-07-01 12:47 +0200 35395 wscript file | revisions | annotate
-
-rw-r--r-- 2009-07-01 12:47 +0200 7673 wutils.py file | revisions | annotate
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
我们的示例脚本在 examples
目录。如果你点击 examples
,你会看到子目录的列表。在 tutorial
子目录有一个 first.cc
文件。如果你点击 first.cc
你会发现刚才我们看的代码。
源代码主要在 src
目录下。你既可以通过点击目录名来查看源代码也可以通过单击 files
链到右边的目录名来查看源代码。如果你点击 src
目录,你将会看见一列 src
子目录。如果你再点击 core
子目录,你将会看见一列文件。你看见的第一个文件(在本文写作时)是 abort.h
。如果单击 abort.h
链接,你将被带到 abort.h
源文件,其中包含有用的宏,该宏用于检测到异常时退出脚本。
我们在本章中所使用的 helper 的源代码可以在 src/applications/helper
目录下找到。在目录树这随意四处闲逛看看有什么,看看 ns-3 程序的风格。