AODV代码实现详解——原理与源码分析(一)

首先来几个标准参考:
RFC 3561
RFC 3561 中文翻译
一个博客
挺好的另一个博客
事件?
字段长度?

事件驱动
各种定时器
状态转移图?

AODV协议

基本概念

AODV(Ad hoc On-Demand Distance Vector)是一种基于距离向量路由算法的自适应路由协议,用于无线自组织网络(MANET)中的路由选择。AODV 协议的实现流程如下:

  1. 节点广播 RREQ(Route Request)消息:当节点需要发送数据包到某个目的地时,它会广播一个 RREQ 消息来查找到达目的地的路由。这个 RREQ 消息包含源节点地址、目的节点地址和一个唯一的序列号,以防止消息循环。

  2. 节点响应 RREQ 消息:当一个节点收到 RREQ 消息时,它会检查自己的路由表,如果没有到达目的地的路由,则向其邻居节点转发 RREQ 消息。如果节点已经有到达目的地的路由,则向源节点发送 RREP(Route Reply)消息。

  3. 节点广播 RREP 消息:当目的节点收到 RREQ 消息时,它会向源节点发送 RREP 消息,其中包含到达源节点的最短路径。每个节点在转发 RREP 消息之前都会将其缓存起来,以便以后使用。

  4. 节点维护路由表:每个节点都会维护一个路由表,用于存储到达其他节点的路由信息。每当一个节点收到一个 RREP 消息时,它会更新自己的路由表并向其邻居节点广播更新的路由信息。

  5. 节点周期性发送 HELLO 消息:节点会周期性地发送 HELLO 消息以检测邻居节点是否仍然存在。如果一个节点连续几次未收到邻居节点的 HELLO 消息,则认为该节点已经离线,并更新其路由表。

AODV 协议的主要优点是其适应性和效率。由于 AODV 协议仅在需要时才会建立路由,因此它可以有效地减少网络中的控制流量。此外,AODV 协议具有自适应性,可以根据网络拓扑和流量负载动态调整路由。

AODV 协议的核心思想是通过按需路由方式来减少网络中的路由开销。当源节点需要向目的节点发送数据时,它会向周围节点发出路由请求(RREQ)。如果某个节点知道如何到达目的节点,它会向源节点发出路由响应(RREP)。源节点将使用路由响应中的信息来向目的节点发送数据。在这个过程中,每个节点会维护一张路由表,记录到达目的节点的最短路径和下一跳节点。

AODV 协议的一些实现细节:
AODV代码实现详解——原理与源码分析(一)_第1张图片

  1. 路由请求(RREQ):
    当源节点需要向目的节点发送数据时,它会向周围节点发出路由请求(RREQ)。每个节点将检查自己的路由表来查找到达目的节点的最短路径。如果找到了,节点将向源节点发送路由响应,否则节点将广播路由请求。

路由请求包括 RREQ ID、目的 ID、目的序列号、源 ID、源序列号、跳数和洪泛标识。洪泛标识用于避免重复转发路由请求,每个节点只能转发一次具有相同 RREQ ID 的路由请求。

  1. 路由响应(RREP):
    如果某个节点知道如何到达目的节点,它会向源节点发出路由响应(RREP)。路由响应包含源节点的地址、目的节点的地址、目的节点的序列号和到达目的节点的最短路径。源节点将使用路由响应中的信息来向目的节点发送数据。

路由响应包括目的 ID、目的序列号、源 ID、序列号、跳数和路径。路径描述了到达目的节点的最短路径,包括每个节点的地址和序列号。

  1. 路由维护:
    每个节点都会维护一张路由表,记录到达目的节点的最短路径和下一跳节点。节点还会周期性地发送心跳(Hello)消息来保持与周围节点的连接。如果某个节点无法与下一跳节点通信,它会向源节点发送路由错误(RERR)消息,通知源节点重新计算路径。

  2. 序列号:
    为了防止路由环路和路由不稳定,AODV 协议使用序列号来标识每个节点和每个路由。每个节点都具有唯一的序列号,每个路由的序列号递增。当节点收到路由响应时,它将比较目的节点的序列号和自己的路由表中的序列号,如果目的节点的序列号更大,则更新路由表中的信息。

如何解决环路问题?

AODV协议使用了两种方法来避免路由环路的问题,分别是序列号和反向路径检查。

  1. 序列号:
    AODV协议引入了序列号的概念,用于标识每个节点和每个路由。每个节点都具有唯一的序列号,每个路由的序列号递增。当一个节点收到路由请求或路由响应时,它会比较消息中的序列号和自己的路由表中的序列号。如果消息中的序列号较大,则说明该消息是更新的,节点会更新自己的路由表;如果消息中的序列号较小,则说明该消息已经过期,节点会丢弃该消息。

  2. 反向路径检查:
    当一个节点转发路由请求时,它会将自己的地址添加到请求的路由记录中。当一个节点收到路由响应时,它会检查响应中的路由记录,以确定响应是否经过了自己。如果响应经过了自己,则说明存在路由环路,节点会丢弃该响应。

通过使用反向路径检查,AODV协议可以及时发现路由环路,避免数据包在环路中循环。另外,如果某个节点收到了一个来自下一跳节点的数据包,但是该数据包不是该节点向下一跳节点发送的,则说明存在路由环路,该节点会向源节点发送路由错误消息,通知源节点重新计算路径。

代码整体框架

如此长的代码,必须提纲挈领地先了解基本架构,再进行具体地逐函数的了解

主要功能

  1. 报文类的定义(RREQ、RREP、RERR等),用于封装和解析AODV控制报文。

  2. 路由表和寻路队列的管理,用于维护和查找路由信息。

  3. 定时器的实现,用于路由发现、路由维护等定时功能。

  4. 控制报文(如RREQ、RREP、RERR等)的创建、转发和处理。

  5. Hello报文的生成和处理,用于邻居发现和链路检测。

  6. 本地链路管理,用于检测和响应链路断开。

  7. 数据包转发和路由错误处理。

  8. 事件驱动的架构,通过事件队列实现异步处理。

  9. 参数配置,支持启用不同的机制如本地链路修复、扩展环搜索等。

  10. 统计信息收集,用于计算延迟、跳数等性能指标。

  11. 模块接口定义,与外部协议交互。

主要函数与数据结构大致信息

1. 整体理解

报文类有:BaseMessage、RREQ、RREP、RERR、MyBusiness这些类实现了各种AODV控制报文和业务报文的封装,包含报文的定义、解析和组装功能。具体而言:

  • BaseMessage是基类,定义了报文的公共字段如类型、源地址、目的地址等。
  • RREQ类封装了路由请求报文,包含跳数、请求id等AODV特有字段。
  • RREP类封装了路由回复报文。
  • RERR类封装了路由错误报文。
  • MyBusiness类封装了业务数据报文。
    以上这些类通过操作报文字段实现了报文的组装和解析功能。

2. 事件类 MyEvent

封装了一个事件,包含事件处理函数、触发时间、数据等。主要用于事件驱动框架。

// 抽象事件的构造函数
MyEvent::MyEvent(event_func_t f, void* d) :
    time(-1), handler(f), data(d), id(-1) {}

bool MyEvent::operator<(const MyEvent& t) const
{
    if (this->time < t.time) {
        return true;
    }
    return t.time == this->time && this->id < t.id;
}

bool MyEvent::operator==(const MyEvent& t)const
{
    return t.time == this->time && t.handler == this->handler && t.data == this->data;
}

定义了一个名为MyEvent的类,该类表示一个抽象事件。该类包含一个构造函数和两个运算符重载。
构造函数MyEvent接受两个参数:一个是函数指针,表示事件d 处理函数;另一个是void指针,可以存储事件处理函数需要的数据。构造函数还初始化了类的一些成员变量,包括时间戳、事件处理函数、事件数据和事件ID。

运算符重载了小于号(<)和等于号(==)。其中,小于号根据事件的时间戳和ID比较事件的先后顺序,用于在事件队列中排序;等于号判断两个事件对象是否相等,如果它们的时间戳、事件处理函数和事件数据都相等,则认为它们相等。这两个是利用了C++面向对象的重载特性,方便了后续对事件队列的操作。

3. 路由表类rt_table_t

保存一条路由表项,包含目的地址、下一跳、跳数等信息。

rt_table_t::rt_table_t(int dest_addr, int next, int hops, unsigned int seqno,
    bool _state, int _flags) :
    dest(dest_addr), nx_hop(next), hcnt(hops), dest_seq(seqno),
    //lifetime(-1), 
    state(_state), flags(_flags),
    rt_timer_timeout(nullptr, nullptr),
    hello_timer_timeout(nullptr, nullptr) {}
  • dest:目的地址
  • nx_hop:下一跳地址
  • hcnt:跳数
  • dest_seq:目的地址的序列号
  • state:状态标志
  • flags:路由表项的标志
  • rt_timer_timeout:指向路由表定时器超时处理函数的函数指针和函数参数的指针
  • hello_timer_timeout:指向hello定时器超时处理函数的函数指针和函数参数的指针

