omnetpp inet


自带了Mingw编译环境,而不再需要Visual C编译环境了。事实上,OMNeT++ 4.0也不支持使用Visual C++的编译器。在4.0版本中还加入了基于Eclipse的IDE,可以直接使用。如果需要重新编译OMNeT++,可以使用msys/msys.bat打开Shell,先使用./configure生成配置文件,然后直接make即可。

1. 在使用IDE的时候,需要在参数中设置OMNeT++的各个选项,包括OMNeT++的目录,Image的目录以及dot和doxygen的程序路径。否则在编译的时候会提示找不到相应的命令,或者说是通过了编译,但是却找不到图标来显示。如果是找不到命令如opp_makedep等,则可以将omnetpp-4.0/bin加入到系统的PATH环境变量中。

2. INET现在也已经移植到了4.0版本下,并且从MF模块中借鉴相关的实现,现在已经支持无线协议。现在可以从http://github.com/inetmanet/inetmanet/tree/master上下载及时更新后的代码。将代码解压后放在OMNeT++的目录下,直接make即可。目录下的buildMakefiles可以用来生成Makefile文件,包括两种,一种是生成dll(Linux下为.so)的,另一种是生成.exe的。如果在运行的时候没有发现对应的可执行文件或者动态链接库,则可以使用相应的Makefile,并重新make即可。

3. 在编译或链接INET的时候,有可能出现无法找到pcap_等符号,作者在邮件列表中也注意到了这个问题,但是我下载下来的时候还没有进行修改,所以直接将那些相应的源代码进行了屏蔽,并没有出现什么问题。可能以后如果需要和外界进行交互的时候再看吧。如果运行的时候出现无法加载libpcap这样的字样,则去下载一个winpcap安装上吧。

4. 在通过命令行启动仿真程序的时候,发现图像无法加载。这是因为我修改了OMNeT++所在目录的盘符,而现在的代码还是使用原来的盘符下的目录来加载图像,重新编译了也无济于事。猜想可能是原来编译的时候对图像的目录进行了设置。找了半天无果,干脆直接到源代码中去看看。结果在src/tkenv/tkenv.cc的Tkenv::run中看到了可以设置图像的路径。将OMNETPP_IMAGE_PATH加入到系统的环境变量中,问题得到解决。


OMNeT++通过NED语言来对网络系统进行描述。NED语言中包含着对信道、模块、节点和网络的完整描述,可以参见OMNeT++参考文档的第三章。在实际的网络仿真中,总是会首先描述一些特定形状和特性的网络。一般说来,网络拓扑结构包括两种,一种是平面(flat)结构,另外一种是层次化(Hierarchy)结构。由于OMNeT++采用的是层次化的模块构建,所以层次化的网络拓扑可以通过平面的拓扑结构来生成。在这里,对常见的几种网络拓扑结构进行描述。更多的代码可以参见OMNeT++包中sample目录下的neddemo。

在构建平面拓扑结构的时候,主要考虑的是各个节点之间的关系。对于规则的拓扑结构来说,节点之间的连线是有关系的。在下面的描述中,主要的是对这种关系进行梳理。

(1) 二叉树结构

一个二叉树结构中的节点包含有三个接口,分别对应父节点和左右子节点。这里用fromUpper和downLeft、downRight来表示。对于高度为height的二叉树结构,其节点之间的关系可以表示如下。



Ned代码 



  1. for i=0..2^height-2, for j=0..2^height-2 {   
  2.       node[i].downLeft <--> node[j].fromUpper if j==2*i+1;   
  3.       node[i].downRight <--> node[j].fromUpper if j==2*i+2;   
  4. }  


  这是OMNeT++中的另外一种表示方法(没有了if条件表达式)。



Ned代码 



  1. for i=0..2^(height-1)-2 {   
  2.     node[i].downLeft <--> node[2*i+1].fromUpper;   
  3.     node[i].downRight <--> node[2*i+2].fromUpper;   
  4. }  



生成的拓扑结构如下所示。从图中可以看到,最终生成了一个二叉树结构。这里的节点并没有显示成从上至下的结构,而是系统自动采用了一种合适的方法来表达。如果需要修改的话,则还需要对接点的位置进行约定。可以参见NED的描述文件。


(2) 链结构

另外一种很常见的网络拓扑结构就是线性结构,就像一条链一样串起来。这种结构在OMNeT++中是很容易描述的。下面的代码这个实现。



Ned代码 



  1. for i=0..n-2 {   
  2.     node[i].right <--> node[i+1].left;   
  3. }  


 生成的网络拓扑结构如下所示。


 (3)完全图

完全图是一个节点和网络中的所有节点都有连接。拓扑结构描述如下所示。



Ned代码 



  1. for i=0..(n-2), for j=(i+1)..(n-1) {   
  2.     node[i].g[j] <--> node[j].g[i];   
  3. }  



  其中, node 表示节点,而这里的 g[] 表示门向量。这里通过二重循环将节点之间的所有接口都连接了起来。另外,这里采用了位置描述符来对网络进行描述,从而让整个网络成为一个环形。

@display("p=,,ring");

  生成的完全图如下所示。



(4)星形图

这种网络拓扑结构是所有节点通过一个中心节点连出去。这种网络的创建并没有什么特殊的地方,一般构建两类节点:中间节点和终端节点,然后将所有的终端节点和中心节点相连即可。

生成的拓扑结构如下所示。


(5)网格网格拓扑结构

这种网络拓扑结构也很常见。这种拓扑结构中的节点包含有四个接口,分别连接上下左右的节点。节点之间的连接关系如下所示。



Ned代码 



  1. for i=0..height-1, for j=0..width-1 {   
  2.     node[i*width+j].down <--> node[(i+1)*width+j].up if i!=height-1;   
  3.     node[i*width+j].right <--> node[(i*width+j)+1].left if j!=width-1;   
  4. }  



  生成的网格图如下。


(6)蜂窝网络拓扑结构

这是上面网格拓扑结构的一个变种,一般可以用于蜂窝网路中。这里的节点最多包含有三个接口,其连接关系如下面的NED代码所示。



Ned代码 



  1. for i=0..num-1 {   
  2.     node[i].port++ <--> node[i+1].port++ if i<num-1 && i%(2*cols+2)!=2*cols;   
  3.     node[i].port++ <--> node[i+2*cols+1].port++ if i<num-2*cols-1 && i%2==0;   
  4. }  



(7)随机拓扑结构图

有些时候需要随机生成网络拓扑结构,这在OMNeT++中是比较容易实现的。下面就是一种实现的方法。



Ned代码 



  1. for i=0..n-1, for j=0..n-1 {   
  2.     node[i].g++ <--> node[j].g++ if i!=j && uniform(0,1)<connectedness;   
  3. }  


代码中connectedness用来控制节点的连接度,1表示连接所有的其他节点。减小此值将减少网络中出现的边数。一种随机图如下所示(connectedness=0.15)。


上面介绍的是比较常见的一些网络拓扑结构。在实际使用中,这些网络拓扑在针对特定问题的简化版本是可以的,但是当网络变得复杂后,这样的拓扑生成还是不行的。此时最好是采用专门的拓扑生成器,来生成网络拓扑。另外,这里介绍的都还是有线网络中的拓扑结构,还没有涉及到无线网络的拓扑生成。


现在的 OMNeT++4.0 已经将这里的结果集成了进去,推荐使用 EV 来输出日志信息。具体的定义见 include/cenvir.h文件。这篇文章详细的讨论了如何设置 EV ,具有普遍的借鉴作用。


ev<< 语句可以用来打印信息从而了解到仿真模型正在做什么。这在进行调试和理解模型运行的时候是很有用的。现在的问题是,当模型需要运行很长时间的时候, ev<< 语句将会消耗大量的 CPU 周期。这个时候该怎么办呢?


本文将介绍如何高效率的进行记录,并且同时介绍如何创建 log channels 或者调试 channels 。


在 Cmdevn 环境下,当设置 express-mode=true 的时候,输出将会被丢弃而不会被打印,从而使得执行可以更快。但是实际上, ev<< 语句还是会带来一些开销。这种速度的减小可能不会被注意到,但是影响却是很大的。这里的问题是, OMNeT++将只会丢弃也就是不打印已经换为文本格式的字符串。举个例子,如下面的语句:



Cpp代码 



  1. ev << "Average bit/sec is: " << totalBits/simTime() << "n";  




[cpp] ​​view plain​ ​​copy​




  1. ev << "Average bit/sec is: " << totalBits/simTime() << "n";  


 即使是在极速模式下, simTime() 将会被调用,浮点数除法也会被执行。结果将会被换为字符串并存储在缓冲区中,而这里的缓冲区则会被丢弃。很遗憾的是, OMNeT++ 核心没有办法阻止这种事情发生,因为这是 C++ 的工作方式。

这时候该怎么办呢?一种常见的方式是采用 #ifdef 。



Cpp代码 



  1. #ifdef DEBUGGING   
  2. ev << "Average bit/sec is: " << totalBits/simTime() << endl;   
  3. #endif  




[cpp] ​​view plain​ ​​copy​




  1. #ifdef DEBUGGING  
  2. ev << "Average bit/sec is: " << totalBits/simTime() << endl;  
  3. #endif  


