文章目录
前言
前面的文章ndnSIM学习(四)——examples之ndn-simple.cpp超详细剖析中,我们剖析了 ndn-simple.cpp 的每一行代码的大致功能。在这篇文章中,我将跳每一行代码的子函数里,对于每个子函数进行更加细致地剖析,用于分析每一行代码的工作机理。
源代码
把代码还是再复制一遍
// ndn-simple.cpp
#include "ns3/core-module.h"
#include "ns3/network-module.h"
#include "ns3/point-to-point-module.h"
#include "ns3/ndnSIM-module.h"
namespace ns3 {
int
main(int argc, char* argv[])
{
// setting default parameters for PointToPoint links and channels
Config::SetDefault("ns3::PointToPointNetDevice::DataRate", StringValue("1Mbps"));
Config::SetDefault("ns3::PointToPointChannel::Delay", StringValue("10ms"));
Config::SetDefault("ns3::QueueBase::MaxSize", StringValue("20p"));
// Read optional command-line parameters (e.g., enable visualizer with ./waf --run=<> --visualize
CommandLine cmd;
cmd.Parse(argc, argv);
// Creating nodes
NodeContainer nodes;
nodes.Create(3);
// Connecting nodes using two links
PointToPointHelper p2p;
p2p.Install(nodes.Get(0), nodes.Get(1));
p2p.Install(nodes.Get(1), nodes.Get(2));
// Install NDN stack on all nodes
ndn::StackHelper ndnHelper;
ndnHelper.SetDefaultRoutes(true);
ndnHelper.InstallAll();
// Choosing forwarding strategy
ndn::StrategyChoiceHelper::InstallAll("/prefix", "/localhost/nfd/strategy/multicast");
// Installing applications
// Consumer
ndn::AppHelper consumerHelper("ns3::ndn::ConsumerCbr");
// Consumer will request /prefix/0, /prefix/1, ...
consumerHelper.SetPrefix("/prefix");
consumerHelper.SetAttribute("Frequency", StringValue("10")); // 10 interests a second
auto apps = consumerHelper.Install(nodes.Get(0)); // first node
apps.Stop(Seconds(10.0)); // stop the consumer app at 10 seconds mark
// Producer
ndn::AppHelper producerHelper("ns3::ndn::Producer");
// Producer will reply to all requests starting with /prefix
producerHelper.SetPrefix("/prefix");
producerHelper.SetAttribute("PayloadSize", StringValue("1024"));
producerHelper.Install(nodes.Get(2)); // last node
Simulator::Stop(Seconds(20.0));
Simulator::Run();
Simulator::Destroy();
return 0;
}
} // namespace ns3
int
main(int argc, char* argv[])
{
return ns3::main(argc, argv);
}
0、先聊聊Simulator
整个仿真的核心就在这个 Simulator 上了。其实总的来说,我们可以这么认为:前面的代码是在给 Simulator 配置一些 Config 或者 Event ,到了最后一起执行。
那么以 Simulator::Run() 为例,它是怎么实现的呢?点开一看,瞬间就惊了。好家伙,这家伙搁着玩踢皮球呢,把任务推给了 GetImpl() 返回的 SimulatorImpl * 了。
void
Simulator::Run (void)
{
NS_LOG_FUNCTION_NOARGS ();
Time::ClearMarkedTimes ();
GetImpl ()->Run ();
}
那么 SimulatorImpl 里是啥玩意?不看不知道,一看吓一跳。我超!几乎全是纯虚函数。
class SimulatorImpl : public Object
{
public:
static TypeId GetTypeId (void);
virtual void Destroy () = 0;
virtual bool IsFinished (void) const = 0;
virtual void Stop (void) = 0;
virtual void Stop (const Time &delay) = 0;
virtual EventId Schedule (const Time &delay, EventImpl *event) = 0;
virtual void ScheduleWithContext (uint32_t context, const Time &delay, EventImpl *event) = 0;
virtual EventId ScheduleNow (EventImpl *event) = 0;
virtual EventId ScheduleDestroy (EventImpl *event) = 0;
virtual void Remove (const EventId &id) = 0;
virtual void Cancel (const EventId &id) = 0;
virtual bool IsExpired (const EventId &id) const = 0;
virtual void Run (void) = 0;
virtual Time Now (void) const = 0;
virtual Time GetDelayLeft (const EventId &id) const = 0;
virtual Time GetMaximumSimulationTime (void) const = 0;
virtual void SetScheduler (ObjectFactory schedulerFactory) = 0;
virtual uint32_t GetSystemId () const = 0;
virtual uint32_t GetContext (void) const = 0;
virtual uint64_t GetEventCount (void) const = 0;
};
既然是纯虚函数,也就是说这个 Simulator 一定是派生类的对象,只是被强制转化为了纯虚基类 SimulatorImpl 。那么,这究竟是那一个派生类呢?经过我的侦察,发现 GetImpl() 里的 g_simTypeImpl 有玄机:
static GlobalValue g_simTypeImpl = GlobalValue
("SimulatorImplementationType",
"The object class to use as the simulator implementation",
StringValue ("ns3::DefaultSimulatorImpl"),
MakeStringChecker ());
说白了,我们以后看到 Simulator 就可以脑补为 DefaultSimulatorImpl 了。这个文件的位置在 ~/ndnSIM/ns-3/src/core/model/default-simulator-impl.cc 。
至于 Run() 函数,看一看里面的实现就明白了,就是一直执行 ProcessOneEvent ,这个函数对每个 m_events 的 next 事件执行 next.impl->Invoke() 。
void
DefaultSimulatorImpl::Run (void)
{
NS_LOG_FUNCTION (this);
// Set the current threadId as the main threadId
m_main = SystemThread::Self();
ProcessEventsWithContext ();
m_stop = false;
while (!m_events->IsEmpty () && !m_stop)
{
ProcessOneEvent ();
}
NS_ASSERT (!m_events->IsEmpty () || m_unscheduledEvents == 0);
}
而事件是由谁添加的呢?其实是由 ScheduleWithContext 函数添加的。
void
DefaultSimulatorImpl::ScheduleWithContext (uint32_t context, Time const &delay, EventImpl *event)
{
NS_LOG_FUNCTION (this << context << delay.GetTimeStep () << event);
if (SystemThread::Equals (m_main))
{
Time tAbsolute = delay + TimeStep (m_currentTs);
Scheduler::Event ev;
ev.impl = event;
ev.key.m_ts = (uint64_t) tAbsolute.GetTimeStep ();
ev.key.m_context = context;
ev.key.m_uid = m_uid;
m_uid++;
m_unscheduledEvents++;
m_events->Insert (ev);
}
else
{
EventWithContext ev;
ev.context = context;
// Current time added in ProcessEventsWithContext()
ev.timestamp = delay.GetTimeStep ();
ev.event = event;
{
CriticalSection cs (m_eventsWithContextMutex);
m_eventsWithContext.push_back(ev);
m_eventsWithContextEmpty = false;
}
}
}
1、Config::SetDefault
代码如下
Config::SetDefault("ns3::PointToPointNetDevice::DataRate", StringValue("1Mbps"));
首先,第一行代码从字面意思就能看出来:是用于配置点对点设备的数据速率为 1Mbps 。那么它具体是如何工作的呢?
总的来说,就是把第一个参数按照最后一个 :: 进行切割,得到 "tidName = ns3::PointToPointNetDevice" 和 paramName = "DataRate" 。而 ns3::IidManager 类的 m_namemap 里,储存了 tidName 到 uid 的映射,每个 uid 唯一表示了一个内容,所以我们就找到的这个待操作的的类。对于这个类,我们只需要遍历它的 attributes ,如果匹配且输入值合法,就可以执行 SetAttributeInitialValue 函数。
为了与 SetDefault 对应,每个类都配备有相应的 GetTypeId 函数。以 Producer 类为例,我们可以通过 "ns3::ndn::Producer" 来找到这个类,并且可以访问、初始化这个类的所有被 AddAttribute 的变量。
TypeId
Producer::GetTypeId(void)
{
static TypeId tid =
TypeId("ns3::ndn::Producer")
.SetGroupName("Ndn")
.SetParent<App>()
.AddConstructor<Producer>()
.AddAttribute("Prefix", "Prefix, for which producer has the data", StringValue("/"),
MakeNameAccessor(&Producer::m_prefix), MakeNameChecker())
.AddAttribute(
"Postfix",
"Postfix that is added to the output data (e.g., for adding producer-uniqueness)",
StringValue("/"), MakeNameAccessor(&Producer::m_postfix), MakeNameChecker())
.AddAttribute("PayloadSize", "Virtual payload size for Content packets", UintegerValue(1024),
MakeUintegerAccessor(&Producer::m_virtualPayloadSize),
MakeUintegerChecker<uint32_t>())
.AddAttribute("Freshness", "Freshness of data packets, if 0, then unlimited freshness",
TimeValue(Seconds(0)), MakeTimeAccessor(&Producer::m_freshness),
MakeTimeChecker())
.AddAttribute(
"Signature",
"Fake signature, 0 valid signature (default), other values application-specific",
UintegerValue(0), MakeUintegerAccessor(&Producer::m_signature),
MakeUintegerChecker<uint32_t>())
.AddAttribute("KeyLocator",
"Name to be used for key locator. If root, then key locator is not used",
NameValue(), MakeNameAccessor(&Producer::m_keyLocator), MakeNameChecker());
return tid;
}
2、CommandLine::Parse
这个其实就是解析 cmd 的参数,首先按照分割符进行 Split ,对于其中的参数用 HandleOption 或者 HandleNonOption 进行解析。
举个例子 --abc=efg 或者 -abc=efg ,用 HandleOption 解析出来就是 name="abc" , value="efg" ,然后执行 HandleArgument(name, value) 。 --vis 解析出来就是 name="vis" , value="" 。
3、NodeContainer::Create
这个就是创建 N N N 个节点,看代码一目了然——
void
NodeContainer::Create (uint32_t n)
{
for (uint32_t i = 0; i < n; i++)
{
m_nodes.push_back (CreateObject<Node> ());
}
}
4、PointToPointHelper
这玩意还挺有意思的,它的构造函数是
PointToPointHelper::PointToPointHelper ()
{
m_queueFactory.SetTypeId ("ns3::DropTailQueue<Packet>");
m_deviceFactory.SetTypeId ("ns3::PointToPointNetDevice");
m_channelFactory.SetTypeId ("ns3::PointToPointChannel");
m_remoteChannelFactory.SetTypeId ("ns3::PointToPointRemoteChannel");
}
这个类是点对点的助手类,比如 Install 函数可以执行节点之间网络拓扑的安装。以代码 p2p.Install(nodes.Get(0), nodes.Get(1)); 为例进行分析。
该函数给两个节点分配了具有唯一 MAC 地址的 PointToPointNetDevice 对象进行管理,并为它们创建信道、传数据包的队列 Queue<Packet> 。
5、ndn::StackHelper
这个构造函数也挺好玩的,配置了一堆策略。其中 PointToPointNetDeviceCallback 很有意思,它用于配置点对点网络对象,创建了 GenericLinkService 和 NetDeviceTransport 的链路层、传输层管理器,并创建了一个 Face 来管理它们,后续的所有发包和收包都靠它。
StackHelper::StackHelper()
: m_isForwarderStatusManagerDisabled(false)
, m_isStrategyChoiceManagerDisabled(false)
, m_needSetDefaultRoutes(false)
{
setCustomNdnCxxClocks();
m_csPolicies.insert({"nfd::cs::lru", [] { return make_unique<nfd::cs::LruPolicy>(); }});
m_csPolicies.insert({"nfd::cs::priority_fifo", [] () { return make_unique<nfd::cs::PriorityFifoPolicy>(); }});
m_csPolicyCreationFunc = m_csPolicies["nfd::cs::lru"];
m_ndnFactory.SetTypeId("ns3::ndn::L3Protocol");
m_netDeviceCallbacks.push_back(
std::make_pair(PointToPointNetDevice::GetTypeId(),
MakeCallback(&StackHelper::PointToPointNetDeviceCallback, this)));
// default callback will be fired if non of others callbacks fit or did the job
}
主要是 ndnHelper.InstallAll(); 这个函数,它用 Simulator::ScheduleWithContext 函数为每一个 Node 都在 Simulator 里添加了一个 StackHelper::doInstall 事件。
而 doInstall 则是让 Node 聚合 L3Protocol ,并为 Node 执行 createAndRegisterFace ,调用了 PointToPointNetDeviceCallback 创建了一个管理 GenericLinkService 和 NetDeviceTransport 的接口 Face 。
6、ndn::StrategyChoiceHelper::InstallAll
ndn::StrategyChoiceHelper::InstallAll("/prefix", "/localhost/nfd/strategy/multicast"); 这一行代码,就是对 NodeContainer 里的每一个 Node 都执行一次 StrategyChoiceHelper::Install 。
void
StrategyChoiceHelper::InstallAll(const Name& namePrefix, const Name& strategy)
{
Install(NodeContainer::GetGlobal(), namePrefix, strategy);
}
其实内容其实很简单,就是把 StrategyChoiceHelper::sendCommand 添加到仿真器的事件,和上面的 ndn::StackHelper 分析方法是类似的,这里还是贴出源代码
void
StrategyChoiceHelper::Install(Ptr<Node> node, const Name& namePrefix, const Name& strategy)
{
ControlParameters parameters;
parameters.setName(namePrefix);
NS_LOG_DEBUG("Node ID: " << node->GetId() << " with forwarding strategy " << strategy);
parameters.setStrategy(strategy);
Simulator::ScheduleWithContext(node->GetId(), Seconds(0),
&StrategyChoiceHelper::sendCommand, parameters, node);
StackHelper::ProcessWarmupEvents();
}
其中 sendCommand 函数的作用是为当前节点添加 parameters 。
7、ndn::AppHelper
生产者和消费者都是由 ndn::AppHelper 来管理。
// Consumer
ndn::AppHelper consumerHelper("ns3::ndn::ConsumerCbr");
// Consumer will request /prefix/0, /prefix/1, ...
consumerHelper.SetPrefix("/prefix");
consumerHelper.SetAttribute("Frequency", StringValue("10")); // 10 interests a second
auto apps = consumerHelper.Install(nodes.Get(0)); // first node
apps.Stop(Seconds(10.0)); // stop the consumer app at 10 seconds mark
// Producer
ndn::AppHelper producerHelper("ns3::ndn::Producer");
// Producer will reply to all requests starting with /prefix
producerHelper.SetPrefix("/prefix");
producerHelper.SetAttribute("PayloadSize", StringValue("1024"));
producerHelper.Install(nodes.Get(2)); // last node
ndn::AppHelper 函数的构造函数是将参数字符串作为 TypeId 。
AppHelper::AppHelper(const std::string& app)
{
m_factory.SetTypeId(app);
}
例如这里 consumerHelper 就是设置 TypeId = "ns3::ndn::ConsumerCbr" ,前缀为 "/prefix" ,频率为 "10" ,将这些设置都存在 m_factory 里。
然后把这个 consumerHelper 的 m_factory 安装到 nodes.Get(0) 上,这个安装函数也是调用了 Simulator 的 ScheduleWithContext 函数设置了 Application::Initialize 事件,该事件对 Node0 进行初始化配置,从而使得 Node0 被配置为 ConsumerCbr 。
当然, Producer 的配置方法也是同理。总之就是用了一个AppHelper设置好配置信息,将配置事件塞到事件队列里,等到仿真开始时执行。
8、ns3::Simulator
说白了,前面的操作都是一堆配置。配置完干什么?当然是塞到仿真器的事件列表里,等 Simulator::Run() 啊!
Simulator::Stop(Seconds(20.0))是设置了20s后的事件,这个事件令m_stop = false。Simulator::Run()是执行m_eventsWithContext和m_events里的所有事件,直到事件空了,或者达到m_stop了。(m_eventsWithContext可以理解为m_event的缓冲队列,真正执行的是m_event,然后每次都从m_eventsWithContext抓出一个塞到m_event里)Simulator::Destroy()是执行m_destroyEvents里的所有事件。
void
DefaultSimulatorImpl::Run (void)
{
NS_LOG_FUNCTION (this);
// Set the current threadId as the main threadId
m_main = SystemThread::Self();
ProcessEventsWithContext ();
m_stop = false;
while (!m_events->IsEmpty () && !m_stop)
{
ProcessOneEvent ();
}
// If the simulator stopped naturally by lack of events, make a
// consistency test to check that we didn't lose any events along the way.
NS_ASSERT (!m_events->IsEmpty () || m_unscheduledEvents == 0);
}
总结与分析
总结一下整个 ndn-simple.cpp 的仿真流程,我们可以总结出整个 ns3 软件的仿真过程。
首先 ns3 的仿真流程由仿真器 Simulator 所控制,而代码的总体流程则是分为:
- 设置配置选项
- 将事件配置到仿真器的事件队列中
- 使用仿真器执行事件队列中的事件
具体而言——
- 其中首先要有基础配置、用
NodeContainer执行节点的创建,这些(特别是节点本身)是后面网络拓扑、策略配置的基础。 - 然后用
PointToPointHelper执行网络拓扑的构建,用ndn::StackHelper执行路由链路层、传输层接口的安装,用ndn::StrategyChoiceHelper执行转发策略配置。 - 接着用
ndn::AppHelper执行生产者和消费者的配置。这里要注意,我们不是创建了一个生产者或者消费者,而是将原本就存在的节点设置为生产者或者消费者。 - 最后,用
Simulator执行仿真。其中Simulator调用了SimulatorImpl类的实现,而这是纯虚类,其中真正调用的是DefaultSimulatorImpl类。
在这一节中,我们理解到了:ns3的仿真器是靠事件在推动仿真的进行。下一节中我们将剖析生产者与消费者,理解从消费者发出 interest 到生产者返回 data 这一路上,生产者和消费者们都做了些什么。
——examples之ndn-simple.cpp每个函数逐行剖析&spm=1001.2101.3001.5002&articleId=120921845&d=1&t=3&u=d3298ca324b84edc866e8d8e5114dd79)
3766

被折叠的 条评论
为什么被折叠?



