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

  1. 当一个simple module有多个输入输出端口时,在NED文件中,定义simple module文件时,gates关键字中的端口,不能定义为类似 input : in 这样的一般单个变量,而应该定义成 input : in[ ] 这样的vector变量,表明一个simple module有多个in端口。
    另外,这种形式布置的端口,在networkconnections中进行连接时,就不能用之前的例子中所写的诸如 xxx.out --> { ... } --> xxx.in;而应该是xxx.out++ --> { ... } --> xxx.in++
  2. network的构建中,如果想快速定义多个同类型simple module节点,可以使用vector变量(也就是数组变量):
        submodules:
            tic[6] : Txc10;

    只是这样定义的话,我们无法在Design模式下设置每一个节点的位置,而只能让IDE运行时自行布置。

  3. 如果我们有一个module vector(比如上文的tic[6]),需要根据module号来决定消息处理方式,那么可以在handleMessage中,用以下语句加以判断:
    if(getIndex()==0){
        //0号module
        ...
    }

     

  4. 如果采用总结1中那种vector型多端口,发送消息时应该指定从哪个端口发出去:send( msg , "out" , k )
  5. 使用gateSize("out")可以知道这个module有多少个out gate
  6. 在ned文件中通过vector一次定义了多个simple moduletic[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关键字,并在其中定义了新的channeltypes关键字只能用在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

  1. 本例中,我们用channel代替之前写的delay=100ms;
  2. channel定义在ned文件下network中的types关键字中,用以实现信道时延的channel都是继承自ned.DelayChannel,定义方式如下:
    network Tictoc11
    {
        types:
            channel Channel extends ned.DelayChannel{
                delay=100ms;
            }
  3. 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++;
        ...
  4. 在ned文件中对network的channel进行修改,可以实现同时对整个链路修改的目的

③双向连接:tictoc12

你可能发现了,connections中每个节点对都有两个连接,每个代表一个方向。OMNET++支持双向连接,所以我们可以用以下方法使用它。

我们通过inout gate定义双向连接,而不是用inputoutput 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

  1. inout gate,相当于某个gate即是input又是output,用起来比单个input和output方便多了;
  2. 如果某个节点有多个inout gate,可以定义一个vector类型的inout gate,实现起来像下边这样:
    simple Txc12
    {
        ...
        gates:
            inout gate[];
    }

    这种vector,就像我们在tictoc11的总结3中所说,在使用时也要用到++符号,就像gate++这样;

  3. 与之前的单向收发的节点相比,使用时的信道连接方式,也是双向的,即<-->这样,而不是<---->这样:
    connections:
        tic[0].gate++ <--> Channel <-->tic[1].gate++;

     

  4. 使用send发送消息时,需要指明通过后缀$i$o指明发送端口
    send(msg,"gate$o",k)
  5. 如果要想知道有多少个双向端口,也是用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.htictoc13_m.cc(从文件名而不是message class名中创建)。这两个文件中将自动生成一个继承自cMessage的子类TicTocMsg13。该class将对每个字段生成gettersetter方法。

我们在写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

  1. 在msg文件中指定message class,每个message中有一些信息字节:
    message TicTocMsg13
    {
        int source;
        int destination;
        int hopCount=0;
    }

    msg文件名为xxx.msg格式;message class定义时用message关键字;

  2. xxx.msg文件建立后,编译器自动生成xxx_m.hxxx_m.cc(与msg文件名而不是message名相对应)。这两个文件中会自动生成一个继承自cMessage消息类,这个消息类就是我们在msg文件中用关键字message建立的那个消息。此外,这个消息类中,对每个字段都实现了gettersetter方法。
  3. 在负责整个网络逻辑的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类。

  4. 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.hxxx_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就可以了。

posted @ 2021-05-11 21:51  ShineLe  阅读(2619)  评论(2编辑  收藏  举报