4. 寻路队列类seek_list_t

保存一个寻路请求相关信息,包含目的地址、序列号、寻路计时器等。

5. 事件队列相关函数

  • eq_insert/eq_erase/eq_set_time/eq_reset_time 用于向事件队列中添加/删除/设置/重置事件。
  • eq_pop 执行队首事件的处理函数;eq_front_time() 返回队首事件的发生时间
  • msg_delete 用于释放报文占用的内存。
  • AodvOutputBuf_push/OutputBuf_push/clear 向发送/接收缓冲区添加业务;或者清空缓冲区。
  • packet_queue_clear/ packet_queue_add 数据包发送缓存队列,清空或者添加消息。packet_queue_set_verdict 对队列里所有包的处理工作,因为当某些包可以发送的时候还是要及时把它发出去的。这个函数比较重要,需要仔细看看
packet_queue_set_verdict

函数设置一个对队列里所有包的处理工作。接受三个参数:

  • dest:目标地址
  • verdict:处理结果,可以是PQ_SENDPQ_DROP,分别表示发送或丢弃该数据包
  • curtime:当前时间,用于计算数据包在队列中的等待时间
    局部变量,包括:
  • count:计数器,用于记录处理了多少个数据包
  • pkt_queue_size:队列长度,用于遍历整个队列
  • rt:指向目标地址对应的路由表项的指针,如果没有对应的路由表项则为nullptr
    函数的主要功能是遍历一个数据包队列,将其中等待目标地址的数据包进行处理,如为他们查找路由等。具体实现如下:
    首先判断如果要发送数据包却没有对应的路由表项,则返回-1表示异常。

然后循环遍历整个数据包队列,从队列头开始处理每个数据包。对于每个数据包,首先判断它在队列中等待的时间是否超过了最大等待时间,如果超过了则释放该数据包的内存,并继续处理下一个数据包。

如果这个数据包不是等待目标地址的数据包,则将其放回队列中,并继续处理下一个数据包。

如果这个数据包是等待目标地址的数据包,且处理结果为丢弃,则释放该数据包的内存,并继续处理下一个数据包。

如果这个数据包是等待目标地址的数据包,且处理结果为发送,则根据目标地址查找路由表,找到下一跳地址,并设置该数据包的源地址为本节点ID,下一跳地址为路由表中找到的地址。如果该数据包是最后一个数据包(即没有下一跳地址),则更新本节点的最后转发数据包时间。如果没有使用二层协议,则释放该数据包的内存;如果使用了二层协议,则将该数据包放入输出缓冲区中。

最后,如果目标地址对应的路由表项存在且有效,并且处理结果为发送,则更新该路由表项和下一跳路由表项的超时时间。
函数返回值是处理了多少个数据包。

6. 定时器相关函数

route_discovery_timeout

处理路由发现请求超时的定时器的回调函数。实际上是实现了协议的部分功能,有些定时器与状态机混用的隐患。

函数输入参数:

  • arg:指向seek_list_t类型结构的指针,该结构记录了路由请求的相关信息
  • curtime:当前时间

函数的主要功能是处理路由请求的超时情况。如果路由请求还未达到最大重试次数,则重新发送路由请求,并更新超时时间。如果路由请求达到了最大重试次数,则表示无法找到目标节点的路由,从seek_list中删除该请求,并尝试使用本地修复机制进行修复。

局部变量:

  • seek_entry:指向seek_list_t类型结构的指针,该结构记录了路由请求的相关信息
  • rt:指向目标节点的路由表项的指针
  • repair_rt:指向要修复的路由表项的指针
  • flooding_flage:泛洪标志,用于区分路由请求和路由响应消息
  • ttl:路由请求消息的TTL值

首先判断seek_entry是否为空,如果为空则直接返回。然后从seek_entry中获取TTL值和重试次数,如果重试次数还未达到最大值,则根据不同的情况更新TTL和超时时间,并重新发送路由请求消息。如果重试次数已经达到最大值,则无法找到目标节点的路由,从seek_list中删除该请求,并尝试使用本地修复机制进行修复。

在重新发送路由请求消息之前,如果目标节点的路由表项存在且其删除时间与当前时间的差小于两倍的网络遍历时间,则更新该路由表项的删除时间。

最后,如果seek_entry已经达到最大重试次数,则从seek_list中删除该请求,并尝试使用本地修复机制进行修复。在尝试本地修复之前,先查找目标节点的路由表项,如果存在并且其状态为需要修复,则调用local_repair_timeout函数进行本地修复。

route_expire_timeout

处理路由表项过期的定时器的回调函数。

  • arg:指向rt_table_t类型结构的指针,该结构记录了路由表项的信息
  • curtime:当前时间

函数的主要功能是处理路由表项的过期情况。如果该路由表项只有一个相邻节点,并且没有接收到该节点的Hello消息,则认为该相邻节点已经离开,需要进行链路断开处理;否则,将该路由表项标记为无效,并清空其前驱节点列表。

首先判断路由表是否为空,如果为空则直接返回。接着判断该路由表项的相邻节点个数hcnt是否为1,如果是,则说明该路由表项只有一个相邻节点,并且没有接收到该节点的Hello消息,即该相邻节点已经离开,需要进行链路断开处理。在链路断开处理中,调用neighbo_link_break函数来删除该相邻节点,同时更新该节点的前驱节点列表。

如果hcnt不为1,则将该路由表项标记为无效,并清空其前驱节点列表。在标记为无效时,调用rt_table_invalidate函数来设置路由表项的状态为无效,并更新其超时时间;在清空前驱节点列表时,调用precursor_list_destroy函数来删除前驱节点列表中的所有节点。

route_delete_timeout 路由删除定时器

如果路由表项指针 rt 为空,则直接返回。
如果路由表项指针 rt 不为空,则调用函数 rt_table_delete(rt) 删除该路由表项。
通过定时器函数删除过期的路由表项,以保证路由表中只保存最新的可用路由信息。

local_repair_timeout 本地修复定时器函数
  1. 指向路由表项的指针 rt,指向 RERR(Route Error)数据包的指针 rerr。
  2. 如果路由表项指针 rt 为空,则直接返回。
  3. 将 RERR 目的地址 rerr_dest 初始化为广播,将路由表项中的 RT_REPAIR 标志位清零。
  4. 如果路由表项中的前驱节点数量不为 0:
    • 调用函数 rerr_create 创建一个 RERR 数据包,包含目的地址、目的序列号以及当前时间等信息。
    • 如果路由表项中的前驱节点数量为 1,则将 RERR 目的地址设置为该前驱节点地址。
    • 将 RERR 数据包发送。
  5. 调用函数 precursor_list_destroy 销毁路由表项中的前驱节点列表。
  6. 将路由表项中的 rt_timer_timeout.handler 成员函数设置为 route_delete_timeout
  7. 调用函数 rt_table_update_timeout 更新该路由表项的定时器,将其设置为 DELETE_PERIOD(路由删除周期),以便在该周期后删除该路由表项。

发生路由故障时,通过本地修复机制对该路由进行修复,以维护路由表中的正确信息。创建一个 RERR 数据包并发送出去,同时将路由表项设置为待删除状态,并在一段时间后删除该路由。

hello_timeout hello 定时器函数
  1. 如果路由表项指针 rt 为空,则直接返回。
  2. 如果路由表项指针 rt 不为空,并且该路由表项状态为 VALID(即有效状态),则执行以下操作:
    • 如果启用了局部修复机制 local_repair,并且该路由表项的 hop count 值小于等于最大修复 TTL(MAX_REPAIR_TTL),则将该路由表项的标志位设置为 RT_REPAIR,表示该路由表项需要进行修复。
    • 调用函数 neighbo_link_break(rt, curtime),将该路由表项中与邻居节点相关的信息进行处理,将其标记为失效状态。

通过定时器函数检查邻居节点是否还处于活动状态,并对失效的邻居节点进行相应的处理,以维护路由表中的正确信息。

echo_timeout

7. 路由表相关函数

  • rt_table_clear 路由表清空
  • rt_table_insert_gateway
  • rt_table_insert_gateway
  • rt_table_update_gateway 添加新表项
  • rt_table_update_timeout
  • rt_table_update_route_timeouts
  • rt_table_find
  • rt_table_invalidate
  • rt_table_delete

8. 寻路队列相关函数

  • seek_list_insert_gateway/remove/find/clear 用于向寻路队列中添加、删除和查找条目。

9. 控制报文处理

rreq相关
rrep相关
rrer相关

10. Hello报文相关函数

用于生成、转发和处理Hello报文,实现邻居发现和链路检测。

11. 本地链路管理函数

  • neighbor_add 处理接收到的报文,更新邻居表。
  • neighbo_link_break 检测链路断开,生成RERR。

