NS3做爲一個網絡仿真庫,出於性能的考量選擇了C++。在寫仿真程序時,不可避免的要對各類實體進行建模,天然C++中的class成了惟一可選的方案。不加任何技巧的class的確能夠知足對某些實體的建模,但是在仿真軟件的編寫中須要有足夠的動態性,好比有這樣一些需求:前端
這些都不是過度的需求,若是真的寫過仿真程序的話確定會很是渴求使用的軟件可以提供實現這些需求的方法。要本身乾巴巴的實現這些需求也不是不能夠,好比能夠提供一些查詢接口來實現1;對於2的話,Qt的signal/slot或許能夠實現。說到Qt了,其實QObject擁有了超越普通C++ class的能力,也都能知足上面指出的這些需求,可是其解決方案彷佛有點重。c++
幸虧,NS3經過TypeId能夠很好的解決上面提出的各種需求。數據庫
TypeId
是什麼class TypdId { uint16_t m_tid; }
這就是TypdId,就是這麼簡單。彷佛有些難以想象,但TypdId
自己只是一個16bit的整型,以前提到的全部的複雜功能都是藏在TypdId
背後的IidManager
完成的,這個m_tid
只是做爲一個索引而存在。編程
TypdId提供了很是多的方法,好比說增長一個屬性(AddAttribute
),增長一個TraceSource(AddTraceSource
),這些方法只是直接了當的將所需信息蒐集起來轉發給IidManager
。能夠看個例子:設計模式
TypeId TypeId::AddAttribute (std::string name, std::string help, uint32_t flags, const AttributeValue &initialValue, Ptr<const AttributeAccessor> accessor, Ptr<const AttributeChecker> checker, SupportLevel supportLevel, const std::string &supportMsg) { NS_LOG_FUNCTION (this << name << help << flags << &initialValue << accessor << checker << supportLevel << supportMsg); IidManager::Get ()->AddAttribute (m_tid, name, help, flags, initialValue.Copy (), accessor, checker, supportLevel, supportMsg); return *this; }
基本上全部的TypdId
的方法都是這個樣子。因此解決問題的核心實際上是IidManager
。IidManager
可認爲是一個類型數據庫,保存了與TypdId
想關聯的Attribute與TraceSource。具體的內部實現就太細節了,做爲使用方是不須要也不該該去關注的。網絡
TypdId
正如在Qt中同樣,想要使本身寫的一個類擁有強大的能力,須要本身動手在類的聲明中添加Q_OBJECT。在NS3設計的TypeId
系統中,這個步驟是要給本身的class添加一個靜態方法static TypeId GetTypeId (void)
,而後在這個函數裏返回一個TypeId
。在這個過程,能夠盡情的使用TypeId
提供的各類方法來給本類加屬性和TraceSource,惟一的限制就是這個返回的TypeId
應該是處於GetTypeId
裏的靜態變量,這是爲了保證全局的惟一性。固然了,寫C++的限制多了去了,這個規則應該納入這個NS3庫的使用方法吧,不太值得吐槽。框架
TypeId
對於咱們平時寫程序的最大的幫助在於,它能夠給本身的類添加Attribute和TraceSource。函數
Attribute能夠表明該實體的一些屬性,好比說一臺PC的IP地址、一隻貓的體重等。你可能會想,這個不就是一個Get函數的事兒麼,值得專門搞這麼一套複雜的系統麼。其實還真值得:你會去寫一個127.0.0.1仍是2130706433?在NS3裏,能夠直接寫127.0.0.1,這也得歸功與這個Attribute系統。性能
TraceSource可類比Qt的Signal,在須要的時候調用這個Functor(想不到更好的名稱了,不過寫C++的應該都知道這個東西),連到這個TraceSource的其餘函數(所謂的Slot)就會被自動調用。好處自沒必要多說,要知道Qt能獲得普遍的承認,Signal/Slot功不可沒,NS3裏的TraceSource系統就是Signal/Slot的翻版。ui
還有一個使用限制就是,須要經過一個宏來告知系統我一個TypeId
要註冊:NS_OBJECT_ENSURE_REGISTERED
。這個宏其實聲明瞭一個靜態類並同時聲明瞭一個本文件內的靜態實例。在這個靜態類的構造函數中調用了咱們定義的類的GetTypeId
,這就實現了自定義TypeId
的註冊。
終於到重頭戲了。其實這兩部分的代碼都切實的體現了C++罪惡的地方:模板與宏。一個新手要看懂這些代碼要補充太多的東西了。說來慚愧,其實我自身也是一個新手,從開始接觸這個庫到如今能初步搞明白這樣一個系統(真的只是大體初步明白),已通過去了3年多。這個系統的實現是模板裏套宏、宏裏套模板,看的時候須要時刻注意這段代碼是宏生成的代碼仍是用了模板。
前面提到了咱們能夠用127.0.0.1來表明ip的那32個bit,這就是Attribute系統的神奇所在。在這個例子裏,127.0.0.1實際上是一個字符串,Attribute系統能夠把字符串轉化爲任何一種類型(能夠是自定義類型)。
就單純的以這個地址類型來解釋好了。咱們的程序中須要使用IP地址,其最合適的存儲方式實際上是一個int,但IP地址最適合人類的表述方式實際上是點分表示,咱們天然也想在使用的時候用這種方式。那這個應該怎麼作?
首先先無論人怎麼讀寫的問題,先考慮程序中的這個屬性的使用方式。做爲一個Ipv4的值,確定有一些相關聯的方法,好比說是否爲廣播地址、是否爲多播地址、是否爲本地地址之類類的。這些能夠以用成員函數的方式實現,既然這樣,那就盡情的實現吧!不須要考慮怎麼集成到Attribute系統中去。同理,這個類裏面有什麼字段,想要什麼字段就盡情的加。想必你也看出來了,咱們在實現一個Attribute的時候,其實根本不須要考慮什麼集成的問題。
可以用ns3的方式來給一個對象設置屬性的這個能力依賴與3個基本的組件
AttributeValue
AttributeAccessor
AttributeChecker
首先看看什麼是ns3的方式爲一個對象設置屬性,看一下官方manual裏的例子
Ptr<ConfigExample> a2_obj = CreateObject<ConfigExample> (); a2_obj->SetAttribute ("TestInt16", IntegerValue (-3)); IntegerValue iv; a2_obj->GetAttribute ("TestInt16", iv);
第一行建立了新的對象ConfigExample
,並存在指針a2_obj
裏。第二行就是所謂的ns3的方式設置屬性,依賴於一個方法SetAttriute
。這個方法屬於ObjectBase
,全部能用Ptr
指向的對象都是objectBase
的子類。因此說,在調用SetAttribute
時,除去C++的語法糖,這句話完整的形式是這樣的:
SetAttribute (a2_obj, "TestInt16", IntegerValue (-3));
好了,咱們跳進去看看實現
void ObjectBase::SetAttribute (std::string name, const AttributeValue &value) { NS_LOG_FUNCTION (this << name << &value); struct TypeId::AttributeInformation info; TypeId tid = GetInstanceTypeId (); if (!tid.LookupAttributeByName (name, &info)) { NS_FATAL_ERROR ("Attribute name="<<name<<" does not exist for this object: tid="<<tid.GetName ()); } if (!(info.flags & TypeId::ATTR_SET) || !info.accessor->HasSetter ()) { NS_FATAL_ERROR ("Attribute name="<<name<<" is not settable for this object: tid="<<tid.GetName ()); } if (!DoSet (info.accessor, info.checker, value)) { NS_FATAL_ERROR ("Attribute name="<<name<<" could not be set for this object: tid="<<tid.GetName ()); } }
這個方法是對象本身身上的方法,因此要記住this
這時候指向的是誰:這裏就是a2_obj
。這個方法也很直白
GetInstanceTypdId()
拿到真正的、ConfigExample
的TypeId
AttributeInformation
,就有了accessor
、checker
,還有做爲參數傳進來的值value
。DoSet
作了實際的設置工做再看看DoSet
:
bool ObjectBase::DoSet (Ptr<const AttributeAccessor> accessor, Ptr<const AttributeChecker> checker, const AttributeValue &value) { NS_LOG_FUNCTION (this << accessor << checker << &value); Ptr<AttributeValue> v = checker->CreateValidValue (value); if (v == 0) { return false; } bool ok = accessor->Set (this, *v); return ok; }
檢查什麼的就不說了,最讓人關心的是這個方法accessor->Set (this, *v)
。這個方法是怎麼定義的,是哪裏來的?這下歡迎進入模板與宏的世界。
AttributeAccessor
答案是這個這個方法是屬於accessor
的,而accessor
的定義是在註冊TypeId的時候生成的。RTFSC:
class ConfigExample : public Object { public: static TypeId GetTypeId (void) { static TypeId tid = TypeId ("ns3::A") .SetParent<Object> () .AddAttribute ("TestInt16", "help text", IntegerValue (-2), MakeIntegerAccessor (&A::m_int16), MakeIntegerChecker<int16_t> ()) ; return tid; } int16_t m_int16; }; NS_OBJECT_ENSURE_REGISTERED (ConfigExample);
看到那句MakeIntegerAccessor (&A::m_int16)
了麼?搞懂了這個,其實就能搞懂ns3的套路了,再看其餘的機制也就順風順水了。咱們慢慢來,一步一步來,保證每一步都善始善終,不會出現跳躍的現象。這個過程稍微有點冗長,能夠去拿包零食邊吃邊看了。
MakeIntegerAccessor
是可調用的一個「東西」。回想一下C++可調用的東西有哪些?1. 函數,2. Functor,就是實現了自定義operator()的一個class的實例。3.實例化一個類型看起來也像是函數調用。我用的Eclipse,f3跳轉到定義,等我過去的時候傻眼了:
ATTRIBUTE_ACCESSOR_DEFINE (Integer);
好傢伙,看來要展開這個看看了,ctrl+=讓它現形:
template <typename T1> \ Ptr<const AttributeAccessor> MakeIntegerAccessor (T1 a1) \ { \ return MakeAccessorHelper<IntegerValue> (a1); \ } \ template <typename T1, typename T2> \ Ptr<const AttributeAccessor> MakeIntegerAccessor (T1 a1, T2 a2) \ { \ return MakeAccessorHelper<IntegerValue> (a1, a2); \ }
這展開了2個函數,到這時能夠肯定,MakeIntegerAccessor
是一個函數,並且咱們調用的是隻有一個入參的那個函數,這個函數返回了一個AttributeAccessor
的智能指針。具體的展開過程就不細講了,也沒有講的必要,看看ATTRIBUTE_ACCESSOR_DEFINE
的定義就明白了。如今須要關心的是咱們如今調用的函數裏有個T1
,要搞明白這個T1
的類型是什麼。
從新回頭看看MakeIntegerAccessor (&A::m_int16)
,這裏的T1
就是&A::m_int16
的類型。先就此打住,這個結論先記下來。咱們繼續追下去,這下應該看真正的實現MakeAccessorHelper<IntegerValue> (a1)
:
// 第一種實現 template <typename V, typename T, typename U> inline Ptr<const AttributeAccessor> DoMakeAccessorHelperOne (U T::*memberVariable) // 第二種實現 template <typename V, typename T, typename U> inline Ptr<const AttributeAccessor> DoMakeAccessorHelperOne (U (T::*getter)(void) const) // 第三種實現 template <typename V, typename T, typename U> inline Ptr<const AttributeAccessor> DoMakeAccessorHelperOne (void (T::*setter)(U))
結果就是匹配到了第一種實現。
其實我曾經不少次追到了這裏,卻沒看懂這裏的類型究竟是什麼意思。也不知道何時突然就明白了。A::m_int16
對應於U T::*
,是個正常人第一眼看上去絕對不會明白這究竟是怎麼聯繫在一塊兒的,我也是正常人,因此我如今也不明白這種怪異的語法究竟是誰第一次使用的。T
對應於A
,那麼U
應該是對應於m_int16
。這個類型能表明一個類裏的一個成員變量的類型,T
代表了它是一個類的成員變量,U
代表了這個變量的類型是uint16_t
,如今就只能這麼死記了,要想真正搞明白我以爲應該去翻一下編譯器裏前端究竟是怎麼解析這個鬼畜般的語法的,先就這麼囫圇吞棗吧!對於另外的兩個反而更好懂一點,那個類型和平時用的函數指針類型聲明挺像的,反而不用多說。一個是getter
,說明這個attribute只提供了獲取的接口;一個是setter,說明這個attribute
只能設置不能獲取。固然了,這是站在ns3的使用方式上說的,直接強行用c++的方式賦值不在咱們的討論範圍以內。
這3個函數都返回了一個指向AttributeAccessor
的指針。如今來看看實現吧!
Ptr<const AttributeAccessor> DoMakeAccessorHelperOne (U T::*memberVariable) { /* AttributeAcessor implementation for a class member variable. */ class MemberVariable : public AccessorHelper<T,V> { public: /* * Construct from a class data member address. * \param [in] memberVariable The class data member address. */ MemberVariable (U T::*memberVariable) : AccessorHelper<T,V> (), m_memberVariable (memberVariable) {} private: virtual bool DoSet (T *object, const V *v) const { typename AccessorTrait<U>::Result tmp; bool ok = v->GetAccessor (tmp); if (!ok) { return false; } (object->*m_memberVariable) = tmp; return true; } virtual bool DoGet (const T *object, V *v) const { v->Set (object->*m_memberVariable); return true; } virtual bool HasGetter (void) const { return true; } virtual bool HasSetter (void) const { return true; } U T::*m_memberVariable; // Address of the class data member. }; return Ptr<const AttributeAccessor> (new MemberVariable (memberVariable), false); }
照樣很鬼畜。這是在函數裏定義了一個類,而且返回了指向這個類的只能指針。這個類繼承自AccessorHelper
,而AccessorHelper
又繼承自AttributeAccessor
。因此將其做爲AttributeAccessor
的子類返回也說得過去。
至於爲何要繼承這麼多?我如今的理解是這樣的
AttributeAccessor
只是一個純虛接口,它只定義了做爲Accessor應當具備的接口。在Java裏的話,估計這就是個Interface。AccessorHelper
提供了Set
和Get
的默認實現,把一些可變的部分留給了它的子類來實現,這些可變的部分是DoSet
和DoGet
。因此在MemberVariable
要實現DoSet
和DoGet
。這應該是某種設計模式,看看那本書就能找到了。到如今爲止,咱們知道能夠造出來一個AttributeAccessor
,並把指向這個AttributeAccessor
的指針存在了咱們的IidManager
的數據庫中。之後想要拿出來這個AttributeAccessor
,就要手拿TypeId
去找IidManager
去要,並且要到的也是一個指針,這個指針指向了在return Ptr<const AttributeAccessor> (new MemberVariable (memberVariable), false);
這句話裏的那個new出來的地址。
總結一下,一個類型的AttributeAccessor
只有一個,就是那個new出來的地方。程序其餘地方都是拿指針去訪問的。在那塊地址存的東西只有兩樣(只考慮咱們如今這個membervariable類型的accessor)
U T::*m_memberVariable
的值,這個值表明了這個變量在擁有TypeId
那個類裏的偏移量AttributeAccessor
的實例是有虛表指針的,這個虛表裏就是真正的、對應類型的函數實現。回頭看看那個DoSet
,裏面那個accessor究竟是什麼應該已經清楚了。那個個accessor的Set
方法在哪兒定義的?答案是AccessorHelper
。我直接把結論公開了,可是你如今應該停下來去看看具體的實現。AttributeAccessor
->AccessorHelper
->DoMakeAccessorHelperOne()裏的MemberVariable
這是一條繼承鏈,到了最下一層的時候全部的方法都已經定義,只是在不一樣的層次提供了不一樣的實現。
假設你已經搞明白Accessor
的繼承鏈條了,也明白這個Accessor
到底支持什麼操做,咱們就進入了真正執行Set
的地方:
// DoMakeAccessorHelperOne()裏的MemberVariable的方法 virtual bool DoSet (T *object, const V *v) const { typename AccessorTrait<U>::Result tmp; bool ok = v->GetAccessor (tmp); if (!ok) { return false; } (object->*m_memberVariable) = tmp; return true; }
這裏的V *v
就是myObj->SetAttribute ("MyIntAttribute", IntegerValue(3));
裏的IntegerValue(3)
。要是沒看懂,就去翻代碼。這個結論是必需要搞懂的,否則就沒有進行下去的必要了。
其實Accessor
的世界已經探索的差很少了,爲了真正搞明白這個函數作了什麼,咱們先轉向看看AttributeValue
。
AttributeValue
NS3的套路是什麼?用宏和模板作代碼生成。這個套路在AttributeValue
裏也是同樣的。自定義了一個AttributeValue
須要寫一個宏,這個宏幫助咱們作了大部分的事情。拿那個IntegerValue
說事兒:
ATTRIBUTE_VALUE_DEFINE_WITH_NAME (uint64_t, Uinteger); // 在頭文件裏寫這個宏,可以展開爲以下的定義 class UintegerValue : public AttributeValue \ { \ public: \ UintegerValue (); \ UintegerValue (const uint64_t &value); \ void Set (const uint64_t &value); \ uint64_t Get (void) const; \ template <typename T> \ bool GetAccessor (T &value) const { \ value = T (m_value); \ return true; \ } \ virtual Ptr<AttributeValue> Copy (void) const; \ virtual std::string \ SerializeToString (Ptr<const AttributeChecker> checker) const; \ virtual bool \ DeserializeFromString (std::string value, \ Ptr<const AttributeChecker> checker); \ private: \ uint64_t m_value; \ } // 上述定義的實現仰賴於對於cc文件裏的實現,也是用宏 ATTRIBUTE_VALUE_IMPLEMENT_WITH_NAME (uint64_t,Uinteger); // 展開後是這樣的 UintegerValue::UintegerValue () \ : m_value () {} \ UintegerValue::UintegerValue (const uint64_t &value) \ : m_value (value) {} \ void UintegerValue::Set (const uint64_t &v) { \ m_value = v; \ } \ uint64_t UintegerValue::Get (void) const { \ return m_value; \ } \ Ptr<AttributeValue> \ UintegerValue::Copy (void) const { \ return ns3::Create<UintegerValue> (*this); \ } \ std::string UintegerValue::SerializeToString \ (Ptr<const AttributeChecker> checker) const { \ std::ostringstream oss; \ oss << m_value; \ return oss.str (); \ } \ bool UintegerValue::DeserializeFromString \ (std::string value, Ptr<const AttributeChecker> checker) { \ std::istringstream iss; \ iss.str (value); \ iss >> m_value; \ do { \ if (!(iss.eof ())) \ { \ std::cerr << "aborted. cond=\"" << "!(iss.eof ())" << "\", "; \ do \ { \ std::cerr << "msg=\"" << "Attribute value " << "\"" << value << "\"" \ << " is not properly formatted" << "\", "; \ do \ { \ std::cerr << "file=" << "D:\\Code\\ns-allinone-3.28\\ns-3.28\\src\\core\\model\\uinteger.cc" << ", line=" << \ 35 << std::endl; \ ::ns3::FatalImpl::FlushStreams (); \ if (true) std::terminate (); \ } \ while (false); \ } \ while (false); \ } \ } while (false); \ return !iss.bad () && !iss.fail (); \ }
具體的展開過程感興趣的能夠去追一下,要是沒有IDE的幫助,要展開一個這麼複雜的宏也仍是須要一點時間的。結合以前accessor的內容(v->GetAccessor
),這個函數就定義在了這裏(頭文件裏做爲模板類成員函數實現了)。
值得一看的卻是那兩個SerializeToString
和DeserializerFromString
。這兩個函數完成了字符串到實際定義類型的轉換,裏面用到了<<
的重載,因此這也是爲何在自定義屬性的時候要去實現全局operator<<
的緣由了。經過這兩個函數,咱們就能夠用一個字面意義上的127.0.0.1去設置一個IP,而非2130706433。其實這個系統在解析string的時候出了一點小bug被我發現了,也算是對開源的一點點小貢獻吧!(https://www.nsnam.org/bugzill... 這個bug在ns3.25裏提出來,以後應該是修好了。
還剩一個AttributeChecker
,但這個好像不影響對系統的理解,就不去看啦!想必搞明白套路以後,要看懂也不是什麼難事兒啦!
TraceSource
Callback
雜談提及TraceSource,那麼Callback
就是繞不過去的坎。能夠做爲一個TraceSource
的屬性關聯一個TracedCallback
類型,用於通知自身值的改變。TracedCallback
只是一個Callback
的容器,裏面有一個std::list<Callback>
用於存放鏈接到該TraceSource
的函數。一個TraceSource
能夠鏈接屢次,每次它被Connect
一次,就會往這個list裏填一個元素。固然,這個元素就是一個Callback
。
Callback
類自己只是提供了建立的接口於調用接口。調用接口就是對各個operator()
的重載,最多有9個參數,也所以有9個operator()
。這種狀況在c++11
以後應該會有更好的寫法,只是我並不知道怎麼寫罷了。Callback
繼承自CallbackBase
,這裏存放了真正的指向實現(CallbackImplBase
)的指針。
怎麼CallbaciImpl
還有繼承?從Callback
開始已經跳轉兩次了還沒見到真正的實現,其實這也不遠了,CallbackImplBase
說到底就是一個Interface
同樣的東西,對CallbackImpl
作了一些限定,這樣繼承了CallbackImpl
的子類就能以比較一致的方法去操做。其實CallbackImplBase
->CallbackImpl
->各類具體的CallbackImpl
弄這麼複雜也是無奈之舉。抽出來中間的CallbackImpl
是爲了實現多輸入參數類型的operator()
的重載,考慮到這個庫的編寫時間,那時候的c++模板編程好像沒有c++11以後的那麼完善,沒有可變長類型參數,這樣作也是無可厚非。我想若是用最新的c++11
以後的標準來寫,這個Callback
可能就不會這麼難以理解了,彷佛能夠直接採用std::function
或者只是作一些小的改動就能夠了。無論怎樣,到了CallbackImpl
這個層級時,單單CallbackImpl
這個名字就已經能夠支撐多達9個入參的operator()
了,再下面層級的類就能夠方便的享用這種便利,這也是爲何MemPtrCallbackImpl
、FunctorCallbackImpl
等子類能夠在一個class裏就重載屢次operator()
同時還能作類型檢查的緣由了。
TracedValue
追蹤先來看看在TypeId
裏怎麼使用TraceSource
吧!我隨便從代碼裏摘了一條出來
.AddTraceSource ("Tx", "A new packet is created and is sent", MakeTraceSourceAccessor (&BulkSendApplication::m_txTrace), "ns3::Packet::TracedCallback")
又看到了熟悉的Accessor
,這個Accessor
爲的就是能拿到類裏的一個成員變量。所幸對於TraceSource
來講,只存在訪問Get
而不存在設置Set
,這個Accessor
相比起AttributeAccessor
來講要簡單一些。追到代碼裏看到的仍是熟悉的的套路:
template <typename T> Ptr<const TraceSourceAccessor> MakeTraceSourceAccessor (T a) { return DoMakeTraceSourceAccessor (a); } // DoMakeTraceSourceAccessor的實現 // 在函數內部定義新的類,這個類實現了TraceSourceAccessor的接口 // 由於`TraceSource`只有一種類型,這種類型就是 // 「類內部的成員變量」 // 因此能夠看到函數的簽名就只有一種 // SOURCE T::a // sigh...又是這個鬼畜的標記 template <typename T, typename SOURCE> Ptr<const TraceSourceAccessor> DoMakeTraceSourceAccessor (SOURCE T::*a) { struct Accessor : public TraceSourceAccessor { virtual bool ConnectWithoutContext (ObjectBase *obj, const CallbackBase &cb) const { T *p = dynamic_cast<T*> (obj); if (p == 0) { return false; } (p->*m_source).ConnectWithoutContext (cb); return true; } virtual bool Connect (ObjectBase *obj, std::string context, const CallbackBase &cb) const { T *p = dynamic_cast<T*> (obj); if (p == 0) { return false; } (p->*m_source).Connect (cb, context); return true; } virtual bool DisconnectWithoutContext (ObjectBase *obj, const CallbackBase &cb) const { T *p = dynamic_cast<T*> (obj); if (p == 0) { return false; } (p->*m_source).DisconnectWithoutContext (cb); return true; } virtual bool Disconnect (ObjectBase *obj, std::string context, const CallbackBase &cb) const { T *p = dynamic_cast<T*> (obj); if (p == 0) { return false; } (p->*m_source).Disconnect (cb, context); return true; } SOURCE T::*m_source; } *accessor = new Accessor (); accessor->m_source = a; return Ptr<const TraceSourceAccessor> (accessor, false); }
TraceSource
存在的理由就是要觸發其餘的邏輯的,所以要提供掛接其餘邏輯的方法,即Connect
。這裏的Accessor
只是簡單的把Connect
的請求轉發給了TraceSource
。好幾個類都有Connect
,一不當心就暈頭轉向了,如今能夠總結一下不一樣類的Connect
到底作了什麼,以及它們究竟時什麼時候被調用的。
假設如今已經有一個ns3的類MyObject
(fifth.cc),也繼承了Object,意味着它實現了GetTypeId
,擁有了Attribute
和TraceSource
的能力。它有一個能夠被trace的值m_myInt
。這個值不是簡單的類型,而是一個TracedValue<int32_t> m_myInt;
。這樣的話,只要對m_myInt
進行賦值,Trace系統就能夠工做了。給這個m_myInt
賦值的話會調用什麼?固然是operator=
了。跳到TracedValue
的對應實現看看:
TracedValue &operator = (const TracedValue &o) { TRACED_VALUE_DEBUG ("x="); Set (o.m_v); return *this; } // 關鍵就在`Set`裏了,裏面確定有觸發`Callback`的代碼 void Set (const T &v) { if (m_v != v) { m_cb (m_v, v); m_v = v; } }
果真,有個m_cb
,這就是咱們以前提到的TracedCallback
。每次給這個m_myInt
賦值,就會調用m_cb
通知這個值已經變化。能夠看到,TracedValue
自己提供了一個Connect
的方法,這意味着咱們能夠直接用m_myInt->Connect
來把本身的處理函數鏈接上去。可是實際中每每是經過myObj->ConnectWithoutContext("myInt", MakeCallback(&mycallback))
這樣的方式。
bool ObjectBase::TraceConnectWithoutContext (std::string name, const CallbackBase &cb) { NS_LOG_FUNCTION (this << name << &cb); TypeId tid = GetInstanceTypeId (); Ptr<const TraceSourceAccessor> accessor = tid.LookupTraceSourceByName (name); if (accessor == 0) { return false; } bool ok = accessor->ConnectWithoutContext (this, cb); return ok; }
Accessor
咱們以前已經講過了,是一個在GetTypeId
裏被調用並生成的一個類型,具體的accessor->ConnectWithoutContext
在上面的DoMakeTraceSourceAccessor
裏有定義,仍是經過了SOURCE T::*a
這個類型獲得了TracedValue
在類中的位置,調用了這個類型的ConnectWithoutContext
。
至於TracedCallback
,理解起來就沒什麼難度了,在這個類型的operator()
裏,註冊進來的Callback
以此執行一遍便可。
因此在運行是整個trace的過程是這樣的:
TracedValue
的operator=
TracedValue::operator=
調用了與m_myInt
相關聯的TracedCallback::operator()
TracedCallback::operator()
以此調用事先註冊好的Callback
。GetTypeId
裏AddAttribute
或者AddTraceSouce
一次就至關於給表裏加一行記錄,全部與Attribute
、TraceSource
相關的操做都會去表裏找本身須要的信息。C++真難。
這套代碼看下來,也是讓人驚歎C++真的是沒有作不到,只有想不到。那些模板和宏生成代碼的套路,基本上把能在編譯期算的都在編譯器搞定,能在初始化搞的全在初始化時完成,運行時跑得都是很難再簡化的必要的邏輯。其實網絡仿真程序也基本算一個"well-defined"的東西,有着明確的需求,又是開源項目,能夠花心思把系統設計的如此巧妙。
但願本身之後能有機會能從頭參與相似項目的開發,而不是在反覆無常的業務邏輯上消磨時光。