OMNeT++ 基础语法:消息

cMessage

cMessage 类是 OMNeT++ 的核心类,它及其子类的对象可以模拟时间、包、帧、蜂窝、网络中的信号传输、系统中的实体传输等。Message在创建时可以指定名字、消息类型、长度、位错误标记、优先权:

  • 名字string(const char *) 类型,在模拟程序中可以自由使用。名字属性继承自 cObject
  • 消息类型:定义为携带消息类型信息,值为 0 或正值。(负值部分被仿真库保留)。
  • 长度:用以计算当消息经过一个具有确定数据速率的信道时的传输时延。
  • 位错误标记:当消息通过具有位错误率 ber 的信道时,仿真内核以 $1-(1-ber)\times length$ 的概率将位错误标记设置为 1。
  • 优先权:仿真内核在对具有相同到达时间值的消息队列(FES)中使用优先权进行消息定制。

消息定义

假设目前需要一些具有源地址、目的地址和跳数的消息对象,可以编写一个 mypacket.msg 文件如下:

1
2
3
4
5
6
message MyPacket {
fields:
int srcAddress;
int destAddress;
int hops = 32;
};

消息子集编译器的任务是生成一个 C++ 类。若使用消息子集编译器来处理 mypacket.msg 文件,则会生成 mypacket_m.h 文件和 mypacket_m.cc 文件。其中,mypacket_m.h 文件包含类的声明。在 field 中支持以下数据类型:

  1. 原始类型:bool, char, short, int, long, unsigned short, unsigned int, unsigned long, double ,string
  2. 结构、类,由额外的 C++ 代码进行声明

消息收发

OMNeT++ 中,对网络的仿真就是一系列简单模块间通过 message 进行通信的过程。在抽象层面上,OMNeT++ 是一组通过消息传递相互通信的简单模块。简单模块的本质是它们创建、发送、接收、存储、修改、调度和销毁消息,而 OMNeT++ 的其余部分都是为了促进这项任务,并收集有关正在发生的事情的统计数据。

OMNeT++ 中的消息是 cMessage 类或其子类之一的实例。网络数据包用 cPacket 表示,它是 cMessage 的子类。消息对象使用 C++ new 运算符创建,并在不再需要时使用 delete 运算符销毁。消息也被称为 cMessage 指针。

消息发送是这样实现的:消息的到达时间和误码标志在 send() 调用中计算得到,然后用计算出的到达时间将消息插入 FES 。仿真内核不会为每个链接单独安排消息,这样可以提高它的运行时效率。

普通发送

1
2
3
send(cMessage *msg, const char *gateName, int index=0);
send(cMessage *msg, int gateId);
send(cMessage *msg, cGate *gate);

其中,第一个函数 gateName 指消息必须通过的门的名称。如果该门是门向量,那么需要在 index 中指定执行的输出门;若不是,则不用考虑 index 参数。第二个函数和第三个函数分别使用门 ID 和门对象指针进行索引,执行速度比第一个函数要快。如果使用的是 IO 门,则需要制定输出门或输入门,例如:

1
2
send(msg, "out");
send(msg, "outv", i);

广播和重传

需要注意的是,不能使用 send() 函数多次发送同一个 message,否则会产生 not owner of message 的错误。在 OMNeT++ 中,消息就是一个具体的对象,在同一时刻不能出现在多个不同的地点。一旦 message 从源节点发出,它就不再属于源节点。当 message 到达目的端后,目的端就具有对该 message 的一切控制权,如接收、转发、删除等。

OMNeT++ 中,广播可以通过在一个简单模块中发送相同消息的副本来实现,例如:

1
2
3
4
5
6
7
8
9
10
11
// Method 1 基础版本
for (int i = 0; i < n; i++) {
cMessage *copy = msg->dup();
send(copy, "out", i);
}
delete msg;

// Method 2 利用门 ID 实现优化
int outGateBaseId = gateBaseId("out");
for (int i = 0; i < n; i++)
send(i==n-1 ? msg : msg->dup(), outGateBaseId+i);

在实现重传时,不能重复发送指向同一个消息对象的指针,因为这样在第一次重发时就会得到 not owner of message 错误。解决方案与广播相似,实现重传需要创建和发送消息的副本并保留原始信息。当确定不会再重新传输该消息时,再删除原始消息:

1
2
3
4
// retransmit packet:
cMessage *copy = packet->dup();
send(copy, "out");
delete packet; // Finish

延迟发送

1
2
3
sendDelayed(cMessage *msg, double delay, const char *gateName, int index);
sendDelayed(cMessage *msg, double delay, int gateId);
sendDelayed(cMessage *msg, double delay, cGate *gate);

延迟发送相较 send() 函数多了一个延迟参数 delaydelay 值必须是非负数。消息的送出时间是当前的模拟时间 + 延迟时间。该功能的效果类似于模块将消息保留了延迟间隔,然后再发送,在函数的内部并不会执行 scheduleAt() 加上 send() 函数,而是预先计算有关消息发送的所有内容,包括到达时间和目标模块。

自传消息

OMNeT++ 中,让简单模块向自身发送消息可以解决仿真模型对未来事件的调度,以实现定时、超时、延迟等功能。以这种方式使用的消息称为 self-messages,模块类为它们提供了特殊的方法,允许在没有门和连接的情况下实现自传消息。

调度事件

模块可以使用 scheduleAt() 向自己发送信息,该函数可以使用一个绝对模拟时间:

1
scheduleAt(absoluteTime, msg);

由于目的时间通常与当前的模拟时间相关联,因此函数还有另一个变体 scheduleAfter(),该函数使用的是时间增量。以下两种用法是等效的:

1
2
scheduleAt(simTime()+delta, msg);
scheduleAfter(delta, msg);

自信息还可以与其他消息以相同的方式传递(如 handleMessage()),模块也可以通过调用接收到的信息对象的 isSelfMessage() 成员函数来确定其是否是自信息,也可以通过 isScheduled() 成员函数确定消息是否在 FES 中。

取消事件

模块可以取消已经预定了的自信息,即从 FES 中删除对应事件:

1
cancelEvent(msg);

cancelEvent() 函数接受一个指向要取消的消息的指针,并返回相同的指针。执行取消命令后,可以删除对应消息或在 scheduleAt() 函数中重新启用事件。但如果没有对消息进行操作,则 cancelEvent() 无效。

还有一种便捷方式可以实现对消息的删除,且通常用于编写析构函数:

1
2
3
if (msg != nullptr){
delete cancelEvent(msg);
}

下面是一个假想停止等待协议的实际应用实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Protocol::handleMessage( cMessage *msg) 
{
if (msg == timeoutEvent) {
// 超时,重新发送数据包并重启定时器
send(currentPacket->dup(), "out");
scheduleAt(simTime() + timeout, timeoutEvent);
}
else if (...) { // 如果收到确认
// 取消超时,准备发送下一个数据包等
cancelEvent(timeoutEvent);
...
}
else {
...
}
}

重新安排事件

要重新安排事件到不同模拟时间时,先要使用 cancelEvent() 取消事件,再进行安排:

1
2
3
if (msg->isScheduled())
cancelEvent(msg);
scheduleAt(simTime() + delay, msg);

或者也可以直接使用成员函数:

1
2
rescheduleAt(absoluteTime, msg);
rescheduleAfter(delta, msg);

消息直传

sendDirect() 函数提供了直接将消息直接发送到另一个模块输入门的方式:

1
2
3
sendDirect(cMessage *msg, cModule *mod, int gateId);
sendDirect(cMessage *msg, cModule *mod, const char *gateName, int index=-1);
sendDirect(cMessage *msg, cGate *gate);

在目标模块,直接接收的消息和通过连接传输的消息没有区别。但是需要注意的是,模块必须有专用的门才能接收通过 sendDirect() 发送的消息。不能一个门既能通过连接也能通过 sendDirect() 接收消息。

官方文档中建议在模块的 NED 声明中使用 @directIn 标记专用于通过 sendDirect() 接收消息的门以避免 OMNeT++ 提示在使用该模块的网络或复合模块中没有连接门,例如:

1
2
3
4
simple Radio {
gates:
input radioIn @directIn; // 接受直传帧
}

sendDirect()方法也接受传播延迟和传输持续时间作为参数:

1
2
3
4
5
6
sendDirect(cMessage *msg, simtime_t propagationDelay, simtime_t duration,
cModule *mod, int gateId);
sendDirect(cMessage *msg, simtime_t propagationDelay, simtime_t duration,
cModule *mod, const char *gateName, int index=-1);
sendDirect(cMessage *msg, simtime_t propagationDelay, simtime_t duration,
cGate *gate);