这并不坏,但是却有一个很严重的问题:在切换打印输出的时候必须重新编译所有的文件。根据墨菲定律(有可能出错的事情,就会出错,Anything that can go wrong will go wrong),当人们需要输出的时候往往看不到有输出。另外,代码中满含有 #ifdef 也不是一个好办法。随后想到的就是如何在编译的时候就将是否输出考虑到,下面是一个示例代码。



Cpp代码 



  1. if (debugging)   
  2.     ev << "Average bit/sec is: " << totalBits/simTime() << endl;  




[cpp] ​​view plain​ ​​copy​




  1. if (debugging)  
  2.     ev << "Average bit/sec is: " << totalBits/simTime() << endl;  


这比前面要好一些。 if 语句的开销是比较小的。这样,就可以在初始化的时候通过 debugging 变量来决定是否输出信息。



Cpp代码 



  1. debugging = par("debugging").boolValue();  




[cpp] ​​view plain​ ​​copy​




  1. debugging = par("debugging").boolValue();  


  这种做法还不是很方便,因为我们需要手工维护输出的状态。有些人会发现此值可以在调试中进行动态设置,但是这还不能令人满意。为什么不能让代码知道我们是否需要记录呢?

实际上,我们可以回答这个问题。一般的, OMNeT++ 知道我们何时需要进行记录:不在极速模式下的时候。幸运的是, ev 对象可以知道这点,所以现在最新的代码如下。



Cpp代码 



  1. if (!ev.disabled())   
  2.     ev << "Average bit/sec is: " << totalBits/simTime() << endl;  




[cpp] ​​view plain​ ​​copy​




  1. if (!ev.disabled())  
  2.     ev << "Average bit/sec is: " << totalBits/simTime() << endl;  



  几乎就是这样了。现在的问题是需要为每个 ev 输出增加一个 if 语句。有经验的 C/C++ 程序员将会马上想到采用宏来产生精炼的代码。第一次尝试:



Cpp代码 



  1. #define EV   if (!ev.disabled()) ev    // *** DANGEROUS!***   
  2. ...   
  3. EV << "Average bit/sec is: " << totalBits/simTime() << endl;  




[cpp] ​​view plain​ ​​copy​




  1. #define EV   if (!ev.disabled()) ev    // *** DANGEROUS!***  
  2. ...  
  3. EV << "Average bit/sec is: " << totalBits/simTime() << endl;  


  注意,这里的宏将会产生很严重的问题。考虑一下下面的代码:



Cpp代码 



  1. if (totalBits>1000)   
  2.     EV << "Average bit/sec is: " << totalBits/simTime() << "n";   
  3. else  
  4.     EV << "Not enough data yet" << endl;  




[cpp] ​​view plain​ ​​copy​




  1. if (totalBits>1000)  
  2.     EV << "Average bit/sec is: " << totalBits/simTime() << "n";  
  3. else  
  4.     EV << "Not enough data yet" << endl;  


  这段代码并不会按照设想的那样工作。当我们宏替换完成并重新缩进代码后,将得到下面的代码:



Cpp代码 



  1. if (totalBits>1000)   
  2.     if (!ev.disabled())   
  3.         ev << "Average bit/sec is: " << totalBits/simTime() << endl;   
  4.     else if (!ev.disabled())   
  5.         ev << "Not enough data yet" << endl;  




[cpp] ​​view plain​ ​​copy​




  1. if (totalBits>1000)  
  2.     if (!ev.disabled())  
  3.         ev << "Average bit/sec is: " << totalBits/simTime() << endl;  
  4.     else if (!ev.disabled())  
  5.         ev << "Not enough data yet" << endl;  


  所以这里的代码将永远不会打印出“ Not enough data ”。


最好是忘记上面的 EV 定义,因为这很容易会使得你栽在上面。即使是在这个宏定义中加上一对括号也不能解决问题,因为打印参数将会在括号之外。看起来这个问题没法修复。

尽管如此,让我们来看看下面这个版本:



Cpp代码 



  1. #define EV   ev.disabled() ? ev : ev   
  2. ...   
  3. EV << "Average bit/sec is: " << totalBits/simTime() << endl;  




[cpp] ​​view plain​ ​​copy​




  1. #define EV   ev.disabled() ? ev : ev  
  2. ...  
  3. EV << "Average bit/sec is: " << totalBits/simTime() << endl;  


  这看起来有点奇怪。这里的宏定义看起来没有区别(无论是为 true 或者是 false ),而且这和所有 C 语言教科书中所倡导的(宏如果需要扩展成表达式需要加上圆括号)相违背。但是确实这个宏是可以工作的。现在 EV<< 将会变成一个简单的表达式。




