NDN代码分析(3) forwarder模块代码
相比起之前的代码, forwarder.cpp
是一个大工程,头铁硬碰显然是不合适的。在看代码之前,我们应当对 Forwarder
类大致在干什么有一些初步的了解。实际上, Forwarder
类主要控制前向转发过程,我们可以把整个 Forwarder
类看作一个贴心的管家,通过 nameTree
管理手下的 fib 、 pit 、 cs
,还通过 m_faceTable
管理了手下的所有的 face 接口,并同时管理着其余的各项东西。
既然是是一个管家,那么涉及的内容肯定是盘根错节的。但幸运的是,我们能在官网上查到NFD的开发者手册,该手册截至目前最新的版本为第11版,下载链接为NFD Developer’s Guide (Latest Revision 11)
在本手册的第四章,对于转发部分有着详细的讲解.
整体转发工作流程
引用手册里的图,整个转发流程其实就很清晰了。整个转发分为三个大板块: onIncomingInterest
、 onIncomingData
、 onIncomingNack
。这里我对这些流程大方向进行一些简单的梳理:
- 如果来了兴趣包,执行
onIncomingInterest
- 如果重复的
Nonce
,则说明发生了循环,执行onInterestLoop
- 如果CS缓存未命中,则执行
onContentStoreMiss
函数,执行相应的策略 - 如果CS缓存命中,则执行
onContentStoreHit
函数,执行相应的策略 - 无论CS命中还是CS非命中,只要最后是决定要转发,都会执行
onOutgoingInterest
- 如果重复的
- 如果来了数据包,执行
onIncomingData
- 如果查PIT查不到请求,那么数据包是不请自来的,执行
onDataUnsolicited
- 查有几个匹配的PIT条目,对所有条目的下游执行
onOutgoingData
- 如果查PIT查不到请求,那么数据包是不请自来的,执行
部分名词说明:
- in-record和out-record我没有进行翻译。
- downstream我翻译为了“下游”
- upstream我翻译为了“上游”
- 其中in-record代表interest的下游接口,是内容的请求者。out-record代表interest的上游接口,是潜在的内容源。interest从下游到上游,data从上游到下游。
onIncomingInterest工作流程
触发方式:
Forwarder::onIncomingInterest
函数从Forwarder::startProcessInterest
方法进入,该方法由接口Face::afterReceiveData
信号触发。
引用手册里的图,转发流程如下图所示。对于整个流程,简要地说:
- 首先以
/localhost
为前缀是本地通信保留的,所以以这个为前缀的将会被直接扔掉。 - 然后检测当前随机数有没有在死亡随机数列表里,如果在,说明兴趣包循环了,执行
onInterestLoop
- 再看有没有到生产者的区域,如果到了,就可以剥掉转发前缀了。
- 接着查PIT(虽然我们在ndn原理里认为先查CS再查PIT,然而查CS开销可能会很大。所以可以先查PIT,如果PIT里有人请求过了,说明该兴趣如今还未决,CS肯定没有,就可以节省开销了),看看PIT里有没有重复随机数。如果没有就取消掉PIT的到期定时器,为PIT续1s
- 最后查CS(其实先查CS再查PIT是等价的,只是先查PIT的话,如果PIT有就不用查CS了,节省开销),决定执行
onContentStoreHit
或者onContentStoreMiss
。
onInterestLoop工作流程
上面 onIncomingInterest
说了,如果检测到相同的随机数,就说明发生了循环,这时就会执行 onInterestLoop
函数。这个时候分两种情况讨论:
- 如果是点对点,那么发生循环相当于Not ACK,于是将原因标记为Nack。
- 如果是多播的话,直接丢弃就行,不需要Nack。
onContentStoreHit工作流程
如果缓存命中,就设置好到期定时器,并执行相应的 afterContentStoreHit
策略,如下图所示:
此管道在 Forwarder::onContentStoreHit
方法中实现,并在执行内容存储查找并找到匹配项后进入传入的兴趣管道。 此管道的输入参数包括兴趣包、传入的 Face、PIT 条目和匹配的数据包。如图 9 所示,此管道首先将到期计时器设置为现在在 Interest 上,然后在 Content Store 命中所选策略的触发器后调用。
onContentStoreMiss工作流程
如果缓存未命中,就给对应条目的PIT插入一个in-record,到期时间设置为最后一个in-record的时间,默认是4秒。如果兴趣包内钦定好了下一跳的接口id,那就对该接口执行 onOutgoingInterest
,不然就执行策略 afterReceiveInterest
,通过FIB路由表查出下一跳(如果查到并决定转发,那么对查到的下一跳执行 onOutgoingInterest
)。若PIT in-record到期计时器到期,则执行 Interest Finalize 管道(第 4.2.6 节)。如下图所示:
onOutgoingInterest工作流程
首先插入一个out-record(如果已经存在相同的接口就更新),记录下最后一个包的随机数和到期时间,然后执行 sendInterest
函数。如下图所示:
onInterestFinalize工作流程
一些善后工作,如插入死亡随机数列表、删除对应的PIT条目等。
onIncomingData工作流程
触发方式:
Forwarder::onIncomingData
函数从Forwarder::startProcessData
方法进入,该方法由接口Face::afterReceiveData
信号触发。
简要地说:
- 首先以
/localhost
为前缀是本地通信保留的,所以以这个为前缀的将会被直接扔掉。 - 然后查PIT,如果PIT没有,但来了这个数据包,说明数据包是不请自来的(unsolicited)。如果PIT有,那么尝试将数据包插入CS。
- 再检测有几个匹配的PIT条目(一般来说应该只有一个,因为多个PIT意味着多个转发策略,可能导致转发策略之间的冲突)
- 如果单个匹配,将PIT到期时间设置为现在,并调用
afterReceiveData
策略,标记该PIT条目已被满足,删掉out-record,尝试插入死亡随机数列表。 - 如果多个匹配,对于每个匹配项目,管道会先记住其pending的下一跳们(最后汇总一起执行
onOutgoingData
),其余操作与单个匹配类似,只不过变成了调用beforeSatisfyInterest
策略,并清除条目的out-record。
- 如果单个匹配,将PIT到期时间设置为现在,并调用
onDataUnsolicited工作流程
如果数据是不请自来的,则根据 m_unsolicitedDataPolicy
的决定来判断。如果认为应当缓存就尝试缓存,否则啥都不干。
onOutgoingData工作流程
首先检查下 /localhost
,(然后下个版本计划进行流量管理,这个版本还没实现,)最后通过 sendData
函数传出去。
onIncomingNack函数
Nack(Not ACK)在NFD里会分为两部分处理: Incoming Nack
和 Outgoing Nack
。这部分讲的是Incoming Nack( processing of incoming Nacks)。
触发方式:
Forwarder::onIncomingNack
函数从Forwarder::startProcessNack
方法进入,该方法由接口Face::afterReceiveNack
信号触发。
- 首先,如果传入接口不是点对点的,那么直接丢弃(因为Nack只在点对点链路上被定义)。
- 如果和out-record的最后一个记录的随机数相同,相当于产生环路,那么该record将会被标记为Nack。如果所有out-record都Nack了,将到期时间设置到期时间为0,并执行策略
afterReceiveNack
。 - 如果随机数不同,那么就不执行操作。
onOutgoingNack函数
触发方式:
Forwarder::onOutgoingNack
函数从Strategy::sendNack
方法进入。
- 首先,查in-record,如果查不到就不执行操作(不知道哪个interest的问题,不知道给谁发Nack)
- 如果下一跳不是点对点的,那么直接丢弃(因为Nack只在点对点链路上被定义)。
- 如果没毛病的话,就给in-record的interest贴上Nack,通过传出接口让链路层执行
sendNack
前置补充内容:信号类
在进入代码分析之前,我想先向大家介绍一下信号(Signal)类。为什么要介绍它呢?因为它在整个 Forwarder
类、甚至整个ndnSIM代码里都是大量出现的。因此,我们单独开了一个小章节来讲解信号类。其实信号类很简单,也很好理解,说白了,一个信号里面储存了一堆操作(函数),每当这个信号被触发的时候,这些操作就会被执行一遍。或者对于基础好一点的同学来说:一个信号里面储存了函数指针链表,并且将运算符 ()
重载为了执行函数指针链表内的所有函数:
- 具体而言,信号类的定义中最重要的部分如下所示。信号类重载了
()
,使得信号类的调用方式和函数一致。而且Owner
作为了友元类,可以随意获取信号的私有成员。每当一个函数被connect
到信号上,就会被添加到m_slot
这个链表中。
1 | template<typename Owner, typename ...TArgs> |
信号类有什么好处呢?信号类可以让一个类调用另一个类的方法,比如 Today
这个函数就只用关心相对抽象层次高的内容,比如每天只需要干 BeforeDoSomething
、 Dosomething
、 AfterDoSomething
,至于具体 BeforeDoSomething
是什么,可以由其他类来往里面添加(connect)事项。这样就可以用一个“管家”方便地管理下面的子类了。
forwarder.hpp
从整个 forwarder.hpp
代码中的 Forwarder
类,结合前面的信号类,我们能够看到整个 forwarder.cpp
想做的事情。总的来说,就是 startProcessInterest
、 startProcessData
、 startProcessNack
这三个函数衍生出来一串函数,并且单独为FIB搞了个 startProcessNewNextHop
,用于添加新的路由。
1 | class Forwarder |
forwarder.cpp的构造函数
整个构造函数主要是在连接各种信号,具体而言,干了这些事——
- 给
m_faceTable
添加保留一个接口(face
)contentstore://
(旧版本0.27代码没有这一行) - 让
m_faceTable
的afterAdd
信号装配操作(使得每次新添加的face
的各信号能连接到对应的Forwarder
的函数):- 让每个
face.afterReceiveInterest
连接到this->startProcessInterest
(处理兴趣包) - 让每个
face.afterReceiveData
连接到this->startProcessData
(处理数据包) - 让每个
face.afterReceiveNack
连接到this->startProcessNack
(处理Nack) - 让每个
face.onDroppedInterest
连接到this->onDroppedInterest
(往FIB添加新路由时)
- 让每个
- 给
m_faceTable
的beforeRemove
信号装配操作:cleanupOnFaceRemoval
(每次移除face
前先cleanup
) - 给
m_fib
的afterNewNextHop
信号装配操作:this->startProcessNewNextHop
- 设置
m_strategyChoice
为默认的fw::BestRouteStrategy2
1 | static NamegetDefaultStrategyName() |
回顾下 forwarder.hpp
的函数的18~25行,构造函数里信号连接的 startProcessXXX
函数转到了 Forwarder
的 onIncomingXXX
了。于是让我们再来关注这几个函数
1 | void startProcessInterest(const FaceEndpoint& ingress, const Interest& interest) |
前三个函数就是手册里讲的那一长串了。
onIncomingInterest函数及其后续函数
onIncomingInterest函数
触发方式:
Forwarder::onIncomingInterest
函数从Forwarder::startProcessInterest
方法进入,该方法由接口Face::afterReceiveData
信号触发。
1 | void |
onInterestLoop函数
onInterestLoop
函数就是执行一下 interest
包陷入循环后的善后操作
1 | void |
onContentStoreMiss函数
如果CS中没有,执行 onContentStoreMiss
函数,给PIT加上输入记录,更新到期时间。
然后看看 interest
包里有没有钦定的下一跳路由的标签(如 ndn-simple.cpp
的例子就是没有),如果有,就让钦定好的下一跳路由进行 onOutgoingInterest
。如果找不到就根据 pitEntry
的状态决定是否转发(如果要转发,就是根据FIB的查出最佳的下一跳,然后执行对应的 onOutgoingInterest
函数)。
1 | void |
onContentStoreHit函数
CS缓存命中将会触发,不过以 ndn-simple.cpp
为例,因为包里面一个Payload的 1024B
里是随机的垃圾数据,所以基本上不可能出现 CS
命中。
1 | void |
onOutgoingInterest函数
如果决定转发 interest
,则会调用本函数
1 | void |
onInterestFinalize函数
一些善后工作
1 | void |
onIncomingData函数及其后续函数
onIncomingData函数
拉来了 data
包后,执行该函数,给每个in-record都发包。
1 | void |
onDataUnsolicited函数
如果数据是不请自来的,执行本函数。看看对于不请自来的数据的策略是啥,如果策略是缓存,那么就尝试插入CS。否则啥都不干。
1 | void |
onOutgoingData函数
做完一些检查工作后,准备发data包
1 | void |
onIncomingNack函数
如果Incoming时Nack了,则执行该函数
1 | void |
onOutgoingNack函数
如果Outgoing时Nack了,则执行该函数
1 | void |
总的来说, Forwarder
类的功能很清晰,主体就是处理interest、data、Nack这三个玩意,其余具体的函数不过是它的实现罢了。比如处理interest,每次发包都要查PIT、CS,找下一跳查FIB。这也是 Forwarder
要管理NameTree的原因,因为转发功能就要用到这三个玩意,而它们通用的接口则是由FaceTable统一管理。具体的转发策略则是由 Strategy
及其派生类实现,具体的内部操作就要看CS、PIT、FIB里面的实现了,它们对于Forwarder而言只暴露出了接口,相当于透明的。
而 onIncomingInterest
、 onIncomingData
、 onIncomingNack
函数则是由信号 Face::afterReceiveInterest
、 Face::afterReceiveData
、 Face::afterReceiveNack
触发,这也降低了各模块的耦合程度。
由 Forwarder
类发散开, Strategy
是具体的转发策略, Cs
、 Pit
、 Fib
是三个基本功能的实现, Producer
和 Consumer
是生产者和消费者, Interest
和 Data
是兴趣包和数据包的类型。这也呼应了ndnSIM学习(三)——ndnSIM源码阅读计划为什么要看这些内容了。