论坛风格切换切换到宽版
  • 1015阅读
  • 0回复

[技巧分享]OMNet++ 10 分钟教程 [复制链接]

上一主题 下一主题
离线rushishuiwen
 
发帖
33
C币
0
威望
2
贡献值
10
银元
0
铜钱
45
人人网人气币
0
只看楼主 倒序阅读 使用道具 楼主  发表于: 2011-07-24
本文适合对网络模拟器有一定了解的读者,阅读本文时,最好同时打开用户手册和API文档以便随时查阅。
1. 在omnetpp.org中提到的仿真模型和框架与OMNet++是什么关系?
OMNet++提供了基本的工具和机制来编写仿真代码,但它本身并不提供任何特定用于计算机网络仿真,系统架构仿真和任意其它领域的组件;具体的仿真是由一些仿真模型和框架如Mobility Framework或INET Framework来支持,这些模型独立于OMNet++开发,并有自己的发布周期。
2. OMNet++提供了什么?
一个C++库,它由仿真内核及一些用来创建仿真组件(简单模块和信息)的工具类(如随机数生成,统计收集,拓扑发现等);组装和配置这些组件的基础设施(NED语言,ini文件);运行时用户接口或仿真环境(TKenv,Cmdenv);一个用来设计,运行和评估仿真的IDE环境;实时仿真的扩展接口;MRIP,并行的分布式仿真,数据库连接等等这些组成。
3. OMNet++的仿真模型是什么样的?
OMNet++提供了一个基于组件的架构,模型是由可重用的组件或模块组成的。模块之间可以通过gates(在其它系统中称为ports,即端口)进行连接,以构成复合模块。每个仿真模型是一个复合模块类型的实例。这一层次(组件和拓扑)由NED文件来处理。例如,一个名为EtherMAC的组件可以用NED来描述:
  //
  // Ethernet CSMA/CD MAC
  //
  simple EtherMAC {
      parameters:
          string address; // others omitted for brevity
      gates:
          input phyIn;    // to physical layer or the network
          output phyOut;  // to physical layer or the network
          input llcIn;    // to EtherLLC or higher layer
          output llcOut;  // to EtherLLC or higher layer
   }
它可以使用在下面的Ethernet station的模型中:
  //
  // Host with an Ethernet interface
  //
  module EtherStation {
      parameters: ...
      gates: ...
          input in;    // for connecting to switch/hub, etc
          output out;
      submodules:
          app: EtherTrafficGen;
          llc: EtherLLC;
          mac: EtherMAC;
      connections:
          app.out --> llc.hlIn;
          app.in <-- llc.hlOut;
          llc.macIn <-- mac.llcOut;           llc.macOout --> mac.llcIn;
          mac.phyIn <-- in;           mac.phyOut --> out;
  }
其中,注释能用来生成文档。简单模块,例如上面的EtherMAC,会与一个C++ 文件关联以提供行为,它是以simple关键字来声明的。复合模块则是用module关键字来声明的,为了仿真一个Ethernet LAN,应该创建一个复合模块EtherLAN并用network关键字来表示它可以通过自身运行。
network EtherLAN {
      ... (submodules of type EtherStation, etc) ...
  }
