ndnSIM仿真平台使用之自定义功能实现示例

 

本文将提供若干个ndnSIM源码修改示例,实现不同的自定义功能,以进一步介绍ndnSIM仿真平台的使用。

版本信息如下:

操作系统:Ubuntu 16.04
ndnSIM:ndnSIM-2.1
ns-3-dev:ns-3.23-dev-ndnSIM-2.1

添加CS表的删除记录操作Erase()

如前文所述,ndnSIM提供了两种Content Store的实现,其中最常用到的是旧版实现 ns3::Ptr<ns3::ndn::ContentStore> m_csFromNdnSim,在forwarder.cpp中存储数据包时的代码为:

shared_ptr<Data> dataCopyWithoutPacket = make_shared<Data>( data );
dataCopyWithoutPacket->removeTag<ns3::ndn::Ns3PacketTag>();
m_csFromNdnSim->Add( dataCopyWithoutPacket );

而ndnSIM并未直接提供删除数据包的操作m_csFromNdnSim->Erase( dataCopyWithoutPacket ),如果需要用到删除功能,则需修改源码。接下来我们将在介绍如何添加删除功能的同时,介绍ndnSIM中ContentStore的实现。

ContentStore类

旧版路径(本文使用旧版):ndnSIM/model/cs/ndn-content-store.hpp,ndnSIM/model/cs/ndn-content-store.cpp

旧版缓存替换策略路径:ndnSIM/utils/trie

新版路径:ndnSIM/NFD/daemon/table/cs.hpp,ndnSIM/NFD/daemon/table/cs.cpp

新版缓存替换策略路径:ndnSIM/NFD/daemon/table

ContentStore类是CS表的基类,在头文件ndn-content-store.hpp里定义了若干操作如下:

class ContentStore : public Object {
public:
	...
  virtual shared_ptr<Data>
  Lookup(shared_ptr<const Interest> interest) = 0; // 查询是否有兴趣包对应的数据包

  virtual bool
  Add(shared_ptr<const Data> data) = 0; // 存储数据包(存储新记录)

  virtual void
  Print(std::ostream& os) const = 0; // 打印记录

  virtual uint32_t
  GetSize() const = 0; // 获取当前CS表大小

  virtual Ptr<cs::Entry> 
  Begin() = 0;	// 获取当前CS表的第一个记录

  virtual Ptr<cs::Entry>
  End() = 0;	// 获取当前CS表的最后一个记录

  virtual Ptr<cs::Entry> Next(Ptr<cs::Entry>) = 0;	// 获取当前CS表某记录的吓一条记录

  static inline Ptr<ContentStore>
  GetContentStore(Ptr<Object> node); // 获取当前CS表
	...
};

可见原生ContentStore类并未提供删除记录功能,因此我们首先在ndn-content-store.hpp中定义删除记录方法Erase(),此处可以依据兴趣包删除记录,也可以依据数据包删除记录,前者用于forwarder.cpp中兴趣包处理相关方法中(如onIncomingInterest()),后者用于数据包处理相关方法中(如onIncomingData()),我们以后者为例:

class ContentStore : public Object {
public:
	...
  virtual void
  Erase(shared_ptr<const Data> data) = 0;
	...
};

此外,这里使用到了纯虚函数的概念,即虚函数后加上“=0”。虚函数用于使用基类指针调用子类的函数,其既可以在基类中实现,也可以在子类中实现。纯虚函数用于定义接口,在基类中并不实现它,而在子类中必须实现它。其他关于虚函数与纯虚函数的详细用法请自行学习,网络上有大量的相关资料。

ContentStoreImpl类

路径:ndnSIM/model/cs/content-store-impl.hpp,ndnSIM/model/cs/content-store-impl.cpp

ContentStoreImpl类是CS表的基础实现类,且是一个模板类(这里建议先对C++的模板进行了解),它有两个父类,一个是上述的ContentStore类,一个是trie_with_policy类(路径:ndnSIM/utils/trie/trie-with-policy.hpp),后者与各种缓存替换策略有关,如果需要实现自定义的缓存替换策略,trie_with_policy类需要详细学习,这一部分暂时不在本文的介绍范围之内:

template<class Policy>
class ContentStoreImpl
  : public ContentStore, // 父类1
    protected ndnSIM::
      trie_with_policy<Name,
      ndnSIM::smart_pointer_payload_traits<EntryImpl<ContentStoreImpl<Policy>>,Entry>,
      Policy> // 父类2
{
public:
  typedef ndnSIM::
    trie_with_policy<Name, ndnSIM::smart_pointer_payload_traits<EntryImpl<ContentStoreImpl<Policy>>,
                                                                Entry>,
                     Policy> super; // super指代父类2
  ...
  virtual inline shared_ptr<Data>
  Lookup(shared_ptr<const Interest> interest);

  virtual inline bool
  Add(shared_ptr<const Data> data);

  virtual inline void
  Print(std::ostream& os) const;

  virtual uint32_t
  GetSize() const;

  virtual Ptr<Entry>
  Begin();

  virtual Ptr<Entry>
  End();

  virtual Ptr<Entry> Next(Ptr<Entry>);

  const typename super::policy_container&
  GetPolicy() const
  {
    return super::getPolicy();
  }

  typename super::policy_container&
  GetPolicy()
  {
    return super::getPolicy();
  }
  ...
};

可以看出该类体中首先分别定义了两个父类中的方法,其中并没有删除记录的方法,因此我们在此处再次定义Erase()方法:

template<class Policy>
class ContentStoreImpl
  : public ContentStore, // 父类1
    protected ndnSIM::
      trie_with_policy<Name,
      ndnSIM::smart_pointer_payload_traits<EntryImpl<ContentStoreImpl<Policy>>,Entry>,
      Policy> // 父类2
{
public:
  ...
  virtual inline void
  Erase(shared_ptr<const Data> data);
  ...
};

然后在类体外实现Erase()方法:

template<class Policy>
void
ContentStoreImpl<Policy>::Erase(shared_ptr<const Data> data)
{
  NS_LOG_FUNCTION(this << " ERASE " << data->getName());
  super::erase(data->getName());
}

此处需要解释为什么用super::erase(data->getName());,这是因为ContentStoreImpl类的父类1并未实现该方法,而父类2中有实现,因此我们在这里直接调用父类2中的erase()方法即可。

使用

在forwarder.cpp中需要删除操作的地方加入以下代码即可:

m_csFromNdnSim->Erase( data );

比如我们在onIncomingData()中作如下修改,先添加记录再删除记录然后再添加记录,此代码并无实际意义,仅用作功能展示:

void
Forwarder::onIncomingData(Face& inFace, const Data& data)
{
  ...
  shared_ptr<Data> dataCopyWithoutPacket = make_shared<Data>(data);
  dataCopyWithoutPacket->removeTag<ns3::ndn::Ns3PacketTag>();

  // CS insert
  if (m_csFromNdnSim == nullptr)
    m_cs.insert(*dataCopyWithoutPacket);
  else{
    m_csFromNdnSim->Add(dataCopyWithoutPacket);
    m_csFromNdnSim->Erase(dataCopyWithoutPacket);
    std::cout<<" erase:"<< dataCopyWithoutPacket->getName() <<std::endl; 
    m_csFromNdnSim->Add(dataCopyWithoutPacket);
  }
  ...
}

然后我们运行ndn-simple.cpp,发现报错:

image

可以看出问题出在content-store-nocache.hpp与content-store-nocache.cpp中,且与纯虚函数有关。我们打开content-store-nocache.hpp,发现其父类也是ContentStore类:

class Nocache : public ContentStore {
  ...
}