消息是一个数据包(cPacket 的实例)时需要指定 duration 参数。对于不是数据包的消息可以忽略 duration

如果消息是一个数据包,则持续时间将被写入数据包,并且可以由接收者通过数据包的getDuration()方法读取。

数据包传输

当一条消息在门上发出后通常会经过一系列连接直到它到达目标模块,称这一系列的连接为连接路径(connection paths)。

路径中的多个连接可能具有关联的通道,但每条路径只能有一个通道来模拟非零传输持续时间。此限制由模拟内核强制执行。这个通道称为传输通道(transition channels)。

发送数据包

只有在传输通道空闲时才能发送数据包。这意味着在每次传输之后,发送模块需要等到通道完成传输才能发送另一个数据包。

我们可以通过在输出门上调用 getTransmissionChannel() 方法获取指向传输通道的指针。通道的 isBusy()getTransmissionFinishTime() 方法可以得到通道当前是否正在传输,以及传输何时结束。(当后者小等于当前模拟时间时,通道空闲)如果通道当前忙,发送需要推迟,则可以将数据包存储在队列中,并且可以使用定时器(自消息)安排在频道变空时进行发送。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
cPacket *pkt = ...; // 要传输的数据包
cChannel *txChannel = gate("out")->getTransmissionChannel();
simtime_t txFinishTime = txChannel->getTransmissionFinishTime();
if (txFinishTime <= simTime()) {
// 通道空闲;立即发送数据包
send(pkt, "out");
}
else {
// 存储数据包和调度定时器;当计时器到期时,
// 数据包应该从队列中移除并发送出去
txQueue.insert(pkt);
scheduleAt(txFinishTime, endTxMsg);
}

★官方建议不要在路径中的传输通道前面使用传播延迟。

接收数据包

对信道的错误建模可能会导致数据包在到达时设置了位错误标志(hasBitError() 方法)。接收模块会检查此标志并丢弃数据包。通常,包对象在完成消息接收的模拟时间(即最后一个比特到达后)被传递到目标模块。但是,接收器模块可以通过使用 setDeliverImmediately() 方法修改接收器门来改变这一点:

1
gate("in")->setDeliverImmediately(true);

此方法只能在简单模块的输入门上调用,它指示模拟内核在对应于接收过程开始的模拟时间传递通过该门到达的数据包。当一个数据包被投递到模块时,可以调用该数据包的 isReceptionStart() 方法来判断它是对应于接收过程的开始还是结束(它应该与输入门的 getDeliverOnReceptionStart() 得到的标志相同),getDuration() 返回传输持续时间。getDeliverOnReceptionStart() 函数只需要调用一次,所以通常在模块的initialize()方法中完成。

包传输模型如下图所示。

image-20220801220240507

需要终止传输时,可以使用 forceTransmissionFinishTime() 方法。此方法用给定值强制覆盖通道的transmissionFinishTime 成员变量,允许发送方发送另一个数据包而不会引发 channel is currently busy 错误。接收方需要通过某些外部方式(例如通过发送另一个数据包或带外消息)来通知传输中止。

使用 activity() 接收消息

基于 activity() 的模块使用 cSimpleModulereceive() 方法接收消息。receive() 不能与基于handleMessage() 的模块一起使用:

1
cMessage *msg = receive();

wait() 函数将模块的执行挂起一段给定的模拟时间(增量)。wait() 也不能与基于 handleMessage() 的模块一起使用:

1
wait(delay);

wait() 函数由 scheduleAt() 后跟一个 receive() 函数实现。wait() 函数在不需要为到达消息做准备的模块中非常方便,例如消息生成器。一个例子:

1
2
3
4
5
6
7
8
for (;;) { 
// 等待一些可能是随机的时间量,
// 在 interarrivalTime volatile 模块参数中指定
wait(par("interarrivalTime").doubleValue());

// 生成并发送消息
...
}

如果消息在等待时间间隔内到达,则会报运行时错误。如果希望消息在等待期间到达,可以使用waitAndEnqueue() 函数。除了等待间隔之外,它还需要一个指向队列对象(属于cQueue类)。在等待时间间隔内到达的消息会累积在队列中,等待 waitAndEnqueue() 调用返回后才能被系统处理:

1
2
3
4
5
6
7
8
cQueue queue("queue");
...
waitAndEnqueue(waitTime, &queue);
if (!queue.empty())
{
// process messages arrived during wait interval
...
}
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2024 lgc0208@foxmail.com
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信