NED文件可以在IDE中以图形化方式或文件模式编辑。
NED文件只定义了模型的结构(拓扑),其行为和模块参数的某个子集则是开放的,如前面所提到的,行为是通过在简单模块相关联的C++代码来定义的,而在NED文件中没有赋值的模块参数则从ini文件中获取它们的值。
4. 如何运行仿真?
如果EtherMAC, EtherLLC 和 EtherTrafficGen 相应的代码已经编写好,就可以编译成可执行文件,并在omnetpp.ini中定义运行的参数,然后就可以作为一个单独的仿真程序来运行。
在最简单的情况下,即当所有东西都在一个目录中的时候,只需要输入:
opp_makemake --deep
make
opp_makemake 创建了一个拥有恰当设置的makefile,所以你不用做任何其它事情。
但是如果源文件在几个不同的目录中,例如要使用INET框架的时候,则需要传递额外的参数,如给opp_makemake 传递-I 参数,这种情况下最好查阅这些框架的文档或从已有的makefile系统中得到一些提示。
为了运行仿真,需要一个omnetpp.ini文件,否则会得到下面的错误:
  $ ./etherlan
  OMNeT++/OMNEST Discrete Event Simulation  (C) 1992-2005 Andras Varga  [....]
   Error during startup: Cannot open ini file `omnetpp.ini'
ini文件的其中一个功能是定义仿真某个特定的网络(因为可以在NED文件中定义一个或多个network.endnetwork定义。同时,也可以动态地指定载入的NED文件(即默认不载入),为模块参数赋值,指定仿真的运行时间和用于生成随机数的种子,以及需要收集的统计量,或者用不同的参数设置来建立几个实验。一个omnetpp.ini的样例如下:
  [General]
  network = etherLAN
  *.numStations = 20
  **.frameLength = normal(200,1400)
  **.station[0].numFramesToSend = 5000
  **.station[1-5].numFramesToSend = 1000
  **.station[*].numFramesToSend = 0
由上可以看到,可以在为模块参数赋值时使用通配符。在NED文件中的参数赋值首先进行,而那些没有赋值的的参数则可以在ini文件中赋值(也就是说,在NED中的参数值不能被ini文件中的值覆盖)。如果最后仍然有一些参数没有被赋值,则会在运行时以交互的方式取得。
如果需要使用不同名字的ini文件,可以传递 –f 参数,这样就可以同时传递多个文件:
$ ./etherlan -f common-settings.ini -f params15.ini -f seeds3.ini
在默认情况下,会以图形用户接口TKenv来运行。如果想要转到命令行接口(为了进行批处理执行),则将可执行文件链接到不同的库即可:通过编辑makefile的下列几行:
# User interface (uncomment one) (-u option)
# USERIF_LIBS=$(CMDENV_LIBS)
    USERIF_LIBS=$(TKENV_LIBS)
注释掉Tkenv那一行,指定为cmdenv,然后输入make进行重新链接。另一种可选的方法是在创建makefile的时候,使用opp_makemake的 –u Cmdenv选项。
注意到,也可以创建自已的GUI。通过阅读src/cmdenv/cmdenv.cc文件会对你有所帮助。将OMNet++嵌入到其它的应用程序(例如需要进行仿真的一些分析和设计工具)也是相似的,这已经被一些商业公司所实践。
5. 仿真的输出是什么?
仿真过程中会有向量输出和标题输出,默认文件名为omnetpp.vec和omnetpp.sca,尽管omnetpp.ini可以指定输出为不同的文件名,仍需要在简单模块中进行编码以让其拥有记录仿真结果的能力,所以在他人编写的仿真中可能并不创建这些文件。
一个输出向量文件包含若干输出向量,每一个向量都是一个 (timestamp,value) 对。输出向量可以存储例如关于时间的队列长度,接收数据包时端到端的延迟,丢弃的数据包或者信道吞吐量—任意在简单模块中的编程所定义的统计量都可以。并且,可以在omnetpp.ini中配置输出向量:即可以启用或者关闭记录某个输出向量,或者用一定的仿真时间间隔来进行限制。可以通过查看cOutVector对象的C++的源代码来查看一个简单模块可以获取的输出向量。
输出向量根据时间来捕获行为,而输出标量文件则包含总的统计量:发送包的数目,丢弃包的数目,平均的端到端延迟,吞吐量的峰值等。可以在recodScala() 方法的调用 ,特别是在一个简单模块类中的finish()方法中查看输出标量。
输出向量可以用Plove程序来绘图,而输出标量则可以使用Scalars程序来绘图。

6. 关于随机数
OMNeT++默认的随机数生成器是 Mersenne Twister,种子可以自动获取,或者在omnetpp.ini中定义。OMNeT++ 支持很多种概率分布,并且在NED和C++中都是可用的(在 3.2 版本中有 14 个连续的分布和 6 个离散的分布, 具体可以查看 API doc)。非常量的模块参数可以用随机变量来赋值,如 exponential(0.2), 它表示C++代码会在每次读取参数时获得一个不同的数字;这是指定随机流量源的一种很方便的方法。( 常量参数也能用表达式如exponential(0.2)来赋值,但它只会被计算一次并且不再改变)。
7. 是否可以在OMNeT++中进行MRIP,分布式并行仿真,网络模拟,或feature X?
可以。OMNet++ 具有很强的可扩展性并且开放源代码,一些特性是可以开箱即用的:
MRIP是指 multiple replications in parallel,而Akaroa是一个很出色的工具。你可以单独地下载并安装它,然后启用OMNet++的Akaroa支持进行重新编译,但AFAIK Akaroa仅可以在 Linux(*nix)中使用,在OMNeT++的手册中可以找到更多信息。
如果仿真需要大量的内存,则可以在集群中运行仿真。分块和其它配置可以在omnetpp.ini中定义,但仿真模型本身却不用进行改变(除非模型包含全局变量可能导致不能分布地运行)。通信层使用的是MPI,但事实上这是可配置的,所以如果你没有MPI的话也仍然可以通过命名管道,或者基于文件的信息交换来进行一些基本的测试。当然如果需要,你也可以通过实现abstract cParsimCommunications接口来添加一个新的方案。
网络模拟,实时仿真和类似于hardware-in-the-loop的功能是可用的,这是因为在仿真内核中的事件调度器是可插拔的。OMNet++的一个Demo展示了如何进行实时仿真和一个简单的网络模拟的样例,足以让你进入这个领域。Real network emulation with the INET Framework is in the queue.
可以用一个数据库来代替omnetpp.ini文件作为配置数据的源,也能将仿真结果重定向到数据库中,OMNeT++ 中包含这样一个Demo,可以作为参考。
由于你拥有全部的源代码和良好的文档,所以你能根据自己的想法来实现很多事情。
8. 如何在C++代码中编写模型?
简单模块其实是C++类,你从cSimpleModule继承子类,重定义一个虚成员函数,并通过Define_Module()宏来将新的类注册到OMNeT++中。
模块间主要使用消息传递进行通讯,而timers(timeouts)也处理模块发送给自身的消息。消息应当是cMessage类或者是其子类,消息将被传递到模块的 handleMessage(cMessage *msg) 方法,在这里应当添加你的代码。几乎你想定义的模块行为都在handleMessage() 之中定义,因此这段代码块可能会很长,所以将其重构为其它成员函数例如命名为processTimer(),processPacket()是一个好主意(可以将activity() 方法视为handleMessage()的一个替换,但是在实践中最好不要这么做。)。
你可以使用send(cMessage *msg, const char *outGateName) 方法来向其它模块传递消息,对于无线仿真和一些情况下,则可以更方便地直接传递消息到其它模块而不用在NED文件中建立连接。这可以由sendDirect(cMessage *msg, double delay, cModule *targetModule, const char *inGateName)方法来完成。Self-messages (that is, timers can be scheduled) 可以通过scheduleAt(simtime_t time, cMessage *msg) 进行传送,并且在它们失效前可以通过cancelEvent(cMessage *msg) 来取消。这些函数都是cSimpleModule类的成员,在文档中可以找到更多的相关信息。
基本的cMessage类包含几个数据成员,最重要也最实用的是name,length,message”kind”(一个int成员)。其它数据成员则存储此消息最近发送/调度的信息:arrival time,arrival gate等。为了进行协议仿真,cMessage可以封装另外一个cMessage对象,可以查看它的encapsulate(cMessage *msg) 方法进行了解。这些方法也能逐步地修改域的长度。如果你需要在消息中运送更多的数据,其它数据成员可以通过继承来添加。然而,你并不需要手工编写新的C++类,可以更方便地在一个.msg文件中定义,从而让OMNet++( opp_msgc 工具) 来为你生成C++类。生成的C++文件会拥有_m.h,m.cc的后缀。.msg文件支持进一步的继承,组合,数组成员等等,并且它拥有可以让在C++类中自定义的语法。一个消息文件的例子如下:
  message NetworkPacket {
    fields:
      int srcAddr;
      int destAddr;
  }
OMNet++经常用来进行网络协议的仿真,cMessage有一个叫control info的域,它包含额外的信息来促进协议层间的通讯。例如,当应用层发送数据(一个消息对象)到TCP来传输时,它能在包含socket标志符的信息中附加一小段控制信息(一个对象)。或者,当TCP发送一个TCP segment到IP层传输时,它附加包含目标IP地址的控制信息,也可能是其它的一些选项,如TTL。控制信息也能发送到其它方向(向上),以便向上一层标记源IP地址或者TCP连接。控制信息是通过cMessage的 setControlInfo(cPolymorpic *ctrl) 和 removeControlInfo() 方法来处理的。
其它cSimpleModule模块中需要重定义的虚成员函数是initialize()( 主要的初始化工作在这里进行,因为在构造函数的调用过程中模型也正在构建),initialize(int stage) 和多阶段初始化的int numinitStages() const(这在一个模块的初始化代码依赖于其它已经初始化完毕的模块时很有用),finish()来记录总的结果(析构函数不适宜用于这种用途)。如果你想要模块感知参数(参数可以交互式地在GUI中或由其它模块改变,因此你需要重新读取这个参数)在运行时的改变,则需要重定义handleParameterChange(const char *paramName)方法。
简单模块的NED参数可以使用使用par(const char *paramName) 方法来读取,在典型的情况下,这个过程会在initialize()中进行,并将这些值存储在模块类的数据成员中。par()返回一个cPar对象的引用,这个引用可以用C++的语法或调用对象的doubleValue()方法等这些途径转为合适的类型(long,double,const char*,etc)。除了基本的类型,也可以是XML文件:参数可以赋值到XML文件中,它能用以DOM的对象树的形式向C++代码展示。
消息传递并不总是模块间通讯的最好方法。例如,如果你设计了一个用于收集统计的模块(常使用全局变量),通过消息来传递统计量的更新,不如将统计模块作为一个C++对象,并调用它的用于此目的的公共成员函数(如updateStatistics(…))来得简便。
对方法的直接调用有一些技术细节。首先你需要查找其它模块:cModule的parentModule() 和submodule(const char*name)方法可以查找到与当前相关的模块,而simulation.moduleByPath(const char *path) 可以通过一个绝对的路径名在全局查找一个模块,一旦你拥有其它模块的指针,你需要将它转化为实际的类型(i.e. to StatisticsCollector* from cModule*),这是由拥有与C++的dynamic_cast相同语法的check_and_cast来完成,但如果转换不成功或指针为NULL时它会抛出一个错误。public的方法应当将Enther_Method()或者Enter_Method_Silent(…) 放置在顶端,它们启用了GUI的动画调用,同时进行了一些与临时上下文切换相近的行为(在这里并不深究,但如果想要在方法中进行消息处理这是必须的)。INET框架广泛地使用了方法调用来访问模块,如RoutingTable, InterfaceTable, NotificationBoard等。
INET的NotificationBoard在一些仿真中很有用,它通过对信息的产生者和消费者进行解耦,在它们之间转发变化的notifications或者事件的notifications,从而支持几个模块间的信息共享(NotificationBoard宽泛地基于blackboard 区域,但在实践中,仅转发notifications比存储blackboard中发布的信息更简单有效。进一步地,notifications可能包含实际的数据或者指向数据的指针的拷贝)。
对于调试,模块中打印一些必要的信息是很关键的。在OMNet++中使用ev<< (类似于printf和cout)将输出写到可以进行过滤的GUI窗口中,同时模块用图标的颜色或文件标签及tooltips来展示状态也有助于减少调试的时间;这个可以通过在执行时改变display string来完成(查看cModule's displayString())。通过调用bubble(const char*text)方法,可以在模块的上方看到一个短暂出现的“气泡”或“气球”,这也是很有帮助的。另外一种调试的辅助是WATCH(variableName) 宏及其变种 (WATCH_VECTOR, 等),它能让你在Tkenv GUI中查看变量的值(you'll find watched variables in the "Contents" tab of the module's inspector)。
为了记录输出向量,你应当添加一个cOutVector对象到类中作为一个数据成员(或者用new来创建),设置name string作为输出向量的名字,然后持续调用它的record(double value)方法来记录数字。输出标量最好编写在模块(请查看cModule中的 recordScalar(const char *name, double value) 方法)的finish() 函数中,可以基于计数器作为模块的数据成员。也可以计算基本的统计和图表:请参考cStdDev, cDoubleHistogram and cLongHistogram classes 这些类。
NED文件描述静态的拓扑,但也能动态地创建模块和连接。这在网络拓扑不是以NED文件(如普通文本文件,Excel sheet,database等)存储,或者你想在运行时动态地创建和删除模块的情况下很有用。在前一种情况下,一般可以通过Awk,Perl,Python,Ruby,TCL或在文本编辑中一系列的查找/匹配操作来将数据转化为NED。然而,当你决定以C++来创建动态模块时,对一个样本NED文件调用nedtool并查看生成的_n.cc文件会很有帮助。

评价一下你浏览此帖子的感受

精彩

感动

搞笑

开心

愤怒

无聊

灌水
快速回复
限100 字节
批量上传需要先选择文件,再选择上传
 
上一个 下一个