而我们在ContentStore类中新定义的纯虚函数Erase()必须在子类中实现,以上我们仅在其一个子类ContentStoreImpl类中实现,并未在子类Nocache类中实现。因此我们接下来只需要在Nocache类中实现该纯虚函数即可。

在content-store-nocache.hpp中添加:

class Nocache : public ContentStore {
public:
  ...
  virtual bool
  Add(shared_ptr<const Data> data);

  virtual void
  Erase(shared_ptr<const Data> data);
  ...
};

在content-store-nocache.cpp中添加:

...
void
Nocache::Erase(shared_ptr<const Data> data)
{
}
...

然后运行ndn-simple.cpp,发现可以正常运行,同一个内容名称连续打印四次是因为在该ndn-simple.cpp中我们设置了四个串联节点。

image

路由节点创建与转发兴趣包(信令)

NDN网络中兴趣包一般是由用户创建并发出,本节将介绍如何在路由节点上创建并发出兴趣包。这一功能与在兴趣包内添加特定字段,可以实现路由节点与服务器互相发送信令的功能,主要涉及的源码为forwarder.cpp。

我们在此假定一个场景,路由节点收到一个数据包后,向服务器发送一个同名兴趣包(即信令,可以是通知服务器当前节点已经缓存了该数据包,也可以是其他通知信息)。该信令可以通过在兴趣包中添加signal字段实现,signal=1时表示该兴趣包是由路由节点发出的信令,沿途的路由节点不需要对此兴趣包做出响应,也就是说沿途路由节点收到该兴趣包后直接转发而不需要查询CS表和存入PIT表。

关于ndnSIM中路由节点转发兴趣包的机制已经在ndnSIM仿真平台使用之转发Forwarder类中的onOutgoingInterest()部分详细介绍,接下来我们对其作简要回顾,基于此实现信令转发功能。

创建与发送兴趣包(信令)

首先我们先查看用户(ndn-consumer.cpp)是如何创建兴趣包的,这一部分的详细介绍可以参考ndnSIM仿真平台使用之用户Consumer类,在此仅简要介绍:

void
Consumer::SendPacket()
{
  ...
  // 生成兴趣包名称
  shared_ptr<Name> nameWithSequence = make_shared<Name>(m_interestName);
  nameWithSequence->appendSequenceNumber(seq);
  // 创建一个空的兴趣包
  shared_ptr<Interest> interest = make_shared<Interest>();
  // 设置一个随机Nonce值
  interest->setNonce(m_rand->GetValue(0, std::numeric_limits<uint32_t>::max()));
  // 设置兴趣包名称
  interest->setName(*nameWithSequence);
  // 设置过期时间,由于假定场景下兴趣包不需要在PIT中聚合,因此这一部分在下文中可以不添加
  time::milliseconds interestLifeTime(m_interestLifeTime.GetMilliSeconds());
  interest->setInterestLifetime(interestLifeTime);
  ...
}

因此,若在路由节点上创建兴趣包,我们需要首先创建一个空的兴趣包,然后分别设置其nonce值和名称即可。根据假定场景,在forwarder.cpp的onIncomingData()方法中缓存数据包的代码后添加创建兴趣包代码:

void
Forwarder::onIncomingData(Face& inFace, const Data& data)
{
  ...
  shared_ptr<Data> dataCopyWithoutPacket = make_shared<Data>(data);
  dataCopyWithoutPacket->removeTag<ns3::ndn::Ns3PacketTag>();

  // CS insert
  if (m_csFromNdnSim == nullptr)
    m_cs.insert(*dataCopyWithoutPacket);
  else{
    m_csFromNdnSim->Add(dataCopyWithoutPacket);
    // add here
    // 创建一个空的兴趣包
    shared_ptr<Interest> interest = make_shared<Interest>();
    // 设置兴趣包名称,与对应数据包同名
    interest->setName( data.getName() );
    // 假设此时已经在兴趣包中添加了新的字段signal,
    // 添加方法见"ndnSIM仿真平台使用之在兴趣包与数据包内添加标签(字段)"
    interest->setSignal( 1 ); 
    // 设置nonce值,此部分参考源码onOutgoingInterest()方法中设置新的兴趣包部分,见下一个代码段
    static boost::random::uniform_int_distribution<uint32_t> dist;
    interest->setNonce( dist( getGlobalRng() ) );
    // 在端口inFace上发出兴趣包,inFace为对应数据包的入端口,从该端口入,也从该端口出
    inFace.sendInterest( *interest );
  }
  ...
}

