aodv-uu 源码解读

目录

    • 前言
    • 源码解读
      • defs.h
      • list.{h, c}
      • params.h
      • nl.{h, c}
      • routing_table.{h, c}
      • timer_queue.{h, c}
      • seek_list.{h, c}
      • llf.{h, c}
      • endian.{h, c}
      • debug.{h, c}
      • aodv_timeout.{h, c}
      • aodv_socket.{h, c}
      • aodv_neighbor.{h, c}
      • aodv_hello.{h, c}
      • aodv_rreq.{h, c}
      • aodv_rrep.{h, c}
      • aodv_rerr.{h, c}
      • packet_input.{h, cc}
      • packet_queue.{h, cc}
      • aodvuu.{h, cc}
      • kaodv...
    • 一些补充
      • 关于ttl
      • 关于队列
      • 关于路由表项的timeout
    • 参考资料



前言

aodvuu协议是一种 Ad Hoc 网络路由协议,是由瑞典乌普萨拉大学(Uppsala University,所以是uu)联合爱立信公司发布的一种路由协议。aodv(Ad hoc On-Demand Distance Vector Routing,无线自组网按需平面距离矢量路由协议)的特点是按需使用,也就是需要的时候才去找路由。

aodvuu包含了几种控制消息(control messages):

  • RREQ:Route Request,路由请求消息
  • RREP:Route Reply,路由回复消息
  • RERR:Route Error,路由错误消息
  • RREP_ack:Route Reply Acknowledgment,路由回复确认消息

当一个节点需要寻路的时候,用的是洪泛的方式,广播RREQ消息,然后沿途的节点自动建立反向路由,目标节点收到RREQ后生成RREP(中间节点如果有足够新的合法路由,也可以生成RREP)返回,用这种方式建立路由。

其它有许许多多的功能和实现细节,这里不一一展开,具体的还是要基于源码以及参考资料仔细阅读才能理解数据是怎么流动的。这里我对于aodv-uu协议的源码按照文件进行解读,便于更好地理解这个协议。

由于本人水平有限(目前还是本科生,之前也没接触过类似的无线网络协议),而且在网上也没有很多关于aodv-uu的中文资料,所以很多地方是结合代码和手册自己推断的,可能有理解错误,如果发现了会及时修改。



源码解读

将按照一个文件一个文件的方式进行解读。

defs.h

定义了一些基本信息(比如aodvuu版本信息,说明文档名字等),定义了两个结构体:

  • dev_info:主要是当前机器可用的 network device(网络设备,可以理解为网卡) 的信息,包括状态enabled、socket、接口名字ifname、接口序号ifindex、ip地址、网络掩码netmask、广播地址broadcast;
  • host_info:当前结点的(最近一次使用的)序列号、上次广播时间bcast_time、上次发包时间fwd_time、RREQ序列号rreq_id、网络接口数目nif、网卡信息devs。

其中定义时间用的是 timeval 这个结构体,它是一个包含x秒y微妙(这两个域)的一个简单结构体;

定义地址用的是 in_addr 这个结构体,它是这样一个结构体:

struct in_addr {
        union {
                struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
                struct { u_short s_w1,s_w2; } S_un_w;
                u_long S_addr;
        } S_un;

我也不知道到底是什么,反正可以用来表示 ip 地址。

除此之外,该文件还定义了如下一些东西:

  • MAX_NR_INTERFACE:最大网络接口数
  • dev_indices[MAX_NR_INTERFACE]:网卡信息的数组
  • ifindex、devindex、dev、name之间的各种等价类型转换
  • DEV_IFINDEX(ifindex)、DEV_NR(n):获取 dev 信息(也就是dev的dev_info结构体)的比较方便的宏定义
  • AODV_BROADCAST:广播地址 ((in_addr_t) 0xFFFFFFFF)
  • AODV信息种类:AODV_HELLO(0), AODV_RREQ(1), AODV_RREP(2), AODV_RERR(3), AODV_RREP_ACK(4)
  • AODV_msg:这是一个信息类别,所有的RREQ、RREP等等消息在发送前都会转换为这个消息,收到这个消息时可以根据前面的类型再转换为对应的具体的消息类别
  • extension:不知道干嘛的,说是可以给消息附加邻居信息?
  • callback function:定义了一个带有一个int参数的函数指针 callback_func_t

list.{h, c}

这个是一个链表头文件,可以给任意结构体实现链表功能,它和 linux 内核里面的链表实现方式是一样的,用一个带有两个指向自己的指针的结构体表示一个链表结点,用来实现双向链表的功能。其基本思想是:所有需要实现链表的数据结构都在首地址(方便强制转换)定义一个 list_t(链表结点)。

然后由于是双向链表,插入头尾都是 O(1) 的复杂度,然后除了插入还定义了删除(list_detach)、遍历(list_foreach)等操作,具体的可以参考 这篇文章 。


params.h

这是一个包含了很多参数定义的头文件,包括:

