翻譯總結自: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就可以了。