前言
事实上ns3的官方手册很全,相关书籍也是有的,官网先贴在这里:
ns-3.35_wifi-he-network.cc_ns-3网络仿真工具wifi脚本解析_wifi脚本网络拓扑_ns-3wifi6吞吐脚本关键注释_吞吐部分_基础ns-3_ns3.35-优快云博客
ns-3-model-library wifi 浅析_ns-3wifi部分解析_ns-3网络模拟器wifi部分文档分析_Part1_ns3 wifiphy物理层冲突-优快云博客
ns-3-model-library wifi 浅析_ns-3wifi部分解析_ns-3网络模拟器wifi部分文档分析_Part2_yansphy-优快云博客
不过现有的要么版本老旧,要么过于现象化,不够本质,正好我最近分析了下官方手册,在这里分享给大家——需要更加完整的内容,可以直接去官网。
整体介绍
NS-3 是一个离散事件网络模拟器,也就是每一步的数据包生成、封包加头、排队、碰撞、退避、进入信道,这些事件的发生时刻是计算好了的,依次发生的,没有“过程中”。
NS-3 是一个离散事件网络模拟器,其中仿真核心和模型是用 C++ 实现的。ns-3 构建为一个库,该库可以静态或动态链接到定义仿真拓扑并启动仿真器的 C++ 主程序。ns-3 还将其几乎所有的 API 导出到 Python,允许 Python 程序导入“ns3”模块,其方式与 ns-3 库由 C++ 中的可执行文件链接的方式大致相同。
ns-3 的源代码大多组织在 src 目录中,可以用图表来描述。我们将自下而上地努力;通常,模块仅依赖于图中位于其下方的模块。
我们首先描述模拟器的核心;这些组件在所有协议、硬件和环境模型中都是通用的。模拟核心在 src/core 中实现。数据包是网络模拟器中的基本对象,在 src/network 中实现。这两个仿真模块本身旨在组成一个通用仿真内核,该内核可由不同类型的网络使用,而不仅仅是基于 Internet 的网络。ns-3 的上述模块独立于特定的网络和设备模型,这些模型将在本手册的后续部分中介绍。
使用前最好知道的基本概念
事实上tutorial我也看了一遍,里面大量讲述了helper“怎么用”,至于为什么这么用、有哪些是触类旁通的,一律没有或含糊不清,各个函数的介绍,专有性很强,难以泛用,同时缺少一些基本概念,导致看源码会有很多地方不知道在干啥,本章节就是介绍ns-3的基本概念。
Events and Simulator
Event
NS-3 是一个离散事件网络模拟器。从概念上讲,模拟器会跟踪计划在指定模拟时间执行的许多事件。模拟器的工作是按顺序执行事件。事件完成后,模拟器将移至下一个事件(如果事件队列中没有更多事件,则模拟器将退出)。例如,如果执行了计划为模拟时间“100 秒”的事件,而下一个事件直到“200 秒”才计划,则模拟器将立即从 100 秒跳转到 200 秒(模拟时间)以执行下一个事件。这就是 “discrete-event” 模拟器的含义。
用平实的话说,写cc的时候安排事件的发生顺序,安排好了就依次执行,中空时间就跳过,谓之离散时间模拟器
Simulator
Simulator 类是访问事件调度工具的公共入口点。一旦安排了几个事件来启动模拟,用户就可以通过进入模拟器主循环(调用 Simulator::Run)来开始执行它们。主循环开始运行后,它将按从最早到最新的顺序顺序执行所有计划事件,直到事件队列中没有更多事件或调用 Simulator::Stop。为了安排事件由模拟器主循环执行,Simulator 类提供了 Simulator::Schedule* 系列函数。这些函数作为 C++ 模板声明和实现,以自动处理在实际使用的各种 C++ 事件处理程序签名。
不同event handlers 的定时执行
例如,要将事件安排为将来 10 秒执行,并使用特定参数调用 C++ 方法或函数,您可以编写以下内容:
void handler(int arg0, int arg1)
{
std::cout << "handler called with argument arg0=" << arg0 << " and
arg1=" << arg1 << std::endl;
}
Simulator::Schedule(Seconds(10), &handler, 10, 5);
常规scheduling 操作
- Schedule 方法,允许您通过提供当前模拟时间与目标事件的到期日期之间的延迟来安排将来的事件。
- ScheduleNow 方法,允许您为当前模拟时间安排事件:它们将在当前事件完成执行后执行,但 _before_ 下一个事件的模拟时间被更改。(main里面,按序执行,在schedule 5s,3s或者2s之前,即相当于schedule 0s)
- ScheduleDestroy 方法,这些方法允许您在 Simulator 的关闭过程中挂接以清理模拟资源:当用户调用 Simulator::D estroy 方法时,将执行每个 'destroy' 事件。
维护模拟上下文
定时到了,操作执行的上下文(也就是NodeID),是current ID,涉及到收发的时候,current ID是发送的NodeID,schedule接收操作的时候,使用的就会是发送的NodeID,这明显不行,所以ScheduleWithContext指定接收NodeID做接收操作
有两种基本方法可以安排事件:有上下文和无上下文
Simulator::Schedule(Time const &time, MEM mem_ptr, OBJ obj);
Simulator::ScheduleWithContext(uint32_t context, Time const &time, MEM mem_ptr, OBJ obj);
此日志记录框架提供的重要功能之一是自动显示与 “currently” 正在运行的事件关联的网络节点 ID。为了将上下文与每个事件关联,Schedule 和 ScheduleNow 方法会自动将当前执行事件的上下文重新用作计划稍后执行的事件的上下文。
在某些情况下,最明显的是模拟数据包从一个节点到另一个节点的传输时,这种行为是不可取的,因为接收事件的预期上下文是接收节点的上下文,而不是发送节点的上下文。为了避免此问题,Simulator 类提供了一个特定的计划方法:ScheduleWithContext,它允许显式提供与接收事件关联的接收节点的节点 ID。在极少数情况下,开发人员可能需要修改或了解第一个事件的上下文 (节点 ID) 如何设置为其关联节点的上下文 (节点 ID)。这是通过 NodeList 类完成的:每当创建新节点时,NodeList 类都会使用 ScheduleWithContext 为此节点安排“初始化”事件。因此,'initialize' 事件在执行时将上下文设置为节点 ID 的上下文,并且可以使用常规的 Schedule 方法。它调用 Node::Initialize 方法,该方法通过调用与节点关联的每个对象的 DoInitialize 方法来传播“initialize”事件。在其中一些对象中重写的 DoInitialize 方法(尤其是在 Application 基类中)将计划一些事件(最值得注意的是 Application::StartApplication),这些事件反过来将计划流量生成事件,而流量生成事件又将计划网络级事件。
注意
- 用户需要小心地通过对其成员对象显式调用 Initialize 来跨对象传播 DoInitialize 方法
- 与每个 ScheduleWithContext 方法关联的上下文 ID 除了日志记录之外还有其他用途:NS-3 的实验分支使用它来使用多线程在多核系统上执行并行模拟。
Simulator::*函数不知道上下文是什么:它们只是确保当相应的事件使用::GetContext执行时,您使用ScheduleWithContext指定的任何上下文都是可用的。由 Simulator::* 之上实现的模型来解释 context 值。在 ns-3 中,网络模型将上下文解释为生成事件的节点的节点 ID。这就是为什么在 ns3::Channel 子类中调用 ScheduleWithContext 很重要的原因,因为我们正在生成从节点 i 到节点 j 的事件,并且我们希望确保将在节点 j 上运行的事件具有正确的上下文。
可用的模拟器引擎
NS-3 提供了两种不同类型的基本模拟器引擎来管理事件执行。这些是从抽象基类 SimulatorImpl 派生的:
- DefaultSimulatorImpl 函数这是一个经典的 sequential discrete event simulator 引擎,它使用单个执行线程。此引擎会尽快执行事件。
- DistributedSimulatorImpl 函数这是一个经典的 YAWNS 分布式(“并行(应该是需要mpi,不能用)”)模拟器引擎。通过适当地标记和实例化模型组件,此引擎将跨多个计算进程并行执行模型,但以时间同步的方式执行模型,就像模型按顺序执行一样。这两个优点是可以更快地执行模型,并且执行的模型太大而无法容纳在一个计算节点中。此引擎还会尝试尽快执行。
- NullMessageSimulatorImpl 函数这实现了 Chandy-Misra-Bryant (CMB) 空消息算法的变体,用于并行仿真。与 DistributedSimulatorImpl 一样,这需要对模型组件进行适当的标记和实例化。此引擎尝试尽快执行事件。
您可以通过设置全局变量来选择要使用的模拟器引擎,例如:
GlobalValue::Bind("SimulatorImplementationType",
StringValue("ns3::DistributedSimulatorImpl"));
或使用命令行参数
$ ./ns3 run "... --SimulatorImplementationType=ns3::DistributedSimulatorImpl"
除了基本的模拟器引擎之外,还有一个用于构建 “适配器” 的通用工具,它为其中一个核心 SimulatorImpl 引擎提供小的行为修改。适配器基类是 SimulatorAdapter,它本身派生自 SimulatorImpl。SimulatorAdapter 使用 PIMPL (指向实现的指针) 惯用语将所有调用转发到配置的基本模拟器引擎。这样,只需覆盖所需的特定 Simulator 调用,并允许 SimulatorAdapter 处理其余部分,即可轻松提供小型自定义。
模拟器引擎类型可以设置一次,但必须在首次调用 Simulator() API 之前设置。在实践中,由于某些模型必须在构造时安排其启动事件,这意味着通常您应该在实例化任何其他模型组件之前设置引擎类型。
Time
NS-3 在内部将模拟时间和持续时间表示为 64 位有符号整数(符号位用于负持续时间)。时间值根据惯用 SI 单位的“分辨率”单位进行解释:fs、ps、ns、us、ms、s、min、h、d、y。该单位定义最小 Time 值。在调用 Simulator::Run() 之前,可以更改一次。它本身不与 64 位时间值一起存储。
时间可以从所有标准数字类型(使用配置的默认单位)或显式单位(如 Time MicroSeconds (uint64_t value))中构造。可以比较时间,测试时间是否为零的符号或相等性,四舍五入到给定单位,转换为特定单位的标准数字类型。支持所有基本算术运算(加法、减法、乘法或除以标量(数值))。时间可以写入 IO 流/从 IO 流中读取。在写入的情况下,很容易选择输出单位,与分辨率单位不同。
Scheduler
Scheduler 类的主要工作是维护未来事件的优先级队列。可以使用全局变量设置调度程序,类似于选择 SimulatorImpl:
GlobalValue::Bind("SchedulerType",
StringValue("ns3::DistributedSimulatorImpl"));
Scheduler 可以随时通过 Simulator::SetScheduler() 更改。默认计划程序是 MapScheduler,它使用 std::map<> 按时间顺序存储事件。
由于事件分布因模型而异,因此没有一个用于优先级队列的最佳策略,因此 ns-3 有几个选项,具有不同的权衡。示例 utils/bench-scheduler.c 可用于测试用户提供的事件分发的性能。对于适度的执行时间(例如,少于一个小时),优先级队列的选择通常并不重要;将 build type 配置为 Optimized 对于减少执行时间更为重要。
下表列出了可用的计划程序类型,以及它们在 Insert() 和 RemoveNext() 上的时间和空间复杂性摘要。有关其他 API 调用复杂性的详细信息,请参阅各个 Scheduler API 页面。
Callbacks
为何需要回调
两个class之间传递信息,可以一个class实例化另一个class,这样就可以调用另一个class 的函数、获取信息了——但是这样就强耦合了,不利于修改,所以ns3拆散成回调函数
对于网络仿真研究来说,这不是一个抽象的问题,而是在以前的模拟器中,当研究人员想要扩展或修改系统以做不同的事情时(就像他们在研究中容易做的那样),它一直是问题的根源。例如,考虑一个想要在 TCP 和 IP 之间添加 IPsec 安全协议子层的用户:如果模拟器做出了假设,并硬编码到代码中,该 IP 始终与上面的传输协议通信,则用户可能会被迫破解系统以获得所需的互连。这显然不是设计通用仿真器的最佳方法。
Callbacks Background
允许解决上述问题的基本机制称为回调。最终目标是允许一段代码调用函数(或 C++ 中的方法),而没有任何特定的模块间依赖关系。以下是一个functor和template的例子,把类A里面的Hello函数与sf绑定的例子:
template <typename T>
class Functor
{
public:
virtual int operator()(T arg) = 0;
};
template <typename T, typename ARG>
class SpecificFunctor : public Functor<ARG>
{
public:
SpecificFunctor(T* p, int (T::*_pmi)(ARG arg))
{
m_p = p;
m_pmi = _pmi;
}
virtual int operator()(ARG arg)
{
(*m_p.*m_pmi)(arg);
}
private:
int (T::*m_pmi)(ARG arg);
T* m_p;
};
class A
{
public:
A(int a0) : a(a0) {}
int Hello(int b0)
{
std::cout << "Hello from A, a = " << a << " b0 = " << b0 << std::endl;
}
int a;
};
int main()
{
A a(10);
SpecificFunctor<A, int> sf(&a, &A::Hello);
sf(5);
}
请注意,上面的类中定义了两个变量。m_p 变量是对象指针,m_pmi 是包含要执行的函数地址的变量。请注意,调用 operator() 时,它反过来使用 C++ PMI 语法调用对象指针提供的方法。ns-3 中的 Callback API 使用 functor 机制实现面向对象的回调。此回调 API 基于 C++ 模板,是类型安全的;也就是说,它执行静态类型检查,以强制调用方和被调用方之间具有适当的签名兼容性。因此,与传统的函数指针相比,它使用起来更符合类型安全性,但语法乍一看可能看起来很宏伟。本节旨在引导您完成 Callback 系统,以便您可以在 ns-3 中舒适地使用它。
使用 Callback API
Callback API 相当小,仅提供两种服务:
1. 回调类型声明:一种使用给定签名声明回调类型的方法,以及
2. 回调实例化:一种实例化模板生成的转发回调的方法,该回调可以将任何调用转发到另一个 C++ 类成员方法或 C++ 函数。
将 Callback API 与静态函数一起使用
static double
CbOne(double a, double b)
{
std::cout << "invoke cbOne a=" << a << ", b=" << b << std::endl;
return a;
}
int main(int argc, char *argv[])
{
// return type: double
// first arg type: double
// second arg type: double
Callback<double, double, double> one;
}
这是一个 C 风格的回调示例 – 不包含或不需要 this 指针。函数模板 Callback 实质上是包含指向函数的指针的变量的声明。在上面的示例中,我们显式显示了一个指向函数的指针,该函数返回一个整数并将单个整数作为参数,回调模板函数是该函数的通用版本——它用于声明回调的类型。Callback 模板需要一个 mandatory argument(要分配给此回调的函数的返回类型)和最多 5 个可选参数,每个参数都指定参数的类型(如果您的特定回调函数具有 5 个以上的参数,则可以通过扩展回调实现来处理)。所以在上面的例子中,我们声明了一个名为 “one” 的回调,它最终将保存一个函数指针。它将保存的函数的签名必须返回 double,并且必须支持两个 double 参数。如果尝试传递签名与声明的回调不匹配的函数,则会发生编译错误。此外,如果尝试将不兼容的回调分配给回调,则编译将成功,但将引发运行时NS_FATAL_ERROR。示例程序 src/core/examples/main-callback.cc 在 main() 程序的末尾演示了这两种错误情况。现在,我们需要将此回调实例与实际目标函数 (CbOne) 捆绑在一起。请注意,上面 CbOne 具有与回调相同的函数签名类型——这很重要。我们可以将任何此类正确类型的函数传递给此回调。让我们更仔细地看一下:
static double CbOne(double a, double b) {}
^ ^ ^
| | |
| | |
Callback<double, double, double> one;
只有当函数具有匹配的签名时,您才能将函数绑定到回调。第一个模板参数是返回类型,其他模板参数是函数签名的参数类型。现在,让我们将回调 “one” 绑定到与其签名匹配的函数:
// build callback instance which points to cbOne function
one = MakeCallback(&CbOne);
从本质上讲,对 MakeCallback 的调用是创建上述专用函子之一。使用 Callback 模板函数声明的变量将扮演泛型 functor 的角色。赋值 one
=
MakeCallback(&CbOne) 是将被调用方已知的专用函子转换为调用方已知的泛型函子的强制转换。然后,稍后在程序中,如果需要回调,可以按如下方式使用:
NS_ASSERT(!one.IsNull());
// invoke cbOne function through callback instance
double retOne;
retOne = one(10.0, 20.0);
检查 IsNull() 可确保回调不为 null,即此回调后面有一个可调用的函数。然后,one() 执行泛型 operator(),它实际上被 operator() 的特定实现重载,并返回与直接调用 CbOne() 相同的结果。
将 Callback API 与成员函数一起使用
通常,您不会调用静态函数,而是调用对象的公共成员函数。在这种情况下,MakeCallback 函数需要一个额外的参数,以告知系统应在哪个对象上调用该函数。请考虑以下示例,同样来自 main-callback.cc:
class MyCb {
public:
int CbTwo(double a) {
std::cout << "invoke cbTwo a=" << a << std::endl;
return -5;
}
};
int main()
{
...
// return type: int
// first arg type: double
Callback<int, double> two;
MyCb cb;
// build callback instance which points to MyCb::cbTwo
two = MakeCallback(&MyCb::CbTwo, &cb);
...
}
在这种情况下,当调用 two() 时:
int result = two(1.0);
将导致对 &cb 指向的对象上的 CbTwo 成员函数(方法)的调用。
构建 Null 回调
回调可以为 null;因此,在使用它们之前检查可能是明智的。null 回调有一个特殊的结构,这比简单地将 “0” 作为参数传递更可取;它是 MakeNullCallback<> 构造:
two = MakeNullCallback<int, double>();
NS_ASSERT(two.IsNull());
调用 null 回调就像调用 null 函数指针一样:它会在运行时崩溃。
绑定Callback
就是在创建回调的时候,锁定某些默认参数
如果希望允许客户端函数(提供回调的函数)提供某些参数,该怎么办?Alexandrescu 将允许客户端指定其中一个参数的过程称为 “binding”。operator() 的其中一个参数已被客户端绑定(锁定)。我们的一些 pcap 跟踪代码提供了一个很好的示例。每当收到数据包时,都需要调用一个函数。此函数调用一个对象,该对象实际以 pcap 文件格式将数据包写入磁盘。这些函数之一的签名将是:
static void DefaultSink(Ptr<PcapFileWrapper> file, Ptr<const Packet> p);
static 关键字表示这是一个不需要 this 指针的静态函数,因此它将使用 C 样式的回调。我们不希望调用代码必须知道除 Packet 之外的任何内容。我们希望在调用代码中只是一个如下所示的调用:
m_promiscSnifferTrace(m_currentPkt);
我们要做的是,在创建 Ptr<PcapFileWriter> file 时,将该文件绑定到特定的回调实现,并安排 Callback 的 operator() 免费提供该参数。为此,我们提供了 MakeBoundCallback 模板函数。它采用与 MakeCallback 模板函数相同的参数,但也采用要绑定的参数。在上面的例子中:
MakeBoundCallback(&DefaultSink, file);
将创建一个特定的回调实现,该实现知道要添加额外的绑定参数。从概念上讲,它使用一个或多个绑定参数扩展了上述特定 functor:当创建特定的 functor 时,bound 参数被保存在 functor / callback 对象本身中。当使用单个参数调用 operator() 时,如下所示:
m_promiscSnifferTrace(m_currentPkt);
也可以绑定两个或三个参数。假设我们有一个带有签名的函数:
static void NotifyEvent(Ptr<A> a, Ptr<B> b, MyEventType e);
可以创建绑定回调绑定前两个参数,例如:
MakeBoundCallback(&NotifyEvent, a1, b1);
假设 a1 和 b1 分别是 A 型和 B 型的对象。同样,对于三个参数,一个参数将具有带有签名的函数:
static void NotifyEvent(Ptr<A> a, Ptr<B> b, MyEventType e);
用以下方法绑定三个参数:
MakeBoundCallback(&NotifyEvent, a1, b1, c1);
再次假设 a1、b1 和 c1 分别是 A、B 和 C 类型的对象。
跟踪回调
ns-3 中的回调位置
实现细节
Object model
ns-3 从根本上说是一个 C++ 对象系统。可以根据 C++ 规则照常声明和实例化对象。ns-3 还向传统 C++ 对象添加了一些功能,如下所述,以提供更强大的功能和特性。本手册章节旨在向读者介绍 ns-3 对象模型。本节介绍 ns-3 对象的 C++ 类设计。简而言之,使用的几种设计模式包括经典的面向对象设计(多态接口和实现)、接口和实现分离、非虚拟公共接口设计模式、对象聚合工具以及用于内存管理的引用计数。熟悉 COM 或 Bonobo 等组件模型的人可以在 ns-3 对象聚合模型中识别设计元素,尽管 ns-3 设计并不严格符合任何一个。
面向对象的行为
指出ns3利用了面向对象的抽象 封装 和多态 重载
通常,C++ 对象提供常见的面向对象功能(抽象、封装、继承和多态性),这些功能是经典面向对象设计的一部分。NS-3 对象利用这些属性;例如:
class Address
{
public:
Address();
Address(uint8_t type, const uint8_t *buffer, uint8_t len);
Address(const Address & address);
Address &operator=(const Address &address);
...
private:
uint8_t m_type;
uint8_t m_len;
...
};
Object base classes
ns-3 中使用了三个特殊的基类。从这些基类继承的类可以实例化具有特殊属性的对象。这些基类是:
- class Object
- class ObjectBase
- class SimpleRefCount
从类 Object 派生的类将获得以下属性。
- ns-3 类型和属性系统
- 对象聚合系统
- 智能指针引用计数系统(类 Ptr)
从类 ObjectBase 派生的类将获得上述前两个属性,但不获取智能指针。派生自类 SimpleRefCount 的类:仅获取智能指针引用计数系统。
内存管理和类 Ptr
介绍Ptr如何实现的内存管理,其实用户并不关心这个
C++ 程序中的内存管理是一个复杂的过程,并且经常不正确或不一致地完成。我们已经确定了如下所述的参考计数设计。所有使用引用计数的对象都维护一个内部引用计数,以确定对象何时可以安全地删除自身。每次获取指向接口的指针时,都会通过调用 Ref() 来增加对象的引用计数。完成后,指针的用户有义务显式 Unref() 指针。当引用计数降至零时,将删除该对象。
引用计数智能指针 (Ptr)
此实现允许您像作普通指针一样作智能指针:您可以将其与 0 进行比较,将其与其他指针进行比较,为其分配 0 等。可以使用 GetPointer() 和 PeekPointer() 方法从此智能指针中提取原始指针。如果要将新对象存储到智能指针中,建议使用 CreateObject 模板函数创建对象并将其存储在智能指针中,以避免内存泄漏。这些函数确实是小的便捷函数,它们的目标只是为您节省一点点输入工作。
CreateObject 和 Create
C++ 中的对象可以是静态、动态或自动创建的。这也适用于 ns-3,但系统中的某些对象有一些额外的框架可用。具体来说,引用计数对象通常使用模板化的 Create 或 CreateObject 方法进行分配,如下所示。对于派生自类 Object 的对象:
Ptr<WifiNetDevice> device = CreateObject<WifiNetDevice>();
请不要使用 operator
new 创建此类对象;请改用 CreateObject() 创建它们。对于派生自类 SimpleRefCount 的对象或支持使用智能指针类的其他对象,可以使用模板化帮助程序函数,建议使用:
Ptr<B> b = Create<B>();
这只是正确处理引用计数系统的 operator new 的包装器。总之,如果 B 不是对象,而只是使用引用计数(例如数据包),则使用 Create<B>,如果 B 派生自 ns3::Object,则使用 CreateObject<B>。
Aggregation
所谓聚合,就是把一些类或者配置统一添加到另一个类里面——给Node添加Ipv4协议
ns-3 对象聚合系统的很大一部分动机是认识到 ns-2 的一个常见用例是使用继承和多态性来扩展协议模型。例如,TCP 的专用版本(如 RenoTcpAgent)派生自类 TcpAgent(并覆盖函数)。
示例
聚合示例
Node 是在 ns-3 中使用聚合的一个很好的示例。请注意,ns-3 中没有 Node 的派生类,例如 InternetNode 类。相反,组件 (协议) 被聚合到一个节点。让我们看看如何将一些 IPv4 协议添加到节点中:
static void
AddIpv4Stack(Ptr<Node> node)
{
Ptr<Ipv4L3Protocol> ipv4 = CreateObject<Ipv4L3Protocol>();
ipv4->SetNode(node);
node->AggregateObject(ipv4);
Ptr<Ipv4Impl> ipv4Impl = CreateObject<Ipv4Impl>();
ipv4Impl->SetIpv4(ipv4);
node->AggregateObject(ipv4Impl);
}
请注意,Ipv4 协议是使用 CreateObject() 创建的。然后,它们将聚合到节点。这样,无需编辑 Node 基类即可允许具有基类 Node 指针的用户访问 Ipv4 接口;用户可以在运行时向 Node 请求指向其 Ipv4 接口的指针。用户如何询问节点将在下一小节中介绍。请注意,将多个相同类型的对象聚合到一个 ns3::Object 是一个编程错误。因此,例如,聚合不是存储节点的所有活动套接字的选项。
GetObject 示例
我们可以把ipv4协议聚合到Node上,也可以从Node上拿到ipv4协议的Ptr
GetObject 是一种类型安全的方法,用于实现安全向下转换并允许在对象上找到接口。考虑一个节点指针m_node它指向一个 Node 对象,该对象之前聚合了 IPv4 的实现。客户端代码希望配置默认路由。为此,它必须访问节点中具有 IP 转发配置接口的对象。它执行以下作:
Ptr<Ipv4> ipv4 = m_node->GetObject<Ipv4>();
如果该节点实际上没有聚合到它的 Ipv4 对象,则该方法将返回 null。因此,最好检查此类函数调用的返回值。如果成功,用户现在可以将 Ptr 用于之前聚合到节点的 IPv4 对象。如何使用聚合的另一个示例是向对象添加可选模型。例如,现有 Node 对象可能在运行时将 “Energy Model” 对象聚合到它(无需修改和重新编译 Node 类)。然后,现有模型(例如无线网络设备)可以稍后对能源模型进行“GetObject”,并在接口已内置到底层 Node 对象或在运行时聚合到该对象时执行适当的作。但是,其他节点不需要了解任何有关能量模型的信息。
Object factories
一个常见的用例是创建大量类似配置的对象。可以重复调用 CreateObject(),但在 ns-3 系统中也使用了工厂设计模式。它在 “helper” API 中被大量使用。类 ObjectFactory 可用于实例化对象并配置这些对象的属性:
void SetTypeId(TypeId tid);
void Set(std::string name, const AttributeValue &value);
Ptr<T> Create() const;
第一种方法允许使用 ns-3 TypeId 系统来指定创建的对象的类型。第二个选项允许在要创建的对象上设置属性,第三个选项允许创建对象本身。
ObjectFactory factory;
// Make this factory create objects of type FriisPropagationLossModel
factory.SetTypeId("ns3::FriisPropagationLossModel")
// Make this factory object change a default value of an attribute, for
// subsequently created objects
factory.Set("SystemLoss", DoubleValue(2.0));
// Create one such object
Ptr<Object> object = factory.Create();
factory.Set("SystemLoss", DoubleValue(3.0));
// Create another object with a different SystemLoss
Ptr<Object> object = factory.Create();
Downcasting
一个已经多次出现的问题是,“如果我有一个指向对象的基类指针 (Ptr),并且我想要派生类指针,我应该向下转换(通过 C++ 动态转换)来获取派生指针,还是应该使用对象聚合系统来获取 GetObject<>
() 来查找子类 API 接口的 Ptr?答案是,在许多情况下,这两种技术都有效。ns-3 提供了一个模板化的函数,使 Object 动态转换的语法更加用户友好:
template <typename T1, typename T2>
Ptr<T1>
DynamicCast(Ptr<T2> const&p)
{
return Ptr<T1>(dynamic_cast<T1 *>(PeekPointer(p)));
}
当程序员具有基类型指针并针对子类指针进行测试时,DynamicCast 将起作用。GetObject 在查找聚合的不同对象时起作用,但也与子类一起工作,其方式与 DynamicCast 相同。如果不确定,程序员应该使用 GetObject,因为它在所有情况下都有效。如果程序员知道所考虑对象的类层次结构,则仅使用 DynamicCast 更为直接。