在forwarder.cpp的onOutgoingInterest方法中已经给出如何在Forwarder类中设置新的nonce值:

void Forwarder::onOutgoingInterest( shared_ptr<pit::Entry> pitEntry,
                                    Face &outFace, bool wantNewNonce ) {
  ...
  if ( wantNewNonce ) {
    interest = make_shared<Interest>( *interest );
    static boost::random::uniform_int_distribution<uint32_t> dist;
    interest->setNonce( dist( getGlobalRng() ) );
  }
  ...
}

转发兴趣包(信令)

NDN网络中路由节点收到兴趣包的一般流程是先检查PIT表中是否有记录,如果有则更新该记录并进一步转发该兴趣包,如果没有,则查询CS表中是否有对应数据包,若有则返回数据包,若没有则创建一条新的PIT记录并进一步转发该兴趣包。在本文所假定场景中,该兴趣包为一个信令,路由节点直接转发即可,不需要查询PIT表与CS表。

ndnSIM仿真平台使用之转发Forwarder类中我们已经介绍了ndnSIM转发兴趣包的机制:当兴趣包未命中缓存CS表时,触发onContentStoreMiss()函数,在该函数中分别更新PIT记录与查询FIB记录,然后通过dispatchToStrategy()函数选择路由策略(基类为Strategy,ndnSIM/NFD/daemon/fw/strategy.hpp,传入的参数包括PIT记录、FIB记录、兴趣包本身和兴趣包转入端口),交由路由策略的afterReceiveInterest()函数处理,在此函数中得到兴趣包的转出端口outFace再调用Strategy类的sendInterest()函数发出兴趣包,然后兴趣包进入转出通道onOutgoingInterest()

综上我们可以看出,ndnSIM的路由节点转发兴趣包时必须选择一个路由策略以得到兴趣包的转出端口outFace,这一步需要PIT记录,在进入onOutgoingInterest()通道时同样也需要PIT记录。然而对于信令而言,我们不需要其在PIT表中留下记录,但我们需要兴趣包的转出端口outFace。因此,这一矛盾使得我们在设计信令的转发方式时不能跳过PIT表。

我们首先看一下涉及兴趣包转发的各函数的传入参数是什么,然后采用逆向推导的方式,理清兴趣包转发的前后参数依赖。

  • onContentStoreMiss()(Forwarder类的方法): 兴趣包传入端口,PIT记录,兴趣包
const Face& inFace, shared_ptr<pit::Entry> pitEntry, const Interest& interest
  • dispatchToStrategy()(Forwarder类的方法):PIT记录,一个触发函数(如&Strategy::afterReceiveInterest
Forwarder::dispatchToStrategy(shared_ptr<pit::Entry> pitEntry, Function trigger)
  • afterReceiveInterest()(Strategy类子类实现的方法):兴趣包传入端口、兴趣包、FIB记录、PIT记录
const Face& inFace, const Interest& interest, shared_ptr<fib::Entry> fibEntry, shared_ptr<pit::Entry> pitEntry
  • sendInterest()(Strategy类父类实现的方法):PIT记录,兴趣包转出端口,是否设置新的nonce的布尔值
shared_ptr<pit::Entry> pitEntry, shared_ptr<Face> outFace, bool wantNewNonce
  • onOutgoingInterest()(Forwarder类的方法):PIT记录,兴趣包转出端口,是否设置新的nonce的布尔值
shared_ptr<pit::Entry> pitEntry, Face &outFace, bool wantNewNonce
  • sendInterest()(端口类的一个方法):兴趣包
const Interest& interest

所以我们需要知道兴趣包的转出端口outFace,然后使用

outFace.sendInterest( *interest )

outFace是在afterReceiveInterest()函数中获得的,因此接下来我们看一下该函数的内部实现,以Strategy类的一个子类BestRouteStrategy类为例:

void
BestRouteStrategy::afterReceiveInterest(const Face& inFace,
                   const Interest& interest,
                   shared_ptr<fib::Entry> fibEntry,
                   shared_ptr<pit::Entry> pitEntry)
{
  if (pitEntry->hasUnexpiredOutRecords()) {
    // not a new Interest, don't forward
    return;
  }

  const fib::NextHopList& nexthops = fibEntry->getNextHops(); // 下一跳列表
  fib::NextHopList::const_iterator it = std::find_if(nexthops.begin(), nexthops.end(),
    bind(&predicate_PitEntry_canForwardTo_NextHop, pitEntry, _1));// 下一跳列表中的一个下一跳

  if (it == nexthops.end()) {
    this->rejectPendingInterest(pitEntry);
    return;
  }

  shared_ptr<Face> outFace = it->getFace(); // 该下一跳的端口
  this->sendInterest(pitEntry, outFace);
}

由上可见,afterReceiveInterest()函数中获得outFace的步骤是(1)通过FIB记录获取下一跳列表;(2)从下一条跳列表中选择一个下一跳;(3)取该下一跳的端口为outFace

因此,我们需要知FIB记录,而FIB记录是在onContentStoreMiss()函数中获取的,继而我们再回顾一下onContentStoreMiss()函数(在“ndnSIM仿真平台使用之转发Forwarder类”中已有介绍,但没有涉及到获取FIB记录的细节):

void
Forwarder::onContentStoreMiss(const Face& inFace,
                              shared_ptr<pit::Entry> pitEntry,
                              const Interest& interest)
{
  NFD_LOG_DEBUG("onContentStoreMiss interest=" << interest.getName());
  shared_ptr<Face> face = const_pointer_cast<Face>(inFace.shared_from_this());
  pitEntry->insertOrUpdateInRecord(face, interest);
  this->setUnsatisfyTimer(pitEntry);
  shared_ptr<fib::Entry> fibEntry = m_fib.findLongestPrefixMatch(*pitEntry); // 获取FIB记录
  this->dispatchToStrategy(pitEntry, bind(&Strategy::afterReceiveInterest, _1,
                                          cref(inFace), cref(interest), 
                                          fibEntry, pitEntry));
}

由上可见FIB记录fibEntry是通过findLongestPrefixMatch()函数获取的,所以我们再看一下findLongestPrefixMatch()函数的内部实现:

shared_ptr<fib::Entry>
Fib::findLongestPrefixMatch(const pit::Entry& pitEntry) const
{
  shared_ptr<name_tree::Entry> nameTreeEntry = m_nameTree.get(pitEntry);
  BOOST_ASSERT(static_cast<bool>(nameTreeEntry));
  return findLongestPrefixMatch(nameTreeEntry);
}

由上可见fibEntry是一个命名树记录,由命名树通过get()方法得到,我们再看一下get()方法的内部实现:

inline shared_ptr<name_tree::Entry>
NameTree::get(const pit::Entry& pitEntry) const
{
  return pitEntry.m_nameTreeEntry;
}

该实现虽然比较简单,直接返回pitEntry的一个m_nameTreeEntry属性,但给我们一个很重要的提示,当我们为了转发信令而创建一个PIT记录pitEntry时,需要为其绑定一个m_nameTreeEntry属性!

此处需要额外说明的是,ndnSIM原始获取pitEntry的方式是将兴趣包插入PIT表中,然后返回一个pitEntry,但对于信令而言,我们不需要它存入PIT表中,因此也无法通过插入返回的方式获得pitEntry,因此需要新创建。

现在问题来到如何绑定m_nameTreeEntry属性,我们查看nameTreeEntry所属类的实现,其中有一个向命名树中插入一条pitEntry的方法insertPitEntry()

void
Entry::insertPitEntry(shared_ptr<pit::Entry> pitEntry)
{
  BOOST_ASSERT(static_cast<bool>(pitEntry));
  BOOST_ASSERT(!static_cast<bool>(pitEntry->m_nameTreeEntry));

  m_pitEntries.push_back(pitEntry);
  pitEntry->m_nameTreeEntry = this->shared_from_this();
}

insertPitEntry()实现了双向绑定,命名树中绑定(插入)一个pitEntry,而pitEntry绑定该命名树。由此我们只需要实现单向绑定即可(因为信令的pitEntry不需要插入到命名树中),记该单向绑定函数为bindPitEntry(),在/ndnSIM/NFD/daemon/table/name-tree-entry.hpp中申明:

class Entry : public enable_shared_from_this<Entry>, noncopyable
{
public:
  ...
  void
  bindPitEntry(shared_ptr<pit::Entry> pitEntry);
  ...
}

在/ndnSIM/NFD/daemon/table/name-tree-entry.cpp中实现:

void
Entry::bindPitEntry(shared_ptr<pit::Entry> pitEntry)
{
  BOOST_ASSERT(static_cast<bool>(pitEntry));
  BOOST_ASSERT(!static_cast<bool>(pitEntry->m_nameTreeEntry));
  pitEntry->m_nameTreeEntry = this->shared_from_this();
}

由于m_nameTreeEntry是私有属性,直接使用pitEntry->m_nameTreeEntry=nameTreeEntry->shared_from_this()将会报错。

现在我们已经理清了兴趣包的处理机制与参数依赖,在forwarder.cpp的onIncomingInterest()方法中实现转发信令如下:

if ( interest.getSignal() == 1 ) {
  // 信令直接转发,不经过PIT表
  NFD_LOG_DEBUG( "forward a signal" );
  shared_ptr<pit::Entry> pitEntry = make_shared<pit::Entry>( interest ); // 创建一个PIT记录
  shared_ptr<name_tree::Entry> nameTreeEntry =
    m_pit.getNameTree().lookup( interest.getName() ); // 找到该兴趣包对应的命名树
  nameTreeEntry->bindPitEntry( pitEntry ); // 绑定命名树记录到PIT记录

  shared_ptr<fib::Entry> fibEntry = 
    m_fib.findLongestPrefixMatch( pitEntry->getName() ); // 查找FIB记录

  const fib::NextHopList &nexthops = fibEntry->getNextHops(); // 获取下一跳列表

  shared_ptr<Face> outFace = nexthops.begin()->getFace();// 获取下一跳列表中的一个下一跳的端口
  outFace->sendInterest( interest ); // 在改端口上发出信令
 }else{...}

最后,我们有可能会想到另外一种转发信令不经过PIT表的实现:先按正常流程处理信令,当转出信令后,在删除其留下的PIT记录,即:

void Forwarder::onContentStoreMiss( const Face &           inFace,
                                    shared_ptr<pit::Entry> pitEntry,
                                    const Interest &       interest ) {
  ...
  // dispatch to strategy
  this->dispatchToStrategy( pitEntry, bind( &Strategy::afterReceiveInterest, _1,
                                            cref( inFace ), cref( interest ),
                                            fibEntry, pitEntry ) );
  if ( interest.getSignal() == 1 ) {
    // 判断为信令后删除对应PIT记录
    m_pit.erase( pitEntry );
  }
}

此种方式是不对的,因为这个PIT记录中不一定只包含该信令,可能也包含该信令到达前聚合的同名兴趣包,而删除后,这些聚合的兴趣包将无法得到响应。