12. 业务报文处理

  • business_process 处理最终到达的业务报文。
  • genBusiness_opt 生成业务报文后的处理。
  • recvMessage 将input中的函数注册成为事件
  • processMessage 报文处理函数。重要!!!!

13. 模块接口函数

  • process 接收外部信息,交给内部处理。
  • getAODVrt_table 输出当前的路由表。
  • subNet_rt_table_insert 在网关处添加子网路由表项。

事件驱动

事件驱动思想的整体代码架构

  1. 当有业务数据需要发送时,首先查找路由表。如果有可用路由,则直接封装报文转发。

  2. 如果没有可用路由,则将数据包缓存,并通过route_discovery发起路由请求RREQ。

  3. RREQ通过网络泛洪,当到达目的地或中间节点有新的可用路由时,会触发响应RREP。

  4. 源节点收到RREP后就获得了路由,取出缓存的数据包并发送。

  5. 整个过程中,需要使用seek_list进行路由请求的发起和监控。

  6. 路由表中的路由都有超时时间,超时后会失效。

  7. Hello报文定期发送用于探测链路状态。

  8. 链路断开时,协议定义了在一定时间内,可以通过local_repair尝试进行局部修复。

  9. 局部修复失败后,通过neighbo_link_break检测到链路断开,发送RERR通知其他节点。其他节点收到RERR会使相关路由失效。

  10. 新进入的节点,通过接收业务报文和Hello报文学习邻居和本地链路信息。不会“主动地”探索网络拓扑。

  11. 业务数据报文通过process和genBusiness_opt函数进行转发。

  12. 模块接口将外部消息转入内部输入队列,内部输出转移出模块。

void AODV::process()

核心函数,用于生成各类型事件。对于每个节点来说,看不到其它节点的事件,只需要关心自己的各种到达业务、控制报文。

void AODV::process(std::vector<std::string>& RT_send, const std::vector<std::string>& RT_recv,
    const std::queue<Business>& BusinessBufferInter, bool isGateway)
{
    // 将数据转移到input
    fromBusinessBufToInput(BusinessBufferInter); // 网关接收到的跨网业务
    fromRTrecvBufToInput(RT_recv);
    // 处理input中的数据
    recvMessage();
    while (!events.empty() && eq_front_time() <= this->timer * SLOT_LEN)
    {
        eq_pop();
    }
    // 将处理过程中生成的数据转移到RT_send
    fromOutputToRTSendBuf(RT_send);
    ++this->timer;
}

“将数据转移到input”部分,是在将数据打包成业务。
recvMessage()是接收
我们再看while中的判断,!events.empty()是为了将事件队列全部轮一遍(这个process()每时隙运行一次),eq_front_time() <= this->timer * SLOT_LEN是为了判断运行总时间是否超过了时隙,如果超过了,就暂停。eq_pop()就是将里面的事件一个一个处理了。
最后fromOutputToRTSendBuf(RT_send)

虽然不是最标准的状态机事件驱动,但是也算是核心的架构,如同汽车的发动机一般,这个while循环一圈一圈的转动,驱动着整个工程的运行。

void AODV::fromBusinessBufToInput()

void AODV::fromBusinessBufToInput(const std::queue<Business>& BusinessBuffer) {
    if (BusinessBuffer.empty())
        return;
    std::queue<Business> tmpBuffer=BusinessBuffer;
    while(tmpBuffer.size())
    {
        Business bs = tmpBuffer.front();
        tmpBuffer.pop();
        int src_msg = bs._sr;
        double recvtime = bs._arrtime;
        double gentime = bs._gentime;
        for (auto dest_msg : bs._dr)
        {
            MyBusiness* mbs = new MyBusiness(INIT_ADDR, INIT_ADDR, gentime, recvtime, dest_msg, src_msg);
            this->input.push(mbs);
        }
    }
}

函数目的:将 BusinessBuffer 中的每个 Business 对象转换为 MyBusiness 对象,并将它们添加到 input 队列中。
首先检查 BusinessBuffer 是否为空。再将 BusinessBuffer 复制到一个临时队列 tmpBuffer 中。然后对tmpBuffer 中的每个 Business 对象 bs,将其源地址 _sr、到达时间 _arrtime 和生成时间 _gentime 分别赋值给变量。接着,对于 bs 中的每个目标地址 _dr,创建一个 MyBusiness 对象 mbs,并将其添加到 input 队列中。
整个函数大量运用了stl库中的操作,这个可以了解下。

你可能感兴趣的:(C++修炼,网络,网络协议,AODV,c++)