這一部分是官方案例介紹
1、Introduction
學習自:Introduction - OMNeT++ Technical Articles
本教程是基於Tictoc的仿真案例,這些案例我們可以在omnet安裝目錄下的sample/tictoc下找到。
2、Part 1 - Getting Started
The model
一開始,讓我們組織起一個只有兩個節點的network。這兩個節點做了一些簡單的事情:一個節點創造了一個包,之后這個包將在這兩個節點間來回傳遞。我們稱這兩個節點為tic和toc。之后我們將逐步加強我們的模型,用以介紹omnet++的特性。
以下是從零開始實現我們的第一個仿真模型的步驟:
Step1、配置工程
- 打開IDE
- 左上角File->New->OMNeT++ Project,進入向導對話框;
- 輸入tictoc作為工程名,創建一個empty project,然后點擊Finish。
這樣一個新工程就被創建了,我們可以在Project Explorer中看到它。(需要注意的是,一些版本的omnet++中會產生package.ned文件,我們不需要它,可以刪除。)
在這個例子中,工程只有單個目錄。在大型仿真中,工程的內容通常分布於src和simulations目錄下,也可能是它們的子目錄。
Step2、NED文件
omnet++使用NED文件來定義功能組件,並將它們組織成更大的單元,比如一整個網絡。我們通過添加NED文件來實現我們的model。
- 在Project Explorer中,右鍵點擊tictoc(即項目文件名),New->NED,給這個NED文件命名為tictoc1.ned
- IDE的NED編輯器有兩種編輯模式Design、Source。在Design中,通過編輯器右邊的Palette完成編輯;在Source中,通過直接編寫NED代碼完成編輯。在其中一個編輯器中的修改內容將會立刻反應到另一個編輯器中,所以在這兩個編輯器中編輯都可以。(由於NED文件時是一般文本文件,所以我們甚至可以用外部文本編輯器來編輯,只是我們看不到語法高亮、內容復制、交叉引用和其它IDE特性等。)
- 進入Source模式下,輸入以下內容:
simple Txc1 { gates: input in; output out; } //兩個Txc1的實例(tic和toc) //tic toc互相傳遞消息 network Tictoc1 { submodules: tic:Txc1; toc:Txc1; connections: tic.out --> { delay = 100ms; } --> toc.in; tic.in <-- {delay=100ms;} <-- toc.out; }
當我們寫完這些內容時,切換到Design模式下,可以看到如下變化:
程序第一塊描述了Txc1,一個Simple module。Simple module是NED中的原子模塊。它們也是活躍的組件,具體行為在C++中實現。上文代碼也告訴了我們Txc1的特性——Txc1有一個input gate叫in,output gate叫out。第二塊描述了Tictoc1,一個network。Tictoc1由兩個子模塊tic和toc組織而成,這兩個子模塊都是Txc1的子模塊。tic的output gate與toc的input gate是互聯的,反之亦然。在這兩種方式的傳播過程中,會有100ms的延遲。
關於NED語言的語法和細節描述,我們可以在OMNeT++ Simulation Manual下找到。
Step3、C++文件
我們現在需要用C++實現Txc1這個simple module的功能。
- New->Source File創建一個txc1.cc文件
- 在文件中輸入以下代碼:
#include<string.h> #include<omnetpp.h> using namespace omnetpp; /** * Txc1繼承自cSimpleModule。在Tictoc1網絡中 * tic和toc模塊都是Txc1類對象,它們在仿真一開始就被 * omnet++創建了 */ class Txc1 :public cSimpleModule { protected: //以下虛函數標志了具體功能 virtual void initialize() override; virtual void handleMessage(cMessage *msg) override; }; //ned中的module需要和cc中class進行綁定 Define_Module(Txc1); void Txc1::initialize(){ //初始化函數在仿真開始時被調用 //為了開啟tic-toc-tic-toc過程, //其中的一個模塊需要發送第一個信息,假設這個模塊是tic if(strcmp("tic",getName())==0){ //在"out" gate中生成並發送第一條消息; //"tictocMsg"是任意一個String,只不過這個String //標識了這個消息對象的名字 cMessage *msg=new cMessage("tictocMsg"); send(msg,"out"); } } void Txc1::handleMessage(cMessage *msg){ //當一條消息到達一個module之后,調用handleMessage()方法 //在這里的消息響應函數中,我們僅僅是把消息發送到其他的module,通過gate 'out' //由於tic和toc都會做同樣的事情,因此消息實際上是在這兩者之間不停反彈的 send(msg,"out"); }
NED中的simple module與C++的class相對應,即NED文件中的Txc1與cc文件中的Txc1相對應。Txc1 class繼承自omnet++的cSimpleModule類,並且需要在cc文件中用Define_Module(ned_module)進行模塊綁定。
我們需要在cc文件中對兩個方法cSimpleModule:initialize()和handleMessage()進行函數重載。在仿真過程中調用這兩個方法的時機:第一個方法在仿真開始時只調用一次;第二個方法在有消息到達module時被調用。
在initialize()中,我們構造了一個message對象(cMessage),然后通過gate out發送出去。該gate是和其他module的input gate相連接的,仿真內核會通過handleMessage()的參數該消息傳送到其他module——在經過了NED文件中設置的100ms的傳播延遲之后。另一個模塊在經歷另一個100ms延遲之后將會傳送回來。
消息Messages(包、幀、jobs等)和事件Events(定時器、超時等)都是用cMessage對象表示的。在我們發送(send)或者調度(schedule)它們后,它們將被仿真內核的“scheduled event”或者“future events” List接收並保持,直到指定時間到達或者它們通過handleMessage()把消息傳送到了別的module。
需要注意的是,仿真的整個過程將不會停止,而是永遠進行下去。你可以從GUI中終止它。(你也可以在配置文件中指定仿真時間限制或者CPU時間限制)
Step4、omnetpp.ini文件
為了能夠正常運行仿真程序,我們需要構造omnetpp.ini文件。這個文件告訴仿真程序哪個網絡需要仿真,向model傳遞的參數,指明隨機數生成器的種子等等。
- New->ini File;創建omnet.ini文件。
- 新文件將在Inifile 編輯器中打開。就像NED編輯器一樣,Ini編輯器也有兩種模式——Form和Source,當然在這兩種模式下的編輯結果也是相同的。前者更適合配置仿真內核,后者適合設置模塊參數。
轉到Source模式后,寫下如下代碼:[General] network = Tictoc1
我們可以Form模式下確認這個結果
tictoc2和以后的例子,都會共享一個公共的omnetpp.ini文件
我們現在創建了第一個Model,接下來就是配置和運行它了。
3、Part 2 - Running the Simulation
Step1、啟動仿真程序
當我們完成了章節2中的所有步驟之后,我們可以在選擇omnetpp.ini文件后(無論是在編輯區域還是Project Explorer均可),再點擊Run按鈕啟動一個仿真程序。
IDE將會自動構造我們的項目。我們可以在菜單中選擇Project->Build All或者快捷鍵CTRL+B手動觸發所有Module的Build。
Step2、運行仿真程序
在成功構造和啟動仿真程序后,我們將會看到一個新的GUI窗口出現。這個窗口屬於Qtenv——omnet++的運行時仿真GUI。你也會看到一個network,它包含了tik和tok,它們以動畫的形式顯示在主區域上。
點擊Run按鈕開始運行仿真過程。我們將會看到tic與toc不停地與另一個module交換消息的動畫流。
toobar中顯示了當前的仿真時間。這其實是個虛擬時間,與程序執行的實際時間沒關系。實際上,現實世界中的仿真時間取決於硬件速度和仿真模型本身的算法復雜度。
需要注意的是,仿真過程中,節點處理消息的時間是0 s。整個過程中唯一使延緩仿真時間的是連接過程中的傳播時延。
我們可以通過圖形工具欄中的按鈕加快或者減慢動畫速度。
其中的各種快捷鍵:
- F8:停止仿真
- F4:運行單步
- F5:運行(有動畫)
- F6:運行(無動畫、速度極快)
- F7:運行(無動畫、急速、無信息跟蹤)
主窗口下方的狀態欄中標識了運行時的信息,包括事件號和事件時間。
調試
點擊工具欄中的Debug進入調試模式,之后仿真程序將在調試模式下進行。
運行時錯誤(Runtime errors)
經常使用調試功能來追蹤運行時錯誤。
我們可以用以下語句試驗一下:
在txc1.cc中,復制handleMessage()中的send()行,之后代碼變為:
void Txc1::handleMessage(cMessage *msg){ send(msg,"out"); send(msg,"out"); }
當我們嘗試運行仿真程序時,就會得到一個錯誤消息:
現在,在Debug模式下運行仿真程序。由於debug-on-errors選項默認開啟,因此仿真程序將會在調試過程中停止。我們就可以通過追蹤函數棧來定位錯誤。
斷點
通過在代碼框最左邊雙擊或者右鍵Toggle Breakpoint構建斷點。所有斷點可以在Breakpoints視圖中查看
結果序列圖可視化
omnet++仿真內核可以在一個event log file(事件日志文件)中記錄下消息交互。為了記錄下事件日志,在RUN/Debug對話中勾選Record eventlog。此外,我們也可以在ini文件文件中指定record-eventlog = true;或者,在運行完程序后的Qtenv圖像環境下點擊Record按鈕。
注意,應該在啟動模擬之前就開啟Eventlog Recording,否則只會記錄下結尾一個Event,而不會顯示出中間的Event及它們之間的消息傳遞過程。
日志文件可以在IDE中的Sequence Chart工具中進行相關分析。即項目文件下的results目錄下的.elog文件。在IDE中雙擊這個文件就可以打開序列圖,並且事件日志表也會同時顯示在窗口底部。
注意:日志文件可能非常大,所以應確保在你真的需要用到它的時候再開啟。
下圖是從一個序列圖構造出來的圖像,展示了消息是如何在網絡中的不同節點間進行路由操作的。在本例中這個圖標很簡單,但是當我們使用了復雜的模型時,序列圖在調試、搜索或者記錄模型的行為時就會變得很有用。
4、Part 3 - Enhancing the 2-node TicToc
①添加icon:Tictoc2
本節中我們將學習使模型變漂亮的方法。我們使用block/routing這個icon(在文件夾/image/block/routing.png),用青色修飾放在tic中,用黃色修飾放在toc。如何實現呢?添加display屬性到NED文件中,在display中使用i=這個tag標識icon。
simple Txc1 { parameters: @display("i=block/routing");//添加默認icon gates: input in; output out; } //兩個Txc1的實例(tic和toc) //tic toc互相傳遞消息 network Tictoc1 { @display("bgb=270,260"); submodules: tic: Txc1 { parameters: @display("p=64,161"); @display("i=,cyan");//不改變icon樣式,只是給它加顏色 } toc: Txc1 { @display("p=142,58"); @display("i=,gold"); } connections: tic.out --> { delay = 100ms; } --> toc.in; tic.in <-- { delay = 100ms; } <-- toc.out; }
結果:
②添加logging
我們還要修改C++代碼。我們為Txc1添加日志說明,來讓Txc1打印出它正在做的事情。omnet++提供了很成熟的日志使用方法,包括日志等級、日志頻道、過濾等。日志在大且復雜的模型中十分有用。在該模型中我們使用最簡單的方式EV:EV相當於C++中的cout。
在tx1.cc文件中的方法initialize()中,添加以下代碼:
EV << "Sending initial message\n";
完整的方法代碼如下:
void Txc2::initialize(){ if(strcmp("tic",getName())==0){ EV << "Sending initial message\n"; cMessage *msg=new cMessage("tictocMsg"); send(msg,"out"); } }
在handleMessage()方法中,添加以下代碼:
EV << "Received message '"<<msg->getName()<<"', sending it out again\n";
完整代碼如下:
void Txc2::handleMessage(cMessage *msg){ EV << "Received message '"<<msg->getName()<<"', sending it out again\n"; send(msg,"out"); }
由於所有network都用同一個ini文件,因此為了與Tictoc1加以區分,需要修改ini文件,修改如下:
[General] # nothing here [Config Tictoc1] network = Tictoc1 [Config Tictoc2] network = Tictoc2
之后就可以正常運行了,運行過程中我們就可以看到日志窗口中輸出的信息了:
我們也可以單獨打開不同Module的日志信息,方法是右鍵Module->Open Componenet Log for xxx。這項功能在大項目(很多日志飛快掃過)中,特別是在我們對項目中的部分Module感興趣時很有用。
③添加變量:tictoc3
本節中我們將為Module添加一個計數器,其作用是:在10次交流后刪除信息。
本節內容是例3的內容了。
我們添加counter作為一個Txc3的成員變量:
class Txc3 :public cSimpleModule { private: int counter;//在此處聲明counter protected: virtual void initialize() override; virtual void handleMessage(cMessage *msg) override; };
我們在initialize()中為counter設置為值10,並在handleMessage()中減少其值,這意味着,每有一個信息到達,counter就會減1。當它減少到0時,仿真結束。
此外在initialize()方法中還要加上一句:
WATCH(counter);
這樣我們在運行過程中的動畫中就能看到counter的變化(在左下角的Module窗口中)。
本例中的修改都在tictoc3.cc中,修改后如下:
#include<string.h> #include<omnetpp.h> #include<stdio.h> using namespace omnetpp; class Txc3 :public cSimpleModule { private: int counter;//在此處聲明counter protected: //以下虛函數標志了算法 virtual void initialize() override; virtual void handleMessage(cMessage *msg) override; }; Define_Module(Txc3); void Txc3::initialize(){ counter = 10; WATCH(counter); if(strcmp("tic",getName())==0){ cMessage *msg=new cMessage("tictocMsg"); send(msg,"out"); } } void Txc3::handleMessage(cMessage *msg){ count--; if(counter==0){ EV << getName()<<"'s counter reached zero,deleting message\n"; delete msg; } else{ EV << getName()<<"'s counter is "<<counter<<", sending back messsage\n"; send(msg,"out"); } }
我們可以試着點擊tic的icon,在左下角的Module窗口中顯示了tic的細節,從中我們可以看到counter的值:
運行仿真后,可以看到counter不斷減小直至0。
④添加Parameters:tictoc4
本節學習如何向仿真添加Parameters,我們將某個parameter的值設置為10,並且再新建一個boolean類型的parameter來決定module是否在它的初始化代碼中發送首個消息(無論是tic還是toc module)。
1)在NED文件中聲明parameter
Module parameters需要首先在NED文件中聲明。數據類型可以是數值、string、bool或者xml。
simple Txc4 { parameters: bool sendMsgOnInit = default(false);//Module是否在其初始化函數中發送消息 int limit = default(2); @display("i=block/routing");//添加默認icon gates: input in; output out; }
2)修改cc文件,在cc文件中接收這些para
之后修改cc文件中的initialize()函數,讀取到這個parameter,並將它分配給counter
counter = par("limit");
用第二個para來決定是否發送初始消息:
void Txc4::initialize(){ counter = par("limit"); if (par("sendMsgOnInit").boolValue()==true){ EV<<"Sending initial message\n"; cMessage *msg = new cMessage("tictocMsg"); send(msg,"out"); } }
現在,我們可以在NED文件或者ini文件中來設置這些para的值了。不過在NED中的賦值是更優先的,我們可以在定義para時用default(xxx)的方法設置默認值,正如我們在1)中的代碼中所寫的那樣。
兩種賦值方式如下:
①NED
network Tictoc4 { submodules: tic: Txc4 { parameters: sendMsgOnInit=true; @display("i=,cyan"); } toc: Txc4 { parameters: sendMsgOnInit=false; @display("i=,gold"); } connections: }
②ini
Tictoc4.toc.limit=5
需要注意的是,由於omnetpp.ini支持通配符,所以我們也可以寫成以下形式:
Tictoc4.t*c.limit=5 Tictoc4.*.limit=5 **.limit=5
*與**的區別在於,*不會匹配單個點.,而**會。
在圖形運行環境中,我們可以在Object Tree(位置在主窗口左邊)中檢查所有module parameters。
具有更小limit的module將會首先刪除消息並隨之終止仿真。
⑤NED繼承:tictoc5
仔細觀察我們之前寫的代碼,我們會發現tic和toc的不同之處僅僅在於parameter和它們的顯示string。
我們可以通過繼承的方式創造一個新的simple module。
本例(例6)中,我們用繼承的方式生成兩個simple module(Tic與Toc),之后我們可以使用這兩個module來定義network中的submodule。
從一個已存在的simple module繼承是很容易實現的,假設我們有一個simple module Txc5:
simple Txc5 { parameters: bool sendMsgOnInit=default(false); int limit=default(2); @display("i=block/routing"); gates: input in; output out; }
這個simple module是將要被繼承的module,之后我們要寫一個繼承module,在繼承module中我們指明parameter和display屬性。
比如,Tic5
simple Tic5 extends Txc5 { parameters: @display("i=,cyan"); sendMsgOnInit=true;//Tic modules將會在初始時發送消息 }
和Toc5,只是parameters值有所不同:
simple Toc5 extends Txc5 { parameters: @display("i=,gold"); sendMsgOnInit=false;//Toc modules在初始時不發送消息 }
當我們構造新的simple module對象時,我們可以直接在network的submodule中使用這兩個simple module:
network Tictoc5 { submodules: tic:Tic5; toc:Toc5; connections: tic.out --> { delay = 100ms; } --> toc.in; tic.in <-- { delay = 100ms; } <-- toc.out; }
正如我們看到的那昂,整個網絡的定義變得很短很簡單。繼承允許我們使用公共部分,避免冗余定義和parameter設置。
⑥模擬節點處理延遲:tictoc6
在之前的model中,tic和toc會立刻把接收到的消息發送回去。本例(例6)中我們添加一些時間操作:tic和toc將會保存消息1s再發送出去。在omnet++中這樣的時間操作的實現是通過節點發送消息給自身實現的。這些消息叫self-messages(這僅僅是因為它們的使用方式,否則就是一個普通消息)
我們給cc文件中的class添加兩個cMessage*變量——event和tictocMsg,用以記錄我們的時間、事件操作,進而在我們仿真時處理延遲。
class Txc6 : public cSimpleModule { private: cMessage * event;//指向用於時間操作的事件對象 cMessage * tictocMsg;//用於記錄消息的變量 public :
我們通過scheduleAt()函數發送self-message,在函數中指明發送時間:
scheduleAt(simTime()+1.0,event)
在handleMessage()中我們必須對新消息加以區分——①通過input gate到達的消息;②self-message。
這里,我們可以用以下語句加以判斷
if(msg==event)
或者我們也可以寫成
if(msg->isSelfMessage())
這樣做,我們就可以不用counter,進一步使源代碼規模變小。
當運行仿真時,我們可以看到以下日志輸出:
⑦隨機數:tictoc7
本例中我們學習隨機數。我們改變延遲時間,從1s到一個隨機時間,可以在NED文件和ini文件中實現。Module parameters可以返回隨機數,為了利用這個特性,我們可以在handleMessage()中讀取這個隨機parameter並使用它。
//"delayTime"module parameter標識了延遲時間 //這個parameter可以在ned和ini文件中設置,例如exponential(5) //之后我們就能得到不同的延遲時間了 simtime_t delay = par("delayTime"); EV<<"Message arrived , starting to wait "<<delay<<" secs..\n"; tictocMsg=msg;
除此之外,我們有小概率丟包的可能:
if(uniform(0,1)<0.1){ EV<<"\"Losing\" message\n"; delete msg; }
我們在ini文件中設置parameter:
Tictoc7.tic.delayTime = exponential(3s)
Tictoc7.toc.delayTime = truncnormal(3s,1s)
實驗時我們會發現無論我們重運行多少次仿真(GUI中,Simulate->Rebuild network),其結果都是一樣的。這是因為重運行時使用了相同的隨機數種子,這在重現某次仿真時是非常有用的。
隨機數種子的設置是在ini文件中,寫法如下:
[General] seed-0-mt=532569 #或者任一32b的值
⑧超時、計時器:tictoc8
為了更進一步模擬網絡協議,我們可以采用stop-and-wait仿真model。
這次(例7)我們將tic和toc分成單獨的類,基本情景為:tic和toc分別向對方發送一個消息。只是toc會有一定概率丟包,在那種情況下tic需要重新發送。
以下是toc丟包時的處理代碼:
void Toc8::handleMessage(cMessage *msg) { if(uniform(0,1)<0.1){ EV <<"\"Losing\" message.\n"; bubble("message lost"); //用彈出框標識消息丟失 delete msg; }
由於bubble()方法,toc將會展示出丟包時它的響應。
當tic發送完一個消息后將會啟動一個計時器。超時時,就可以認為丟包了,並且需要重發一個。如果toc響應准時到達了,那么這個計時器就會被取消。這里的timer可以是一個self-message:
scheduleAt(simTime()+timeout,timeoutEvent);
定時器的取消,可以用cancelEvent()。我們可以不受限制地重用相同的超時消息。
cancelEvent(timeoutEvent);
確認消息的發送,可以用發回一個原消息代替:
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 ack.\n"; send(msg,"out"); } }
⑨重傳相同消息:tictoc9
本節中,我們要對之前一節的model進行優化。在上一個例子中,當我們需要重傳時,我們創建了另一個包。這樣做是OK的,因為一個包並不占用多少空間。但是在實際中,我們通常是保留一個原始包的備份,這樣,當我們需要重傳時就不用再創建新的包了。
一個思路是:創建一個指針,並始終讓它指向剛發送過的消息。這聽上去很簡單,但是當這個消息在另一個節點中被銷毀之后,這個指針就無效了。
本節中我們使用的方法是,保留原始包並只發送它的備份。當toc的Ack到達時,我們再刪除這個原始包。為了讓這一過程在模型中可視化,我們為每個消息記錄一個消息號。
為了避免handleMessage()函數過於龐大,我們將相關代碼放在兩個新的函數generateNewMessage()和sendCopyOf()中,並在handleMessage()中調用它們:
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"); }
由於tictoc9的例子集合了之前所有例子中的知識點,現一句一句加以分析:
NED文件:tictoc9.ned
simple Tic9 //① { parameters: @display("i=block/routing"); gates: input in; output out; } simple Toc9 //② { parameters: @display("i=block/process"); gates: input in; output out; } network Tictoc9 //③ { submodules: //④ tic:Tic9{ parameters: @display("i=,cyan"); } toc:Toc9{ parameters: @display("i=,gold"); } connections: //⑤ tic.out --> {delay=100ms;} -->toc.in; tic.in <-- {delay=100ms;} <-- toc.out; }
解釋:
①、②:simple module
simple Tic9 { parameters: @display("i=block/routing"); gates: input in; output out; }
Tic9、Toc9分別是兩種節點的simple module,本例中的這兩個module有兩部分內容parameters和gates——parameters中定義了module的組織、布局信息;gates中定義了輸入輸出端口input、output,不過本例中的輸入輸出端口各只有一個,所以用普通變量的形式;如果有多個端口,則要用[ ]表示的端口vector,具體用法和寫法,可以見例子Tictoc10。
③:network
network Tictoc9
{
submodules:
...
connections:
...
}
構建了一個網絡Tictoc9,一個網絡最少要由submodules和connections兩部分,submodules部分指明構成網絡的各個module,connections指明所有module的連接方式。
④:submodules
submodules: tic:Tic9{ parameters: @display("i=,cyan"); } toc:Toc9{ parameters: @display("i=,gold"); }
每個network中的submodule都是由之前①、②中定義的simple module的實例對象,一個實例對象就對應着模塊圖中的一個節點,有多少實例對象就有多少個節點。
既可以一句話定義一個簡單的simple module對象:
tic : Tic9 ; //obj : Module 左對象,右module
也可以在定義時指明額外的內容:
tic : Tic9{ parameters: @display("i=,cyan"); //附加某種顏色 }
⑤connections
所有module的所有gate都必須說明連接方式。也就是說,每一個module的output gate都必須從另一個module的input gate相連接,連接方式總是:
xxx1.out --> { ... } --> xxx2.in; //或 xxx2.in <-- { ... } <-- xxx1.out;
這兩種連接方式的結果完全相同,只是寫法不同而已,實際寫代碼的過程中哪種都可以。
注意:所有定義的gate都必須實現,如果某個gate真的不需要實現,就需要另外寫一個類,在這個類中不寫這個gate。換言之,只要說某個submodule是實現自某個simple module,那么這個submodule就必須實現該simple module的所有gate。
cc文件:txc9.cc
#include<stdio.h> #include<string.h> #include<omnetpp.h> using namespace omnetpp; class Tic9 : public cSimpleModule //① { private: //② simtime_t timeout; cMessage *timeoutEvent; int seq; cMessage * message; public: //③ Tic9(); 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(){//⑥ timeoutEvent=message=nullptr; } Tic::~Tic9(){//⑦ cancelAndDelete(timeoutEvent); delete message; } void Tic9::initialize(){//⑧ //初始化變量 seq=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{ //如果收到確認消息,就 //1、刪除確認消息 //2、刪除原始消息原件 //3、取消超時事件 //4、生成另一個新消息,發送 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"); } class Toc9:public cSimpleModule { protected: virtual void handleMessage(cMessage * msg) override; }; Define_Module(Toc9); void Toc9::handleMessage(cMessage *msg){//⑫ if(uniform(0,1)<0.3){ EV<<"\"Losing\" message\n"; bubble("message lost"); delete msg; } else{ EV<<msg>>" received, sending back an ACK.\n"; delete msg; send(new cMessage("ACK"),"out"); } }
①
class Tic9 : public cSimpleModule { ... }
cc文件中的class需要和ned文件中的simple module一一對應;這些simple module對應的class都是繼承自cSimpleModule。
所以,cc文件中的class的寫法基本上都是:
class xxx : public cSimpleModule
②
private: simtime_t timeout; cMessage * timeoutEvent; int seq; cMessage * message;
把變量的權限設置為private,在本例中有兩個cMessage * ——message和timeoutEvent,分別是最初message和超時self-message。
這里的時間變量都是simtime_t類型,而不是int類型。seq是用來標記發送信息的序號。
③
public : Tic9(); virtual ~Tic9();
構造函數和析構函數,構造函數中是對我們在private中定義的變量進行初始化的過程,析構函數中則要對對必要的指針進行delete。
④
protected: //④ virtual cMessage * generateNewMessage(); virtual void sendCopyOf(cMessage * msg); virtual void initialize() override; virtual void handleMessage(cMessage * msg) override;
⑤
Define_Module(Tic9);//⑤
cc文件中的class名要與ned文件中的同名simple module相綁定,通過方法Define_Moduel(xxx);
⑥
Tic9::Tic9(){//⑥ timeoutEvent=message=nullptr; }
構造函數中,對指針對象進行初始化,本例中為兩個cMessage消息初始化為空指針
⑦
Tic::~Tic9(){//⑦ cancelAndDelete(timeoutEvent); delete message; }
析構函數中,清理指針對象,釋放空間。可以用關鍵字delete或者cancelAndDelete(msg)兩種方法。
⑧
void Tic9 :: initialize(){ seq=0; timeout=1.0; timeouEvent=new cMessage("timeoutEvent"); //生成並發送消息 EV<<"Sending initial message.\n"; message=generateNewMessage(); sendCopyOf(message); scheduleAt(simTime()+timeout,timeoutEvent); }
1)在initialize初始化函數中,對我們之前的private中的變量初始化。
2)生成新消息,並發送:
message = generateNewMessage();
sendCopyOf(message);
3)發送消息后,再用scheduleAt發送self-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"); }
⑩給出了創建不同名消息的方法
⑪給出了備份消息並發送副本的方法
⑨
void Tic9::handleMessage(cMessage *msg){//⑨ if(msg==timeoutEvent){ //收到超時消息就意味着丟包,需要重傳 EV<<"Timeout expired,resending message and restarting timer\n"; sendCopyOf(message) scheduleAt(simTime()+timeout,timeoutEvent); } else{ //如果收到確認消息,就 //1、刪除確認消息 //2、刪除原始消息原件 //3、取消超時事件 //4、生成另一個新消息,發送 EV<<"Received: "<<msg->getName()<<"\n"; delete msg; EV<<"Timer cancelled.\n"; cancelEvent(timeoutEvent); delete message;//消息原件 message=generateNewMessage();//新消息原件 sendCopyOf(message); scheduleAt(simTime()+timeout,timeoutEvent);//計時器 } }
處理消息,由於我們會收到兩種消息,普通消息和self-message,所以需要在這個函數中用if語句加以判斷:
1)判斷是否是self-message,如果是,表明超時了,就需要再發送消息副本,並重新計時:
if(msg==timeoutEvent){ //收到超時消息就意味着丟包,需要重傳 EV<<"Timeout expired,resending message and restarting timer\n"; sendCopyOf(message) scheduleAt(simTime()+timeout,timeoutEvent); }
其中在scheduleAt中的timeoutEvent和我們在initialize中的timeoutEvent是同一個,並且在整個過程中都是同一個。
2)如果是一般Message,就說明正確收到了,就需要做4件事:刪除確認消息、刪除原消息原件、取消超時事件、生成另一個新消息(備份並重置計時器)
else{ //如果收到確認消息,就 //1、刪除確認消息 EV<<"Received: "<<msg->getName()<<"\n"; delete msg; EV<<"Timer cancelled.\n"; //3、取消計時器 cancelEvent(timeoutEvent); //2、刪除原始消息原件 delete message; //4、生成另一個新消息,發送副本,重置計時器 message=generateNewMessage(); sendCopyOf(message); scheduleAt(simTime()+timeout,timeoutEvent); }
⑫對接收消息模塊的處理,需要模擬丟包概率,代碼都在handleMessage:
void Toc9::handleMessage(cMessage *msg){//⑫ if(uniform(0,1)<0.3){ EV<<"\"Losing\" message\n"; bubble("message lost"); delete msg; } else{ EV<<msg>>" received, sending back an ACK.\n"; delete msg; send(new cMessage("ACK"),"out"); }
1)丟包的判斷,使用隨機數生成函數
if(uniform(0,1)<0.3)
2)如果丟包了,就要彈出消息,說明丟包了,並把接收到的消息丟棄(因為實際上收到了消息,只是我們用隨機數的方法模擬丟包,所以丟包發生時我們需要手動把包丟棄)
if(uniform(0,1)<0.3){ EV<<"\"Losing\" message\n"; bubble("message lost");//彈出丟包消息 delete msg; //丟棄接收到的消息,以模擬丟包 }
3)如果沒丟,處理消息(本例中沒有涉及,僅僅是說明收到了),輸出日志,刪除收到的消息,並發回ACK消息:
else{ EV<<msg>>" received, sending back an ACK.\n"; delete msg; send(new cMessage("ACK"),"out"); }
未完待續,之后還有幾個例子,涉及到真實網絡架構的搭建,具體可以見官方文檔翻譯總結(三)
總結
- NED文件的一般形式:
//simple moduel simple XXX { parameters: @display("i=block/routing");//添加默認icon gates: input in;//輸入端口 output out;//輸出端口 } //network network XXXNet { submodules://子模塊,寫法 子模塊名:子模塊類型 sm1:XXX;{ @display("i=,cyan");//icon顏色 } sm2:XXX; connections://連接情況 sm1.out --> {delay=100ms;} -->sm2.in; //一個的輸出端口與另一個的輸入端口連接,鏈路延遲100ms sm1.in <-- {delay=100ms;} <-- sm2.out; }
- 每個NED都一個與之對應的cc文件,在cc文件中必須實現與NED中定義的module相對應的class,定義該class的寫法一般是:
class xxx : public cSimpleModule
並且要在隨后,將這個class與NED文件中的module進行綁定,綁定方法為:
Define_Module(xxx);//xxx是Ned文件中的module
cc文件中的class名和ned文件中的對應module名必須相同!!!!
- C++中接收ned文件中定義的變量時,需要用par關鍵字:
var xxx1 = par("xxx0") //xxx0是ned文件中需要承接的變量名
- send(msg,"out"):通過out gate發送消息msg;
- 在initialize()中創建新消息並發送:
cMessage *msg=new cMessage("tictocMsg"); send(msg,"out");
- 在handleMessage()中處理接收到的消息;
- 關於initialize和handleMessage方法的使用時機——前者只在該module仿真開始時調用一次;后者在消息到達module時被調用;
- Message(包、幀等)和Event(計時器、超時等)都是用cMessage對象實現的,只是表現方式不同(具體見下邊總結的部分);
- 仿真過程的執行時間可以在Run Configuration中指定;
- 所有同一個工程下的項目,共享一個ini文件,但是可以在其中對每個項目進行分別的配置和運行,使它們互不干擾:
[General] [Config Tictoc1] network = Tictoc1 …… [Config Tictoc7] network = Tictoc7 Tictoc7.tic.delayTime = exponential(3s) Tictoc7.toc.delayTime = truncnormal(3s,1s)
- cc文件中的EV,在功能和用法上相當於cout(不過開頭要加using namespace omnetpp);如果在initialize和handleMessage中用EV,可以輸出信息到運行時的日志窗口中
- 我們可以查看單個節點的日志消息:
這在大型項目中,有很多個節點時很有幫助。
- 類變量的定義方法和C++中的語法完全相同,例如:
class Txc3 :public cSimpleModule { private: int counter;//在此處聲明counter
如果要在運行過程中監視其變化,需要在initialize方法中用WATCH關鍵字:
void Txc3::initialize(){ counter = 10; WATCH(counter);
- NED繼承,通過simple module繼承得到新的simple module,更好地實現代碼重用:
simple Txc5 { ...//共同的代碼部分 } simple Tic5 extends Txc5 { ...//獨特的代碼部分 }
繼承下來的simple module,寫在network的submodules下:
network Tictoc5 { submodules: tic:Tic5; toc:Toc5; }
- 模擬處理延遲:self-message;通過scheduleAt()方法發送self-message,需要在函數中說明發送時間,一般用法為:
sceduleAt(simTime()+1.0,event);
這里的event是class中額外定義的一個cMessage *指針,用來表明一個self-message,此外還有個cMessage *指針xxxMsg,用來表明一個一般消息,即發送給別的節點的消息。
如何對這兩種消息加以區分?
if(msg==event)//self-message //或 if(msg->isSelfMessage())
- 隨機數:在module中的parameter中定義一個隨機數,在cc文件中讀取並使用它,需要用到par關鍵字:
simtime_t delay = par("delaytime");
比如上段代碼中,我們在NED中定義隨機數,在ini文件中對這個隨機數進行賦值:
//NED simple Txc7 { parameters: volatile double delayTime @unit(s); //在發回message之前有一段延遲 //ini [Config Tictoc7] network = Tictoc7 Tictoc7.tic.delayTime = exponential(3s) Tictoc7.toc.delayTime = truncnormal(3s,1s)
- 在handleMessage()函數中使用uniform函數來模擬丟包
if(unifor(0,1)<0.1){ ... delete msg; }
- 隨機數種子在ini文件的General下設置
[General] seed-0-mt=532569 #或者任一32b的值
- 使用self-message構建計時器,從而實現超時重傳的效果:
scheduleAt(simTime()+timeout,timeoutEvent);
關於一般消息和self-message的區分,可以看總結部分第15條所述。
ACK消息收到后,需要取消計時器,方法cancelEvent():
cancelEvent(timeoutEvent);
- 消息重傳。原理:在收到ACK前保留原件,收到ACK后刪除原件,發送並保存新消息的原件;如果超時(具體實現見19),則保留原件並發送原件的備份:備份消息(Tictoc9):
void Tic9::sendCopyOf(cMessage *msg){ //復制消息,發送備份版 cMessage *copy=(cMessage *) msg->dup(); send(copy,"out"); }
- 空指針:nullptr
- 發送ACK消息的方法:
if(uniform(0,1)<0.3){ ...//丟包處理 } else{ //沒丟 ...//處理消息 ...//刪除收到的消息 //發回確認 send(new cMessage("ACK"),"out"); }