OMNeT++ tutorial 2节点tic-toc的进阶
OMNeT++ tutorial 2节点tic-toc的进阶
实验目的
成功运行tictoc之后,实现了两个节点相互发送消息的功能。本组实验包含从tictoc2到tictoc9的内容,在tictoc的基础上进行修改,实现各种新功能。实验的完整源代码在tutorial中可以找到。
实验内容
前置知识
- 安装和运行OMNeT++模拟器,并运行tictoc(可参考链接)
- tictoc项目由一个ned文件,一个cc文件和
omnetpp.ini
文件构成。ned文件用于定义组件并组合成大的单元(例如网络),C++文件中实现模块的具体功能,omnetpp.ini
指定希望模拟的具体网络 - 接下来的实验代码可以和tictoc放在同一个目录下,每次运行实验需要修改对应的
omnetpp.ini
tictoc2&3:添加图标、输出日志与计数器设置
在tictoc3.ned
中,描述了在GUI中添加图标的方法:
simple Txc3
{
parameters:
@display("i=block/routing");//添加一个默认图标
gates:
input in;
output out;
}
network Tictoc3
{
submodules:
tic: Txc3 {
parameters:
@display("i=,cyan");//不改变图标,只设置颜色(青色)
}
toc: Txc3 {
parameters:
@display("i=,gold");//同上,设置金色
}
connections:
tic.out --> { delay = 100ms; } --> toc.in;
tic.in <-- { delay = 100ms; } <-- toc.out;
}
原始tictoc中互相发送消息的操作是不会自动停止的,本实验新增一个计数器,在十次交换之后删除消息,使交换过程停止。其中EV<<
语句用于在日志窗口中输出日志信息。
#include <stdio.h>
#include <string.h>
#include <omnetpp.h>
using namespace omnetpp;
//添加一个计数器,在十次交换之后删除信息
class Txc3 : public cSimpleModule
{
private:
int counter; //添加一个计数器,int类型
protected:
virtual void initialize() override;
virtual void handleMessage(cMessage *msg) override;
};
Define_Module(Txc3);
void Txc3::initialize()
{
// 初始化计数器
counter = 10;
WATCH(counter);
//watch可以用来在qtenv下可视化检查变量
if (strcmp("tic", getName()) == 0) {
EV << "Sending initial message\n";
//用EV输出日志信息
cMessage *msg = new cMessage("tictocMsg");
send(msg, "out");
}
}
void Txc3::handleMessage(cMessage *msg)
{
//计数器-1
counter--;
if (counter == 0) {
EV << getName() << "'s counter reached zero, deleting message\n";
//getName是当前实体的名字,例如toc
delete msg;//删除消息,模拟将会停止,提示没有更多事件
}
else {
EV << getName() << "'s counter is " << counter << ", sending back message\n";
send(msg, "out");
}
}
运行效果如下图,当消息被删除后,会弹出No more events
的提示,日志窗口中也会出现由EV
语句输出的INFO信息,提示tic或toc中的计数器当前值。
tictoc4:添加参数、通配符
本实验添加一个输入参数(limit),控制消息交换的次数;并添加一个布尔类型的参数,决定该模块在初始化代码中是否发送第一条消息。
tictoc4.ned
如下所示,在simple
中声明了参数及其默认值
simple Txc4
{
parameters:
bool sendMsgOnInit = default(false); //指定初始化时是否发送消息
int limit = default(2); // limit参数,默认为2
@display("i=block/routing");
gates:
input in;
output out;
}
network Tictoc4
{
submodules:
tic: Txc4 {
parameters:
sendMsgOnInit = true;//由tic来发送第一条消息
@display("i=,cyan");
}
toc: Txc4 {
parameters:
sendMsgOnInit = false;
@display("i=,gold");
}
connections:
tic.out --> { delay = 100ms; } --> toc.in;
tic.in <-- { delay = 100ms; } <-- toc.out;
}
在C++代码中,读取参数并赋值给计数器
class Txc4 : public cSimpleModule
{
private:
int counter;
protected:
virtual void initialize() override;
virtual void handleMessage(cMessage *msg) override;
};
Define_Module(Txc4);
void Txc4::initialize()
{
counter = par("limit");//用limit参数初始化计数器
// 不再使用模块名决定初始化时是否发送初始消息
if (par("sendMsgOnInit").boolValue() == true) {//参数值为真,说明是tic
EV << "Sending initial message\n";
cMessage *msg = new cMessage("tictocMsg");
send(msg, "out");
}
}
在ned文件或omnetpp.ini
中都可以配置参数,但ned文件中的配置优先。如果ned使用了default
,可以在ini中设置参数的值,也可以使用ned文件的默认值。
如果omnetpp.ini
如下所示:
network = Tictoc4
Tictoc4.toc.limit = 5
那么toc的limit值为5,tic的limit值为2(是ned文件中设置的默认值),由于tic的计数器值更小,运行过程中执行两次交互就会结束消息传输,如下图所示:
- 关于通配符
如果将ini文件改为Tictoc4.*.limit=5
,那么tic和toc的limit值都会是5
通配符有*和**
,区别在于**
可以通配.
符号,*
不可以。例如**.limit=5
tictoc5:使用ned继承
simple Tic5 extends Txc5
{
parameters:
@display("i=,cyan");
sendMsgOnInit = true; //tic模块发送初始消息
}
一旦我们创建了新的简单模块,我们就可以将它们用作网络中的子模块类型,可以让网络定义变得更短更简单:
network Tictoc5
{
submodules:
tic: Tic5; //limit参数这里没有指定,可以从ini文件获取
toc: Toc5;
connections:
tictoc6:模拟处理时延
在之前的实验中,tic和toc在接收到消息之后直接发回,本实验模拟tic和toc在发送回信息之前,持有消息1秒。这种时延是通过模块向自己发送一个消息实现的,这种消息被称为自消息。自消息用scheduleAt()
函数发送,可以指定自消息何时返回该模块。
ned文件内容不变,cc文件如下
#include <stdio.h>
#include <string.h>
#include <omnetpp.h>
using namespace omnetpp;
class Txc6 : public cSimpleModule
{
private:
// 将指针设置为nullptr,这样即使启动过程中initialize()没有被调用,析构函数也不会崩溃
cMessage *event = nullptr; // 指向一个事件对象,该事件对象用于延时
cMessage *tictocMsg = nullptr; // 记录消息的变量,直到消息发回
public:
virtual ~Txc6();
protected:
virtual void initialize() override;
virtual void handleMessage(cMessage *msg) override;
};
Define_Module(Txc6);
Txc6::~Txc6()
{
// 处理动态分配的对象
cancelAndDelete(event);
delete tictocMsg;
}
void Txc6::initialize()
{
// Create the event object we'll use for timing -- just any ordinary message.
event = new cMessage("event");
// No tictoc message yet.
tictocMsg = nullptr;
if (strcmp("tic", getName()) == 0) {
//并不马上开始,而是发送一个消息给自己,当自消息到达(5.0s)时,才发送第一条消息
EV << "Scheduling first send to t=5.0s\n";
tictocMsg = new cMessage("tictocMsg");
scheduleAt(5.0, event);//指定在5.0s时,自消息event返回tic
}
}
void Txc6::handleMessage(cMessage *msg)
{
//有几种区分信息的方法,例如消息类型(cMessage的一个整型属性),代码中是直接识别指针
if (msg == event) {//是自消息到达
EV << "Wait period is over, sending back message\n";
send(tictocMsg, "out");//向对方发送消息
tictocMsg = nullptr;//让消息指向空
}
else {//是对方发来的tictoc消息
EV << "Message arrived, starting to wait 1 sec...\n";
tictocMsg = msg;//将tictoc消息的指针存到变量中
scheduleAt(simTime()+1.0, event);//指定自消息在1s后返回
}
}
用if (msg->isSelfMessage())
也等同于if (msg == event)
的判断。模拟处理时延的效果如下所示,从模拟时间的变化可以观察到:
tictoc7:引入随机数、概率丢包
将处理时延从1s改为一个随机值,这个随机值可以在ned文件中设置,也可以在omnetpp.ini文件中设置。在.ned文件中增加一个参数volatile double delayTime @unit(s);
指定等待的时间,单位是秒。注意omnet++中生成的随机数是确定性的,即重复多次运行,产生的随机数相同。
在txc7.cc文件中:
if (uniform(0, 1) < 0.1) {//以0.1的概率丢弃这个消息
EV << "\"Losing\" message\n";
delete msg;
}
else {
//delayTime参数可以在ned或ini文件中设置,在这里就可以每次获得不同的值
simtime_t delay = par("delayTime");//变量类型是simtime_t
EV << "Message arrived, starting to wait " << delay << " secs...\n";
tictocMsg = msg;
scheduleAt(simTime()+delay, event);
}
本实验在ini文件中设置随机数的范围
Tictoc7.tic.delayTime = exponential(3s)//exponential()的参数是平均值
Tictoc7.toc.delayTime = truncnormal(3s,1s)//truncnormal()返回正态分布,只保留非负值
产生的随机时延效果如下:
tictoc8:超时、取消计时器
本实验模拟停等协议,其中toc会以某个非零的概率丢失消息,在这种情况下,tic需要重新发送消息。
ps:如果不容易观察到丢弃消息的效果,可以提高丢包概率
#include <stdio.h>
#include <string.h>
#include <omnetpp.h>
using namespace omnetpp;
//如果数据包在一段时间内不能到达,就假设丢包并创建一个新数据包
class Tic8 : public cSimpleModule
{
private:
simtime_t timeout; //指定超时时间
cMessage *timeoutEvent = nullptr; //指向超时自消息的指针
public:
virtual ~Tic8();
protected:
virtual void initialize() override;
virtual void handleMessage(cMessage *msg) override;
};
Define_Module(Tic8);
Tic8::~Tic8()
{
cancelAndDelete(timeoutEvent);
}
void Tic8::initialize()
{
timeout = 1.0;//超时时间设为1s
timeoutEvent = new cMessage("timeoutEvent");
EV << "Sending initial message\n";
cMessage *msg = new cMessage("tictocMsg");
send(msg, "out");//发送初始消息
scheduleAt(simTime()+timeout, timeoutEvent);//自消息将在超时时间之后到达
}
void Tic8::handleMessage(cMessage *msg)
{
if (msg == timeoutEvent) {
// 收到超时的自消息
EV << "Timeout expired, resending message and restarting timer\n";
cMessage *newMsg = new cMessage("tictocMsg");
send(newMsg, "out");//创建一个新消息并发送
scheduleAt(simTime()+timeout, timeoutEvent);//进入下一个等待超时
}
else {
//收到消息,也就是收到确认
EV << "Timer cancelled.\n";
cancelEvent(timeoutEvent);//取消超时事件
delete msg;//删除收到的消息
cMessage *newMsg = new cMessage("tictocMsg");
send(newMsg, "out");//创建一个新的消息并发送
scheduleAt(simTime()+timeout, timeoutEvent);//进入下一个等待超时
}
}
class Toc8 : public cSimpleModule
{
protected:
virtual void handleMessage(cMessage *msg) override;//只决定是否向tic发送一个确认
};
Define_Module(Toc8);
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 acknowledgement.\n";
send(msg, "out");
}
}
toc丢弃消息的动画效果如图所示:
tictoc9:重传相同的信息
在tictoc8中,如果需要重传,只创建了另一个数据包。数据包没有包含太多内容时是可以的,但实际中通常更实用的方法是保留原始数据包的副本,可以重新发送而不用重新构建。
保留已发送消息的指针是简单的,但消息在另一个节点上被破坏时,指针就会失效。因此,本实验保留原始数据包,只发送其副本,当toc确认消息之后,才删除原始数据包。此外,在消息名中会包含消息序列号,更容易直观验证模型。
class Tic9 : public cSimpleModule
{
private:
simtime_t timeout; //超时时间
cMessage *timeoutEvent = nullptr; //超时自消息的指针
int seq; //消息的序列号
cMessage *message = nullptr; //需要被重新发送的消息
public:
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()
{
cancelAndDelete(timeoutEvent);
delete message;
}
void Tic9::initialize()
{
seq = 0;//初始序列号为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 {//收到确认
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");
}
消息名的变化如下图所示: