ns-3和NDN代码分析(1) ndn-simple
代码分析
首先先从最简单的 ~/ndnSIM/ns-3/src/ndnSIM/examples/ndn-simple.cpp 来切入,对整个ndnSIM的框架进行分析。
1 | // ndn-simple.cpp |
头文件
作用
引入头文件。前3个是ns-3的文件,最后是ndnSIM的文件。源代码如下——
1 |
详细解析
首先先看前面 include 的文件,在文章ndnSIM学习(二)——配置VScode的跨文件转到定义中提到,这些文件的地址是相对于 ~/ndnSIM/ns-3/build/ns-3 的
,里面的内容都是清一色的 #include ,这里面每个头文件又包括多个头文件。
这里面include的库中,前3个都是ns-3软件的函数,最后一个才是ndnSIM自己的,里面就只有一行 #include "ndn-all.hpp" ,这里面include了ndnSIM的所有头文件。
1.配置网络初始参数
作用
配置网络的初始参数,如网络数据传输速率、信道时延、队列最大容量。源代码如下——
1 | // setting default parameters for PointToPoint links and channels |
详细解析
从字面意思我们就能看出, Config::SetDefault 函数的作用是配置一些网络初始参数,其作用相当于ns-3的 SetAttribute 函数。具体而言,就是配置 DataRate=1Mbps 、 Delay=10ms 、 MaxSize=20p ,相当于——
- 点对点数据传输速率为 10^6 bit/s(此处
1M=10^6,不是1024*1024) - 点对点路由延迟为 10 ms ,相当于每一跳到下一跳的延迟都是固定的 10 ms。
- 队列最多容纳 20 p,相当于最多有20个包。
注:
- 其中用到了
Config::SetDefault函数,这是ns-3的函数,关于该函数,可以参考链接https://www.nsnam.org/doxygen/group__config.html#ga2e7882df849d8ba4aaad31c934c40c06- 关于
ns3::PointToPoint类,可以参考链接https://blog.csdn.net/loongkingwhat/article/details/53455691- 关于
ns3:QueueBase类,可以参考链接 https://www.nsnam.org/doxygen/classns3_1_1_queue_base.html。
2.命令行参数解析
作用
解析命令行参数,例如在命令行里使用 --vis 参数就可以启动可视化。源代码如下——
1 | // Read optional command-line parameters (e.g., enable visualizer with ./waf --run=<> --visualize |
3.创建3个网络节点
作用
创建3个网络节点。源代码如下——
1 | // Creating nodes |
详细解析
看起来没什么好说了,毕竟短短两行代码,但这里涉及一些ns-3的语法需要特别指出一下。
NodeContainer 是装有一个指向 Node 类的智能指针的 vector 容器,具体而言,其中的成员为
1 | class NodeContainer |
std::vector 大家都很熟悉,就是 vector 容器。这里值得一提的是, Ptr 类是ns-3自己实现的智能指针类,可能是作者想要强制大家代码规范,将内存交给智能指针管理,以防内存泄漏吧。
至于 Create 函数,我们一看源代码里面的实现,马上就能明白了。传的参数 n 就代表往 vector 容器里 push_back n 个 Node 嘛。
1 | // node-container.cc |
4.定义网络拓扑结构
作用
定义了网络的拓扑结构为 Node0->Node1->Node2 。源代码如下——
1 | // Connecting nodes using two links |
详细解析
创建了一个 PointToPointHelper 对象来协助管理整个网络,通过 point-to-pint-helper.h 文件可以看出, Install (Ptr<Node> a, Ptr<Node> b) 函数的作用在于连接两个 Node ,相当于定义拓扑关系。
node.Get(2) 操作相当于取出 node 这个容器的第2个元素,类比到数组上, node.Get(2)==node[2] 。
5.将所有节点添加到ndn Stack里
作用
将所有节点添加到ndn Stack里。源代码如下——
1 | // Install NDN stack on all nodes |
详细解析
ndnHelper.SetDefaultRoutes(true) 代表FIB(Forwarding Information Base)启动默认路由, ndnHelper.InstallAll() 代表将所有节点都添加到ndn Stack里,这个 InstallAll 里面内容很多,我只能大致介绍下思路,因为我自己都快被绕晕了。
InstallAll 其实执行的是 Install(NodeContainer::GetGlobal()) ,取出了前面创建的 NodeContainer 存的全部节点,为每个节点安排 doInstall 事件,该事件将节点与 L3Protocol 聚合,并为与该节点相关的 Device 注册 Face 接口。
6.设置转发策略
作用
设置Consumer前缀为 /prefix ,并设置ndn的转发策略为 multicast 。源代码如下——
1 | // Choosing forwarding strategy |
详细解析
"/prefix" 参数好像是让Consumer的请求前缀变成 /prefix 。
重点在于第二个参数 "/localhost/nfd/strategy/multicast" 上,经过我的反复测试与查找后,终于知道了:这个转发策略对应的文件是 ~/newndnSIM/ns-3/src/ndnSIM/NFD/daemon/fw/multicast-strategy.hpp 和~/newndnSIM/ns-3/src/ndnSIM/NFD/daemon/fw/multicast-strategy.cpp 。
其中 class MulticastStrategy 公有继承了 Strategy 类以及另一个类。从 class MulticastStrategy 构造函数的实现中来看,继承的 Strategy 类的构造函数传的参数正是 Forwarder 类的对象。代码胜千言,说这么多还不如来一段代码——
1 | // multicast-strategy.hpp |
也就是说,我们想要了解转发策略的代码,就应当看懂 Forwarder 类,也就是 ~/ndnSIM/ns-3/src/ndnSIM/NFD/daemon/fw/forwarder.* 。
7.Consumer设置
作用
设置Consumer(消费者)的信息,包括设置 TypeId 、 Prefix 、 Frequency 、指定哪个节点作为Consumer、请求持续时长。源代码如下——
1 | // Installing applications |
详细解析
1 | ndn::AppHelper consumerHelper("ns3::ndn::ConsumerCbr"); |
创建 AppHelper 对象,设置 TypeId 为 "ns3::ndn::ConsumerCbr"
1 | consumerHelper.SetPrefix("/prefix"); |
设置请求前缀 Prefix 为 /prefix
1 | consumerHelper.SetAttribute("Frequency", StringValue("10")); |
设置属性 "Frequency" 为 10 (每秒发10个兴趣包)
1 | auto apps = consumerHelper.Install(nodes.Get(0)); |
将 Node0 设置为Consumer(消费者)
1 | apps.Stop(Seconds(10.0)); |
该消费者持续请求10s后停止
8.Producer设置
作用
设置Producer(生产者)的信息,包括设置 TypeId 、 Prefix 、 PayloadSize 、指定哪个节点作为Producer。源代码如下——
1 | // Producer |
详细解析
1 | ndn::AppHelper producerHelper("ns3::ndn::Producer"); |
创建 AppHelper 对象,设置 TypeId 为 "ns3::ndn::Producer"
1 | producerHelper.SetPrefix("/prefix"); |
设置回应前缀 Prefix 为 /prefix
1 | producerHelper.SetAttribute("PayloadSize", StringValue("1024")); |
设置属性 "PayloadSize" 为 1024 (数据包大小为1024Bytes=8192bit)
1 | producerHelper.Install(nodes.Get(2)); |
将 Node2 设置为Producer(生产者)
9.开始执行仿真
作用
设置仿真时长为20s,并执行仿真,。源代码如下——
1 | Simulator::Stop(Seconds(20.0)); |
如何运行代码
根据 ndn-simple.cpp 注释的提示,我们执行如下命令行
1 | cd ~/ndnSIM/ns-3 |
其中 NS_LOG=ndn.Consumer:ndn.Producer 代表我们希望程序输出 ndn.Consumer 和 ndn.Producer 的信息,所有可以被输出显示的 NDN 相关内容如下表所示——
1 | ndn-cxx.ndn.Face=0 |
结果分析
这个代码里面有一处我个人怀疑是BUG的地方,就是: ~/ndnSIM/ns-3/src/ndnSIM/apps/ndn-consumer.cpp 的 Consumer::OnData 函数和 ~/newndnSIM/ns-3/src/ndnSIM/apps/ndn-producer.cpp 的 Producer::OnInterest 函数的 NS_LOG_FUNCTION 输出居然是指针形式。
所以我个人把 NS_LOG_FUNCTION(this << data); 改成了 NS_LOG_FUNCTION(this << *data); ,把 NS_LOG_FUNCTION(this << interest); 改成了 NS_LOG_FUNCTION(this << *interest); ,这样修改后输出日志的可读性大幅提高。
完整输出很长,我们并不需要全看,因为绝大多数都是循环的。实际上,整个输出信息分为准备工作->循环部分0->循环部分1->…->循环部分99->结束程序。为了将输出日志变得更加可读,我手动加了注释分段,让程序变得更加清晰。
1 | # 第一部分:准备工作 |
1.准备工作和结束工作
先看 0s 时的准备工作的输出,整个输出格式是 时间 对象Id 执行的函数 。
1 | # 第一部分:准备工作 |
前两行(不含注释)是定义 Node0=Consumer 和 Node2=Producer ,第三行是开始整个应用程序仿真,第四行是结束程序。中间则是循环部分。这就是整个运行程序的框架。
2.循环体部分
可以看到,循环部分 n nn 对应的时间片段为 [ n , n + 1 ) × 0.1 s \left[n, n+1\right)\times0.1\mathrm{s}[n, n+1)×0.1s
1 | # 循环部分0:[0s, 0.1s) |
兴趣包经过2跳路由到达生产者处,用时 20.56ms ,相当于每一跳都是 10.28ms 。考虑到固定时延为 10ms ,数据速率为 Mbps ,说明兴趣包传输的信息量为 0.28ms*1Mbps=35Byte 。
到达其中 Interest0 的 Name = "/prefix/%FE%00" (共 2+(2+6 + 2+2)byte ,其中是 2 是对应地 Type+Length ,外层是一个 Interest 标志,内层是两个 GenericNameComponent 标志),随机数 Nonce 是32bits的数(共 2+4byte ),生存时间 Lifetime=2000 (共 2+2byte ),报头tlv 2byte ,一共有 26byte 。那么消失的 9byte 去哪里了呢?我也不知道,懒得测了,有空再说吧。有可能在数据链路层打了 9byte 的标签之类的。
数据包每跳时延则是 (57.664 - 20.56)/2=19.052ms ,其中减去固定的 10ms 后,算出数据长度为 8.552ms*1Mbps=1069byte ,其中 SignatureValue 里占了 2+1byte , SignatureInfo 里占了 2+3byte , Content 里占了 4+1024byte , MetaInfo 里占了 2byte , Name 里占了 2+12byte ,报头tlv 4byte ,一共有 1056byte 。那么消失的 13byte 去哪里了呢?我也不知道,懒得测了,有空再说吧。有可能在数据链路层打了 13byte 的标签之类的。
顺带一提测试方法,添加头文件 #include "ns3/core-module.h" ,然后在你想要显示的地方敲一个 NS_LOG_UNCOND("Total length=" << totalLength); 。