omnet++:官方文档翻译总结(二)
这一部分是官方案例介绍
1、Introduction
学习自:Introduction - OMNeT++ Technical Articles
本教程是基于Tictoc的仿真案例,这些案例我们可以在omnet安装目录下的sample/tictoc下找到。
2、Part 1 - Getting Started
The model
一开始,让我们组织起一个只有两个节点的network。这两个节点做了一些简单的事情:一个节点创造了一个包,之后这个包将在这两个节点间来回传递。我们称这两个节点为tic和toc。之后我们将逐步加强我们的模型,用以介绍omnet++的特性。
以下是从零开始实现我们的第一个仿真模型的步骤:
Step1、配置工程
- 打开IDE
- 左上角File->New->OMNeT++ Project,进入向导对话框;
- 输入tictoc作为工程名,创建一个empty project,然后点击Finish。
这样一个新工程就被创建了,我们可以在Project Explorer中看到它。(需要注意的是,一些版本的omnet++中会产生package.ned文件,我们不需要它,可以删除。)
在这个例子中,工程只有单个目录。在大型仿真中,工程的内容通常分布于src和simulations目录下,也可能是它们的子目录。
Step2、NED文件
omnet++使用NED文件来定义功能组件,并将它们组织成更大的单元,比如一整个网络。我们通过添加NED文件来实现我们的model。
- 在Project Explorer中,右键点击tictoc(即项目文件名),New->NED,给这个NED文件命名为tictoc1.ned
- IDE的NED编辑器有两种编辑模式Design、Source。在Design中,通过编辑器右边的Palette完成编辑;在Source中,通过直接编写NED代码完成编辑。在其中一个编辑器中的修改内容将会立刻反应到另一个编辑器中,所以在这两个编辑器中编辑都可以。(由于NED文件时是一般文本文件,所以我们甚至可以用外部文本编辑器来编辑,只是我们看不到语法高亮、内容复制、交叉引用和其它IDE特性等。)
- 进入Source模式下,输入以下内容:
simple Txc1 { gates: input in; output out; } //两个Txc1的实例(tic和toc) //tic toc互相传递消息 network Tictoc1 { submodules: tic:Txc1; toc:Txc1; connections: tic.out --> { delay = 100ms; } --> toc.in; tic.in <-- {delay=100ms;} <-- toc.out; }
当我们写完这些内容时,切换到Design模式下,可以看到如下变化:
程序第一块描述了Txc1,一个Simple module。Simple module是NED中的原子模块。它们也是活跃的组件,具体行为在C++中实现。上文代码也告诉了我们Txc1的特性——Txc1有一个input gate叫in,output gate叫out。第二块描述了Tictoc1,一个network。Tictoc1由两个子模块tic和toc组织而成,这两个子模块都是Txc1的子模块。tic的output gate与toc的input gate是互联的,反之亦然。在这两种方式的传播过程中,会有100ms的延迟。
关于NED语言的语法和细节描述,我们可以在OMNeT++ Simulation Manual下找到。
Step3、C++文件
我们现在需要用C++实现Txc1这个simple module的功能。
- New->Source File创建一个txc1.cc文件
- 在文件中输入以下代码:
#include<string.h> #include<omnetpp.h> using namespace omnetpp; /** * Txc1继承自cSimpleModule。在Tictoc1网络中 * tic和toc模块都是Txc1类对象,它们在仿真一开始就被 * omnet++创建了 */ class Txc1 :public cSimpleModule { protected: //以下虚函数标志了具体功能 virtual void initialize() override; virtual void handleMessage(cMessage *msg) override; }; //ned中的module需要和cc中class进行绑定 Define_Module(Txc1); void Txc1::initialize(){ //初始化函数在仿真开始时被调用 //为了开启tic-toc-tic-toc过程, //其中的一个模块需要发送第一个信息,假设这个模块是tic if(strcmp("tic",getName())==0){ //在"out" gate中生成并发送第一条消息; //"tictocMsg"是任意一个String,只不过这个String //标识了这个消息对象的名字 cMessage *msg=new cMessage("tictocMsg"); send(msg,"out"); } } void Txc1::handleMessage(cMessage *msg){ //当一条消息到达一个module之后,调用handleMessage()方法 //在这里的消息响应函数中,我们仅仅是把消息发送到其他的module,通过gate 'out' //由于tic和toc都会做同样的事情,因此消息实际上是在这两者之间不停反弹的 send(msg,"out"); }
NED中的simple module与C++的class相对应,即NED文件中的Txc1与cc文件中的Txc1相对应。Txc1 class继承自omnet++的cSimpleModule类,并且需要在cc文件中用Define_Module(ned_module)进行模块绑定。
我们需要在cc文件中对两个方法cSimpleModule:initialize()和handleMessage()进行函数重载。在仿真过程中调用这两个方法的时机:第一个方法在仿真开始时只调用一次;第二个方法在有消息到达module时被调用。
在initialize()中,我们构造了一个message对象(cMessage),然后通过gate out发送出去。该gate是和其他module的input gate相连接的,仿真内核会通过handleMessage()的参数该消息传送到其他module——在经过了NED文件中设置的100ms的传播延迟之后。另一个模块在经历另一个100ms延迟之后将会传送回来。
消息Messages(包、帧、jobs等)和事件Events(定时器、超时等)都是用cMessage对象表示的。在我们发送(send)或者调度(schedule)它们后,它们将被仿真内核的“scheduled event”或者“future events” List接收并保持,直到指定时间到达或者它们通过handleMessage()把消息传送到了别的module。
需要注意的是,仿真的整个过程将不会停止,而是永远进行下去。你可以从GUI中终止它。(你也可以在配置文件中指定仿真时间限制或者CPU时间限制)
Step4、omnetpp.ini文件
为了能够正常运行仿真程序,我们需要构造omnetpp.ini文件。这个文件告诉仿真程序哪个网络需要仿真,向model传递的参数,指明随机数生成器的种子等等。
- New->ini File;创建omnet.ini文件。
- 新文件将在Inifile 编辑器中打开。就像NED编辑器一样,Ini编辑器也有两种模式——Form和Source,当然在这两种模式下的编辑结果也是相同的。前者更适合配置仿真内核,后者适合设置模块参数。
转到Source模式后,写下如下代码:[General] network = Tictoc1
我们可以Form模式下确认这个结果
tictoc2和以后的例子,都会共享一个公共的omnetpp.ini文件
我们现在创建了第一个Model,接下来就是配置和运行它了。
3、Part 2 - Running the Simulation
Step1、启动仿真程序
当我们完成了章节2中的所有步骤之后,我们可以在选择omnetpp.ini文件后(无论是在编辑区域还是Project Explorer均可),再点击Run按钮启动一个仿真程序。
IDE将会自动构造我们的项目。我们可以在菜单中选择Project->Build All或者快捷键CTRL+B手动触发所有Module的Build。
Step2、运行仿真程序
在成功构造和启动仿真程序后,我们将会看到一个新的GUI窗口出现。这个窗口属于Qtenv——omnet++的运行时仿真GUI。你也会看到一个network,它包含了tik和tok,它们以动画的形式显示在主区域上。
点击Run按钮开始运行仿真过程。我们将会看到tic与toc不停地与另一个module交换消息的动画流。
toobar中显示了当前的仿真时间。这其实是个虚拟时间,与程序执行的实际时间没关系。实际上,现实世界中的仿真时间取决于硬件速度和仿真模型本身的算法复杂度。
需要注意的是,仿真过程中,节点处理消息的时间是0 s。整个过程中唯一使延缓仿真时间的是连接过程中的传播时延。
我们可以通过图形工具栏中的按钮加快或者减慢动画速度。
其中的各种快捷键:
- F8:停止仿真
- F4:运行单步
- F5:运行(有动画)
- F6:运行(无动画、速度极快)
- F7:运行(无动画、急速、无信息跟踪)
主窗口下方的状态栏中标识了运行时的信息,包括事件号和事件时间。
调试
点击工具栏中的Debug进入调试模式,之后仿真程序将在调试模式下进行。
运行时错误(Runtime errors)
经常使用调试功能来追踪运行时错误。
我们可以用以下语句试验一下:
在txc1.cc中,复制handleMessage()中的send()行,之后代码变为:
void Txc1::handleMessage(cMessage *msg){ send(msg,"out"); send(msg,"out"); }
当我们尝试运行仿真程序时,就会得到一个错误消息:
现在,在Debug模式下运行仿真程序。由于debug-on-errors选项默认开启,因此仿真程序将会在调试过程中停止。我们就可以通过追踪函数栈来定位错误。
断点
通过在代码框最左边双击或者右键Toggle Breakpoint构建断点。所有断点可以在Breakpoints视图中查看
结果序列图可视化
omnet++仿真内核可以在一个event log file(事件日志文件)中记录下消息交互。为了记录下事件日志,在RUN/Debug对话中勾选Record eventlog。此外,我们也可以在ini文件文件中指定record-eventlog = true;或者,在运行完程序后的Qtenv图像环境下点击Record按钮。
注意,应该在启动模拟之前就开启Eventlog Recording,否则只会记录下结尾一个Event,而不会显示出中间的Event及它们之间的消息传递过程。
日志文件可以在IDE中的Sequence Chart工具中进行相关分析。即项目文件下的results目录下的.elog文件。在IDE中双击这个文件就可以打开序列图,并且事件日志表也会同时显示在窗口底部。
注意:日志文件可能非常大,所以应确保在你真的需要用到它的时候再开启。
下图是从一个序列图构造出来的图像,展示了消息是如何在网络中的不同节点间进行路由操作的。在本例中这个图标很简单,但是当我们使用了复杂的模型时,序列图在调试、搜索或者记录模型的行为时就会变得很有用。
4、Part 3 - Enhancing the 2-node TicToc
①添加icon:Tictoc2
本节中我们将学习使模型变漂亮的方法。我们使用block/routing这个icon(在文件夹/image/block/routing.png),用青色修饰放在tic中,用黄色修饰放在toc。如何实现呢?添加display属性到NED文件中,在display中使用i=这个tag标识icon。
simple Txc1 { parameters: @display("i=block/routing");//添加默认icon gates: input in; output out; } //两个Txc1的实例(tic和toc) //tic toc互相传递消息 network Tictoc1 { @display("bgb=270,260"); submodules: tic: Txc1 { parameters: @display("p=64,161"); @display("i=,cyan");//不改变icon样式,只是给它加颜色 } toc: Txc1 { @display("p=142,58"); @display("i=,gold"); } connections: tic.out --> { delay = 100ms; } --> toc.in; tic.in <-- { delay = 100ms; } <-- toc.out; }
结果:
②添加logging
我们还要修改C++代码。我们为Txc1添加日志说明,来让Txc1打印出它正在做的事情。omnet++提供了很成熟的日志使用方法,包括日志等级、日志频道、过滤等。日志在大且复杂的模型中十分有用。在该模型中我们使用最简单的方式EV:EV相当于C++中的cout。
在tx1.cc文件中的方法initialize()中,添加以下代码:
EV << "Sending initial message\n";
完整的方法代码如下:
void Txc2::initialize(){ if(strcmp("tic",getName())==0){ EV << "Sending initial message\n"; cMessage *msg=new cMessage("tictocMsg"); send(msg,"out"); } }
在handleMessage()方法中,添加以下代码:
EV << "Received message '"<<msg->getName()<<"', sending it out again\n";
完整代码如下:
void Txc2::handleMessage(cMessage *msg){ EV << "Received message '"<<msg->getName()<<"', sending it out again\n"; send(msg,"out"); }
由于所有network都用同一个ini文件,因此为了与Tictoc1加以区分,需要修改ini文件,修改如下:
[General] # nothing here [Config Tictoc1] network = Tictoc1 [Config Tictoc2] network = Tictoc2
之后就可以正常运行了,运行过程中我们就可以看到日志窗口中输出的信息了:
我们也可以单独打开不同Module的日志信息,方法是右键Module->Open Componenet Log for xxx。这项功能在大项目(很多日志飞快扫过)中,特别是在我们对项目中的部分Module感兴趣时很有用。
③添加变量:tictoc3
本节中我们将为Module添加一个计数器,其作用是:在10次交流后删除信息。
本节内容是例3的内容了。
我们添加counter作为一个Txc3的成员变量:
class Txc3 :public cSimpleModule { private: int counter;//在此处声明counter protected: virtual void initialize() override; virtual void handleMessage(cMessage *msg) override; };
我们在initialize()中为counter设置为值10,并在handleMessage()中减少其值,这意味着,每有一个信息到达,counter就会减1。当它减少到0时,仿真结束。
此外在initialize()方法中还要加上一句:
WATCH(counter);
这样我们在运行过程中的动画中就能看到counter的变化(在左下角的Module窗口中)。
本例中的修改都在tictoc3.cc中,修改后如下:
#include<string.h> #include<omnetpp.h> #include<stdio.h> using namespace omnetpp; class Txc3 :public cSimpleModule { private: int counter;//在此处声明counter protected: //以下虚函数标志了算法 virtual void initialize() override; virtual void handleMessage(cMessage *msg) override; }; Define_Module(Txc3); void Txc3::initialize(){ counter = 10; WATCH(counter); if(strcmp("tic",getName())==0){ cMessage *msg=new cMessage("tictocMsg"); send(msg,"out"); } } void Txc3::handleMessage(cMessage *msg){ count--; if(counter==0){ EV << getName()<<"'s counter reached zero,deleting message\n"; delete msg; } else{ EV << getName()<<"'s counter is "<<counter<<", sending back messsage\n"; send(msg,"out"); } }
我们可以试着点击tic的icon,在左下角的Module窗口中显示了tic的细节,从中我们可以看到counter的值:
运行仿真后,可以看到counter不断减小直至0。
④添加Parameters:tictoc4
本节学习如何向仿真添加Parameters,我们将某个parameter的值设置为10,并且再新建一个boolean类型的parameter来决定module是否在它的初始化代码中发送首个消息(无论是tic还是toc module)。
1)在NED文件中声明parameter
Module parameters需要首先在NED文件中声明。数据类型可以是数值、string、bool或者xml。
simple Txc4 { parameters: bool sendMsgOnInit = default(false);//Module是否在其初始化函数中发送消息 int limit = default(2); @display("i=block/routing");//添加默认icon gates: input in; output out; }
2)修改cc文件,在cc文件中接收这些para
之后修改cc文件中的initialize()函数,读取到这个parameter,并将它分配给counter
counter = par("limit");
用第二个para来决定是否发送初始消息:
void Txc4::initialize(){ counter = par("limit"); if (par("sendMsgOnInit").boolValue()==true){ EV<<"Sending initial message\n"; cMessage *msg = new cMessage("tictocMsg"); send(msg,"out"); } }
现在,我们可以在NED文件或者ini文件中来设置这些para的值了。不过在NED中的赋值是更优先的,我们可以在定义para时用default(xxx)的方法设置默认值,正如我们在1)中的代码中所写的那样。
两种赋值方式如下:
①NED
network Tictoc4 { submodules: tic: Txc4 { parameters: sendMsgOnInit=true; @display("i=,cyan"); } toc: Txc4 { parameters: sendMsgOnInit=false; @display("i=,gold"); } connections: }
②ini
Tictoc4.toc.limit=5
需要注意的是,由于omnetpp.ini支持通配符,所以我们也可以写成以下形式:
Tictoc4.t*c.limit=5 Tictoc4.*.limit=5 **.limit=5
*与**的区别在于,*不会匹配单个点.,而**会。
在图形运行环境中,我们可以在Object Tree(位置在主窗口左边)中检查所有module parameters。
具有更小limit的module将会首先删除消息并随之终止仿真。
⑤NED继承:tictoc5
仔细观察我们之前写的代码,我们会发现tic和toc的不同之处仅仅在于parameter和它们的显示string。
我们可以通过继承的方式创造一个新的simple module。
本例(例6)中,我们用继承的方式生成两个simple module(Tic与Toc),之后我们可以使用这两个module来定义network中的submodule。
从一个已存在的simple module继承是很容易实现的,假设我们有一个simple module Txc5:
simple Txc5 { parameters: bool sendMsgOnInit=default(false); int limit=default(2); @display("i=block/routing"); gates: input in; output out; }
这个simple module是将要被继承的module,之后我们要写一个继承module,在继承module中我们指明parameter和display属性。
比如,Tic5
simple Tic5 extends Txc5 { parameters: @display("i=,cyan"); sendMsgOnInit=true;//Tic modules将会在初始时发送消息 }
和Toc5,只是parameters值有所不同:
simple Toc5 extends Txc5 { parameters: @display("i=,gold"); sendMsgOnInit=false;//Toc modules在初始时不发送消息 }
当我们构造新的simple module对象时,我们可以直接在network的submodule中使用这两个simple module:
network Tictoc5 { submodules: tic:Tic5; toc:Toc5; connections: tic.out --> { delay = 100ms; } --> toc.in; tic.in <-- { delay = 100ms; } <-- toc.out; }
正如我们看到的那昂,整个网络的定义变得很短很简单。继承允许我们使用公共部分,避免冗余定义和parameter设置。
⑥模拟节点处理延迟:tictoc6
在之前的model中,tic和toc会立刻把接收到的消息发送回去。本例(例6)中我们添加一些时间操作:tic和toc将会保存消息1s再发送出去。在omnet++中这样的时间操作的实现是通过节点发送消息给自身实现的。这些消息叫self-messages(这仅仅是因为它们的使用方式,否则就是一个普通消息)
我们给cc文件中的class添加两个cMessage*变量——event和tictocMsg,用以记录我们的时间、事件操作,进而在我们仿真时处理延迟。
class Txc6 : public cSimpleModule { private: cMessage * event;//指向用于时间操作的事件对象 cMessage * tictocMsg;//用于记录消息的变量 public :
我们通过scheduleAt()函数发送self-message,在函数中指明发送时间:
scheduleAt(simTime()+1.0,event)
在handleMessage()中我们必须对新消息加以区分——①通过input gate到达的消息;②self-message。
这里,我们可以用以下语句加以判断
if(msg==event)
或者我们也可以写成
if(msg->isSelfMessage())
这样做,我们就可以不用counter,进一步使源代码规模变小。
当运行仿真时,我们可以看到以下日志输出:
⑦随机数:tictoc7
本例中我们学习随机数。我们改变延迟时间,从1s到一个随机时间,可以在NED文件和ini文件中实现。Module parameters可以返回随机数,为了利用这个特性,我们可以在handleMessage()中读取这个随机parameter并使用它。
//"delayTime"module parameter标识了延迟时间 //这个parameter可以在ned和ini文件中设置,例如exponential(5) //之后我们就能得到不同的延迟时间了 simtime_t delay = par("delayTime"); EV<<"Message arrived , starting to wait "<<delay<<" secs..\n"; tictocMsg=msg;
除此之外,我们有小概率丢包的可能:
if(uniform(0,1)<0.1){ EV<<"\"Losing\" message\n"; delete msg; }
我们在ini文件中设置parameter:
Tictoc7.tic.delayTime = exponential(3s)
Tictoc7.toc.delayTime = truncnormal(3s,1s)
实验时我们会发现无论我们重运行多少次仿真(GUI中,Simulate->Rebuild network),其结果都是一样的。这是因为重运行时使用了相同的随机数种子,这在重现某次仿真时是非常有用的。
随机数种子的设置是在ini文件中,写法如下:
[General] seed-0-mt=532569 #或者任一32b的值
⑧超时、计时器:tictoc8
为了更进一步模拟网络协议,我们可以采用stop-and-wait仿真model。
这次(例7)我们将tic和toc分成单独的类,基本情景为:tic和toc分别向对方发送一个消息。只是toc会有一定概率丢包,在那种情况下tic需要重新发送。
以下是toc丢包时的处理代码:
void Toc8::handleMessage(cMessage *msg) { if(uniform(0,1)<0.1){ EV <<"\"Losing\" message.\n"; bubble("message lost"); //用弹出框标识消息丢失 delete msg; }
由于bubble()方法,toc将会展示出丢包时它的响应。
当tic发送完一个消息后将会启动一个计时器。超时时,就可以认为丢包了,并且需要重发一个。如果toc响应准时到达了,那么这个计时器就会被取消。这里的timer可以是一个self-message:
scheduleAt(simTime()+timeout,timeoutEvent);
定时器的取消,可以用cancelEvent()。我们可以不受限制地重用相同的超时消息。
cancelEvent(timeoutEvent);
确认消息的发送,可以用发回一个原消息代替:
void Toc8::handleMessage(cMessage *msg){ if(uniform(0,1)<0.1){ EV<<"\"Losing\" message.\n"; bubble("message lost"); delete msg; } else{ EV<<"Sending back same message as ack.\n"; send(msg,"out"); } }
⑨重传相同消息:tictoc9
本节中,我们要对之前一节的model进行优化。在上一个例子中,当我们需要重传时,我们创建了另一个包。这样做是OK的,因为一个包并不占用多少空间。但是在实际中,我们通常是保留一个原始包的备份,这样,当我们需要重传时就不用再创建新的包了。
一个思路是:创建一个指针,并始终让它指向刚发送过的消息。这听上去很简单,但是当这个消息在另一个节点中被销毁之后,这个指针就无效了。
本节中我们使用的方法是,保留原始包并只发送它的备份。当toc的Ack到达时,我们再删除这个原始包。为了让这一过程在模型中可视化,我们为每个消息记录一个消息号。
为了避免handleMessage()函数过于庞大,我们将相关代码放在两个新的函数generateNewMessage()和sendCopyOf()中,并在handleMessage()中调用它们:
cMessage * Tic9::generateNewMessage() { //每次产生不同名的消息 char msgname[20]; sprintf(msgname,"tic-%d",++seq); cMessage *msg=new cMessage(msgname); return msg; } void Tic9::sendCopyOf(cMessage *msg){ //复制消息,发送备份版 cMessage *copy=(cMessage *) msg->dup(); send(copy,"out"); }
由于tictoc9的例子集合了之前所有例子中的知识点,现一句一句加以分析:
NED文件:tictoc9.ned
simple Tic9 //① { parameters: @display("i=block/routing"); gates: input in; output out; } simple Toc9 //② { parameters: @display("i=block/process"); gates: input in; output out; } network Tictoc9 //③ { submodules: //④ tic:Tic9{ parameters: @display("i=,cyan"); } toc:Toc9{ parameters: @display("i=,gold"); } connections: //⑤ tic.out --> {delay=100ms;} -->toc.in; tic.in <-- {delay=100ms;} <-- toc.out; }
解释:
①、②:simple module
simple Tic9 { parameters: @display("i=block/routing"); gates: input in; output out; }
Tic9、Toc9分别是两种节点的simple module,本例中的这两个module有两部分内容parameters和gates——parameters中定义了module的组织、布局信息;gates中定义了输入输出端口input、output,不过本例中的输入输出端口各只有一个,所以用普通变量的形式;如果有多个端口,则要用[ ]表示的端口vector,具体用法和写法,可以见例子Tictoc10。
③:network
network Tictoc9
{
submodules:
...
connections:
...
}
构建了一个网络Tictoc9,一个网络最少要由submodules和connections两部分,submodules部分指明构成网络的各个module,connections指明所有module的连接方式。
④:submodules
submodules: tic:Tic9{ parameters: @display("i=,cyan"); } toc:Toc9{ parameters: @display("i=,gold"); }
每个network中的submodule都是由之前①、②中定义的simple module的实例对象,一个实例对象就对应着模块图中的一个节点,有多少实例对象就有多少个节点。
既可以一句话定义一个简单的simple module对象:
tic : Tic9 ; //obj : Module 左对象,右module
也可以在定义时指明额外的内容:
tic : Tic9{ parameters: @display("i=,cyan"); //附加某种颜色 }
⑤connections
所有module的所有gate都必须说明连接方式。也就是说,每一个module的output gate都必须从另一个module的input gate相连接,连接方式总是:
xxx1.out --> { ... } --> xxx2.in; //或 xxx2.in <-- { ... } <-- xxx1.out;
这两种连接方式的结果完全相同,只是写法不同而已,实际写代码的过程中哪种都可以。
注意:所有定义的gate都必须实现,如果某个gate真的不需要实现,就需要另外写一个类,在这个类中不写这个gate。换言之,只要说某个submodule是实现自某个simple module,那么这个submodule就必须实现该simple module的所有gate。
cc文件:txc9.cc
#include<stdio.h> #include<string.h> #include<omnetpp.h> using namespace omnetpp; class Tic9 : public cSimpleModule //① { private: //② simtime_t timeout; cMessage *timeoutEvent; int seq; cMessage * message; public: //③ Tic9(); virtual ~Tic9(); protected: //④ virtual cMessage * generateNewMessage(); virtual void sendCopyOf(cMessage * msg); virtual void initialize() override; virtual void handleMessage(cMessage * msg) override; }; Define_Module(Tic9);//⑤ Tic9::Tic9(){//⑥ timeoutEvent=message=nullptr; } Tic::~Tic9(){//⑦ cancelAndDelete(timeoutEvent); delete message; } void Tic9::initialize(){//⑧ //初始化变量 seq=0; timeout=1.0; timeoutEvent=new cMessage("timeoutEvent"); //生成并发送消息 EV<<"Sending initial message\n"; message=generateNewMessage(); sendCopyOf(message); scheduleAt(simTime()+timeout,timeoutEvent); } void Tic9::handleMessage(cMessage *msg){//⑨ if(msg==timeoutEvent){ //收到超时消息就意味着丢包,需要重传 EV<<"Timeout expired,resending message and restarting timer\n"; sendCopyOf(message) scheduleAt(simTime()+timeout,timeoutEvent); } else{ //如果收到确认消息,就 //1、删除确认消息 //2、删除原始消息原件 //3、取消超时事件 //4、生成另一个新消息,发送 EV<<"Received: "<<msg->getName()<<"\n"; delete msg; EV<<"Timer cancelled.\n"; cancelEvent(timeoutEvent); delete message;//消息原件 message=generateNewMessage();//新消息原件 sendCopyOf(message); scheduleAt(simTime()+timeout,timeoutEvent);//计时器 } } cMessage * Tic9::generateNewMessage(){//⑩ //每次产生一个不同名的消息 char msgname[20]; sprintf(msgname,"tic-%d",++seq); cMessage *msg = new cMessage(msgname); return msg; } void Tic9::sendCopyOf(cMessage * msg){//⑪ //复制原件,发送复制版本 cMessage * copy =(cMessage*)msg->dup(); send(copy,"out"); } class Toc9:public cSimpleModule { protected: virtual void handleMessage(cMessage * msg) override; }; Define_Module(Toc9); void Toc9::handleMessage(cMessage *msg){//⑫ if(uniform(0,1)<0.3){ EV<<"\"Losing\" message\n"; bubble("message lost"); delete msg; } else{ EV<<msg>>" received, sending back an ACK.\n"; delete msg; send(new cMessage("ACK"),"out"); } }
①
class Tic9 : public cSimpleModule { ... }
cc文件中的class需要和ned文件中的simple module一一对应;这些simple module对应的class都是继承自cSimpleModule。
所以,cc文件中的class的写法基本上都是:
class xxx : public cSimpleModule
②
private: simtime_t timeout; cMessage * timeoutEvent; int seq; cMessage * message;
把变量的权限设置为private,在本例中有两个cMessage * ——message和timeoutEvent,分别是最初message和超时self-message。
这里的时间变量都是simtime_t类型,而不是int类型。seq是用来标记发送信息的序号。
③
public : Tic9(); virtual ~Tic9();
构造函数和析构函数,构造函数中是对我们在private中定义的变量进行初始化的过程,析构函数中则要对对必要的指针进行delete。
④
protected: //④ virtual cMessage * generateNewMessage(); virtual void sendCopyOf(cMessage * msg); virtual void initialize() override; virtual void handleMessage(cMessage * msg) override;
⑤
Define_Module(Tic9);//⑤
cc文件中的class名要与ned文件中的同名simple module相绑定,通过方法Define_Moduel(xxx);
⑥
Tic9::Tic9(){//⑥ timeoutEvent=message=nullptr; }
构造函数中,对指针对象进行初始化,本例中为两个cMessage消息初始化为空指针
⑦
Tic::~Tic9(){//⑦ cancelAndDelete(timeoutEvent); delete message; }
析构函数中,清理指针对象,释放空间。可以用关键字delete或者cancelAndDelete(msg)两种方法。
⑧
void Tic9 :: initialize(){ seq=0; timeout=1.0; timeouEvent=new cMessage("timeoutEvent"); //生成并发送消息 EV<<"Sending initial message.\n"; message=generateNewMessage(); sendCopyOf(message); scheduleAt(simTime()+timeout,timeoutEvent); }
1)在initialize初始化函数中,对我们之前的private中的变量初始化。
2)生成新消息,并发送:
message = generateNewMessage();
sendCopyOf(message);
3)发送消息后,再用scheduleAt发送self-message,用来启动定时器
scheduleAt(simTime()+timeout,timeoutEvent);
先看⑩和⑪
cMessage * Tic9 :: generateNewMessage(){ //⑩ //每次产生一个不同名的消息 char msgname[20]; sprintf(msgname,"tic-%d",++seq); cMessage *msg = new cMessage (msgname); return msg; } void Tic9 :: sendCopyOf(cMessage * msg){ //⑪ cMessage * copy = (cMessage*)msg->dup(); send(copy,"out"); }
⑩给出了创建不同名消息的方法
⑪给出了备份消息并发送副本的方法
⑨
void Tic9::handleMessage(cMessage *msg){//⑨ if(msg==timeoutEvent){ //收到超时消息就意味着丢包,需要重传 EV<<"Timeout expired,resending message and restarting timer\n"; sendCopyOf(message) scheduleAt(simTime()+timeout,timeoutEvent); } else{ //如果收到确认消息,就 //1、删除确认消息 //2、删除原始消息原件 //3、取消超时事件 //4、生成另一个新消息,发送 EV<<"Received: "<<msg->getName()<<"\n"; delete msg; EV<<"Timer cancelled.\n"; cancelEvent(timeoutEvent); delete message;//消息原件 message=generateNewMessage();//新消息原件 sendCopyOf(message); scheduleAt(simTime()+timeout,timeoutEvent);//计时器 } }
处理消息,由于我们会收到两种消息,普通消息和self-message,所以需要在这个函数中用if语句加以判断:
1)判断是否是self-message,如果是,表明超时了,就需要再发送消息副本,并重新计时:
if(msg==timeoutEvent){ //收到超时消息就意味着丢包,需要重传 EV<<"Timeout expired,resending message and restarting timer\n"; sendCopyOf(message) scheduleAt(simTime()+timeout,timeoutEvent); }
其中在scheduleAt中的timeoutEvent和我们在initialize中的timeoutEvent是同一个,并且在整个过程中都是同一个。
2)如果是一般Message,就说明正确收到了,就需要做4件事:删除确认消息、删除原消息原件、取消超时事件、生成另一个新消息(备份并重置计时器)
else{ //如果收到确认消息,就 //1、删除确认消息 EV<<"Received: "<<msg->getName()<<"\n"; delete msg; EV<<"Timer cancelled.\n"; //3、取消计时器 cancelEvent(timeoutEvent); //2、删除原始消息原件 delete message; //4、生成另一个新消息,发送副本,重置计时器 message=generateNewMessage(); sendCopyOf(message); scheduleAt(simTime()+timeout,timeoutEvent); }
⑫对接收消息模块的处理,需要模拟丢包概率,代码都在handleMessage:
void Toc9::handleMessage(cMessage *msg){//⑫ if(uniform(0,1)<0.3){ EV<<"\"Losing\" message\n"; bubble("message lost"); delete msg; } else{ EV<<msg>>" received, sending back an ACK.\n"; delete msg; send(new cMessage("ACK"),"out"); }
1)丢包的判断,使用随机数生成函数
if(uniform(0,1)<0.3)
2)如果丢包了,就要弹出消息,说明丢包了,并把接收到的消息丢弃(因为实际上收到了消息,只是我们用随机数的方法模拟丢包,所以丢包发生时我们需要手动把包丢弃)
if(uniform(0,1)<0.3){ EV<<"\"Losing\" message\n"; bubble("message lost");//弹出丢包消息 delete msg; //丢弃接收到的消息,以模拟丢包 }
3)如果没丢,处理消息(本例中没有涉及,仅仅是说明收到了),输出日志,删除收到的消息,并发回ACK消息:
else{ EV<<msg>>" received, sending back an ACK.\n"; delete msg; send(new cMessage("ACK"),"out"); }
未完待续,之后还有几个例子,涉及到真实网络架构的搭建,具体可以见官方文档翻译总结(三)
总结
- NED文件的一般形式:
//simple moduel simple XXX { parameters: @display("i=block/routing");//添加默认icon gates: input in;//输入端口 output out;//输出端口 } //network network XXXNet { submodules://子模块,写法 子模块名:子模块类型 sm1:XXX;{ @display("i=,cyan");//icon颜色 } sm2:XXX; connections://连接情况 sm1.out --> {delay=100ms;} -->sm2.in; //一个的输出端口与另一个的输入端口连接,链路延迟100ms sm1.in <-- {delay=100ms;} <-- sm2.out; }
- 每个NED都一个与之对应的cc文件,在cc文件中必须实现与NED中定义的module相对应的class,定义该class的写法一般是:
class xxx : public cSimpleModule
并且要在随后,将这个class与NED文件中的module进行绑定,绑定方法为:
Define_Module(xxx);//xxx是Ned文件中的module
cc文件中的class名和ned文件中的对应module名必须相同!!!!
- C++中接收ned文件中定义的变量时,需要用par关键字:
var xxx1 = par("xxx0") //xxx0是ned文件中需要承接的变量名
- send(msg,"out"):通过out gate发送消息msg;
- 在initialize()中创建新消息并发送:
cMessage *msg=new cMessage("tictocMsg"); send(msg,"out");
- 在handleMessage()中处理接收到的消息;
- 关于initialize和handleMessage方法的使用时机——前者只在该module仿真开始时调用一次;后者在消息到达module时被调用;
- Message(包、帧等)和Event(计时器、超时等)都是用cMessage对象实现的,只是表现方式不同(具体见下边总结的部分);
- 仿真过程的执行时间可以在Run Configuration中指定;
- 所有同一个工程下的项目,共享一个ini文件,但是可以在其中对每个项目进行分别的配置和运行,使它们互不干扰:
[General] [Config Tictoc1] network = Tictoc1 …… [Config Tictoc7] network = Tictoc7 Tictoc7.tic.delayTime = exponential(3s) Tictoc7.toc.delayTime = truncnormal(3s,1s)
- cc文件中的EV,在功能和用法上相当于cout(不过开头要加using namespace omnetpp);如果在initialize和handleMessage中用EV,可以输出信息到运行时的日志窗口中
- 我们可以查看单个节点的日志消息:
这在大型项目中,有很多个节点时很有帮助。
- 类变量的定义方法和C++中的语法完全相同,例如:
class Txc3 :public cSimpleModule { private: int counter;//在此处声明counter
如果要在运行过程中监视其变化,需要在initialize方法中用WATCH关键字:
void Txc3::initialize(){ counter = 10; WATCH(counter);
- NED继承,通过simple module继承得到新的simple module,更好地实现代码重用:
simple Txc5 { ...//共同的代码部分 } simple Tic5 extends Txc5 { ...//独特的代码部分 }
继承下来的simple module,写在network的submodules下:
network Tictoc5 { submodules: tic:Tic5; toc:Toc5; }
- 模拟处理延迟:self-message;通过scheduleAt()方法发送self-message,需要在函数中说明发送时间,一般用法为:
sceduleAt(simTime()+1.0,event);
这里的event是class中额外定义的一个cMessage *指针,用来表明一个self-message,此外还有个cMessage *指针xxxMsg,用来表明一个一般消息,即发送给别的节点的消息。
如何对这两种消息加以区分?
if(msg==event)//self-message //或 if(msg->isSelfMessage())
- 随机数:在module中的parameter中定义一个随机数,在cc文件中读取并使用它,需要用到par关键字:
simtime_t delay = par("delaytime");
比如上段代码中,我们在NED中定义随机数,在ini文件中对这个随机数进行赋值:
//NED simple Txc7 { parameters: volatile double delayTime @unit(s); //在发回message之前有一段延迟 //ini [Config Tictoc7] network = Tictoc7 Tictoc7.tic.delayTime = exponential(3s) Tictoc7.toc.delayTime = truncnormal(3s,1s)
- 在handleMessage()函数中使用uniform函数来模拟丢包
if(unifor(0,1)<0.1){ ... delete msg; }
- 随机数种子在ini文件的General下设置
[General] seed-0-mt=532569 #或者任一32b的值
- 使用self-message构建计时器,从而实现超时重传的效果:
scheduleAt(simTime()+timeout,timeoutEvent);
关于一般消息和self-message的区分,可以看总结部分第15条所述。
ACK消息收到后,需要取消计时器,方法cancelEvent():
cancelEvent(timeoutEvent);
- 消息重传。原理:在收到ACK前保留原件,收到ACK后删除原件,发送并保存新消息的原件;如果超时(具体实现见19),则保留原件并发送原件的备份:备份消息(Tictoc9):
void Tic9::sendCopyOf(cMessage *msg){ //复制消息,发送备份版 cMessage *copy=(cMessage *) msg->dup(); send(copy,"out"); }
- 空指针:nullptr
- 发送ACK消息的方法:
if(uniform(0,1)<0.3){ ...//丢包处理 } else{ //没丢 ...//处理消息 ...//删除收到的消息 //发回确认 send(new cMessage("ACK"),"out"); }