Cpp代码 



  1. ev.disabled() ? ev : ev << "Average bit/sec is: " << totalBits/simTime() << endl;  




[cpp] ​​view plain​ ​​copy​




  1. ev.disabled() ? ev : ev << "Average bit/sec is: " << totalBits/simTime() << endl;  


  这和下面的相同(注意符号的优先级) :




Cpp代码 



  1. ev.disabled() ? ev : (ev << "Average bit/sec is: " << totalBits/simTime() << endl);  




[cpp] ​​view plain​ ​​copy​




  1. ev.disabled() ? ev : (ev << "Average bit/sec is: " << totalBits/simTime() << endl);  


  这时候,当 ev 被禁止的时候(条件为 true ),这只是简单的一个 ev 对象的引用(这最终将会被编译器所优化,而不会产生任何的 CPU 指令);当 ev 启用的时候(条件为 false ),将会被还原成原始的 ev<< 语句。这正是我们所需要的。证明这里的 EV 定义在任何使用场景中都是可行的,这可以作为练习。无论是否有 if 语句,或者是还有一个 ?: 操作符,或者是其他的场景,这都是适用的。

实际上,上面 EV 的 ?: 版本并不能在 VC++ 7.0 中通过编译(因为在 VC++ 7.0 中需要?:三元操作符的第二个和第三个参数的类型是一样的,而不会做默认的换)。所以在 VC7 中的版本是这样的:



Cpp代码 



  1. #define EV   ev.disabled() ? (std::ostream&)ev : ev  




[cpp] ​​view plain​ ​​copy​




  1. #define EV   ev.disabled() ? (std::ostream&)ev : ev  


 (译者注:实际上,由于现在最新的 OMNeT++ 4.0 并不支持使用 VC 编译器进行编译,所以也没有采用这样的方式)。


如果你没有用过 log4j 或者是 C++ 中类似的工具( log4Cpp , libCWD 等),那你有可能错过调试管道或者说是日志管道。简单地说, channels 是针对快速滚动日志问题的答案(因为你几乎不可能在日志的海洋中找到有用的信息)。你的代码日志将会记录到多个管道中,而在调试的时候可以只关注自己感兴趣的管道。有两个标准可以用来区分管道日志: topic 和调试级别(如 detail, info, warnings )。其中第三个标准是位置(模块位置),这已经被 OMNeT++ 内置了。可以通过查看模块的输出来得到你想要的信息。

一个比较好的消息是通过上面的 EV 定义,可以用来简单的模拟日志管道。当书写一个 IP 模块的时候,检查下面的定义:



Cpp代码 



  1. #define fwdingEV   (ev.disabled()||!fwdingChannel) ? (std::ostream&)ev : ev   
  2. #define localEV    (ev.disabled()||!localChannel) ? (std::ostream&)ev : ev   
  3. #define mcastEV    (ev.disabled()||!mcastChannel) ? (std::ostream&)ev : ev   
  4. #define dropEV     (ev.disabled()||!dropChannel) ? (std::ostream&)ev : ev  




[cpp] ​​view plain​ ​​copy​




  1. #define fwdingEV   (ev.disabled()||!fwdingChannel) ? (std::ostream&)ev : ev  
  2. #define localEV    (ev.disabled()||!localChannel) ? (std::ostream&)ev : ev  
  3. #define mcastEV    (ev.disabled()||!mcastChannel) ? (std::ostream&)ev : ev  
  4. #define dropEV     (ev.disabled()||!dropChannel) ? (std::ostream&)ev : ev  


上面的定义提供了四个日志信道( wdingEV, localEV, mcastEV, dropEV ),可以通过单独设置fwdingChannel, localChannel, mcastChannel, dropChannel 布尔变量的值来进行开关。可以像下面这样使用日志管道。



Cpp代码 



  1. ...   
  2. EV << "packet received" << endl;   
  3. ...   
  4. if (destAddress.isMulticast())   
  5. {   
  6.     mcastEV << "multicast packet, addr=" << destAddress << endl;   
  7.     ...   
  8. }   
  9. ...   
  10. if (!routeFound)   
  11. {   
  12.     dropEV << "unroutable packet, dropping" << endl;   
  13.     delete datagram;   
  14. }   
  15. ...  




[cpp] ​​view plain​ ​​copy​




  1. ...  
  2. EV << "packet received" << endl;  
  3. ...  
  4. if (destAddress.isMulticast())  
  5. {  
  6.     mcastEV << "multicast packet, addr=" << destAddress << endl;  
  7.     ...  
  8. }  
  9. ...  
  10. if (!routeFound)  
  11. {  
  12.     dropEV << "unroutable packet, dropping" << endl;  
  13.     delete datagram;  
  14. }  
  15. ...