TicToc Part3
- 增强2节点 TicToc
- 增加图标
- 增加 日志
- 添加状态变量
- 增加参数
- 使用NED 继承
- 模拟处理延时
- 随机数字和参数
- 超时、取消计时器
- 重传同样的消息
官方文档
在官方文档中,你可以看见所有的代码
增强2节点 TicToc
增加图标
为了使模型在GUI中看起来更好看,可以在ned文件中添加显示字符串来实现。如下代码指定了 块/路由图标(文件images/block/routing.png),并将tic绘制为青色,将toc绘制为黄色。
simple Txc2
{
parameters:
@display("i=block/routing"); // add a default icon
gates:
input in;
output out;
}
//
// Make the two module look a bit different with colorization effect.
// Use cyan for `tic', and yellow for `toc'.
//
network Tictoc2
{
submodules:
tic: Txc2 {
parameters:
@display("i=,cyan"); // do not change the icon (first arg of i=) just colorize it
}
toc: Txc2 {
parameters:
@display("i=,gold"); // here too
}
connections:
tic.out --> { delay = 100ms; } --> toc.in;
tic.in <-- { delay = 100ms; } <-- toc.out;
}
可以看见显示效果如下图:
总结:这就是教我们可以对图标、颜色等进行设置,让其仿真显示更好看
增加 日志
可以修改C++代码,将日志语句添加到Txc1中,打印出它现在正在做的事。
omnet++提供了一个复杂的日志记录功能,包括日志级别、日志通道、过滤等,对于大型和复杂的模型非常有用,但在这个模型中,我们将使用其最简单的形式EV:
EV << "Sending initial message\n";
EV << "Received message `" << msg->getName() << "', sending it out again\n";
在运行代码时,要运行的是tictoc2相关的代码,记得在ini文件中进行更改;
这样在运行仿真时会出现选择,选择要进行的仿真:Tictoc2
我们可以在日志窗口中查看:
使用右键单击某个模块查看日志在大型仿真中会非常有用
总结:这就是在cc文件中添加日志,在仿真时可以查看日志,帮助我们了解仿真运行过程中的细节,在调试时也会有用
添加状态变量
在这一步骤中我们添加一个计数器到模型中,在进行10次数据交换后会删除消息
将计数器作为一个类成员添加:
class Txc3 : public cSimpleModule
{
private:
int counter; // Note the counter here
protected:
我们在initialize()中将该变量设置为10,并在handleMessage()中减1,即在每条消息到达时减1。当它达到0时,模拟将耗尽事件并终止。
注意:
WATCH(counter)
这一行可以让我们可以在图形化运行界面中看见计数器的值
如果你点击tic的图标,主窗口左下角的inspector窗口将显示tic的详细信息。确保从顶部的工具栏中选择Children模式。检查器现在显示counter变量。
当继续运行模拟时,可以跟踪计数器的递减过程,直到达到0。
总结: 在cc文件中添加变量,该变量可以仿真的运行进行控制,并且可以通过WATCH()然后在仿真运行时在左下角界面的 children模型中查看该变量,了解仿真运行的一些参数细节
增加参数
在这一步中,你将学习如何在模拟中添加输入参数:我们将把“魔数”10变成一个参数,并添加一个布尔参数来决定模块是否应该在其初始化代码中发送第一个消息(无论是tic还是toc模块)。
模块参数必须在NED文件中声明。数据类型可以是数值型、字符串型、布尔型或xml(后者便于访问xml配置文件)等。
simple Txc4
{
parameters:
bool sendMsgOnInit = default(false); // whether the module should send out a message on initialization
int limit = default(2); // another parameter with a default value
@display("i=block/routing");
gates:
我们要在C++代码中读取该参数,并将其分配给变量counter
35 counter = par("limit");
我们可以用第二个参数决定是否开启最初的消息发送
39 if (par("sendMsgOnInit").boolValue() == true) {
现在,我们可以在NED文件或omnetpp.ini中分配参数。NED文件中的赋值优先。如果在NED文件中使用default(…)语法,则可以定义参数的默认值。在这种情况下,您可以设置omnetpp.ini中的参数值,或者使用NED文件提供的默认值。
我们在下面分配一个参数在NED文件中:
network Tictoc4
{
submodules:
tic: Txc4 {
parameters:
sendMsgOnInit = true;
@display("i=,cyan");
}
toc: Txc4 {
parameters:
sendMsgOnInit = false;
@display("i=,gold");
}
connections:
分配另一个在 omnetpp.ini中:
17 Tictoc4.toc.limit = 5
请注意,因为omnetpp.ini支持通配符,并且NED文件分配的参数优先于omnetpp.ini中的参数,所以我们可以使用
Tictoc4.t*c.limit=5
Tictoc4.*.limit=5
**.limit=5
这些表述的作用是相同的
在图形化运行时环境中,您可以在主窗口左侧的对象树中,或者在模块检查器的参数页中检查模块参数(单击模块后,信息显示在主窗口的左下角)。
限制较小的模块将删除消息,从而结束仿真。
总结 :上一节我们是在cc文件中定义参数在cc文件中使用,然后通过WATCH()让其可以在仿真界面中查看 ;而本节,我们是在NED文件中定义参数(数值型、字符串型、布尔型或xml),然后在cc文件中也可以通过par()直接使用,并且可以在NED文件或ini文件中对在NED文件中声明的参数进行初始化(NED中的初始化优先级较高),在仿真界面左侧可以查看在NED中声明的这些参数的值。
使用NED 继承
如果我们仔细看一下NED文件,我们会意识到tic和toc的不同之处在于它们的参数值和显示字符串。我们可以通过继承另一个模块类型并指定或覆盖它的一些参数来创建一个新的简单模块类型。在我们的例子中,我们将派生出两个简单的模块类型(Tic和Toc)。稍后我们可以在定义网络中的子模块时使用这些类型。
从现有的简单模块派生很容易。这是基本模块:
simple Txc5
{
parameters:
bool sendMsgOnInit = default(false);
int limit = default(2);
@display("i=block/routing");
gates:
input in;
output out;
}
注:我们会感觉这和之前的Txc很像,但之前是创建了一个Txc模块,在构建网络时再创建tic和toc,两者基于Txc添加了一些参数,这样之后是无法直接拿来使用,并且每使用一次都要再创建一次;而这里是先创建Txc5模块,然后直接根据Txc5继承出Tic5和Toc5,在所有网络中都可以快捷的使用这两个模块
如下为派生模块,简单地指定参数值并添加一些显示属性:
simple Tic5 extends Txc5
{
parameters:
@display("i=,cyan");
sendMsgOnInit = true; // Tic modules should send a message on init
}
simple Toc5 extends Txc5
{
parameters:
@display("i=,gold");
sendMsgOnInit = false; // Toc modules should NOT send a message on init
}
注:c++实现继承自基本简单模块(Txc5)。
我们创建了新的简单模块之后,我们就可以在网络中使用它们作为子模块类型:
network Tictoc5
{
submodules:
tic: Tic5; // the limit parameter is still unbound here. We will get it from the ini file
toc: Toc5;
connections:
正如您所看到的,网络定义现在更短、更简单。继承允许您在网络中使用通用类型,并避免冗余定义和参数设置。
总结:面对结构相似而只有部分参数等不同的模块,我们可以先创建基类,在继承基类并添加或覆盖一些参数来创建模块,可以使编程更快捷与简单。
模拟处理延时
在之前的模型中,tic和toc会立即将接收到的消息发送回来。在这里,我们将添加一些计时:tic和toc将在发送回消息之前保存消息模拟1秒。在omnet++中,这种定时是通过模块向自身发送消息来实现的。这样的消息称为自消息(但这只是因为它们的使用方式,否则它们就是普通的消息对象)。
我们在类中添加了两个cMessage *变量event和tictocMsg,以记住我们用于计时的消息和我们模拟的处理延迟的消息。
class Txc6 : public cSimpleModule
{
private:
// Set the pointers to nullptr, so that the destructor won't crash
// even if initialize() doesn't get called because of a runtime
// error or user cancellation during the startup process.
cMessage *event = nullptr; // pointer to the event object which we'll use for timing
cMessage *tictocMsg = nullptr; // variable to remember the message until we send it back
public:
我们使用scheduleAt()函数“发送”self-message,指定何时将其传递回模块。
92 scheduleAt(simTime()+1.0, event);
在handleMessage()中,现在我们必须区分新消息是通过输入门到达的还是通过自消息返回的(定时器过期)。这里我们使用
78 if (msg == event) {
也可以这样写:
if (msg->isSelfMessage())
为了保持源代码短小,我们省略了计数器。
运行模拟时,你会看到以下日志输出:
总结:本节的主要目的是为了仿真处理时延,时延的产生是通过向自身发送自消息来实现的,使用scheduleAt()函数来指定何时发送自消息,函数两个变量第一个是发送消息的时间,第二个变量是发送的内容,其中还使用到函数simTime()该函数是获得当前的仿真时间,配合 simTime()+1可以获得当前时间的下一秒的时间,控制消息在1s后发送。
注:Txc6.cc文件完整源代码
另外在完整cc代码中,Txc6中定义了两个消息,event指向自消息,tictocMsg是发送给对方的消息。程序运行的流程是,在initialize()中,会在第5s时发送自消息,所以程序会在开头等待5s,我们可以把这5s视作系统启动,在tic在第5s向自己发送了自消息event后,在tic的handleMessage(cMessage *msg)函数中接收到event,判断msg == event成立,会往网口 out发送 tictocMsg ;然后toc接受到该消息,使用tic的handleMessage(cMessage *msg)函数处理该消息,判断 msg = = event 不成立,在else中调用 scheduleAt(simTime()+1.0, event);向自己发自消息,该消息我们视作toc的处理时延,在1s后toc接收到自己的自消息,调用handleMessage(cMessage *msg)函数处理,判断msg = = event成立,也往网口 out发送 tictocMsg 到tic 。就这样循环下去。
随机数字和参数
在这一步中,我们将引入随机数。我们将延迟从1更改为随机值,该值可以从NED文件或omnetpp.ini中设置。模块参数能够返回随机变量;然而,为了利用这个特性,每次使用handleMessage()时都必须读取它的形参。
// The "delayTime" module parameter can be set to values like
// "exponential(5)" (tictoc7.ned, omnetpp.ini), and then here
// we'll get a different delay every time.
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;
}
我们将在omnetpp.ini中分配参数:
Tictoc7.tic.delayTime = exponential(3s)
Tictoc7.toc.delayTime = truncnormal(3s,1s)
可以查看仿真日志如下:
无论您重新运行模拟多少次(或重新启动它,Simulate -> Rebuild network菜单项),您都可以尝试,您将得到完全相同的结果。 这是因为omnet++使用确定性算法(默认为Mersenne Twister RNG)来生成随机数,并将其初始化为相同的seed
。这对于可重复性的模拟很重要。你可以在omnetpp.ini中添加以下几行代码来试验不同的seed:
[General]
seed-0-mt=532569 # or any other 32-bit value
根据语法,您可能已经猜到omnet++支持多个rng。没错,然而,本教程中的所有模型都使用RNG 0。(RNG,Random Number Generators,随机数生成器)
总结:这里就是想给处理时延添加随机性,为了便于后续调整该参数,在Ned文件中定义处理时延,在cc文件中每次模拟处理时延时使用par()函数来调用,在Ini文件中对时延参数进行设置。
代码注释:
uniform(0, 1) < 0.1
uniform(0, 1): 这是一个随机数生成表达式,表示生成一个在区间[0, 1]内均匀分布的随机数
volatile double delayTime @unit(s); // delay before sending back message
volatile :volatile 用于声明一个变量是“易失性”的,意味着该变量的值可以在未经优化的情况下被访问和修改。这通常用于与仿真时间或其他可能在仿真期间变化的值相关的变量。这个修饰符确保了在仿真运行过程中,该变量的值能够被准确地获取和修改。
@unit(s): 这是一个单位注解。在OMNeT++中,你可以使用 @unit 注解来指定变量的单位。在这里,(s) 表示该变量的单位是秒(seconds)
Tictoc7.tic.delayTime = exponential(3s)
Tictoc7.toc.delayTime = truncnormal(3s,1s)
exponential(3s)表示使用指数分布生成随机数,其中(3s)是指定的参数,表示指数分布的均值为3秒。这意味着tictoc7.tic.delayTime的初始值将以指数分布的方式随机生成,其中平均值为3秒。
truncnormal(3s,1s)表示使用截断正态分布生成随机数,其中(3s)是指定的均值,(1s)是指定的标准差。
超时、取消计时器
为了离网络协议建模更近一步,让我们将模型转换为停等仿真。这次我们将为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"); // making animation more informative...
delete msg;
}
else {
由于代码中调用了bubble(), toc在删除消息时将显示一个调出框。
当定时器到期时,我们假定消息丢失并发送另一条消息。如果toc的回复到达,定时器必须取消。计时器将是(还有什么?)一条自消息。
65 scheduleAt(simTime()+timeout, timeoutEvent);
取消定时器可以通过调用cancelEvent()来完成。请注意,这并不妨碍我们反复重用相同的超时消息。
71 cancelEvent(timeoutEvent);
在日志中我们可以看见:
总结: 该例子就是仿真toc会有一定丢包的概率,而tic对丢包有处理能力,在一定时间内未获得toc回传的消息时tic将会重发消息。因为tic与toc工作方式差异较大,所以不能再基于同一个类Txc,而是要各自创建一个类Tic、Toc。
在toc丢包时,使用delete删除收到的消息,并且为了在仿真界面上展现出丢包使用bubble()函数。
tic使用timeout定义设置超时时间,使用timeoutEvent作为超时事件的消息,在handleMessage(cMessage *msg)中正常接收到消息时会进入else,先取消timeoutEvent,这样就不会发送timeoutEvent的自消息,这样就不会进入timeoutEvent的if中,然后会正常发送消息,如果toc及时返回消息,会再次进入else重复这个过程;而如果toc没有及时返回消息,scheduleAt(simTime()+timeout, timeoutEvent);就会发送timeoutEvent的自消息,if (msg == timeoutEvent)成立,进入到超时重发。
代码解析:
cancelEvent(timeoutEvent);
cancelEvent()函数用于取消当前正在处理的仿真事件,使得该事件不再执行。这可以用于在仿真运行过程中取消已经计划的事件,以便在需要时中止特定的仿真行为或进行动态调整。后续事件都不会受到取消事件的影响,它们将继续按照预定的顺序执行。
重传同样的消息
在这一步中,我们完善之前的模型。在这里,如果需要重传,我们刚刚创建了另一个数据包。这样做没有问题,因为数据包包含的内容不多,但在现实生活中,通常更实际的做法是保留原始数据包的副本,这样我们就可以重新发送它,而不需要重新构建。保留一个指向已发送消息的指针——以便我们可以再次发送它——可能看起来更容易,但当消息在另一个节点被销毁时,指针就变得无效了。
我们在这里做的是保留原始数据包,只发送它的副本。收到toc的确认后,我们删除了原件。为了便于可视化地验证模型,我们将在消息名称中包含一个消息序列号。
为了避免handleMessage()变得太大,我们将把相应的代码放在两个新函数中,generateNewMessage()和sendCopyOf(),并在handleMessage()中调用它们。
cMessage *Tic9::generateNewMessage()
{
// Generate a message with a different name every time.
char msgname[20];
sprintf(msgname, "tic-%d", ++seq);
cMessage *msg = new cMessage(msgname);
return msg;
}
void Tic9::sendCopyOf(cMessage *msg)
{
// Duplicate message and send the copy.
cMessage *copy = (cMessage *)msg->dup();
send(copy, "out");
}
总结 : 发送消息的副本,保留原始消息方可能需要的重传