omnet++:官方文档翻译总结(三)
翻译总结自:Turning it Into a Real Network - OMNeT++ Technical Articles
接官方文档翻译总结(二),本节主要是真实网络的搭建
Part 4 - Turning it Into a Real Network
①多于两个节点的网络:Tictoc10
现在我们要迈出一大步了:创造多个tic module并把它们连入网络。
现在,我们构建一个简单的多节点网络:其中一个节点产生消息发往一个随机方向,该节点继续随机发送,……,剩下的节点执行同样的行为,直到它到达一个预先确定好的目的节点。
NED文件需要一些改变:
Txc module需要有多个input、output gates
simple Txc10 { parameters: @display("i=block/routing"); gates: input in[];//定义in[]和out[]标注一系列的进出口 output out[]; }
[ ]把单个gate变成了gate数组。数组大小(数组中gate的数量)决定了网络中输入输出端口的数量:
network Tictoc10 { submodules: tic[6]:Txc10; connections: tic[0].out++ --> {delay=100ms;} --> tic[1].in++; tic[0].in++ <-- {delay=100ms;} <-- tic[1].out++; tic[1].out++ --> { delay = 100ms; } --> tic[2].in++; tic[1].in++ <-- { delay = 100ms; } <-- tic[2].out++; tic[1].out++ --> { delay = 100ms; } --> tic[4].in++; tic[1].in++ <-- { delay = 100ms; } <-- tic[4].out++; tic[3].out++ --> { delay = 100ms; } --> tic[4].in++; tic[3].in++ <-- { delay = 100ms; } <-- tic[4].out++; tic[4].out++ --> { delay = 100ms; } --> tic[5].in++; tic[4].in++ <-- { delay = 100ms; } <-- tic[5].out++; }
上段NED代码中我们构建了6个module作为一个module vector,并将它们相连接,结果拓扑如下:
其中tic[0]产生消息。这一步是在initialize()中实现的,实现过程中需要借助函数getIndex()——这个函数返回module在vector中的下标。
代码的核心是forwardMessage()函数,当一个消息到达时,我们在处理消息的handleMessage()中调用这个函数。这个方法中产生了一个随机数,并将消息从这个随机数代表的gate中发送出去:
void Txc10::forwardMessage(cMessage * msg){ //在本例中,我们选择一个随机gate将消息发送出去 //这个随机数的取值范围为0~size(out[])-1 int n=gateSize("out"); int k=intuniform(0,n-1); EV<<"Forward message "<<msg<<" on port out["<<k<<"]\n"; send(msg,"out",k); }
当消息到达tic[3]时,它的handleMessage()将会删除该消息(即目标节点是tic[3])
补充:使用过程中,你可能会发现这个简单的路由算法并不是十分有效的——包会经常在两个节点间循环反弹一会儿再发送到别的节点。我们可以改进这个算法——通过某些中间节点后不从输入端口发送出去。提示:cMessage::getArrivalGate(),cGate::getIndex()。另外,如果某个消息不经过端口发送出去,也就是说这个消息是一个self-message,那么getArrivalGate()将返回null。
总结:tictoc10
- 当一个simple module有多个输入输出端口时,在NED文件中,定义simple module文件时,gates关键字中的端口,不能定义为类似 input : in 这样的一般单个变量,而应该定义成 input : in[ ] 这样的vector变量,表明一个simple module有多个in端口。
另外,这种形式布置的端口,在network的connections中进行连接时,就不能用之前的例子中所写的诸如 xxx.out --> { ... } --> xxx.in;而应该是xxx.out++ --> { ... } --> xxx.in++ - 在network的构建中,如果想快速定义多个同类型simple module节点,可以使用vector变量(也就是数组变量):
submodules: tic[6] : Txc10;
只是这样定义的话,我们无法在Design模式下设置每一个节点的位置,而只能让IDE运行时自行布置。
- 如果我们有一个module vector(比如上文的tic[6]),需要根据module号来决定消息处理方式,那么可以在handleMessage中,用以下语句加以判断:
if(getIndex()==0){ //0号module ... }
- 如果采用总结1中那种vector型多端口,发送消息时应该指定从哪个端口发出去:send( msg , "out" , k )
- 使用gateSize("out")可以知道这个module有多少个out gate
- 在ned文件中通过vector一次定义了多个simple module:tic[6] : Txc10,这些节点无法在运行时手动在Design模式下设置它们的位置;只能在运行时让IDE自行布局;如果对布局不满意的话,可以通过“Re-layout”按钮进行重布局,不过样式有限,多次重布局后就会回到最初的布局结构了。
②通道channel和内部类型定义:tictoc11
我们的网络定义已经变得非常复杂和庞大了,特别是在connections这一节。我们可以对其尝试优化:首先,我们注意到connections中总是用到了delay parameter。我们可以为connections创造相关types(这里是所谓channels),就像我们给simple modules添加para那样。我们可以创造一个channel类型指定delay,之后我们就可以使用它来构建网络中的connections
network Tictoc11 { types: channel Channel extends ned.DelayChannel{ delay=100ms; } submodules:
我们在network中添加了types关键字,并在其中定义了新的channel。types关键字只能用在network中。它是一种局部的、内部的type。如果我们想要的话,我们可以使用simple modules作为内部type。
之后connections中的代码就变成了:
connections: tic[0].out++ --> Channel --> tic[1].in++; tic[0].in++ <-- Channel <-- tic[1].out++; ... tic[4].out++ --> Channel --> tic[5].in++; tic[4].in++ <-- Channel <-- tic[5].out++; }
我们在connections中通过channel名指定了这个channel标记的delay,这样,我们就可以在随后为整个网络轻松修改所有delay了。
总结:tictoc11
- 本例中,我们用channel代替之前写的delay=100ms;
- channel定义在ned文件下network中的types关键字中,用以实现信道时延的channel都是继承自ned.DelayChannel,定义方式如下:
network Tictoc11 { types: channel Channel extends ned.DelayChannel{ delay=100ms; }
- channel的使用,用在network下的connections关键字中,用来对端口与端口间的信道进行某些规定:不是xxx.out、xxx.in而是xxx.in++、xxx.out++
connections: tic[0].out++ --> Channel --> tic[1].in++; tic[0].in++ <-- Channel <-- tic[1].out++; ...
- 在ned文件中对network的channel进行修改,可以实现同时对整个链路修改的目的
③双向连接:tictoc12
你可能发现了,connections中每个节点对都有两个连接,每个代表一个方向。OMNET++支持双向连接,所以我们可以用以下方法使用它。
我们通过inout gate定义双向连接,而不是用input和output gate这种我们之前使用的形式:
simple Txc12 { parameters: @display("i=block/routing"); gates: inout gate[]; }
修改后的connections就将像下边这样:
connections: tic[0].gate++ <--> Channel <--> tic[1].gate++; tic[1].gate++ <--> Channel <--> tic[2].gate++; ... tic[4].gate++ <--> Channel <--> tic[5].gate++; }
由于我们修改了gate名,所以我们需要在C++中进行修改:
void Txc12::forwardMessage(cMessage * msg) { int n = gateSize("gate"); int k = intuniform(0,n-1); EV<<"Forwarding message " <<msg<<" on gate["<<k<<"]\n"; //$o与$i后缀用以区分一个双向gate的output/input端口 send(msg,"gate$o",k); }
总结:tictoc12
- inout gate,相当于某个gate即是input又是output,用起来比单个input和output方便多了;
- 如果某个节点有多个inout gate,可以定义一个vector类型的inout gate,实现起来像下边这样:
simple Txc12 { ... gates: inout gate[]; }
这种vector,就像我们在tictoc11的总结3中所说,在使用时也要用到++符号,就像gate++这样;
- 与之前的单向收发的节点相比,使用时的信道连接方式,也是双向的,即<-->这样,而不是<--、-->这样:
connections: tic[0].gate++ <--> Channel <-->tic[1].gate++;
- 使用send发送消息时,需要指明通过后缀$i与$o指明发送端口
send(msg,"gate$o",k)
- 如果要想知道有多少个双向端口,也是用gateSize,就像我们在tictoc10总结5中所说:
int n = gateSize("gate");
④消息类(message class):tictoc13
在本节中,目的节点不再是固定的tic[3]——我们用一个随机的目的地,我们把目的地址添加到message中。
最好的方法是继承cMessage得到新的message子类,并将目的地指定为成员属性。手写全部代码通常不太现实,因为它包含了太多的样版代码,所以我们可以用OMNET++来为我们生成class。本例中我们在tictoc13.msg中指定message class:
message TicTocMsg13 { int source; int destination; int hopCount=0; }
生成文件tictoc13.msg建立后,message编译器就会自动生成tictoc13_m.h与tictoc13_m.cc(从文件名而不是message class名中创建)。这两个文件中将自动生成一个继承自cMessage的子类TicTocMsg13。该class将对每个字段生成getter与setter方法。
我们在写C++代码的cc文件中,需要引入tictoc13_m.h,这样我们就可以使用TicTocMsg13这个message class了。
#include <tictoc13_m.h>
例如,我们可以在generateMessage()中通过如下代码生成message,并填充它的各个字段:
TicTocMsg * msg = new TicTocMsg13(msgname); msg->setSource(src); msg->setDestination(dest); return msg;
之后的handleMessage()的开始几行代码就可以写成如下的形式:
void Txc13::handleMessage(cMessage * msg){ TicTocMsg13 * ttmsg = check_and_cast <TicTocMsg13 *>(msg); if( ttmsg->getDestination()==getIndex()){
在handleMessage中,我们接受一个消息作为参数,其类型是cMessage *指针。只是,我们当我们将普通的cMessage转化为TicTocMsg*后,就只能访问TicTocMsg13中定义的那些字段。我们经常使用的那种消息类型转化方式,如(TicTocMsg13 *) msg并不安全,因为如果随后的程序中得到的msg并不是TicTocMsg13类型,就会报错。
C++用dynamic_cast机制来解决这种问题。本例中我们使用check_and_cast<>(),该方法尝试通过dynamic_cast的方式传递指针,如果方法失败,它就会终止仿真并弹出错误消息,类似下边这样:
下一行中,我们检查目的地址是否和节点地址相同。为了使model执行的更长远,在一个消息到达目的地时,目的节点将生成另一条包含着随机目的地址的消息,发送出去……
当我们运行model,它看起来像下边这样:
我们可以点击消息(就是图中的小红点)在左下角的窗口中查看它的内容。
在本model中,在任意指定的时候只有一个正在运行着的消息:当另一个消息到达时,节点只生成一个消息。我们之所以这样做,是为了使仿真更简单。如果想让消息的产生存在间隔,我们可以修改module以达成这一目的。消息间隔应该是一个module parameter,返回指数分布的随机数。
总结:tictoc13
- 在msg文件中指定message class,每个message中有一些信息字节:
message TicTocMsg13 { int source; int destination; int hopCount=0; }
msg文件名为xxx.msg格式;message class定义时用message关键字;
- xxx.msg文件建立后,编译器自动生成xxx_m.h与xxx_m.cc(与msg文件名而不是message名相对应)。这两个文件中会自动生成一个继承自cMessage的消息类,这个消息类就是我们在msg文件中用关键字message建立的那个消息。此外,这个消息类中,对每个字段都实现了getter与setter方法。
- 在负责整个网络逻辑的cc文件中,通过#include<xxx_m.h>引入之前创建的message,在其中访问和设置字段值,通常,在生成message的代码之后,通过msg->setXXX()设置值,在handleMessage()中,通过msg->getXXX()获取这些值。
通常,我们可以单独写一个产生消息的函数generateMessage()函数,在其中实现创建新消息、设置字段值、返回创建的新消息的功能:
xxxMsg * Txc13 :: generateMessage() { ... xxxMsg * msg = new xxxMsg( msgname ); msg->setSource(src); msg->setDestination(dest); return msg; }
上文中的xxxMsg就是我们在msg文件中指定的message类。
- 在handleMessage()中,用xxxMsg处理收到的普通message的代码为:
void handleMessage(cMessage * msg){ xxxMsg * xmsg = check_and_cast <xxxMsg *>(msg); if( xmsg->getXXX()==getIndex() )
用check_and_cast < xxxMsg *> (msg)可以安全地把一个普通的cMessage类型,变为我们需要的那种xxxMsg。转换完成后,就可以用getter方法提取我们之前定义的和设置了值的字段。
由于tictoc13这个例子很有代表性,现对其代码逐句加以分析解释。
NED文件:tictoc13.ned
simple Txc13 { parameters: @display("i=block/routing"); gates: inout gate[]; } network Tictoc13 { types: channel Channel extends ned.DelayChannel{ delay = 100ms; } submodules: tic[6] : Txc13; connections: tic[0].gate++ <--> Channel <--> tic[1].gate++; tic[1].gate++ <--> Channel <--> tic[2].gate++; tic[1].gate++ <--> Channel <--> tic[4].gate++; tic[3].gate++ <--> Channel <--> tic[4].gate++; tic[4].gate++ <--> Channel <--> tic[5].gate++; }
ned文件比较简单,没什么需要多说的,需要注意的地方都在上个代码中给标红了。
msg文件:tictoc13.msg
message TicTocMsg13 { int source; int destination; int hopCount = 0; }
message定义了每个节点发送、接收的消息的格式。
本例中,每个接收、发送、在信道中传输的消息中都有三个字段:source、destination、hopCount;分别标识源地址(创建新消息的节点地址)、目的地址、当前跳数。由于每个新消息的hopCount都是0,所以可以在此处直接将hopCount在定义时初始化为0。而source、destination都需要在消息传递过程中动态确定,所以此处并不初始化,而是在cc文件中建立消息时,通过setter方法设置。在cc文件中访问这些字段时,通过getter方法设置。
cc文件:txc13.cc
#include<stdio.h> #include<string.h> #include<omnetpp.h> using namespace omnetpp; #include<tictoc13_m.h> //① class Txc13 : public cSimpleModule { protected: virtual TicTocMsg13 * generateMessage(); //② virtual void forwardMessage(TicTocMsg13 * msg); virtual void initialize() override; virtual void handleMessage(cMessage * msg) override; }; Define_Module(Txc13); void Txc13::initialize() //③ { if(getIndex() == 0){ TicTocMsg13 * msg = generateMessage(); scheduleAt(0.0 , msg); } } void Txc13::handleMessage(cMessage * msg){ //④ TicTocMsg13 * ttmsg = check_and_cast <TicTocMsg13 *>(msg); if(ttmsg->getDesination()==getIndex()){ EV<<"Message "<<ttmsg<<" arrived after "<<ttmsg->getHopCount()<<" hops.\n"; bubble("ARRIVED, starting new one!"); delete ttmsg; EV<<"Generating another message: "; TicTocMsg * newmsg = generateMessage(); EV<<newmsg <<endl; forwardMessage(newmsg); } else{ forwardMessage(ttmsg); } } TicTocMsg13 * Txc13::generateMessage() //⑤ { int src = getIndex(); int n = getVectorSize(); int dest = intuniform(0,n-2); if(dest >= src) dest++; char msgname[20]; sprintf(msgname,"tic-%d-to-%d",src,dest); TicTocMsg13 * msg = new TicTocMsg13(msgname); msg->setSource(src); msg->setDestination(dest); return msg; } void Txc13 :: forwardMessage(TicTocMsg13 * msg) //⑥ { msg->setHopCount(msg->getHopCount()+1); int n = gateSize("gate"); int k = intuniform(0,n-1); EV<<"Forwarding message "<<msg<<" on gate["<<k<<"]\n"; send(msg,"gate$o",k); }
①引入之前message所在的文件
#include<tictoc13_m.h>
在我们完成xxx.msg之后,IDE就会自动生成一个xxx_m.h和xxx_m.cc,在其中自动实现了我们自己写的message,使用时需要用#include引入,之后才能使用。
②
virtual TicTocMsg13 *generateMessage(); virtual void forwardMessage(TicTocMsg13 *msg); virtual void initialize() override; virtual void handleMessage(cMessage *msg) override;
除了我们最常用、也是最常见的initialize()和handleMessage()方法之外,我们又加入了两个方法generateMessage()、forwardMessage().。这两个函数的作用分别是创建新消息、转发消息。
③initialize()
void Txc13::initialize() { if (getIndex() == 0) { TicTocMsg13 *msg = generateMessage(); scheduleAt(0.0, msg); } }
在初始化函数中,我们指定了消息的起点——节点号为0的点,这个点是开启整个仿真的地方。
if(getIndex()==0)
对每一个节点初始化时,都会检查它的节点号,如果是0,就进行如下操作:
TicTocMsg13 * msg = generateMessage(); scheduleAt(0.0,msg);
第一句是,该节点创建了新消息,这个消息也是整个网络的起始消息,由它激活整个网络。
第二句话是这个消息被创建后直接发给自己,是一个self-message。这样,我们就不用在初始化函数中指定这个消息从哪里发出去,而是采用handleMessage()方法中跟普通消息一样的转发方式。省却了很多代码。
④我们把handleMessage()方法放在最后说,先说另外两个方法。
⑤generateMessage()
TicTocMsg13 * Txc13::generateMessage() { int src = getIndex(); // our module index int n = getVectorSize(); // module vector size int dest = intuniform(0, n-2); if (dest >= src) dest++; char msgname[20]; sprintf(msgname, "tic-%d-to-%d", src, dest); TicTocMsg13 *msg = new TicTocMsg13(msgname); msg->setSource(src); msg->setDestination(dest); return msg; }
在generateMessage()中我们创建并返回了一个新消息,由于需要返回新消息,所以函数类型就是TicTocMsg13 *,而不同于另外三个方法的void。
1)
int src = getIndex(); int n = getVectorSize(); int dest = intuniform(0, n-2); if (dest >= src) dest++;
第一部分,我们指定了源地址和目的地址,源地址也就是创建消息的节点的地址(其实就是节点号),通过getIndex()直接获取到,其实也就是该节点的节点号。目的地址是除了该节点以外的任意其他节点(通过随机数函数intuniform()来确定),至于dest >= src的判断,个人认为应该是用 ==(关于这点,评论区有人指出原文是正确的,请移步评论区自行判断)。
2)
char msgname[20]; sprintf(msgname,"tic-%d-to-%d",src,dest); TicTocMsg13 * msg = new TicTocMsg13(msgname);
第二部分,我们根据源地址和目的地址的不同,创造了不同的消息,其中用sprintf创建消息名的语句,我们会经常用到。
3)
msg->setSource(src); msg->setDestination(dest); return msg;
第三部分,我们为消息的部分字段进行赋值,通过setter方法。
消息创建完了,消息内的各字段也有了,就完成的新消息的创建,将它return。
⑥forwardMessage()
void Txc13::forwardMessage(TicTocMsg13 *msg) { msg->setHopCount(msg->getHopCount()+1); int n = gateSize("gate"); int k = intuniform(0, n-1); EV << "Forwarding message " << msg << " on gate[" << k << "]\n"; send(msg, "gate$o", k); }
1)
msg->setHopCount(msg->getHopCount()+1);
消息转发前,使该消息的跳数加一
2)
int n = gateSize("gate"); int k = intuniform(0, n-1); EV << "Forwarding message " << msg << " on gate[" << k << "]\n"; send(msg, "gate$o", k);
选择合适端口(第一二行)把消息转发(第四行)出去,转发前向控制台输出信息(第三行),表明已经进行了消息转发。
通过gateSize("gate")我们知道了这个节点有多少可供使用的端口。再通过intuniform(0,n-1)我们选择了一个随机的和正常的端口以供消息转发,这里的正常是指,不会选择大于端口数量的端口号进行转发(由gateSize进行保证)。
④handleMessage
void Txc13::handleMessage(cMessage *msg) { TicTocMsg13 *ttmsg = check_and_cast<TicTocMsg13 *>(msg); if (ttmsg->getDestination() == getIndex()) { // Message arrived. EV << "Message " << ttmsg << " arrived after " << ttmsg->getHopCount() << " hops.\n"; bubble("ARRIVED, starting new one!"); delete ttmsg; // Generate another one. EV << "Generating another message: "; TicTocMsg13 *newmsg = generateMessage(); EV << newmsg << endl; forwardMessage(newmsg); } else { // We need to forward the message. forwardMessage(ttmsg); } }
消息处理函数一直是链路转发的核心函数,所以我们放在最后来说。
1)
TicTocMsg13 *ttmsg = check_and_cast<TicTocMsg13 *>(msg);
由于本例中我们的消息都是之前自己建立的消息TicTocMsg13这种类型,而handleMessage默认收到的消息是cMessage类型,所以我们要进行类型转换,这就是这句话的目的。
转换之后,我们就得到自定义的message ttmsg。
2)
if (ttmsg->getDestination() == getIndex())
如果消息的目的地字段(即destination字段)是当前节点,那么说明该节点就是该消息的终点,我们就可以在其中写消息到达目的节点后的相关处理了;否则本节点就是消息传递的中间节点,就需要做另一些处理了。
3)
if(ttmsg->getDestination() == getIndex()) { // Message arrived. EV << "Message " << ttmsg << " arrived after " << ttmsg->getHopCount() << " hops.\n"; bubble("ARRIVED, starting new one!"); delete ttmsg; // Generate another one. EV << "Generating another message: "; TicTocMsg13 *newmsg = generateMessage(); EV << newmsg << endl; forwardMessage(newmsg); }
消息到达终点时,终点节点要做三件事——I、显示消息到达的信息;II、删除该消息(因为没用了);III、产生另一个新消息,继续之前的转发过程。
-
EV << "Message " << ttmsg << " arrived after " << ttmsg->getHopCount() << " hops.\n"; bubble("ARRIVED, starting new one!");
输出消息和经历的跳数到控制台日志中;弹出一个bubble,告诉人们消息已经到达了;
-
delete ttmsg;
删除旧的消息,因为已经没用了;
-
EV << "Generating another message: "; TicTocMsg13 *newmsg = generateMessage(); EV << newmsg << endl; forwardMessage(newmsg);
通过generateMessage()生成新消息,通过forwardMessage把它转发出去;整个网络会一直重复这一创建——转发——删除过程,过程中只有一个消息在网络上传播。
4)
else{ forwardMessage(ttmsg); }
如果节点是中间节点,就只需要转发消息forwardMessage就可以了。