  • ACTIVE_ROUTE_TIMEOUT:一个路由的lifetime,可动态变化
  • TTL_START:RREQ消息的初始TTL(time_to_live,可以理解为生存时间)值,它等于 ttl_start,这是一个动态变化的值
  • DELETE_PERIOD:当一个路由项失效之后,再过这个时间才把它删除,可动态变化
  • ACTIVE_ROUTE_TIMEOUT_LLF
  • TTL_START_LLF
  • DELETE_PERIOD_LLF
  • ACTIVE_ROUTE_TIMEOUT_HELLO
  • TTL_START_HELLO
  • DELETE_PERIOD_HELLO
  • ALLOWED_HELLO_LOSS:当丢失了超过这个数目的hello消息之后,认为到那个节点的连接中断
  • BLACKLIST_TIMEOUT:进入RREQ黑名单后在黑名单里面待的时间
  • HELLO_INTERVAL:广播hello消息的时间间隔
  • LOCAL_ADD_TTL:在路径修复过程中用来计算RREQ的TTL
  • MAX_REPAIR_TTL:进行路径修复的最大跳数(也就是如果到目标结点的跳数超过这个,那么不进行路径修复)
  • MY_ROUTE_TIMEOUT:节点发送RREP消息时,给RREP消息设置的lifetime就是这个值,也就是RREP消息的生存时间
  • NET_DIAMETER:网络直径,也就是无线网络系统中两个节点间可能的最大跳数,它是RREQ的TTL的上界
  • NET_TRAVERSAL_TIME:网络横跨时间
  • NEXT_HOP_WAIT:不知道干嘛的
  • NODE_TRAVERSAL_TIME:节点横跨时间,可以理解为发包的时候,一跳所需要的时间
  • PATH_DISCOVERY_TIME:当一个节点广播RREQ消息之前,它会把RREQ的ID和源IP缓存这么长的时间,这样当它收到邻居的RREQ时(实际上本来是自己发的)不会做出回应;当一个节点收到RREQ时,它也会检查在过去的这么长时间内是否收到过同样的ID和源IP,如果有则丢弃这个RREQ
  • RERR_RATELIMIT:每秒发送RERR消息数目的限制
  • RING_TRAVERSAL_TIME:节点进行路径搜索时用的方法是扩展环搜索(expanding ring search),一开始给RREQ消息的TTL设置为TTL_START,如果在RING_TRAVERSAL_TIME内没有受到RREP则认为搜索失败,然后再次广播RREQ,只不过把TTL在上一次的基础上加上TTL_INCREMENT,然后再次搜索,直到TTL达到阈值TTL_THRESHOLD,之后的每次搜索TTL都是NET_DIAMETER,等待RREP时间都是RING_TRAVERSAL_TIME
  • RREQ_RETRIES:每次寻路时发送RREQ数目上限
  • RREQ_RATELIMIT:每秒发送RREQ的数目上限
  • TIMEOUT_BUFFER:不知道干嘛的,好像跟RREP有关
  • TTL_INCREMENT
  • TTL_THRESHOLD

nl.{h, c}

netlink ???

这个好像是用来通过套接字(netlink socket)和内核(netfilter)通信的,我不太了解它的具体作用和实现。


routing_table.{h, c}

定义了一个序列号增加的宏seqno_incr:0就保持为0,其它数值在 1-0xFFFFFFFF之间循环;

定义了路由表项(route table entry)结构体 rt_table(rt_table_t),包含以下内容:

  • l:链表域
  • dest_addr:目标地址
  • dest_seqno:目标序列号
  • ifindex:网络接口序号
  • next_hop:下一跳的地址
  • hcnt:到达目标地址所需要的跳数
  • flags:路由标识,用来标识是前向(forward)、反向(reverse)、邻居(neighbor)、单向(uni-directional)还是路径修复(local repair)
  • state:当前路由表项的状态(VALID or INVALID)
  • rt_timer:当前路由表项的计时器,是一个timer结构体(这个之后会讨论到),它绑定的callback函数是 aodv_timeout.c 里面的 route_expire_timeout(),作用是如果对应路由表项是邻居节点,那么调用 neighbor_link_break,否则使当前的路由项无效(总体过程就是对路由到期的处理)。
  • ack_timer:RREP_ack的计时器,绑定的callback是 rrep_ack_timeout(),作用是把该路由表项的目的节点地址加入黑名单(之后会讨论到)
  • hello_timer:hello消息的计时器,绑定的callback是 hello_timeout()
  • last_hello_time:
  • hallo_cnt:
  • hash:一个哈希值,用来快速定位这个路由表项
  • nprec:先驱节点(也就是这样的节点,它们也有到达同样目标地址的路由表项,并且下一跳是这个节点)数目
  • precursors:这是一个链表表头,用来存储先驱节点地址信息

定义了一个路由表的变量 rt_tbl,它是一个这样的结构体:包含了表项数目、活跃节点数目、路由表项数组tbl(也是一个链表)。

定义了若干个函数:

  • rt_table_init, rt_table_destroy
  • rt_table_insert
  • rt_table_update
  • rt_table_update_timeout
  • rt_table_invalidate:这个说一下,它会将路由表项的state记为INVALID,然后生成一个local_repair_timeout或者route_delete_timeout的timer
  • ……

其实无非就是插入、删除、修改路由表的操作,还有前驱节点的更新操作,结合路由表项的结构体应该很容易理解这些函数的实现。

另外补充一点,rt_tbl(routing_table)里面的 tbl 数组实际上是一个hash表,每个路由表项会获得一个哈希值,然后 tbl 数组的每个元素都是一个链表头(这实际上就是处理hash冲突的一种方式),这样可以加速路由表项的查找(从中我们可以推断路由表项可能会比较多,并且查找路由的操作也很频繁)


timer_queue.{h, c}

定义了一个带有一个void指针的函数指针timeout_func_t,然后定义了 timer结构体:

  • l:链表域
  • used:主要是用来记录该timer是否已经进入队列,当我们在添加一个timer的时候,如果它已经进入了队列,首先要把它删除,然后再添加
  • timeout:记录时间的 timeval 变量
  • handler:记录时间到了的时候调用的处理函数指针(类型为 timeout_func_t)
  • data:一个void类型的指针,存储一些handler需要的数据

之后定义了一些针对 timeval 的计算的函数,包括 timeval_diff(时间差)、timeval_add_msec(增加时间)等等。

然后定义了 timer_queue 的一些相关操作,对于 timer queue,可以理解为一个 timer 的链表,这个链表是有序的,按照每个 timer 的发生顺序从近到远排序,这样离散事件模拟的时候每次取链表头的事件来处理就行了。相关操作如下:

  • timer_queue_init:未实现(不过在 timer_queue.c 里面定义了一个链表头TQ)
  • timer_remove:在TQ链表中删除指定timer
  • timer_init:用来初始化一个timer(绑定处理函数等等)
  • timer_add:如果已经used,首先删除,然后添加,在添加的时候遍历整个TQ,找到合适的位置插入(保证时序)
  • timer_set_timeout:改变某个timer的timeout(如果这个timer已经进入队列也就是used了,首先要移出)
  • timer_timeout:当一个timer到时的时候调用的函数,作用是把某个时间前的所有timer都移除,并且调用他们的处理函数
  • timer_timeout_now:删除当前timer,然后调用处理函数(不知道这个有啥用)
  • time_age_queue:首先把当前时间(now)之前的timer全部到时(也就是调用timer_timeout(&now)),然后返回下一个即将到期的timer剩下的时间

总览整个 timer_queue 的功能,其实就是一个离散事件模拟器,用一个链表记录即将到来的事件,然后对相应的事件调用相应的处理函数,需要计时的比如hello消息的统计都会通过这个方式来完成。另外里面还有一个 gettimeofday函数,用来获取当前时间,它实际上是获取了调度器 Scheduler的时钟。


seek_list.{h, c}

这个文件是用来实现 seek list 的,seeklist记录的是寻路的队列(有点儿神奇),实现了一个寻路请求的结构体 seek_list_t 以及寻路队列链表头 seedhead,该队列支持插入、删除、(通过 dest_addr)查找的操作。其中每个 entry(seek_list_t)的结构体包含如下内容:

  • l:链表域
  • dest_addr:寻路目的节点的地址
  • dest_seqno:目的节点的序列号
  • ipd:一个 ip_data 类型的数据,记录一个地址,当寻路失败时向这个地址发送目的主机不可达的ICMP报文
  • flags:一个用来重发送RREQ的标识
  • reqs:已经发送RREQ的数目
  • ttl:发送RREQ时使用的TTL值
  • seek_timer:寻路使用的计时器,绑定的处理函数是 route_discovery_timeout,这个函数的功能是:如果 reqs 小于 RREQ_RETRIES(定义与 params.h),那么 reqs自增,ttl增加(增加方式也已经说过),然后重新设置 seek_timer,时间为 RING_TRAVERSAL_TIME,然后更新路由表中相应目的节点表项的timeout(表示,啊,我要重新发送RREQ了,所以你还得再等 2 * NET_TRAVERSAL_TIME 的时间才能从路由表中删去),然后发送RREQ;否则的话,表示已经达到了一次寻路所限制的最大的发送RREQ数目限制,那么就认为寻路失败,寻路失败首先调用了一个 nl 相关的函数发送一些寻路失败信息,然后移出当前seek表项,然后从路由表找到目的节点路由信息(可能已经失效但是还没有移除),如果该表项的flags标识了RT_REPAIR,那么调用local_repair_timeout(表示当前发送RREQ是为了路由修复,然后修复失败了,它的作用主要是广播RERR信息,然后生成一个删除路由的timer,绑定 route_delete_timeout函数,然后在DELETE_PERIOD之后删除该路由)

插入、删除和查找的三个函数都非常简单,很容易理解。


llf.{h, c}

暂时还不知道这是啥……


endian.{h, c}

用来说明字节顺序(大小端),主要是有些机器缺失这个文件,所以放在这里以备万一。


debug.{h, c}

里面有好多各种数据结构到 char* 类型的转换,主要是用来输出调试信息


aodv_timeout.{h, c}

里面定义了各种 timeout 时的处理函数,之前讨论的很多 timer 的handler 函数都是定义在这里面的,这里就不作详细说明了。


aodv_socket.{h, c}

aodv-uu协议是基于UDP的,也就是说它发送各种控制消息(RREQ,RREP,RERR)都是通过满足UDP协议的socket来发送的,所以socket才是aodv与网络通信的方式(不知道我的理解对不对)。这两个文件就是与这个有关。

aodv-uu 源码解读_第1张图片
packet和控制消息的处理流程

在 aodv_socket.c 里面定义了两个 buffer:recv_buf 和 send_buf,这两个缓冲区一次都只能存放一个 message,然后两个缓冲区的最大长度也是 AODV_msg 可能的最大长度。

里面实现的几个函数如下:

  • aodv_socket_init:主要就是为每个网络接口(interface)绑定一个socket,然后调整好多个socket的乱七八糟的属性,最后给每个接口的socket绑定一个callback函数aodv_socket_read
  • aodv_socket_process_packet:这个函数的作用是接受一个 AODV_msg 以及附带的长度、源地址、目的地址、ttl、接口序号等信息,然后根据 AODV_msg 的 type 去判断消息类别(HELLO、RREQ等等),然后根据类别调用各种处理函数(rreq_process, rrep_process, hello_process, rerr_process, rrep_ack_process),在调用具体的函数之前还要调用 neighbor_add 来更新邻居信息
  • aodv_socket_read:我对于这一块了解得不多,只知道会根据 fd(文件描述符)找到对应的网络接口,然后从 recv_buf 获取 AODV_msg,然后调用 aodv_socket_process_packet
  • aodv_socket_send:我也不了解,反正经过一大堆处理之后把信息放在send_buf里面,然后调用 sendto 函数发送
  • aodv_socket_new_msg:把 send_buf 清零,返回send_buf起始地址
  • aodv_socket_queue_msg:把指定的 AODV_msg 复制到 send_buf 里面
  • aodv_socket_cleanup:检查所有 interface,把 enabled 为 0 的socket关闭
  • recvAODVUUPacket:主要是把包解析之后把控制消息部分放进recv_buf,然后调用aodv_socket_process_packet

反正 socket 就是一个底层的用于通信的东西,发送和接受aodv控制消息都要通过它这种形式,这两个文件就定义了发送和接受的方法。


aodv_neighbor.{h, c}

主要也是基于 routing_table 下的操作,里面有两个函数:

  • neighbor_add:这个是给非HELLO的控制消息更新邻居用的,其实就是对 routing_table 操作一下,然后更新一下 hello_timer
  • neighbor_link_break:邻居链接中断,会首先invalidate一下路由表项,然后假如该路由表项不是RT_REPAIR并且有前驱节点,会生成一个RERR信息,然后会遍历所有路由表项,找到下一跳地址等于该邻居地址的路由表项,如果当前邻居路由表项标识了RT_REPAIR,则也把遍历路由表项标识为RT_REPAIR,然后invalidate该遍历路由表项,然后如果该遍历路由表项有前驱节点,还会在RERR消息上附加udest消息。最后发送RERR消息。

顺便提一句,在aodv里面判断某个路由是不是记录邻居节点信息,是通过 hcnt是否等于1来判断的(显然)。


aodv_hello.{h, c}

在aodvuu协议中,节点会定时广播一个hello消息用于维护和邻居的拓扑性质,具体体现在,aodv_hello.c里面定义了一个hello专属的timer:hello_timer,它初始化的时候会绑定一个hello_send函数,然后调用hello_send。每次执行hello_send时,在最后又会给hello_timer重新timer_set_timeout一下,相当于再次设置了一个定时,这样实现了定时发送的功能(这个定时其实不准确,因为实际上它还在HELLO_INTERVAL上加了个jitter)。

另外hello消息实际上是一个RREP消息。

这两个文件里面定义了如下一些函数:

  • hello_start:初始化hello_timer
  • hello_stop:从timer_queue中移除hello_timer,相当于停止定时发送hello消息
  • hello_send:首先要在当前时间减去上一次广播时间大于等于HELLO_INTERVAL时才进行HELLO消息的发送,发送hello消息前首先遍历所有接口,然后生成一个目的节点地址和序列号都是自己的RREP,lifetime设置为ALLOWED_HELLO_LOSS * HELLO_INTERVAL,然后进行广播,发送完之后重新给hello_timer进行timer_set_timeout(里面还有一个ext消息,这里不作讨论)。
  • hello_process:对hello消息进行处理的函数,其中最核心的是对路由表的更新吧(包括更新路由表项的hello_timer、last_hello_time等)
  • hello_process_non_hello:
  • hello_update_timeout:在hello_process最后会调用这个函数,更新路由表项里面的hello_timer

总而言之,这两个文件是专门用来处理hello消息的,而hello消息的作用是用来维护邻居信息,主要更新和操作的是路由表。


aodv_rreq.{h, c}

定义了RREQ消息的格式:

  • type:消息类型,一般赋值为AODV_RREQ,这个是用来匹配AODV_msg的
  • j, r, g, d:每个各占一位,用来标识RREQ_JOIN,RREQ_REPAIR,RREQ_GRATUITOUS,RREQ_DEST_ONLY
  • res1, res2:保留字段
  • hcnt:记录当前经过的跳数
  • rreq_id:这个RREQ的id
  • dest_addr:目的节点地址
  • dest_seqno:目的节点序列号
  • orig_addr:源节点地址
  • orig_seqno:源节点序列号
aodv-uu 源码解读_第2张图片
RREQ消息格式

定义了rreq_record结构体(包含源节点地址、rreq_id和一个rec_timer),并且定义了该结构体的链表头rreq_records,这个的作用相当于对收到的RREQ消息进行缓存。rec_timer绑定的处理函数是rreq_record_timeout,该函数的作用就仅仅是把这个record从链表中清除,timeout设置为 PATH_DISCOVERY_TIME。

定义了blacklist结构体以及rreq_blacklist链表头,顾名思义这是一个黑名单,这个黑名单的作用是这样的(这是我的理解):由于路由可能是单向的,所以一个节点收到RREQ之后可能无法返还RREP消息,而每个节点对于同一个rreq_id只会处理第一个,这就导致在寻路的时候可能存在合法的双向路径,但是被忽略了。所以当一个节点发送RREP消息而不被下一跳接收到时,就会把下一跳结点的地址加入黑名单,表示之后的一段时间内(BLACKLIST_TIMEOUT),都不接受它的RREQ消息,这就解决了上述问题。而 BLACKLIST_TIMEOUT 的时间设置应当为允许多次寻路之后的时间上限。所以加入黑名单只在等不到RREP_ack消息的时候进行,也就是说只有rrep_ack_timeout会调用它。

定义了若干个函数:

  • rreq_create:生成一个RREQ消息结构体,生成的时候自己的seqno要加一
  • rreq_send:就是遍历所有接口,然后生成RREQ消息,然后调用aodv_socket_send进行广播(广播只需要把aodv_socket_send的dest地址设置为AODV_BROADCAST)
  • rreq_forward:主要操作就是把RREQ消息里面的hcnt加一,然后遍历所有接口进行广播
  • rreq_process:首先判断如果这个RREQ是自己发出去的就丢弃,然后判断如果上一跳地址在黑名单中就丢弃,然后如果这个RREQ已经来过(源节点地址和rreq_id在record中)就丢弃,然后插入record,之后是处理一些ext信息,然后是建立反向路由的一些操作(查路由表,没有就插入,有就更新),之后检查自己是否就是目标节点,如果是的话首先更新自己的seqno(自己的比RREQ里的小,就变为RREQ里的,相等就把自己加一,大就啥也不干,其实就是为了保持序列号的单一性),然后生成RREP后调用rrep_send,如果不是的话说明自己是中间节点,那么检查路由表里面是否有目的节点的路由表项,如果有并且相应的序列号大于等于RREQ里面的序列号(说明足够新鲜),那么也可以生成RREP然后直接返回(这里假如RREQ的g有标记,还必须免费生成一个到目标节点的RREP并且发送),否则调用rreq_forward同时ttl减一(这里如果ttl等于1了就不再forward了,只有ttl>1才进行forward,此外也要更新seqno,如果RREQ的目的节点序列号比自己路由表中目的节点表项的序列号小,要更新RREQ目的节点序列号)。
  • rreq_route_discovery:寻路的函数(比上一个简单多了),首先查找seek_list,存在则忽略本次寻路,然后查找路由表是否已经存在目的节点信息,有的话可以复用一些信息(seqno、hcnt等),然后调用rreq_send,并且把当前寻路信息加入seek_list,然后设置seek_timer的timeout(这里的ttl以及timeout要考虑是否设置了expanding_ring_search,这在之前已经讨论过)
  • rreq_local_repair:和上一个基本一样,除了重新设置了rt_timer的handler函数以及对timeout和ttl的设置不太一样

在rreq_process函数参数中有两个参数是ip_src和ip_dst,ip_src记录的是上一跳节点的地址,ip_dst好像没啥用。


aodv_rrep.{h, c}

定义了RREP消息格式:

  • type:跟RREQ一样,是为了适应AODV_msg而设置的;
  • r, a:每个占一位,标识RREP_REPAIR、RREP_ACK
  • prefix:暂时不知道拿来做啥的
  • res1, res2:保留位
  • hcnt:跳数
  • dest_addr:目标节点地址
  • dest_seqno:目标节点序列号
  • orig_addr:源节点地址
  • orig_addr:源节点序列号(注意这里的源节点和目标节点不是基于发送RREP的,而是基于发送RREQ的,也就是说这个RREP消息会从目标节点地址发往源节点地址)
  • lifetime:生存时间
aodv-uu 源码解读_第3张图片
RREP消息格式

定义了RREP_ack消息的格式:type和reserved。RREP_ack消息是当发送的RREP中的 a 位被置为 1 时,接受端就要返回这个消息表示已经收到RREP消息,这是为了防止单向路由导致的寻路失败(之前已经讨论过)。

定义了如下若干函数:

  • rrep_create:传入参数然后返回一个RREP结构体
  • rrep_ack_create:传入参数返回一个RREP_ack结构体(这里说明一下这里返回的结构体是一个指针(包括之前的RREP和RREQ也是),他们都是调用了aodv_socket_new_msg,得到了send_buf的地址,然后直接在那个地址上操作返回)
  • rrep_add_ext:关于ext的不讨论
  • rrep_send:首先检查是否需要别人返回RREP_ack信息,假设检测到时单向路由,那么需要RREP_ack信息,并且要把邻居节点断开(其实这里我自己都还不是很清楚……),然后设置ack_timer,之后就是调用aodv_socket_send进行发送了,并且添加precursor信息(表示有人要通过我去往某个目的节点)
  • rrep_forward:中间节点传递RREP,同时更新反向路由rev_rt的timeout
  • rrep_process:RREP处理函数。跳过之前的一些完备性判断以及ext相关的处理,首先要做的是找源节点和目的节点的路由表项rev_rt和fwd_rt,如果源节点路由表项rev_rt不存在还要插入一个,然后检测是否有a标志,有的话还得发送一个RREP_ack,然后移除a标志(也就是说这个a标志是一次性的,需要就请求),然后检查RREP的源节点是不是就是自己,如果是的话还需要判断该RREP是不是路径修复返回的信息(也就是是不是我发起了路径修复,然后得到了一个RREP消息),如果是并且判断修复失败还要广播带有RERR_NODELETE的RRER消息,如果RREP的源节点不是自己,那么调用rrep_forward同时ttl减一
  • rrep_ack_process:直接把路由表项中ack_timer从队列中移除(表示我已经收到了ack,不用继续计时了)

注意RREP的send、forward等等操作传进去的参数和RREQ很不一样,为两个路由表项指针rev_rt和fwd_rt,分别代表反向路由和正向路由,RREP消息是由正向路由发过来,即将发往反向路由的。在其中更新hcnt时,只需要更新为正向路由fwd_rt的hcnt即可(到正向路由目的节点的跳数就是这个RREP走过的跳数)。


aodv_rerr.{h, c}

RERR消息是当一个节点检测到活跃路由的下一跳断链、路由修复失败、发包时发现没有活跃路由或者从邻居接收到RERR消息时才可能进行RERR消息的发送,并且发送RERR消息有单播(unicast)和广播(broadcast)两种形式,分别对应涉及到的前驱节点数目是否等于1 。这两个文件定义了RERR消息的格式:

  • type:类型,应当为AODV_RERR,是为了适应AODV_msg消息格式
  • n:占1位,用来标识RERR_NODELETE(这个域的作用是在做路由修复的时候,让上游节点先不要删除路由)
  • res1, res2:保留位
  • dest_count:不可到达的目标节点数目(大于等于1)
  • dest_addr:不可到达的目标节点的地址
  • dest_seqno:不可到达的目标节点的序列号
aodv-uu 源码解读_第4张图片
RERR消息格式

原来手册中还给RERR消息定义了附带的(addr,seqno),在代码中体现为另一个结构体 RERR_udest(dest_addr,dest_seqno)。这里顺便提一句,之前讨论aodv_socket的时候,里面有两个缓冲区send_buf和recv_buf,它们的最大长度AODV_MSG_MAX_SIZE就等于RERR_SIZE + 100 * RERR_UDEST_SIZE,也就是说最多允许一个RERR消息携带一百个udest。

定义了若干个与RERR相关的函数:

  • rerr_create:跟rrep_create差不多就是传入参数然后返回RERR结构体
  • rerr_add_udest:找到RRER消息大小后面那个地址,然后把那一块空间添加udest信息,然后把RERR消息的dest_count加一
  • rerr_process:首先是一些完备性检查。然后先遍历RERR消息中的每个udest,对于每个udest(都有一个不可达的目的节点地址和序列号)中的目的节点地址,找到自己路由表中对应的表项rt(这里说明一下,当一个节点收到RERR消息需要处理的时候,可以获得一个上一跳节点地址ip_src,既然这个RERR消息是从这里发过来的,那就说明对于src这个节点这所有的udest都是它不可达的,所以我就要检查路由表项,如果目的节点是udest的目的节点,并且下一跳的地址是这个ip_src,那就说明现在对于我这个目的节点也不可达了),如果rt有效并且下一跳是ip_src,那么:首先有一个seqno的检查(源码中没有启用),然后如果n位没有标识则invalidate该路由表项,然后要更新rt的dest_seqno为该udest的seqno,然后如果该rt没有标识路径修复并且有前驱节点,那么还要把当前不可达的目的节点信息加入新的(也就是即将要转发的)RERR消息里面(以上的一堆讨论都是针对同一个udest的,然后对所有udest都要做上面的事情)。最后如果新的RERR有东西的话,再进行RERR的发送,这里在之前遍历每个udest的时候也会动态维护单播和广播的信息。

packet_input.{h, cc}

只定义了一个processPacket函数。首先看一下packet是一个怎样的结构:packet定义在packet.h中,包含了

  • bits_:主要是包头信息,有一个public的access函数,专门用来访问包头中的信息(packet.h一开始的好多宏定义都是拿来访问这个信息的)
  • data_:记录数据信息
  • fflag_ …(其它的成员似乎不太重要的样子)

在processPacket函数中,首先会从packet的包头中获取一个hdr_cmn(这个结构体包括了包的类别(向上还是向下)、大小、uid、上一跳和下一跳地址等等信息)和一个hdr_ip(这个结构体主要记录地址信息),然后进行一些完备性检查(如果包的类别不确定直接丢弃、如果是一个TCP的包则把RREQ的g位置为1),然后如果是一个广播的包的话,直接根据它的方向选择发包(也就是无条件通过)还是发给target_(这个东西好像是一个指向下一层的指针,直接调用它去recv就可以实现包跨层传输的需求)然后返回,否则,就从路由表查询fwd_rt和rev_rt(这两个非常熟悉,一个是前向路由,一个是反向路由),并且更新他们的timeout,然后就根据这个包的信息来做出如下行为:如果这个包的dest_addr就是我自己,那么我调用target_把它接收直接return;如果存在有效的走向dest的路由,那么就调用sendPacket把它发送出去;其它情况就是我要发这个包但是没有有效路由,那么根据这个包的方向(UP的话就生成RERR消息并且丢弃这个包,其实我这里不太理解,包的方向是UP但是目标节点不是自己,那么我接受这个包是要做什么吗;DOWN的话就首先把抱缓存,然后开始寻路或者路径修复)来决定怎么做。


packet_queue.{h, cc}

主要是用来缓存packet的吧,里面主要定义了一个结构体q_pkt,它也是队列链表的每个结点的结构体(里面包括了链表域、目标节点地址、q_time(进入队列时间)、一个包结构体p),还定义了一个packet_queue结构体(里面包括了表头、长度以及一个垃圾回收的计时器garbage_collect_timer)。

定义了最大队列长度以及最大排队时间,定义了垃圾回收时间。

定义了如下几个函数:

  • packet_queue_init:给garbage_collect_timer这个计时器绑定packet_queue_timeout处理函数,这个处理函数的作用是调用packet_queue_garbage_collect,然后再次设置一样的计时器,计时时间为GARBAGE_COLLECT_TIME,这样能达到定时回收的作用

  • packet_queue_add:如果当前队列已经满了,那么首先销毁第一个包(队列的性质是先进先出,第一个包是最早的),然后在队列末尾添加一个新的q_pkt

  • packet_queue_destroy:销毁所有包

  • packet_queue_set_verdict:这是一个对队列里所有包的处理工作,因为当某些包可以发送的时候还是要及时把它发出去的。会传入一个参数dest_addr以及verdict(PQ_SEND、PQ_DROP、PQ_ENC_SEND),然后会寻找一个到达dest_addr的路由表项rt,然后遍历整个队列,找到目的节点地址是dest_addr的包,根据verdict把它们处理(发送或者销毁),然后更新rt的timeout。通过搜索引用的地方可以看到,在rt_table_update和rt_table_insert、route_discovery_timeout这几个地方会调用到这个函数,其实就是在更新路由或者寻路失败的时候,要对相应dest_addr的包进行处理。

  • packet_queue_garbage_collect:遍历整个PQ,找到排队时间已经超过MAX_QUEUE_TIME的包把它们丢弃


aodvuu.{h, cc}

最神奇的地方就在这里了,aodvuu表面上看分成了上面那么多文件,但是实际上在阅读那些文件的时候就会发现,实现函数的头部都有一个NS_CLASS,它实际上是一个AODVUU::的宏定义,所以事实上那些所有文件定义的函数,都是AODVUU这个类里面的成员函数,它的实现方式是在AODVUU类里面把所有头文件先undef然后再include进来,另外还有一些全局变量的声明(我还是头一次看到这么骚的操作),这样AODVUU就变成了一个超级大的类。

除了这个之外,它本身还定义了其它的一些东西,主要包含如下:

  • 一个友元类TimerQueueTimer,这是一个事件调度的类,估计是让它来实现自己的事件调度吧
  • recv():这是一个宏观的接收包的函数,会对包进行解析,然后会对包做一个方向的设置,然后根据报的类别,如果是控制消息则调用redvAODVUUPacket,如果是数据包则调用processPacket()
  • command():这个是用来和tcl脚本交互的,传入tcl的参数,然后做出适当的回应
  • packetFailed():这个是连接层发包失败调用的函数,可以发现只有link_layer_callback这个callback调用了它,而link_layer_callback是在发包(sendPacket)的时候进行绑定的,所以说发包失败就会调用这个函数。这个函数首先丢弃一些不care的包(比如广播的包),然后假如dest_addr在seek_list里面,也就是说正在寻路了,那么直接把包丢进packet_queue,否则查找路由表项,如果路径无效(INVALID)直接丢弃包,否则进行路径修复
  • sendPacket():首先是一些ttl的检查以及各种不合法的判断,然后调用Scheduler实例进行发送
  • ……

我们在编写tcl脚本的时候,运行时同时会生成对应的C++的类,并且通过bind进行变量绑定,而Otcl类调用C++类就需要用到command方法。


kaodv…

这一块还没看,是和内核相关的文件



一些补充

关于ttl

一开始我一直以为ttl是一个aodvuu协议里面定义的东西,但是一直没找到它,后来才发现它是socket通信中定义的一个变量。所以ttl和hcnt不太一样,不能弄混,在socket通信中数据包在转发过程中会每次-1(这个似乎是aodv操作的),然后到0的时候就不会再被发送了。

关于队列

主要是我在思考为什么会有一个seeklist的时候想到的,如果你在做了某件事情之后,要等一会儿才能知道结果,那么必须把它加入队列(这个队列实际上是一个保存信息的作用),因为你在等待的时候还可以继续做其他事情。

说白了就是需要保留某些消息一段时间,那么就会出现队列和计时器……

关于路由表项的timeout

可以发现很多地方都会更新路由表项的timeout,原则上应该是每次成功使用了一个路由,都要把它的timeout重新设置为ACTIVE_ROUTE_TIMEOUT。



参考资料

  1. aodv手册rfc3561.txt,在aodvuu源码中自带;
  2. 《Porting AODV-UU Implementation to ns-2
    and Enabling Trace-based Simulation》,这是uppsala大学学生写的一篇论文,里面也有很多对于源码以及整体数据流的分析,博客中“packet和控制消息的处理流程”那张图片来源于此;
  3. 一些杂乱的网上资料。

你可能感兴趣的:(无线网络协议,无线网络协议,aodv)