Ns cn手册
- 1. NS 手册(中文版)
NS 手册(中文版)
NS 手册(中文版)翻译委员会
主编 liguo
编委 跑 咖啡伴侣 海底浮云
满地找牙 卿袅 walker
阿月 宝哥 飘 结
- 2. 序言
本书主要是翻译 ns manual 上的内容。涵盖了从 ns 的基本架构到各个常用模
块的使用,从 ns 的构件到各种调试追踪的使用。范围应该是相当的广泛。本书
是完全依照 ns manual 英文版进行翻译。对于部分特殊单词,有中英对照。希望
能对 NSer 有一定的帮助。
本书的使用建议,建议将《ns 手册(中文版) 》作为浏览和索引使用,由于
译者的水平和精力有限,读者如果遇到具体问题,希望能都根据中文版找到原文
的相应位置,认真阅读英文原文。
NS 是一个相对来说比较复杂的工具,学习 NS 对于一些初学者来说,是痛
苦的,有时甚至是绝望的!但是,经历了这个过程之后,很多人会发现,NS 的
学习其实是痛并快乐着的。虽然你经历了痛苦,但是,你的收获也是巨大的。
NS 的入门就是一座山,其实只要你翻过了这座山,剩下的就只有一履平川了。
这里我要特别感谢 NS 网络模拟论坛,给我们所有 NSers 提供了一个交流,
讨论的平台。有时,或者有些人会抱怨怎么没有人回答我的问题,怎么这里的人
都不热心……其实,我想给这种人一点建议:多看看其他人的帖子,这样做不但
可能找到你的答案, 也可以减少版面上很多重复的问题, 方便其他人查找。当然,
我们也会尽量回答问题的。还有,问问题的时候一定要具体,其实有很多问题,
不是别人不愿意回答,而是因为问题太模糊,人家根本就看不懂,所以,问问题
一定要清楚,说明你要问的问题,贴出相关代码和报错信息等等。
啰嗦了这这么多,下面也来点儿和这本书有关的东东。这里要感谢我们参
与翻译的十一位同学,在巨大的学习压力(毕业、发论文)下,还抽出宝贵的时
间来无偿的为大家提供服务。他们当中有计算机系和通信系的同学,有本科生、
研究生和博士生。有来自北方和南方的各个高校的。总之,我们这群背景各不相
同的人,为了同一个目标,尽心竭力地为大家提供准确、流畅的《NS 手册(中
文版)。 》
下面是翻译委员会各个成员的分工:
liguo 3,4,5,6,7,8
跑 20
咖啡伴侣 26,31
海底浮云 40,41
满地找牙 10,13
卿袅 14,15
walker 37,38,39
阿月 18,19
宝哥 12,13
飘 17,21
结 44
本书的其他部分翻译由麦子的幸福同学友情提供,在此我们向他表示衷诚
挚感谢。
由于本书作者自身水平和时间有限,翻译中可能存在大量不足,希望大家
多多包涵。
最后,推荐一些我觉得比较有用的资料,对于初学者来说比较有参考价值。
-I-
- 4. 目录
序言............................................................................................................................... I
第 3 章 OTcl 连接....................................................................................................... 1
第 4 章 类 Simulator................................................................................................. 22
第 5 章 节点和包转发.............................................................................................. 29
第 6 章 链路:简单链路.......................................................................................... 49
第 7 章 队列管理和包调度...................................................................................... 56
第 8 章 延时和链路.................................................................................................. 73
第 9 章 ns 中的差异服务模型 ................................................................................. 75
第 10 章 代理............................................................................................................ 82
第 11 章 定时器........................................................................................................ 99
第 12 章 分组头及其格式...................................................................................... 103
第 13 章 误差模型...................................................................................................116
第 14 章 局域网...................................................................................................... 121
第 15 章 NS 中(修改的)寻址结构 ................................................................... 130
第 16 章 Mobile networking in ns .......................................................................... 133
第 17 章 NS 中的卫星网络 ................................................................................... 157
第 18 章 无线传播模型.......................................................................................... 175
第 19 章 NS 中的能量模型 ................................................................................... 181
第 20 章 定向扩散.................................................................................................. 183
第 21 章 XCP:显式拥塞控制协议...................................................................... 192
第 22 章 对跟踪和监控的支持.............................................................................. 200
第 23 章 对 Test Suite 的支持................................................................................ 214
第 24 章 单播路由.................................................................................................. 217
第 25 章 组播路由.................................................................................................. 226
第 26 章 ns 编码风格 ............................................................................................. 237
第 27 章 分级路由.................................................................................................. 243
第 28 章 UDP Agent ............................................................................................... 246
第 29 章 TCP Agent................................................................................................ 248
第 30 章 Agent/SRM .............................................................................................. 257
第 31 章 UDP 代理................................................................................................. 271
第 32 章 应用程序和传输 Agent API.................................................................... 273
第 33 章 作为应用程序的网络缓冲区.................................................................. 281
第 34 章 Session-level packet distribution ............................................................. 297
第 35 章 Emulation................................................................................................. 302
第 36 章 Nam.......................................................................................................... 307
第 37 章 会话级包分发.......................................................................................... 313
第 38 章 Asim:近似的分析的模拟........................................................................ 318
第 39 章 仿真.......................................................................................................... 322
第 40 章 Nam.......................................................................................................... 328
第 41 章 NAM trace ............................................................................................... 332
第 44 章 NS 和 NAM 的教学使用 ........................................................................ 344
- III -
- 5. 附录一 网络模拟器 NS2 简析 .............................................................................. 348
附录二 开发新(路由)协议整个流程和要点.................................................... 388
- IV -
- 6. 第三章
OTcl 连接
ns 是一个用 C++编写, 并且以 OTcl 为前端的面向对象的模拟器。
模拟器支持 C++
中的类的层次结构(在本文档中我们把这种层次结构叫做编译层次)和 OTcl 解
释器中的类似的层次结构(在文档中我们把这种层次结构叫做解释层次) 。这两
种层次密切相关,从用户的脚度,编译层次的类和解释层次的类是一一对应的。
(在解释层次中, 类 TclObject 是基类。
) 用户通过解释器创建一个新的模拟对象;
这些对象首先在解释器中被实例化;然后在编译层次中同样镜像一个相应的对
象。接着解释类层次通过类 TclClass 中定义的方法自动建立, 用户则是通过
TclClass 类中定义的方法镜像这些实例化对象。当然 C++和 OTcl 中的其他层次
结构还是存在的,这些其他的层次是不会被 TclObject 镜像的。
3.1 概念综述
为何使用两种语言?因为模拟所需要做的有两种不同类型的事情。一方面,细节
性的模拟协议,这需要一种系统的编程语言,这种语言可以有效的控制字节,包
头和实现使用大规模数据的算法。对于这些任务,执行时的速度非常重要,但是
转化时的速度(如,执行模拟,找错,修正错误,重编译,重新执行)相对而言
并不重要。
但另一方面,大部分网络研究都是只有对于参数、配置或者快速搜索大量场景都
只有微小的变化。针对这些方面,重复的时间(改变模式和重执行)变得更加重
要。因此配置只设置一次(在模拟开始的时候),执行这部分的工作变得不太要
紧。
ns 使用 C++和 OTcl 同时满足了这两种需求。C++能够快速执行但是难于变化,;
这一性质使得其适合于;具体协议的实现。OTcl 执行比较慢但是可以迅速的变
化(甚至是交互式的) 这一性质使得其成为模拟配置的理想对象。 (通过 tclcl)
, ns
提供了将两种语言的对象和变量同时出现的纽带。
如果需要更多关于脚本语言和分裂语言编程的信息,请参考 IEEE Computer[26]
上 Outerhout 的文章。关于更多网络模拟分裂编程的信息,参考 ns paper[12]
两种语言分别何时使用?使用两种语言我们将面对不同语言使用在不同场合的
问题
1
- 7. 我们基本的建议是:OTcl 适用于:
配置、安装和一次性的工作
如果你能够使用已有的 C++对象做你所希望的
C++适用于:
如果你做的是需要流中每个包都运行的工作
如果你不得不改变了 C++中已有的类,使他们不可预测
例如,链接是 OTcl 对象,他集合了延迟,排队和可能的丢失模型。如果仅仅使
用这些,可以使你的实验成功,那当然最好。但是如果你想做得更实际点(需要
一种特殊的排队策略和模型) ,你就需要新的 C++对象了。
显然这里存在灰色区域:大多数路有在 OTcl 中实现(虽然核心 Dijkstra 算法是
在 C++中实现的)。同时,我们模拟 HTTP 时,让所有数据流在 OTcl 中实现,而
所有的包处理却在 C++中。除非在模拟时间内每秒有 100 个数据流, 不然这种方
法是可行的。总的来说,如果我们需要在每秒钟调用许多次 Tcl,那最好还是将
这些代码移植入 C++代码中。
3.2 代码综述
在这个文档里面,我们用名词解释器(interpreter)表示 OTcl 里面的解释器。解
释器接口代码在单独的目录:tclcl。其他的仿真代码在目录:ns-2。我们将用符
号~tclcl/<file>来表示在 tcl 目录的一个特定的<file>,同样的,我们将用符号
~ns/<file>来表示 ns-2 目录下一个特定的<file>。
在~tclcl 里面定义了很多类。 我们只着重讲述在 ns 中使用的 6 个类: 类 Tcl (在
3。3 节) ,它包含 c++代码将要用来访问解释器的方法;TclObject 类(sec 3.4)
是所有模拟器对象的基类。 这些对象在编译层次也有对应; TclClass 类(section3。
5 ) 定 义 了 被 解 释 类 的 层 次 , 以 及 允 许 用 户 实 例 化 TclObject 的 方 法 ; 类
TclCommand(section3.6)用来定义简单的全局解释器命令。类 EmbeddedTcl
(section3。7)含有装载高层次内建命令的方法,这些命令使得仿真的配置更加
容易。最后,类 InstVar(section3.8)含有访问 c++成员变量作为 otcl 瞬时变量
的方法。
本章介绍的过程与函数可以在~tclcl/tcl.{cc,h},~tclcl/tcl2.cc,~tclcl/tcl-object.tcl,
和~tclcl/tracedvar.{cc,h}找到。文件~tclcl/tcl2c++.c 用来建立 ns,而且在本章有简
略的介绍。
2
- 8. 3.3 类 Tcl
Tcl 类 封装 OTcl 解释器的真正的实例,并提供与解释器访问和交流的方法,本节
描述的方法和用 c++代码的 ns 程序员相关,该类提供了以下操作方法.
获得 tcl 实例的一个指针
通过解释器激活 OTcl 过程
追踪和将结果返回解释器
报告错误状态并退出统一的状态
存储并查找”TclObject”
获得对解释器的直接访问
在下面各小节中,我们分别介绍各种方法
3.3.1 获取类 Tcl 的实例的指针(reference)
一个简单的类的实例被作为一个静态成员变量,在~tclcl/Tcl.cc 中声明;而程序
员必须获取一个这个实例的指针去访问这个部分描述的其他方法。 这种访问这个
实例的(命令形式的)描述为:
Tcl& tcl = Tcl::instance();
3.3.2 激活 OTcl 过程
通过 Tcl 实例,一共有四种方式可以激活一个 OTcl 命令。从他们使用参数的方
面来说,有本质的不同。每个函数都传递一个字符串给解释器,然后解释器通过
一个全局文本来识别这个字符串。 如果解释器返回一个 TCL_OK,则这些函数将
会返回一个相应 OTcl 过程,如果解释器返回一个 TCL_ERROR,则函数调用
。用户可以重载这个过程,让它;忽略某些种类的错误。OTcl 编程的
tkerror()
复杂过程属于本文件讨论范围之外。下一小节(3.3.3)将描述获取解释器返回结
果的方法。
tcl.eval(char* s)调用 Tcl_GlobalEval(),通过解释器执行 s。
tcl.evalc(const char* s)保存字符串参数 s。它将字符串 s 复制到中间
缓冲区; 然后再在中间缓冲区里面调用前面的 eval(char* s) 。
tcl.eval 假设命令已经存在于类 internal bp_;它直接调用 tcl.eval
() (char*
bp_).缓冲区自己的指针可以通过方法 tcl.buffer(void)获得。
tcl.evalf(const char* s,.....)是一个类似于的 Printf(3)。它在内部使用
vsprintf(3)来创建输入字符串。
3
- 9. 作为一个例子,这里有一些使用上面所讲函数的使用方法:
Tcl& tcl = Tcl::instance();
char wrk[128];
strcpy(wrk, "Simulator set NumberInterfaces_ 1");
tcl.eval(wrk);
sprintf(tcl.buffer(), "Agent/SRM set requestFunction_ %s", "Fixed");
tcl.eval();
tcl.evalc("puts stdout hello world");
tcl.evalf("%s request %d %d", name_, sender, msgid);
3.3.3 从解释器传出或传入结果
当解释器调用一个 C++方法时,它所期待的是结果能够返回私有成员变量
tcl_->result.两个方法可以设置这个变量
tcl.result(const char* s)
将结果字符串 s 传回给解释器。
tcl.resultf(const char* fmt,...)
Varargs(3) Variant of above 用 vsprintf(3)来格式化结果,再将结果字符串
返回给解释器。
if (strcmp(argv[1], "now") == 0) {
tcl.resultf("%.17g", clock());
return TCL_OK;
}
tcl.result("Invalid operation specified");
return TCL_ERROR;
反之,当一个 C++方法激活一个 OTcl 命令,解释器返回结果到 tcl_->result.
tcl.result(void)必须用于取回结果。需要注意的是这里的结果是一个字符
串,它必须被转化成一个适合结果类型的内部格式。
tcl.evalc("Simulator set NumberInterfaces_");
char* ni = tcl.result();
if (atoi(ni) != 1)
tcl.evalc("Simulator set NumberInterfaces_ 1");
3.3.4 错误报告和退出
4
- 10. 在编译代码里,提供了一种统一的报告错误的方法。
tcl.error(const char* s)完成以下功能:将 s 写入 stdout;将 tcl_->result 到
stdout;退出,并把 error code 置 1。
tcl.resultf("cmd = %s", cmd);
tcl.error("invalid command specified");
/*NOTREACHED*/
注意,这里所讲的返回的 TCL_ERROR 和我们前面一小节(3.3.3)讲的调用
Tcl::error()有些小的差别。前者在解释器内产生一个特例;用户可以捕捉这个特
例而可能从错误中恢复。如果用户没有设置专门的陷阱,解释器将打印出一个追
踪堆栈,然后退出。但是,如果代码激活 error(),那么仿真的用户将不能捕获这
个错误;同时,ns 也将不会打印出任何追踪堆栈。
3.3.5 解释器内的哈希函数
ns 将每个 TclObject 在编译层次的的一个指针存在一个哈希表中;这样就能快速
获取该对象了。这个哈希表在解释器的内部。ns 用户以 TclObject 的名字为关键
字在哈希表中进行插入,查找或者删除 TclObject 的操作。
tcl.enter(TclOject* o)将插入一个指向 TclObject 的指针到哈希表中。
tcl.lookup(char* s) 将 返 回 名 为 s 的 TclObject 。 可 以 这 样 被 使 用 :
TclObject::lookup()。
tcl.remove(TclObject* o)将删除哈希表中 TclObject 的参考。可以用
TclClass::delete_shadow()来移出哈希表中存在的入口,而此时该入口对
应的对象已经被删除了。
这些函数是在类 TclObject 和 TclClass 内部被使用的。
3.3.6 解释器上的其他操作
如果以上的方法仍然不够,那么我们必须获得一个解释器的句柄,写我们自己的
函数。
tcl.interp(void)返回一个解释器的句柄,这个句柄被储存在类 Tcl 里。
3.4 类 TclObject
5
- 11. 类 TclObject 是在解释和编译层次中所有类的基类。每个类 TclObject 的对象都是
用户在解释器中创建的。一个与之等价的影子对象(shadow object)同时也在编
译层次中被创建。这两个对象紧密的联系在一起。下一部分所描述的类 TclClass
就包含了执行这种映射的机制。
在文档剩余的部分,我所说的对象通常指的是TclObject1。照此,我们所涉及到
的对象要么在类TclObject里,要么是类TclObject的派生类。如果有必要的话,我
们将清楚的标出对象到底是解释器中的,还是编译器中的。因此,我们将用解释
对象和编译对象两区别这两者,这些将在编译代码中分别标志出来。
同 ns 版本1的差别:和版本1不同的是,类 TclObject 包含了早期的 NsObject
的功能。因此,它储存了变量绑定的接口,这些变量绑定绑定了解释对象中的
Otcl 实例变量和相应的编译对象中的 C++成员变量。 这种绑定比 ns 版本1要强,
任何 Otcl 变量的变化都是被跟踪的,而且在 Otcl 和 C++的值被解释器访问后要
保持一致。 (这里,可以看出,这个绑定是单向的) 这种一致性是由类 InstVar(3.8)
来完成的。同样,和 ns 版本1不同的是类 TclObject 的对象不再储存在一个全局
的链表中。而是储存在类 Tcl(3.3.5)的一个哈希表中。
TclObject 配 置 的 例 子 : 下 面 的 例 子 是 一 个 SRM agent 的 配 置 ( 类
Agent/SRM/Adaptive)。
set srm [new Agent/SRM/Adaptive]
$srm set packetSize_ 1024
$srm traffic-source $s0
根据 ns 里的转换, Agent/SRM/Adaptive 是类 Agent/SRM 的子类, Agent/SRM
类 而
又是 Agent 的子类,且 Agent 是 TclObject 的子类。相应的编译类层次结构是
ASRMAgent 由 SRMAgent 派生,SRMAgent 由 Agent 派生,最终由 TclObject 派
生。上面例子中第一行是一 TclObject 的创建(或撤销)(3.4.1);下一行配置一个约
束变量(3.4.2);最后一行解释对象要激活一个 C++方法充当一个实例过程(3.4.4)。
3.4.1 创建和撤销 TclObjects
当用户创建一个新的TclObject,通常调用过程new()和delete();这些过程定义在
~tclcl/tcl-object.tcl中。他们可以用于创建和撤销一切类的对象,包括TclObject2。
在这部分中,我们将描述创建TclObject时内部行为是如何发生的。
创建 TclObjects 用户可以用 new()方法创建一个解释类的 TclObject。这时解释器
将执行这个对象的构造函数 init(),同时传递用户提供的一切参数。ns 将自动创建
1
在ns和ns/tclcl发行的最新的书里,这个对象被重新命名为SplitObject,这更精确的反映了这个对象存在的本质。但是,现在,
我们将继续使用TclObject这个名字来指代这些对象和这个类
2
做为例子,类Simulator、Node、Link或者rtObject不是由类TclObject派生而来。 这些类中的对象因此不是TclObject。但是,一
个Simulator、Node、Link或者route 对象也是使用ns中的new过程实例化的。
6
- 12. 相应的编译对象。影子对象(shadow object)是通过基类 TclObject 的构造函数被创
建的。因此,新的 TclObject 的构造函数必须首先调用父类的构造函数。new{}
方法返回一个对象的句柄,而用户可以通过这个句柄对该对象进行其他操作。
下面是 Agent/SRM/Adaptive 的构造函数:
Agent/SRM/Adaptive instproc init args {
eval $self next $args
$self array set closest_ "requestor 0 repairor 0"
$self set eps_ [$class set eps_]
}
当解释器实例化一个新的 TclObject 时,将完成下面一系列的动作。为了方便起
见,我们就描述执行创建一个 Agent/SRM/Adaptive 对象的步骤。他们是:
1. 从 TclObject 的名字空间中获取一个独一无二的新的对象的句柄。这样句
柄将被返回给用户。大多数 ns 中的句柄都是以_o(NNN)形式出现的,这
里(NNN)是一个整数。 这个句柄是由 getid()产生的。 它可以从 C++中的方
法 name() {}获得。
2. 执行新对象的构造函数。任何特定的用户输入参数将作为构造函数的参
数传入。这个构造函数必须激活 其父类的构造函数。
以上面例子来说,Agent/SRM/Adaptive 在第一行就调用了其父类。
注意:每个构造函数,依次激活其父类的构造函数 ad nauseum。那么 ns
中的最后一个构造函数就是 TclObject 的构造函数。 这个构造函数用来创
建对应的影子对象(shadow object),执行其他初始化工作和绑定变量。我
们将在后面给出例子。我们一般最好在构造函数中首先调用父类的构造
函数, 然后才是所需要的初始化工作。 这样可以使影子对象(shadow object)
先被建立,从而有变量可以绑定。
3. TclObject 的 构 造 函 数 为 类 Agent/SRM/Adaptive 激 活 实 例 过 程
create-shadow()。
4. 当影子对象(shadow object)建立好后,ns 调用所有编译对象的构造函数, 他
们每个都将可能建立相应的变量绑定,同时执行必要的初始化过程。因
此我们应该把调用父类的构造函数的语句放在类初始化语句之前。
5. 在影子对象(shadow object)成功创建以后,create_shadow(woid)
(a) 如前(3.3.5)所述将新的对象加入到 TclObject 的哈希表中
(b) 使 cmd()成为一个新创建的解释对象的实例化过程。这个实例化过程
会调用编译对象中的 command()方法。在后面部分(3.4.4),我们将介
绍 command 方法是如何定义及激活的。
注意,所有上述的映射机制只有当用户用过解释器创建新 TclObject 的时候才有
效。但是当程序员只是单向的创建编译 TclObject,这个机制将会失效。因此,
程序员最好不要直接用 C++的新方法来创建编译对象。
7
- 13. 撤销 TclObject 操作 delete 将撤销解释对象, 同时也撤销相应的影子对象(shadow
object)。例如,use-scheduler{<scheduler>}用 delete 过程删除默认的链表计划(list
scheduler),同时在原处实例化一个替代的计划。
Simulator instproc use-scheduler type {
$self instvar scheduler_
delete scheduler ;#first delete the existing list scheduler
set scheduler_ [new Scheduler/$type]
}
同构造函数一样,对象的析构函数必须在最后部分明确的调用其父类的析构函
数。TclObject 的析构函数将激活实例过程 delete-shadow,从而激活对应的编译
方法去撤销影子对象(shadow object)。解释其自己将撤销解释对象。
3.4.2 变量绑定
大多数情况来说,获取编译的成员变量只能通过编译代码,而获取解释的成员变
量则只能通过解释代码;但是,在他们之间建立一个双向连接,是有可能的。这
样可以使编译的成员变量和解释的成员变量代表的是同样的值,而且他们中的任
何一个值的变化可以使另一个相应的变为同样的值。
这种绑定是由编译的构造函数在对象实例化的时候建立的; 它也自动的可以被解
释对象以实例变量的身份获得。ns 支持五种不同的数据类型:实型(reals),带宽变
量(bandwidth valued variables),时间变量(time valued variables),整型(integers),和
布尔型(booleans)。怎样在 OTcl 中说明这些值的语法是随类型不同而不同的。
实型和整型变量以“一般”形式("normal" form)说明,例如
$object set realvar 1.2e3
$object set intvar 12
带宽象实型一样说明,而且后面可以加/不加后缀‘k’或‘K’表示
kilo-quantities,后缀‘m’或‘M’表示 megaquantities。最后可以加一个
后缀‘B’表示单位是位每秒。默认的带宽单位是字节每秒。例如,下
面所有的表示方式是等价的:
$object set bwvar 1.5m
$object set bwvar 1.5mb
$object set bwvar 1500k
$object set bwvar 1500kb
$object set bwvar .1874MB
$object set bwvar 187.5kB
8
- 14. $object set bwvar 1.5e6
时 间 型 象 实 型 一 样 说 明 , 可 以 加 上 后 缀 ‘ m’ 表 示 时 间 单 位 微 秒
‘n’表示时间单位纳秒(nano-seconds),或者‘p’表示时
(mili-seconds),
间单位皮秒(pico-seconds)。默认的时间单位是秒。例如,下面所有的表
示方式是等价的:
$object set timevar 1500m
$object set timevar 1.5
$object set timevar 1.5e9n
$object set timevar 1500e9p
注意,我们也可以在最后加上一个‘s’表示这个实数是时间。ns 将会忽
略除了有效的实数表示和最后所带的‘m’‘n’或‘p’外的任何符号。
布尔既可以表示成整型,也可以表示成‘T’或‘t’
(真)。除了第一个
字母以外的符号将被忽略。如果这个值既不是整型, 也不是真值,那么,
其默认为假。例如,
$object set boolvar t ;#置为真
$object set boolvar true
$object set boolvar 1 ;# 或者是其他非零值(均为真)
$object set boolvar false ;#置为假
$object set boolvar junk
$object set boolvar 0
下面是ASRMAgent的构造函数3。
ASRMAgent::ASRMAgent() {
bind("pdistance_", &pdistance_); /*实型可获得*/
bind("requestor_", &requestor_); /*整型可获得*/
bind_time("lastSent_", &lastSessSent_); /*时间型可获得*/
bind_bw("ctrlLimit_", $ctrlBWLimit_); /*带宽型可获得*/
bind_bool("running_", $running_); /*布尔型可获得*/
}
注意:上述的所有函数都需要两个参数,OTcl 变量的名字和相应绑定的编译成
员变量的地址。通常来说这些绑定是在构造函数执行过程中建立的,但是也可以
通过其他的方式进行绑定。 我们将在以后的章节(3.8)类 InstVar 中讨论其他的替
代方法。
3
注意这个构造函数为了突出变量的绑定机制,因而有些大的修改
9
- 15. 每个被绑定的变量将在对象建立的初始化时被自动的赋予默认值。 这些默认值被
当作解释类变量。这个初始化过程是通常在 init-instvar()中被执行的,而这又是
被类 InstVar 的方法激活的(这个将在 3.8 中介绍)。init-instvar()核查解释对象的
类和它的所有父类,从而找到第一变量的第一个类。它将利用在那个类中变量的
值来初始化这个对象。大部分绑定的初始化值定义在~ns/tcl/lib/ns-default.tcl。
例如,如果下面的类的变量是为 ASRMAgent 定义的:
Agent/SRM/Adaptive set pdistance_ 15.0
Agent/SRM set pdistance_ 10.0
Agent/SRM set lastSent_ 8.345m
Agent set ctrlLimit_ 1.44M
Agent/SRM/Adaptive set running_ f
因此,对于每个新的 Agent/SRM/Adaptive 对象设置 pdistance_的值为 15.0,把
lastSent_的值设为 8.345m,这个是在其父类的变量中设置的;而 strlLimit_的值
为 1.44M 是在它的上两层父类变量中设置的;running_的值为 false;实例变量
pdistance_没有被初始化,因为它不存在于解释对象层次的任何现存的类中。用
户可以在自己的模拟脚本里有选择的覆盖这些过程,同时也可以避免这种警告
(将没有赋值的变量赋值) 。
注意:实际的绑定过程是在类 InstVar 的对象实例化时发生的。类 InstVar 中的每
个对象绑定一个编译成员变量和一个解释成员变量。一个 TclObject 储存一个
InstVar 对象及其相应的成员变量的列表。这个列表头都被存储在 TclObject 的成
员变量 instvar_里。
最后一点需要注意的是,ns 将确保解释对象和编译对象中的变量的实际值要在
任何时候保持一致。但是,如果编译对象中有一些方法和其他变量跟踪这个变量
的值,当这个变量改变的时候,他们必须被明确的激活或者改变。这就通常需要
用户增加一些附加元素(primitives)。在 ns 中增加这种附加元素的一种方法是通
过下一节将介绍的 command()方法。
3.4.3 变量跟踪
除变量绑定以外,TclObject 同样也可以跟踪 C++和 Tcl 实例变量。一个被跟踪的
变量可以既在 C++又在 Tcl 中创建和设置。如果在 Tcl 层建立变量跟踪,变量必
须在 Tcl 下是可视的(visible),这也就是说它必须是一个绑定的 C++/Tcl 或者是纯
的 Tcl 实例对象。trace 方法的第一个参数是变量的名字。第二个参数是可选的,
为了说明负责跟踪那个变量的跟踪对象。如果跟踪对象没有被说明,那么拥有这
个变量的对象将负责跟踪它。
10
- 16. 如果一个 TclObject 去跟踪变量,它必须扩展 C++中的 trace 方法,那本来是一个
定义在 TclObject 中的虚函数。Trace 类只实现一个简单的 trace 方法,因此,它
可以作为一个一般的变量跟踪函数。
class Trace : public Connector {
...
virtual void trace(TracedVar*);
};
下面是一个在 Tcl 中设置跟踪变量的简单例子:
# $tcp 跟踪它自己的变量 cwnd_
$tcp trace cwnd_
# $tcp 中的变量 ssthresh_ 被一个一般的 $tracer 跟踪
set tracer [new Trace/Var]
$tcp trace ssthresh_ $tracer
如果一个 C++变量是可跟踪的,那么它必定是类 TracedVar 的派生类。虚基类
TraceVar 拥有跟踪变量的名字,所属者和跟踪对象。 TraceVar 派生的类都必须
从
实现虚函数 value,它是把一个字符缓冲区当作一个参数,并把变量的值放入这
个缓冲区。
class TraceVar {
...
virtual char* value(char* buf) = 0;
protected:
TraceVar(const char* name);
const char* name_; //变量的名字
TclObject* owner_; //拥有这个变量的对象
TclObject* tracer_; //当变量改变时反馈
...
};
TclCL 库里有两种 TraceVar 类,TracedInt 和 TracedDouble。这些类可以分别用于
基本类型 int 和 double。TracedInt 和 TracedDouble 都重载了像赋值,增加和减少
那样可以改变变量值得运算符。这些重载运算符用 assign 方法将新值赋给变量,
并且当新值与旧的不同时调用跟踪对象(tracer)。TracedInt 和 TracedDouble 同样
也实现了他们的 value 方法,用于将变量的值用字符串输出。输出的宽度和精度
可以事先申明。
3.4.4 command 方法:定义和激活
11
- 17. 对于每个建立的 TclObject,ns 都将建立实例过程 cmd(),作为一个挂钩使编译的
影子对象可以执行一些方法。过程 cmd()自动激活影子对象的方法 command(),
并将 cmd()的参数以向量的形式传递给 command()方法。
用户可以通过下面两种方法中的一种来激活 cmd():显示地调用过程,将所要进
行的操作作为第一个参数;隐示地调用,就好像有一个同名的实例对象作为所需
的操作。大多数模拟脚本都采用后者,因此,我们将先介绍那种调用方式。
我们都知道 SRM 中的距离计算是在编译对象中执行的;但是,它却通常被解释
对象使用。它通常以如下方式被调用:
$srmObject distance? (agentAddress)
如果没有实例过程叫做 distance?, 那么解释器将调用实例过程 unknown(),这个过
程在基类 TclObject 中定义。然后 unknown 过程将调用
$srmObject cmd distance? <agentAddress>
通过编译对象的 command()过程来执行操作。
当然,用户可以显示的直接调用操作。其中一个原因可能是用一个同名的实例过
程来重载这个操作。例如,
Agent/SRM/Adaptive instproc distance? addr {
$self instvar distanceCache_
if ![info exists distanceCache_($addr)] {
set distanceCache_($addr) [$self cmd distance? $addr]
}
set distanceCache_($addr)
}
我们现在就来说明 command()方法是怎样定义的,以 ASRMAgent::command()为
例。
int ASRMAgent::command(int argc, const char*const*argv) {
Tcl& tcl = Tcl::instance();
if (argc == 3) {
if (strcmp (argv[1],"distance?") == 0) {
int sender = atoi (argv[2]);
SRMinfo* sp = get_state(sender);
tcl.tesultf("%f", sp->distance_);
return TCL_OK;
}
12
- 18. }
return (SRMAgent::command(argc,argv));
}
我们可以从上述代码中得到一下几点提示:
函数调用时需要两个参数:
第一个参数(argc)代表解释器中该行命令说明的参数个数。
命令行向量(argv)包括:
——argv[0]为方法的名字,"cmd"。
——argv[1]为所要求的操作。
——如果用户还有其他特殊的参数, 他们就被放在 argv[2...(argc-1)]。
参数是以字符串的形式传递的;他们必须被转换成适合的数据形式。
如果操作成功匹配,将通过前面(3.3.3)的方法返回操作的结果。
command()必须以 TCL_OK 或 TCL_ERROR 作为函数的返回代码,来
表明成功或者失败。
如果操作在这个方法中没有找到匹配的,它将调用其父类的 command
方法,同时也就返回相应的结果。
这就允许用户创建和对象过程或编译方法一样层次特性的操作。
当 command 方法是为多继承的类定义时,程序员可以自由的选择其中
一个实现;
1)可以调用其中一个的父 command 方法,然后返回其相应的结构,或
2)可以以某种顺序依次调用每一个的父 command 方法,然后返回第一个
调用成功的结果。如果没有调用成功的,将返回错误。
在 我 们 这 个 文 件 里 , 我 们 把 通 过 command() 执 行 的 操 作 叫 做 准 成 员 函 数
(instproc-likes)。这个名字反映了这些操作作为一个对象的 OTcl 实例过程的用途,
但是也存在一些微小的实际和用途上的差距。
3.5 类 TclClass
这个编译类(class TclClass)是一个纯的虚类。从这个基类派生的类提供两种功能:
建立与编译类镜像的解释类;和提供实例化新 TclObject 的方法。每一个派生类
和一个编译类层次特定的编译类关联,同时可以实例化这个关联类的新对象。
举个例子,类 RenoTcpClass。它是由类 TclClass 派生,和类 RenoTcpAgent 关联。
它将实例化类 RenoTcpAgent 的新对象。RenoTcpAgent 编译类层次结构是:
RenoTcpAgent 由 TcpAgent 派生,TcpAgent 由 Agent 派生,而 Agent(粗略地说)
由 TclObject 派生。RenoTcpAgent 是这样定义的:
13
- 19. static class RenoTcpClass: public TclClass {
public:
RenoTcpClass() : TclClass("Agent/TCP/Reno") {}
TclObject* create(int argc, const char*const* argv) {
return (new RenoTcpAgent());
}
} class_reno;
我们可以从这个定义里面得到几点提示:
1. 类只定义了构造函数和另外一个方法来创建关联的 TclObject 实例。
2. 当静态变量 class_reno 第一次创建时,ns 将执行 RenoTcpClass 的构造
函数。这就建立了适当的方法和解释类层次。
3. 构造函数明确说明了解释类为 Agent/TCP/Reno。这也同时隐示的说明
了解释类的层次结构。
注意 ns 的转义符号,斜杠('/')是一个分隔符。对任意类 A/B/C/D,类
A/B/C/D 是类 A/B/C 的子类,而 A/B/C 又 是 A/B 的子类,同理
A/B 是 A 的子类,而 A 本身是 TclObject 的子类。
上面的例子, TclClass 的构造函数创建了三个类,Agent/TCP/Reno 子类,
Agent/TCP 子类,Agent 子类。
4. 这个类与类 RenoTcpAgent 关联;它将在这个关联类中创建新对象。
5. 方法 RenoTcpClass::create 返回类 RenoTcpAgent 中的 TclObject。
6. 当用户定义一个新 Agnet/TCP/Reno,常规的 RenoTcpClass:create 将被
激活。
7. 参数向量(argv)包含:
——argv[0]为对象名。
——argv[1...3]为$self,$class 和$proc。由于 create 通过实例过程
create-shadow 调用 create,argv[3]中 包含 create-shadow。
——argv[4]为用户提供的任何附加参数(以字符串形式传递) 。
类 Trace 表明参数由 TclClass 的方法控制。
class TraceClass : public TclClass {
public:
TraceClass() : TclClass("Trace") {}
14
- 20. TclObject* create(int args, const char*const* argv) {
if (args >= 5)
return (new Trace(*argv[4]));
else
return NULL;
}
} trace_class;
新的 Trace 对象的创建:
new Trace "X"
最后,将有条理的详细的描述解释层次结构是怎样建立的:
1. 当 ns 第一次启动时,对象的构造函数被执行。
2. 这个构造函数将;以解释类名作为参数调用 TclClass 的构造函数。
3. TclClass 的构造函数储存这个类的名字,并把这个对象插入到 TclClass 对
象的链表中。
4. 模拟器初始化过程中,Tcl_AppInit(void)调用 TclClass::bind(void)。
5. 对在 TclClass 对象链表中的每一个对象,bind()调用 register(),以解释类名
作为参数。
6. register()建立类层次,建立那些需要,但是又没有建立的类。
7. 最后,bind()为这个新类定义实例过程 create-shadow 和 delete-shadow。
3.5.1 怎样绑定静态 C++类成员变量
在 3.4 中,我们怎样将 C++对象中的成员变量导入 OTcl 空间。但是这个并不适
用于 C++类的静态成员变量。当然,你可以为每个 C++对象创建一个 OTcl 静态
成员变量;显然这违反了静态成员的本义了。
我们不能用相似于绑定 TclObject 基于 InstVar)
( 的解决办法来解决这种绑定 (静
态类成员变量的绑定) ,因 TclCL 中的 InstVar 需要一个 TclObject 的出现。但是,
我们可以为相应的 TclClass 创建一个方法, 可以通过这个方法获取 C++类的静态
成员。其过程如下:
1. 创建一个新的 TclClass 的派生类;
15
- 21. 2. 在你的派生类中,申明 bind()和 method()方法;
3. 在用 add_method("your_method")实现 bind()时建立自己的绑定方法,
然后用于实现 TclObject::command()相类似的方法实现 method()中的
handler。注意传递给 TclClass::method()和 TclObject::command()的参
数的个数不一样。前者在开始的时候要多两个参数。
一个例子,我们将展示一个在~ns/packet.cc 里的简单版本的 PacketHeaderClass。
假设我们有以下的类 Packet,它里面有个静态变量 hdrlen_,我们想从 OTcl 中获
得它:
class Packet {
......
static int hdrlen_;
};
然后我们通过以下改变来创建一个获取这个变量的通道:
class PacketHeaderClass : public TclClass {
protected:
PacketHeaderClass(const char* classname, int hdrsize);
TclObject* create(int argc, const char*const* argv);
/*这是两个 OTcl 类的获取方法的实现*/
virtual void bind();
virtual int method(int argc, const char*const* argv);
};
void PacketHeaderClass::bind()
{
/*调用基类的 bind()方法必须在 add_method()方法之前*/
TclClass::bind();
add_method("hdrlen");
}
int PacketHeaderClass:: method(int ac, const char*const* av)
{
Tcl& tcl = Tcl::instance();
/*注意这个参数变化:我们然后就可以像在 TclObject::command()里一样控制他们了*/
int argc = ac -2;
const char*const* argv = av + 2;
if (argc == 2) {
if (strcmp(argv[1], "hdrlen") == 0) {
tcl.resultf(%d", Packet::hdrlen_);
16
- 22. return (TCL_OK);
}
} else if (argc == 3) {
if (strcmp(argv[1], "hdrlen") == 0) {
Packet:;hdrlen_ = atoi(argv[2]);
return (TCL_OK);
}
}
return TclClass::method(ac,av);
}
在这之后,我们可以使用前面的 OTcl 命令来获得或者改变 Packet::hdrlen_的值:
PacketHeader hdrlen 120
set i [PacketHeader hdrlen]
3.6 类 TclCommand
类 TclCommand 仅仅是为 ns 提供一种在全局范围内由解释器执行的一些简单命
令的机制。有两个函数,他们的定义在~ns/misc.cc:ns-random 和 ns-version。这
两个函数被 init_misc(void)启动(init_misc(void)定义在~ns/misc.cc);init_misc 又
是被 Tcl_AppInit(void)在启动时调用。
类 VersionCommand 定义了命令 ns-version。它不需要参数,同时返回
表示 ns 当前版本的字符串。
% ns-version ;#获取当前版本号
2.0a12
类 RandomCommand 定义了命令 ns-random。ns-random 也不需要参数,
同时返回一个整数。这个整数服从在区间[0, 2 31 − 1 ]上的均值分布。
如果给出一个参数,它将把这个参数视为种子。如果这个种子的值为 0,
命令将使用自定义的种子值;否则,它将给随即数字产生器设置特定的
种子,从而获得特定的值。
% ns-random ;#返回一个随机数
2078917053
% ns-random 0 ;#自定义地设置种子
858190129
% ns-random 23786 ;#设置特定的种子得到特定的值
17
- 23. 23786
注意:我们通常不建议创建可以由用户调用的顶层命令。我们现在说一下怎样定
义一个新的命令,以类 say_hello 为例。 这个例子定义了命令 hi。然后打印出字
符串"hello world"加上用户后面接的任何参数。例如,
% hi this is ns [ns-version]
hello world, this is ns 2.0a12
1. 命令必须是类 TclCommand 的派生类。类的定义是:
class say_hello : public TclCommand {
public:
say_hello();
int command(int argc, const char*const* argv);
};
2. 该类的构造函数必须以命令为参数调用 TclCommand 的构造函数;例如,
say_hello() : TclCommand("hi") {}
TclCommand 的 构 造 函 数 将 设 置 "hi" 为 全 局 过 程 , 可 以 调 用
TclCommand::dispatch_cmd()。
3. 方法 command()必须有它特定的动作。
方法传递了两个参数。第一个参数是 argc,是用户实际传递参数的个数。
用户实际传递的参数将以一个参数向量 argv 被传递,它包括下面的:
——argv[0]为命令名(hi)。
——argv[1...(argc-1)]为用户在命令行附加的参数。
command()是被 dispatch_cmd()调用的。
#include <streams.h> /*因为我们要用到 I/O 流*/
int say_hello::command(int argc, const char*const* argv) {
cout << "hello world:"
for (int i = 1; i< argc; i++)
cout << ' ' << argv[i];
cout << ' n';
return TCL_OK;
}
4. 最 后 , 我 们 需 要 类 TclCommand 的 一 个 实 例 , 这 个 可 以 在 方 法
init_misc(void)里面创建。
new say_hello;
18
- 24. 注意:通常还有一些函数如 ns-at 和 ns-now 可以通过这种方式调用。大多数这些
函数都包含在已有的类中。 特别地, ns-at 和 ns-now 可以通过 TclObject 调度获得。
这些函数在~ns/tcl/lib/ns-lib.tcl 中定义。
% set ns [new Simulator] ;#获得新的模拟类实例
_o1
% $ns now ;#向模拟器查询当前时间
0
% $ns at ... ;#为模拟器定义特定的操作
...
3.7 类 EmbeddedTcl
ns 允许对编译代码或者解释代码的功能扩展,这扩展代码将在初始化的时候被
执行。例如,在脚本~tclcl/tcl-object.tcl 或 ~ns/tcl/lib 里的脚本。这种装载和赋值
是通过类 EmbeddedTcl 来完成的。
最简单的扩展 ns 的办法是在~tclcl/tcl-object.tcl 脚本中加入 OTcl 代码或者在
~ns/tcl/lib 目录下加入脚本。注意,对于后一种情况,由于 ns 是自动读取
~ns/tcl/lib/ns-lib.tcl 脚本,因此程序员必须加几行代码, 以便使新添的这些脚本可
以在 ns 启动时被自动加入源文件库中。例如,文件~ns/tcl/mcast/srm.tcl 定义了一
些执行 SRM 的实例过程。在~ns/tcl/lib/ns-lib.tcl 里,我们就有下面的语句:
source tcl/mcast/srm.tcl
使得 srm.tcl 在 ns 启动时自动被加入源文件库中。
EmbeddedTcl代码有三点需要注意的地方:第一,如果在执行过程中遇到错误,
ns将停止运行。第二,用户可以显示的重载脚本中的任何代码。特别地,他们可
以在做了自己的修正后重新定义整个源文件库脚本。最后,从添加了脚本到
~ns/tcl/lib/ns-lib.tcl之后,每次改变他们的脚本,用户必须重新编译他们修改的不
用,才能使之有效。当然,对于大多数情况来说4,用户加入他们自己的源文件
来重载embedded代码。
这一小节的剩余部分将介绍怎样把单个的脚本直接整合到 ns 里。第一步将脚本
转化成 EmbeddedTcl 对象。下面的代码行扩展 ns-lib.tcl,创建了 EmbeddedTcl 对
象,命名为 et_ns_lib:
tclsh bin/tcl-expand.tcl tcl/lib/ns-lib.tcl |
../Tcl/tcl2c++ et_ns_lib > gen/ns_tcl.cc
4
少数不行的情况是当一定的变量可能被或不被定义,或者相反的脚本包含代码而不是过程和变量定义并且直接执行不可以逆转
的行为
19
- 25. 脚本~ns/bin/tcl-expand.tcl 通过将 ns-lib.tcl 中所有的 source 行替代成相应的 source
文 件 来 扩 展 ns-lib.tcl 。 程 序 , ~tclcl/tcl2cc.c , 将 OTcl 代 码 转 化 成 等 价 的
TmbeddedTcl 对象 et_ns_lib。
当初始化时,激活的方法 EmbeddedTcl::load 显示地给数组赋值。
— ~tclcl/tcl-object.tcl 是 被 方 法 Tcl::init(void) 载 入 ; Tcl_AppInit() 激 活
Tcl::Init()。准确的加载 命令的语法为:
et_tclobject.load();
— 同样的,~ns/tcl/lib/ns-lib.tcl 直接被~ns/ns_tclsh.cc 中 Tcl_AppInit 加载。
et_ns_lib.load();
3.8 类 InstVar
这个小节描述类 TnstVar 的内部。这个类定义了将编译的影子对象(shadow object)
中的一个 C++成员变量绑定到与之对应的解释对象中的 OTcl 实例变量上的方法
和机制。这种绑定可以使在任何时候变量既可以从解释器又可以从编译代码内设
置和获得。
有五种实例变量类:类 InstVarReal,类 InstVarTime,类 TnstVarBandwidth,类
和类 InstVarBool。
InstVarInt, 分别对应绑定 real,
time,bandwidth,integer 和 boolean
类型的变量。
我们现在来介绍实例变量建立的机制。我们用类 InstVarReal 来说明这个概念。
但是,这个机制对于五种类型的实例变量都适合。
当建立一个解释变量来获取一个成员变量,类 InstVar 的成员函数假设他们是在
以一个适当的方法执行脚本;因此,他们不要求解释器确定文本中这个变量是必
须存在的。
为了保证脚本可以按正确的方法执行,如果一个变量的类已经在解释器中建立并
且解释器已经正确地在那个类中打开了一个对象,那么这个变量必须受到限制
(本人理解为一个变量不能同时被两个方法调用)。注意:前者要求当一个给定
类的方法能通过解释器访问它的变量时,必须定义一个关联类 TclClass(3.5)来识
别解释器正确的类层次结构。因此一个方法可以由下面两种方法中的一种创建。
一个隐示的解决方法是当一个新的 TclObject 在解释器内被创建时。这将在解释
器内建立一个方法执行文本。当一个解释的 TclObject 的编译的影子对象(shadow
20
- 26. object)创建时,那个编译对象的构造函数可以绑定他对象的成员变量到解释的实
例变量用过新创建解释对象文本的方式
一个显示的解决方案是在一个 command 函数中定义一个 bind-varialbles 操作,这
个操作可以通过 cmd 方法激活。为了执行 cmd 方法,正确的执行文本方法必须
建立。相似地,编译代码也同样工作在相应的影子对象(shadow object)上,因此
可以安全的绑定需要的成员变量。
一个实例变量是通过说明解释变量的名字和编译对象中成员变量的地址来创建
的。基类 InstVar 的构造函数在解释器里创建一个变量的实例和设置一个常规陷
阱来捕捉通过解释器访问变量的所有通道。
每当解释器读取变量,常规陷阱就会在读取发生之前被激活。按常规再激活相应
的 get 函数,然后将返回变量的当前值。这个值常常用来设置解释变量的值,最
后解释变量的值被解释器读取。
相似地,每当解释器设置变量,常规陷阱将在写操作发生后被激活。按常规可以
得到解释器的当前值,然后激活相应的 get 函数,用解释器中的当前值去设置编
译成员的值。
21
- 27. 第四章
类 Simulator
模拟器全部定义在 Tcl 类 Simulator 中。它提供了一套接口用于配置一次模拟和
为这次模拟选择一个时间调度方案。一个模拟脚本通常以创建这个类的实例开
始,然后调用各种方法来创建节点,拓扑结构和设置模拟的其他方面。Simulator
类的一个子类 OldSim 是用于支持 ns 版本 1 的,从而保证向后兼容的特性。
这 章 所 讲 的 过 程 和 函 数 可 以 在 ~ns/tcl/lib/ns-lib.tcl, ~ns/scheduler.{cc,h},
和,~ns/heap.h 中找到。
4.1 Simulator 初始化
当一个新的模拟对象在 tcl 中创建时,初始化过程将执行以下操作:
初始化包格式(调用 create_packetformat)
创建一个调度器(默认为一个时间调度器(calendar scheduler))
创建一个“空代理”("null agent")(一个可以抛弃的适用于各种场合的接
收器)
包格式初始化设置用于整个模拟的包的偏移长度域。这将在后面的章节(12 章)
中更加详细的说明。调度器以事件驱动的方式执行模拟,也可以用其他的调度器
代替,这些调度器提供了一些不同的语义(下面的小节将有详细的描述)。空代
理是通过下列调用来创建的:
set nullAgent_ [new Agent/Null]
这个非常有用代理通常作为一个接收器,接收丢弃的包或者作为一个目的地,接
收那些没有被计算或者记录的包。
4.2 调度器和事件
模拟器是一个事件驱动的模拟器。现在,模拟器重有四种调度器,每种都是由不
22
- 28. 同的数据结构实现的:一个简单的链表(a simple linked-list),堆(heap),时间队列
(calendar queue),和一种叫做“实时”("real-time")的特殊类型。每一种都将在下
面介绍。调度器通过选择下一个最早事件,执行完它,和返回继续执行下一个事
件的方式工作。用于调度器的时间单位是秒。现在,模拟器是单线程的,所以任
何一个时间只能执行一个事件。如果在同一时刻有多个事件需要执行,那么他们
的执行将按照先调度先分配(first scheduled-first dispatched)的原则执行。同时发生
的事件将不被调度器重新排序(和早期的版本相似) ,所有调度器对应同一输入
应该产生相同的分派顺序。
并行执行事件和预先占用(pre-emption)是不支持的。
一个事件通常包含;一个“开始时间”("firing time")和函数句柄。事件的实际定
义在~ns/scheduler.h:
class Event {
public:
Event* next_; /*事件链表*/
Handler* handler_; /*当事件准备就绪时调用事件的句柄*/
double time_; /*事件准备就绪的时间*/
int uid_; /*事件唯一的 ID*/
Event() : time_(0), uid_(0) {}
};
/*
* 全部事件句柄的基类。当一个事件被调度的
* 时间到了时,它将被传给要使用时间的那个句柄。
* 例如,如果它想被释放,它必须被句柄释放。
*/
class Handler {
public:
virtual void handle(Event* event);
};
基类 Event 派生了两种对象:包和“执行事件”("at-event")。包将在以后的章节
(12.2.1 章)做详细的说明。一个执行事件实际上是一个 tcl 过程,他是按照执
行调度在一个特定时间发生的。这是一个经常使用的模拟脚本。下面是一个简单
的例子,告诉我们怎样使用它:
...
set ns_ [new Simulator]
$ns use-scheduler Heap
$ns_at 300.5 "$self complete_sim"
...
这个 tcl 代码片断首先创建了一个模拟对象。然后用基于堆(heap-based)(见下)
23
- 29. 的调度方式代替了默认的调度实现,最后调度函数$self complete_sim,使之在时
间 300.5(秒)时执行(注意这个特殊的代码片断应该是包含在一个对象实例过
程之中,而$self 的指代对象在这个过程中应该是被正确定义了的) 。执行事件是
被作为句柄在 tcl 解释器中可以有效执行的事件而实现的。
4.2.1 链表式调度器
链表式调度器(class Scheduler/List)是采用一个简单的链表结构来实现的。这个链
表以时间顺序(从最先的到最晚的)来维持的。所以事件的插入和删除需要扫描
链表来找寻合适的入口。选择下一执行事件需要将链表的第一个入口从表头删
除。这种实现保证了事件的执行是以先进先出的方式来执行同时发生的事件的。
4.2.2 堆式调度器
堆式调度器(class Scheduler/Heap)是采用一个堆结构来实现的。这个结构在用于
大数量事件时比链表结构优秀。因为插入和删除的时间复杂度是 O(log n),其中
n 为事件数。这种实现是 ns2 借鉴于 MaRS-2.0 模拟器[1];而 MaRS 自己又是借
鉴 NetSim[14],即使这种线性关系没有被完全证实。
4.2.3 时间队列调度器
时间队列调度器(class Scheduler/Calendar)采用一种一年的桌面时间的模拟数据
结构,在这种数据结构中同一天同一月但是不同年年的事件可以纪录在一天中。
正规描述在[6]中,在 Jain(410页)[15]里有非正式的描述。ns2 中时间队列
的实现由 David Wetherall(现在在 MIT/LCS)实现。
4.2.4 实时调度器
实时调度器(class scheduler/RealTime)尝试着象实时一样同步执行事件。这种调度
器当前只是作为链表式调度器的一个子类来实现的。ns 中的实时性能仍在开发
之中。但是也被用于将 ns 模拟的网络导入实时的拓扑进行实验,这样可以简单
的设置网络拓扑,交错通信(cross-traffic)等。这只能用于相对较低的网络数据通
信率,比如模拟器必须能够跟上真实世界的包的到达率,这种同步在当前来说不
是必须的。
4.2.5 ns 中的调度器时钟的精确度
24
- 30. 调度器时钟的精确度的定义是模拟器能够表示的最小的时间跨度。ns 的时钟变
量是一个双精度实型变量。 像每一个 IEEE std 的浮点数一样,一个双精度实型(有
64 位)必须分配下面的位在符号,指数和尾数域之间。
符号(sign) 指数(expenent) 尾数(mantissa)
1 bit 11 bit 52 bit
任何浮点数都可以表示成( X ∗ 2 n )的形式,这里 X 是尾数,n 是指数。因此 ns 的
时钟的精度可以表示成( 1 / 2 52 )。随着模拟的进行,剩余的表示时间的位不断减少,
因此精度也随之减少。 对于 52 位来说,我们可以说( 2 ( 40 ) )以上的时间可以被认为
是准确的。由于你必须保留 12 位来表示时间的改变,所以任何比这个时间大的
数都不精确。但是( 2 ( 40 ) )是一个非常大的数,我们在 ns 中已经不会考虑时间的精
确度了。
4.3 其他方法
类 Simulator 提供了许多用于设置模拟的方法。他们通常分为三类:创建和管理
拓扑的方法(这些也就包括了节点管理(第五章)和链路管理(第六章),执行 )
跟踪的方法(第二十三章)和帮助处理调度器的函数。下面是与拓扑结构不相关
的模拟方法的清单:
Simulator instproc now ;#返回调度器执行的当前时间
Simulator instproc at args ;#特定时间执行的调度代码
Simulator instproc cancel args ;#取消事件
Simulator instproc run args ;#启动调度器
Simulator instproc halt ;#暂停调度器
Simulator instproc flush-trace ;#擦除所有缓冲区内的跟踪对象
Simulator instproc create-trace type files src dst ;#创建一个跟踪对象
Simulator instproc create_packetformat ;#设置模拟器包格式
4.4 命令一览
概要:
ns <otcl 文件> <参数> <参数> ..
描述:
执行 ns 模拟脚本的基本命令。
25
- 31. 模拟器(ns)是通过 ns 解释器激活,这是一种 vanilla otclsh command shell 的一种
扩展。一次模拟可以在一个 OTcl 脚本(文件)里定义。在目录 ns/tcl/ex 下可以
找到几个 OTcl 脚本的例子。
下面是一个在模拟脚本中常用模拟器命令清单:
set ns_ [new Simulator]]
这个命令创建了一个模拟器对象实例。
set now [$ns_ now]
调度器在模拟时跟踪时间。这个返回给调度器当前时间。
$ns_ halt
这是停止或暂停调度器。
$ns_ run
这是启动调度器。
$ns_ at <time> <event>
这是在一个特定的<time>调度一个<event>(这个事件通常是一段代码)执行。
例如: $ns_ at $opt(stop) "puts NS EXITING.. ; $ns_ halt"
或者 $ns_ at 10.0 "$ftp start"
$ns_ cancel <event>
取消这个事件。从效果来说,事件是从准备好执行的调度器中删除。
$ns_ create-trace <type> <file> <src> <dst> <optional arg:op>
这是在<src><dst>对象之间创建一个<type>类型的 trace-object,同时为了存储跟
踪输出(trace-outputs)将 trace-object 附加到<file>上。如果<op>被定义为"nam",
这就将创建 nam 跟踪文件;否则,如果<op>没有被定义,ns 将以默认方式创建
26
- 34. 第5章
节点和包转发
这一章描述了 ns 中创建拓扑结构的一部分:创建节点。下一章将介绍另一个部
分:节点间的链路。
回忆前面所述,每一次模拟均需要用类 Simulator 的一个简单实例来控制和执行
模拟。这个类提供了创建和管理拓扑结构的实例过程,同时类的内部也存在调用
每种拓扑元素的方法。我们将从介绍类 Simulator 里的过程(5.1)开始。我们然后
介绍类 Node 中获取和操作单个节点的实例过程(5.2)。我们将以介绍 Classifier(5.4)
结束,Classifier 组成了更加复杂的节点对象。
这 章 介 绍 的 过 程 和 函 数 可 以 在 ~ns/tcl/lib/ns-lib.tcl, ~ns/tcl/lib/ns-node.tcl,
~ns/tcl/lib/ns-tmodule.tcl, ~ns/rtmodule.{cc,h}, ~ns/classifier.{cc,h},
~ns/classifier-addr.cc, ~ns/classifier-mcast.cc, ~ns/classifier-mpath.cc 和
~ns/replicator.cc 中找到。
5.1 节点的基本元素
最基础的建立节点的命令:
set ns [new Simulator]
$ns node
实例过程 node 建立包含多个简单的 classifier 对象(5.4)的节点。该节点本身是一
个 OTcl 中单独的一个类。但是,构成节点的大部分构件自身也是 TclObject。一
个(单播)节点的典型结构如图 5.1 所示。这个简单的结构包含两个 TclObject:
一个地址分类器(classifier_)和一个端口分类器(dmux_)。这些分类器的功能是分
派传入的包到正确的代理或者链路出口去。
所有节点包含至少下面的部分:
一个地址或者 id_,当节点建立时,模拟的名字空间将自动加 1(初始值
是 0)。
一个邻居链表(neighbor_),
一个代理链表(agent_),
29
- 35. 图 5.1 单播节点的结构
注意:entry_仅仅是一个标志变量,而不是像 classifier_一样,是一个真实对象。
一个节点类型识别器(nodetype_),和
一个路由模块(将在下面的 5.5 介绍)
默认地,ns 中节点被创建为单播模拟。为了使用多播模拟,模拟创建时,应该
设置为"-multicast on",例如:
set ns [new Simulator -multicast on]
多播模拟节点的内部结构如图 5.2 所示。
当一次模拟使用多播路由, 地址的最高位表示这个特殊的地址是多播地址还是单
播地址。如果该位为 0,则地址表示一个单播地址,否则,地址为多播地址。
5.2 节点方法:设置节点
30
- 36. 图 5.2 一个多播节点的内部结构
设置单个节点的过程可以分为以下几类:
—— 控制函数
—— 地址和端口号管理,单播路由函数
—— 代理管理
—— 添加邻居
我们将在下面的段落中依次介绍各个函数。
控制函数
1. $node entry 返回节点的入口指针。这是第一个控制到达节点的包的元素。
节点实例变量,entry_,储存这个元素的指针。对于单播节点,这是一个
用来查看目的地址高段位的地址分类器。实例变量,classifier_包含这个地
址分类器的指针。但是,对于多播节点,入口指针是 switch_,这 是查看
第一位,从而确定该把包发给单播分类器还是多播分类器合适。
2. $node reset 将重新设置节点上的所有代理。
地址和端口号管理 过程 $node id返回节点的节点号。这个号码是在类Simulator
31
- 37. 用方法$ns node创建每个节点是自动增加和赋值的。类Simulator同时也储存了一
个实例变量数组1,Node_,以节点的id为下标,包含了一个指向下标所表示节点
的指针。
过程$node agent (port)返回一个端口号为 port 的代理的句柄。如果不存在这种代
理,过程返回空字符串。
过程 alloc-port 返回下一个可获得的端口号。它使用了一个实例变量,np_,来跟
踪下一个没有分配的端口号。
过程,add-route 和 add-routes,是单播(26 章)的时候用来增加路由路径从而产
生 classifier_。使用的语法为:$node add-route <destination id> <TclObject>。
TclObject 是 dmux_的入口,节点的多路复用端口,如果,destination id 和这个节
点的 id 一样,它通常是一个发送到那个节点的包的链路的终端,但是也可能是
另外一些分类器或者另外种类的分类器的入口。
$node add-routes <destination id> <TclObjects>用于添加多重路由路径到同一个目
的地,这个目的地必须用于同时分配带宽,这个通过所有链路到达那个目的的带
宽必须是以循环方式平均分配的。它也仅仅用于当实例变量 multiPath_是 1,复
杂 动 态 路 由 (detailed dynamic routing) 策 略 有 效 , 而 且 使 用 一 个 多 路 分 类 器
(multiPath classifier)的时候。我们将在这章的后面部分(5.4)介绍多路分类器的实
现;但是,我们将多路路由的讨论(26 章)延迟到单播路由的章节之后。
add-routes()的对立面是 delete-routes()。它有参数 id,一个 TclObjects 的列表,和
一个模拟器的空代理的指针。 然后它从多路分类器中设置了的路由路径中移出链
表中的 TclObjects。如果分类器中路由入口没有指向多路分类器,那么按照惯例
将直接将 classifier_的入口和相应的设置的空代理移出。
复杂动态路由也使用了两个附加的方法:实例过程 init-routing()设置实例变量
mutiPath_,使之与同名的类变量相同。它也增加了该节点上的路由控制器对象
的一个实例变量,rtObject_。过程 rtObject()返回该节点的路由对象的句柄。
最后,当节点上的一个链路关联改变状态时,过程 intf-changed()可以被网络的动
态代码激活。关于这个过程怎样使用的详细讨论可以在动态网络的章节(28 章)
中找到。
代理管理 过程 attach()可以将给定的代理加入到一个 agents_链表中,分配给该
代理一个端口号和设置它的源地址,设置代理的指针为它的(例如,节点的)
entry(),添加一个节点上多路复用的端口(dmux_)的指针到代理相应的 dmux_分
类器的一块上。
相反地,detach()就是从 agents_中移出代理,代理目标指针和节点 dmux_指向空
1
例如,一个同样也是数组变量的类的实例变量
32
- 38. 代理的入口。
记录邻居 每个节点都保存了一个他附近邻居的链表在他的对象变量, neighbor_
中。过程 add-neighbor()就是增加一个邻居到这个链表中。过程 neighbors()返回这
个链表。
5.3 节点设置接口
注意:这个 API,特别是它至少现在来说还是非常混乱的内部实现,仍然在变化
中。它可能在不久的将来有大的变化。 但是,我们将尽力保持和这章一样的接口。
另外,这个 API 当前没有包含所有旧格式的节点,名义上,使用继承建立的节
点和一部分移动 IP。它主要是定位于创建无线和卫星模拟。[二零零零年九月十
五;修改二零零一年六月]。
Simulator::node-config()采用弹性的和模式化的方式在同一个基类 Node 下来创建
不同节点定义。例如,创建一个有无线通信能力的移动节点,不再需要一个特殊
的节点创建命令,例如,dsdv-create-mobile-node();只需要改变默认参数设置,
例如:
$ns node-config -adhocRouting dsdv
只要放在实际创建节点命令$ns node 之前,就可以了。除了路由模块,还可以在
一个单一节点里合并“专用‘路由函数,而不需要依靠多重继承和其他上层对象
的技巧。我们将在 5.5 种更详细的介绍。关于新节点 API 的函数和过程可以在
~ns/tcl/lib/ns-node.tcl 中找到。
节点设置接口包括两个部分。第一部分处理节点的设置,第二部分实际上创建特
殊类型的节点。我们已经在 5.1 种看到了后者,在这部分我们将介绍设置部分。
节点设置本质上包含在创建节点前定义不同的节点特性。 他们可能包含模拟钟使
用的地址结构类型,定义移动节点的网络成分,打开或者关闭代理/路由/MAC
层的跟踪选项,为无线节点选择 adhoc 路由协议的种类或者定义他们的能量模
型。
例如,设置使用 AODV 协议作为 adhoc 路由协议,并且在层次结构拓扑中的无
线移动节点可以有如下定义。我们假定只打开代理层和路由层的跟踪。同时,我
们假设一个拓扑已经由"set top [new Topography]"实例化。节点设置命令如下:
$ns_ node-config -addressType hierachical
-adhocRouting AODV
-llType LL
33
- 39. -macType Mac/802_11
-ifqType Queue/DropTail/PriQueue
-ifqLen 50
-antType Antenna/OmniAntenna
-propType Propagation/TwoRayGround
-phyType Phy/WirelessPhy
-topologyInstance $topo
-channel Channel/WirelessChannel
-agentTrace ON
-routerTrace ON
-macTrace OFF
-movementTrace OFF
上面选项的默认值都是空,除了-addressingType 为 flat。选项-reset 可以用来重新
设置所有 node-config 的参数为默认值。
节点设置命令可以分别写在不同的行上,比如:
$ns_ node-config -addressingType hier
$ns_ node-config -macTrace ON
可以只调用那些需要改变的选项。例如,在像上面一样为移动节点设置 AODV
之后(和创建 AODV 移动节点之后),我们可以按照如下方法设置 AODV 基站
节点:
$ns_ node-config -wireRouting ON
当基站节点的其他所有特性与移动节点一样时,基站节点就可以采用有线路由,
而移动节点则不行。按照这种方法我们就可以只改动我们需要改动的地方了。
所有在 node-config 命令执行之后创建的节点都拥有同样的性质,直到 node-config
命令中的一部分参数改变并执行了之后。 同样所有参数值在其被显示改变之前是
不会变的。所以,在创建一个 AODV;基站和移动节点之后,如果我们想创建
简单节点,我们将使用下面的 node-config 命令:
$ns_ node-config -reset
这个将所有参数值改变为他们的默认值,也就是设置一个简单节点的值。
当前,这种类型的节点配置是趋向于无线和卫星节点的。表 5.1 列出了这种类型
节点可选的变量。脚本例子~ns/tcl/ex/simple-wireless.tcl 和~ns/tcl/ex/sat-mixed.tcl
提供了使用实例。
34
- 40. 表 5.1 节点可配置的所有选项 (见:tcl/lib/ns-lib.tcl)
5.4 分类器
当接受到一个包时节点的任务就是检查包的域:通常是它的目的地址,有时也可
能是它的源地址。然后它应该绘画出值所要到的出口的接口对象,也就是包的下
一步接收器。
35
- 41. 在 ns 中,这个任务是由一个简单的分类器对象来执行的。多重分类器对象,每
个将察看包的一个特殊部分,在通过节点转发包。ns 中的一个节点可以使用很
多不同类型用于不同目的的分类器。 这个部分将描述一些非常常见或者比较简单
的 ns 分类器对象。
我们将从描述这部分里的基类开始。下个小节描述地址分类器(5.4.1),多播分类
器(5.4.2),多路分类器(5.4.3),哈希分类器(5.4.4),和复制器(5.4.5)。
一个分类器提供一种方法, 这种方法通过一些逻辑准则匹配包和找回另一个基于
匹配结果的模拟对象的指针。每个分类器包含一张将其下标标注为 slot number
的模拟对象的表。一个分类器的工作就是决定接收包所关联的 slot number 和传
递包到那个特殊的 slot 所指向的对象。C++类的分类器(定义在~ns/classifier.h)
提供了一个可以派生其他分类器的基类。
class Classifier : public NsObject {
public:
~Classifier();
void recv(Packet*, Handler* h = 0);
protected:
Classifier();
woid install(int slot, NsObject*);
void clear(int slot);
virtual int command(int argc, const char*const* argv);
virtual int classify(Packet *const) = 0;
void alloc(int);
NsObject** slot_; /*描绘 NsObject 的 slot number 的表*/
int nslot_;
int maxslot_;
};
方法 classify()是一个纯虚函数,表示类 Classifier 仅仅是用作基类的。 方法 alloc()
在表中动态分配足够的空间用于 slot 的特殊号码。方法 install()和 clear()是从表
中添加和移出对象。方法 recv()和 OTcl 接口在~ns/classifier.cc 中是如下实现的:
/*
* 对象只会见到“包”事件,这些事件或者是从进入链路来
* 或者来自于本地的代理(例如:发包源)
*/
void Classifier::recv(Packet* p, Handler*)
{
NsObject* node;
int cl = classify(p);
if (cl < 0 || cl >= nslot_ || (node = slot_[cl]) == 0) {
Tcl::instance().evalf("%s no-slot %d", name(), cl);
36
- 42. Packet::free(p);
return;
}
node->recv(p);
}
int Classifier::command(int argc, const char*const * argv)
{
Tcl& tcl = Tcl::instance();
if (argc ==3) {
/*
* $classifier clear $slot
*/
if (strcmp(argv[1],"clear') == 0) {
int slot = atoi(argv[2]);
clear(slot);
return (TCL_OK);
}
/*
* $classifier installNext $node
*/
if (strcmp(argv[1],"installNext") == 0) {
int slot = maxslot_ + 1;
NsObject* node = (NsObject*)TclObject::lookup(argv[2]);
install(slot,node);
tcl.resultf("%u", slot);
return TCL_OK;
}
if (strcmp(argv[1], "slot") == 0) {
int slot = atoi(argv[2]);
if ((slot >= 0) || (slot < nslot_)) {
tcl.resultf("%s",slot_[slot]->name());
return TCL_OK;
}
tcl.resultf("Classifier: no object at slot %d", slot);
return (TCL_OK);
}
} else if (argc == 4) {
/*
*$classifier install $slot $node
*/
if (strcmp(argv[1], "install") == 0) {
int slot = atoi(argv[2]);
NsObject* node = (NsObject*) TclObject::lookup(argv[3]);
37
- 43. install(slot, node);
return (TCL_OK);
}
}
return (NsObject::command(argc,argv));
}
当一个分类器 recv()一个包时,它将包传给 classify()方法。这个在每个基类的派
生类中的定义是不同的。方法 classify()通常的定义是用来决定和返回一个 slot
下标到 slot 表中。如果下标识是无效的,分类器将调用实例过程 no-slot()去试着
对表做正确的增加。但是,在基类中 Classifier:no-slot()打印错误信息同时结束操
作。
command()方法向解释器提供了下面的类成员函数(instproc-likes):
clear{<slot>}清除特殊 slot 的入口。
installNext{<object>}将对象加入下一个空闲的 slot,返回 slot 号。
注意:这个类成员函数被储存了这个存储对象指针的一个同名实例过程
重载。然后可以从 OTcl 中快速查询分类器中加入的对象。
slot{<index>}返回储存在特殊 slot 下的对象。
install{<index>,<object>}将特殊的对象加入指定的 slot(下标)中。
注意:这个类成员函数也是被储存了这个存储对象指针的一个同名实例
过程重载,然后也可以从 OTcl 中快速查询分类器中加入的对象。
5.4.1 地址分类器
地址分类器是用于支持单播包传送的。 它应用位移和掩码操作包的目的地址来产
生一个 slot 码。 这个 slot 码是通过 classify()方法返回的。 class AddressClassifier
类
(在~ns/classifier-add.cc 中定义)的模型定义如下:
class AddressClassifier : public Classifier {
public:
AddressClassifier() : mask_(~0), shift_(0) {
bind("mask_", (int*)&mask_);
bind("shift_", &shift_);
}
protected:
int classify(Packet *const p) {
IPHeader *h = IPHeader::access(p->bits());
return ((h->dst() >> shift_) & mask_);
}
nsaddr_t mask_;
int shift_;
};
38
- 44. 类并没有对包的目的地址域的语义意义做直接的改变。 它只是根据包的 dst_域返
回一些数字当作 slot 码,在 Classifier::recv()方法中使用而已。mask_和 shift_的
值通过 OTcl 设置。
5.4.2 多播分类器
多播分类器是根据源和目的地址 (成组的) 将包分类。它保存了一个(链式哈希)
表来绘制源/组对成 slot 码。当一个包到达时,分类器还不知道源/组对,这时它
就调用一个 OTcl 过程 Node::new-group{}在表中加一个入口。这个 OTcl 过程可
能要使用方法 set-hash 来增加新(源,组,slot)三元组到分类器的表中。多播
分类器定义在 ~ns/classifier-mcast.cc 中,如下:
static class MCastClassifierClass : public TclClass {
public:
MCastClassifierClass() : TclClass("Classifier/Multicast") {}
TclObject* create(int argc, const char*const* argv) {
return (new MCastClassifier());
}
} class_mcast_classifier;
class MCastClassifier : public Classifier {
public:
MCastClassifier();
~MCastClassifier();
protected:
int command(int argc, const char*const* argv);
int classify(Packet *const p);
int findslot();
void set_hash(nsaddr_t src, nsaddr_t dst, int slot);
int hash(nsaddr_t src, nsaddr_t dst) const {
u_int32_t s = src ^ dst;
s ^= s >> 16;
s ^= s >> 8;
return (s & 0xff);
}
struct hashnode {
int slot;
nsaddr_t src;
nsaddr_t dst;
hashnode* next;
};
hashnode* ht_[256];
39
- 45. const hashnode* lookup(nsaddr_t src, nsaddr_t dst) const;
};
int MCastClassifier::classify(Packet *const pkt)
{
IPHeader *h = IPHeader::access(pkt->bits());
nsaddr_t src = h->src() >> 8; /*XXX*/
nsaddr_t dst = h->dst();
const hashnode* p = lookup(src, dst);
if (p == 0) {
/*
* 没有找到入口。
* 调用一次 tcl 加入一个。
* 如果 tcl 没有通过,则失败。
*/
Tcl::instance().evalf("%s new-group %u %u", name(), src,dst);
p = lookup(src,dst);
if (p == 0)
return (-1);
}
return (p->slot);
}
类 MCastClassifier 实现了一个链式哈希表还应用一个哈希函数到包的源和目的
地址。哈希函数返回一个 slot 码去标注潜在对象到 slot_表中。一个哈希表中的
空位表示包被传递到一个先前的未知的组中;OTcl 将被调用来控制这种情况。
OTcl 代码就可以在哈希表的适当位置插进入口。
5.4.3 多路分类器
这个对象是提供相同代价多路传输的, 即当节点有多条同代价路由路经到同一个
目的地,也想同时地使用它们。这个对象不查看包的任何域。对每个连续到达的
包 , 它 仅 仅 返 回 以 轮 换 形 式 出 现 的 下 一 个 slot 。 这 种 分 类 器 的 定 义 在
~ns/classifier-mpath.cc,如下:
class MultPathForwarder : public Classifier {
public:
MultiPathForward() : ns_(0), Classifier() {}
virtual int classify(Packet* const) {
int cl;
int fail = ns_;
do {
cl = ns_++;
40
- 46. ns_ %= (maxslot_ + 1);
} while (slot_[cl] == 0 && ns_ != fail);
return cl;
}
pivate:
int ns_; /*下一个要使用的 slot。可能是一个误称*/
};
5.4.4 哈希分类器
这个对象是将一个包作为一个特殊的流的一部分来分类的。 像这个名字所显示的
那样,哈希分类器用一个内部哈希表来分配包到相应的流中。这些对象被用在需
要知道流水平(flow-level)信息的地方(例如,在流的特殊队列要求(flow-specific
queuing)和统计收集中) 。一些“流的间隔尺度”("flow granularities")是可以获得
的。特别地,包可能基于 ID 流、目的地址流、源/目的地址流、或者源/目的地
址加上 ID 流的集合分配到流中。 可以被哈希分类器获得的只有 ip 头、 src()、dst()、
flowid()(见 ip.h)。
哈希分类器和一个说明哈希表初始大小的整数参数一起被创建。 当前的哈希表大
小可能逐渐被方法 resize 改变(见下) 。当对象变量 shift_和 mask_被创建时,他
们就是用模拟器当前的 NodeShift 和 NodeMask 分别初始化的。当哈希分类器实
例化后这些值可以从 AddrParams 对象取回。 如果 AddrParams 结构体没有被初始
化的话,哈希分类器将不能正常操作。下面的构造函数用于不同的哈希分类器:
Classifier/Hash/SrcDest
Classifier/Hash/Dest
Classifier/Hash/Fid
Classifier/Hash/SrcDestFid
哈希分类器接受包,根据他们的流标准分类,然后取出指示下一个应该接受这个
包的节点的分类器 slot。有几种哈希分类器的环境里,大多数包应该与一个 slot
关联,然而有些流却不是这样的。哈希分类器包含了对象变量 default_,这是用
于那些 slot 不满足前面所有标准的包。default_可以有选择的设置。
如下是哈希分类器的方法:
$hashcl set-hash buck src dst fid slot
$hashcl lookup buck src dst fid
$hashcl del-hash src dst fid
$hashcl resize nbuck
方法 set-hash()在哈希分类器范围内插入一个新的入口到哈希表中。buck 参数是
41
- 47. 哈希表的桶号(bucket number),这是用来插入这个入口的。当桶号未知时,buck
可以设为 auto。参数是用于匹配流分类的 src、dst 和 fid 为 IP 源、目的和 IO 流。
没有被特定的分类器使用的域(例如,一个 id 流分类器的 src 域)将被忽略。slot
参数表明了在从哈希分类器派生的基分类器对象里的潜在 slot 表的下标。lookup
函数返回与给定的 buck/src/dst/fid 元组关联的对象名。buck 参数可以像 set-hash
中一样是 auto。del-hash 函数将从哈希表中移除特定的入口。当前,这是由简单
的标记入口为非活动的来完成,所以可以用那些没有使用的入口来增大哈希表。
resize 函数通过参数 nbuck 来重新设置桶的数量从而改变哈希表的大小。
由于没有默认值的定义, 当一个哈希分类器收到一个不能匹配任何标准的包后将
向 OTcl 发一个请求。请求将是如下的形式:
$obj unknown-flow src dst flowid buck
因此,当一个没有匹配任何流标准的包收到后,实例化哈希分类器对象的方法
unknown-flow 将被包的源、目的和 id 流激活。另外,如果哈希表如果用了 set-hash,
buck 域则表示包含这个流的哈希桶。如果当执行插入到分类器操作而且这个桶
又是已知的时候,这个参数避免了其他哈希表查找。
5.5 复制器
复制器与我们先前讨论的分类器不同,它不使用分类功能。而是,仅仅将分类器
用作一个 n 个 slot 的表;它重载了 recv()方法,用它来产生 n 个包的副本,然后
根据表将这些副本投送向 n 个相应的对象。
为了支持多播包的发送,一个分类器从源 S 收到一个多播包, 要送到目的地群 G,
这个分类器通过一个哈希函数 h(S,G)从分类器对象表中给出计算一个“slot 码” 。
在多播传送中,包必须被复制给每个通向 G 的子节点集的链路减一个副本。产
生包副本的操作由一个 Replicator 类执行,它被定义在 replicator.cc 中:
/*
* 一个复制器不是一个真正的包分类器,
* 我们仅仅需要影响它的 slot 表。
* (这个对象用来实现输出一个多播
* 路由或者是 LAN 中的广播)
*/
class Replicator : public Classifier {
public:
Replicator();
void recv(Packet*, Handler* h = 0);
42
- 48. virtual int classify(Packet* const) {};
protected:
int ignore_;
};
void Replicator::recv(Packet* p, Handler*)
{
IPHeader *iph = IPHeader::access(p->bits());
if (maxslot_ < 0) {
if (!ignore_)
Tcl::instance().evalf("%s drop %u %u", name(),
iph->src(), iph->dst());
Packet::free(p);
return;
}
for (int i = 0; i < maxslot_; ++i) {
NsObject* o = slot_[i];
if (o != 0)
o->recv(p->copy());
}
/*我们知道 maxslot 是非空的*/
slot_[maxslot_]->recv(p);
}
我们可以从代码中看到,这个代码并不是将包分类,而是,复制一个包,为表中
的每一个入口复制一个,然后将副本向表中列的每一个节点投送。表中的最后一
个入口获得“原始”的包。由于 classify()方法是在基类中一个纯虚函数,复制器
定义了一个空的 classify()方法。
5.5 路由模块和分类器的组织
正如我们看到的那样,一个 ns 节点本质上是一个分类器的集合。最简单的节点
(单播)只包含一个地址分类器和一个端口分类器,如图 5.1 所示。如果节点有
扩展的功能,更多的分类器将被加到这个基础节点里,例如,图 5.2 所示的多播
节点。随着更多的功能块的加入,这些功能块每个都需要自己的分类器,这样节
点能够提供一种统一的接口来管理这些分类器, 同时将这些分类器连接到路由计
算模块就变得非常重要。
控制这种情况的经典方法就是通过类的继承。例如,如果想要一个支持层次路由
的节点,可以仅仅从基类节点派生一个 Node/Hier,然后重写分类器的 setup 方法
来插入层次分类器。当新的功能模块被实现而且不能被“随意”的混淆,这种方
43
- 49. 法就是十分有效的。例如,层次路由和 ad hoc 路由使用它们自己的一套分类器。
继承需要我们有 Node/Hier,这个就是支持前者的,同时 Node/Mobile 支持后者。
但是当想要 ad hoc 路由节点支持层次路由就出现问题了。对于这个简单的例子
来说也许可以用多重继承来解决这个问题, 但是随着要加入的功能模块的数量增
长,这很快就变得不可行。
解决这个问题的唯一办法就是对象合成。基类节点需要为分类器的获取和组织定
义一组接口。这些接口应该
容许那些实现它们自己的分类器的路由模块插入节点;
容许路由计算块在所有需要这些信息的路由模块中增加路由到分类器;
提供一个单独的管理存在的路由模块的方法。
另外,我们也应该定义一个统一的用于路由模块连接节点接口的接口,也就是提
供一种系统的方法来扩展节点的功能。在这一部分我们将介绍路由模块的设计和
相应的节点接口。
5.5.1 路由模块
通常,ns 中每个路由的实现包含三个功能模块:
路由代理(Routing agent)和邻居交换路由包,
路由逻辑(Routing logic)用路由代理收集来的信息(或者是静态路由情况
下全局的拓扑数据库)执行实际的路由计算,
节点中的分类器。他们用来计算路由表,从而执行包的传递。
注意:当实现一个新的路由协议,不是必须实现所有的三个模块。例如,当实现
一个链路状态路由协议(link state routing protocol)时,我们仅仅实现以链路状态方
式交换信息的路由代理,和一个采用 Dijkstra 计算拓扑数据库的路由逻辑。然后
就可以用和其他单播路由协议一样的分类器。
图 5.3 节点、路由模块和路由之间的关系。 虚线表示一个路由模块细节
44
- 50. 当一个新路由协议的实现包含了多于一个功能块时, 特别是当包含了自己的分类
器时,这个协议就需要一个另外的对象,我们称之为路由模块(routing module),
他可以管理所有这三个功能块还可以与节点交流从而管理它的分类器。图 5.3 展
示了这些对象之间的功能关系。注意路由模块可能和路由计算块有直接的关系,
例如,路由逻辑和/或者路由代理。但是,路由计算可能不直接通过路由模块安
装它们的路由路径,因为可能存在另外的模块对学习新的路由路径感兴趣。虽然
这不是必须,但是还是尽量这么做,因为一些路由计算是一个特殊路由模块专用
的,例如 MPLS 模块中 label 的安装。
一个路由模块包括下面三个主要功能:
1. 一 个 路 由 模 块 通 过 register() 将 初 始 化 连 接 到 一 个 节 点 , 也 可 以 通 过
unregister()断开连接。通常,在 register()里面一个路由模块(1)告诉节点它
是否对路由更新和传输代理的依附感兴趣,(2)创建它的分类器,然后将之
加到节点里(下一个小节里有具体描述) 。在 unregister()里,一个路由模
块所做的事情刚好相反,它删除它的分类器,然和移出它的对节点上路由
更新的连接。
2. 如 果 一 个 路 由 模 块 对 路 由 更 新 感 兴 趣 , 节 点 通 过
RtModule::add-route(dst,target)和 RtModule::delete-route(dst,nullagent)来通
知模块。
3. 如果一个路由模块对学习节点里传输代理的依附和脱离感兴趣的话,节点
将会通过 RtModule::attach(agent,port) 和 RtModule::detach(agent,nullagent)
来通知模块。
在~ns/tcl/lib/ns-rtmodule.tcl(这个可以当作你的模块的模版)里,有几种派生的
路由模块例子。
当前,在 ns 中有六种实现了的路由模块:
模块名 功能
RtModule/Base 单播路由协议接口。提供添加/删除路由、附属/撤销代理等基本功能。
多播路由协议接口。 它的唯一目的是建立多播分类器。所有其它的多播功能是作
RtModule/Mcast
为节点类的成员函数实现的。这将在以后改回来。
层次路由。它为管理层次分类器和路由安装做包装。可以和其它路由协议联合使
RtModule/Hier
用,例如,ad hoc路由。
RtModule/Manual 手动路由
RtModule/VC 使用虚拟分类器取代vanilla分类器。
实现MPLS功能。这是现存的唯一一个自主式模块,而且它不会污染节点的名空
RtModule/MPLS
间。
表5.2 实现了的路由模块
45