目次
はじめに
ns-2というネットワークシミュレータが存在します. 私の研究分野(TCP)ではデファクトスタンダードとなりつつあるシミュレータです.しかしながら, (リリース版として公開されているオリジナルのns-2のソースコードを含めて) ns-2のソースコードに関しては,コピー&ペーストのせいでコードが非常に冗長になっている箇所も 多く存在します.その結果,非常に分かりづらいコードになっており,保守やメンテナンス,拡張等が 非常にやりづらいものになっています.そこで,今回はこれらのことを考慮したコーディング指針に 関して,私が考えていることを述べます.今回は,ns-2のモジュールの中のTcpAgentへの拡張を考えた 場合について述べていますが,それ以外のモジュールに関しても有効である指針はいくつか存在する と思います.
ns-2のソースコードを見ていて,目に付くソースコードは以下の2つです.
- TcpAgent(多くのTCPの基となるクラス)に直接修正を加える.
- クラス内のメソッドを一旦全てコピーしてクラス名を変えた後,修正を加える.
これらの修正方法のどちらにも言える問題点として,作成したソースコードの保守が難しい という事が挙げられます.ns-2は,現在でもそれなりの頻度でバージョンアップが繰り返されています. オリジナルとして公開されているのソースコードにおいてすら,そのようなコピー&ペーストの コーディングが多く見受けられます.そのため,開発グループはバージョンアップを行う際に何らかの 修正を加えた場合,その度にそれら(コピーした)全てのソースコードをチェックしなければなりません.
また,各研究者が独自で実装したソースコードに関しては,状況はさらに悪化します. 各研究者が独自で実装したソースコードに対してあ,開発グループがサポートを行うことはありません. 加えて,そのソースコードを書いた人は既に(卒業するなどして)もういない,という事もしばしば起こります. その結果,多くの研究者は既にバージョンアップがなされているにも関わらず,そのソースコードを実行させる ためだけに,古いバージョンのns-2を使い続けてしまいます.ns-2では,バージョンアップされる度に少なからずの バグフィックスも行われているので,古いバージョンを使い続けることは,シミュレーション結果にバグの影響が 及ぶ恐れという観点から見てもあまり良くありません.
上記の問題に加えて,TcpAgentに直接修正を加える場合においては,さらに別の問題も発生します. それは,基底クラス(TcpAgent)の肥大化です.現在の最新バージョン(ver. 2.29)においても,TcpAgentには64 のメソッドと141ものメンバ変数が存在しており,既にクラスの全体を把握する事はかなり困難な状況です. クラス(ベースとなるようなクラスの場合は特に)は完全かつ最小の原則で設計を行っていくべきですが, TcpAgentに関しては,最小とはかけ離れた方向へ進んでいます.
そこで,今回は上記のような状況を改善するために,保守性を向上させるためのコーディング指針に関して検討します. 具体的には,ベースクラス(TcpAgent)で用意されているヘルパメソッドを有効に利用することにより,新しいTcpAgent を作成する際の修正量をできるだけ少なくし,ns-2がバージョンアップされた際にも容易に新しいバージョンに移行できる ことを目指します.
TcpAgent主要メソッドの概要
1章で述べたコーディング指針について検討する前に,この章ではTcpAgentの主要メソッドについてその概要を述べます. TcpAgentには,大きく分けて次の3つの(重要な)機能が存在します.
- 別のノードからパケットを受信するためのrecv()メソッド.
- 別のノードへパケットを送信するためのsend_much()およびoutput()メソッド.
- TCPの持つ各タイマがタイムアウトした際に呼ばれるtimeout()メソッド.
これらの3つの主要メソッドについて,次節以降でその概要を述べます.
recv()メソッド
recv()メソッドは,TcpAgentにおいて最も重要なメソッドの一つです.TCPにおいては,ACKパケットの受信を契機として, RTTや輻輳ウィンドウサイズの更新が行われるため,頻繁にコピー&ペーストされて修正されるメソッドでもあります. recv()メソッドの概要は以下のようになります.
void TcpAgent::recv(Packet* pkt, Handler*)
{
.....
recv_helper(pkt);
if (tcph->seqno() > last_ack_) {
recv_newack_helper(pkt);
}
else if (tpph->seqno() == last_ack_) {
.....
if (++dupacks_ == numdupacks_ && !noFastRetrans_) {
dupack_action();
}
else if (duacks_ < numdupacks_ && singleup_) {
send_one();
}
}
.....
send_much(0, 0, maxburst_);
}
ACKパケットを受信すると,そのパケットが正常なACKパケットかそうではないACKパケット(重複ACK)かを判断し, それぞれの場合において必要な処理を行います.また,RTTや輻輳ウィンドウサイズもこのrecv()メソッド (からコールされるメソッド)によって更新されます.その後,send_much()メソッドをコールし,現在の状態 (輻輳ウィンドウサイズなど)に基づいてデータパケットの送信を行います.
send_much(),output()メソッド
send_much()およびoutput()メソッドは,通信相手に対してパケットを送信するメソッドです. これらのメソッドの概要は以下のようになります.
void TcpAgent::send_much(int force, int reason, int maxburst)
{
send_idle_helper();
.....
while (t_seqno_ <= highest_ack_ + win &
& t_seqno_ < curseq_) {
if (overhead == 0 || force || qs_approved_) {
output(t_seqno_, reason);
npackets++;
.....
}
.....
if (maxburst && npackets == maxburst) break;
}
send_helper(maxburst);
}
void TcpAgent::output(int seqno, int reason)
{
Packet* p = allocpkt();
.....
output_helper(p);
++ndatapack_;
ndatabytes_ += databytes;
send(p, 0);
.....
}
TCPは輻輳ウィンドウサイズの情報を基にして,現在,自分が送信することのできる数のパケットを一度に送信します. send_much()は,その数だけoutput()メソッドをコールする役割を持つメソッドです.そして,output()メソッド内で, 新しいパケットを作成し,必要な情報をTCPヘッダに付加した後,実際にパケットを送信します(send()メソッド).
timeout()メソッド
TCPは,スムーズなデータ転送を実現するために,いくつかのタイマを持っています(代表的なものは再送タイマ). timeout()メソッドは,これらのタイマがタイムアウトした時にコールされるメソッドです.以下に,概要を述べます.
void TcpAgent::timeout(int tno)
{
if (tno == TCP_TIMER_RTX) {
.....
send_much(0, TCP_REASON_TIMEOUT, maxburst);
}
else timeout_nonrtx(tno);
}
timeout()メソッドは,tnoによってタイマの種類を見分けます.tcp.hでは,現在のところ以下の6種類が定義されています.
#define TCP_TIMER_RTX 0 #define TCP_TIMER_DELSND 1 #define TCP_TIMER_BURSTSND 2 #define TCP_TIMER_DELACK 3 #define TCP_TIMER_Q 4 #define TCP_TIMER_RESET 5
この中で,TCP_TIMER_RTXが再送タイマに当たります.TcpAgentでは,timeout()メソッド内では, 再送タイマのタイムアウト時の処理のみを記述し,残りのタイマに関してはtimeout_nonrtx()メソッドに記述する というポリシーのようです.
ヘルパメソッドの活用
前章において,メソッドの概要を記述した際に,いくつかの箇所を強調表示しました.具体的には,以下に示す8箇所です.
- recv_helper(pkt);
- recv_newack_helper(pkt);
- dupack_action();
- send_one();
- send_idle_helper();
- send_helper(maxburst);
- output_helper(p);
- timeout_nonrtx(tno);
これらのメソッドは,ヘルパメソッドと呼ばれています(厳密には,いくつかのメソッドはヘルパメソッドではないが, ここでは同様に扱う).TcpAgentには,拡張性を考えて,コード上の要所に上記のようなヘルパメソッドが埋め込まれて います.TcpAgentにおいては,これらのほとんどのメソッドに関しては,何も記述がされていない状態になっています. そこで,独自のTCPを作成する際にはこれらのメソッドをオーバーライドし,その中に必要な記述を加えることにより, 既存のTcpAgentへの依存度を最小限に抑えられます.そのため(上記にtimeout()メソッドを加えた), 9種類のメソッドを有効に利用することにより,バージョンアップの際にも容易に移行が可能となるソースコードを 記述することが可能となります.以下に,それぞれのヘルパメソッドのコールされるタイミングや注意点について 記述します.
パケット受信に関するヘルパメソッド
パケット受信に関するヘルパメソッドは,recv_helper(),recv_newack_helper(),dupack_action(),send_one() の4種類です(厳密には,dupack_action(),send_one()はヘルパメソッドではない). これらのメソッドのコールされるタイミングはほとんど同じです.
受信されると,まず始めにrecv_helper()メソッドがコールされます.その後,受信したACKパケットのシーケンス番号と ラストACK(もっとも直近に受信した新規ACKパケットのシーケンス番号)を比較し,新規ACKであれば, recv_newack_helper()がコールされます.また,重複ACKであった場合,1,2回の重複であればsend_one()がコールされ, 3回の重複であればdupack_action()がコールされます.どのヘルパメソッドにおいても,ヘルパメソッドがコールされた時点 では,TCP内部で保持されているパラメータは前回ACKパケットを受信したときのままです.
recv_newack_helper(),dupack_action(),send_one()メソッドにおいては,TcpAgentにおいても必要な処理が記述されて います.主な処理として,前者の2つのメソッドでは,輻輳ウィンドウサイズの更新(opencwnd(),slowdown()), 最後のメソッドでは“1個だけ”新たなデータパケットを送信するというものがあります.そのため,これらのメソッドを オーバーライドした場合,最後にスーパークラスの同名メソッドをコールする(e.g., TcpAgent::recv_newack_helper();) 必要があります.スーパークラスで行われている処理を含めて全ての処理を記述する方法もありますが, 移行を容易にするために,スーパークラスで行われていることはできるだけそのままスーパークラスに行わせる方が良い と思います.
新しいTCPの提案の際に修正される多くは,輻輳ウィンドウの制御方式ですので,上記に挙げたヘルパメソッドを把握する ことは,移行を容易にする観点から見ても,他の問題点をできるだけ切り離す観点から見ても非常に重要と考えられます (opencwnd(),slowdown()メソッドに関しては,後述する).
パケット送信に関するヘルパメソッド
パケット送信に関するヘルパメソッドは,send_much()メソッドからコールされるsend_idle_helper(),send_helper() メソッド,output()メソッドからコールされるoutput_helper()メソッドです.
send_idle_helper(),send_helper()メソッドはそれぞれ,send_much()がコールされた直後, 終了する直前にコールされます.それぞれのタイミングにおいて何らかの処理を加える必要がある場合には, 該当メソッドをオーバーライドして変更します.
send_helper()メソッドには,引数としてmaxburstが渡されます.通常,TCPは輻輳ウィンドウサイズの値を基に 一度に送信するパケット数を決定しますが,TcpAgentではそれとは別に,一度に最大に送ることのできる数 (メンバ変数maxburst_.デフォルト値は0)が指定されています.maxburstは,今回一度に送信したパケット数が 記憶されているので,その値を基に次回,一度に送信するパケット数を決定する場合などは,send_helper()メソッドで 更新を行うようです.
一方,output_helper()メソッドは,TCPヘッダに必要な情報を付加した後,実際にパケットを送信する直前に コールされます.そのため,このヘルパメソッドはTCPヘッダのオプション領域に何らかの情報を付加したい場合などに 利用します.
タイマのタイムアウトに関する(ヘルパ)メソッド
パケットの送受信時とは異なり,タイマのタイムアウト時にはヘルパメソッドは定義されていません.そのため, タイマのタイムアウト時に何らかの処理を行う際には,timeout(),およびtimeout_nonrtx()メソッドを直接オーバーライド して必要な処理を記述することになります.この際も,加える記述はできるだけ拡張した機能のみに抑え, 残りの処理はスーパークラスの同名メソッドをコールする(e.g., TcpAgent::timeout();)形を取るのが良いと思います.
輻輳ウィンドウサイズ更新のためのメソッド
この章では,最後に,輻輳ウィンドウサイズを更新するためのメソッドopencwnd(),slowdown()について触れておきます. 先にも述べたように新規TCPを提案する際,その多くは輻輳ウィンドウサイズの制御方式に終始します.そのため, そのようなTCPを実装する際には,opencwnd()(輻輳ウィンドウサイズを増加させるためのメソッド)およびslowdown() (輻輳ウィンドウサイズを減少させるためのメソッド)をオーバーライドして修正する方法が考えられます.
しかし,これには一つ問題があります.以下に,TcpAgentにおける上記のメソッドの宣言を示します.
- void opencwnd();
- void slowdown(int how);
これを見ると,輻輳ウィンドウサイズを更新するためのメソッドは仮想化されていないことが分かります.このため, これらのメソッドをオーバーライドして修正した場合,期待する動作が得られない可能性があります(現在, TcpAgent外部からコールされているのは,recv()およびtimeout()メソッドなので,その可能性は低いが).
また,仮想化されていないということは,TcpAgentの設計者がそれらのメソッドをオーバーライドされることを想定して いないことを意味します.
virtual を使うと、基底クラスのポインタから派生クラスの関数が呼び出せるようになります。ということは、 基底クラスのポインタから派生クラスの関数を呼び出したくない時は virtual を指定してはいけない、 ということになります。
・・・(中略)・・・
オーバーライドによって振舞いを変えられてはいけない場合は結構あります。
実際,wnd_option_というパラメータ(このパラメータ値によって輻輳ウィンドウサイズの制御方法を変更する) を用意していることから考えても,その是非は別として,当初の設計では輻輳ウィンドウサイズの制御は, TcpAgentに集めることを想定していたと考えられます.その意味でも,これらのメソッドをオーバーライドして 修正することはあまり好ましくないと思います.
したがって,輻輳ウィンドウサイズの制御方式を修正するには,以下の2つの方法が考えられます.
- recv_helper()など各種ヘルパメソッドに輻輳ウィンドウサイズの制御も組み込む.
- 新しいwnd_option_を定義して,TcpAgentに直接組み込む.
既存のソースコードへの依存度を最小にするという観点から見ると前者の方が良いと考えられますが, 実際には時と場合によって柔軟に対応する必要がありそうです.
クラステンプレートの活用
この章では,クラステンプレートの活用方法について述べます.TCPには,既に多くの種類(e.g., Reno, NewReno, SACK, ...)が存在しています.ns-2においては,それらのTCPは別々に実装されています(e.g., RenoTcpAgent, NewRenoTcpAgent, Sack1TcpAgent, ...).そのため,場合によっては継承するクラスだけ違うほとんど同じクラスが 複数必要となる場合があります.例えば,SACKオプションがあります.新しいTCPを作成したとき, しばしばSACKオプションが有効な場合と無効な場合の2種類のTCPが必要となります.このとき, SACKオプションの有効なTCPは,Sack1TcpAgentという一つのクラスとして存在しています.そのため, SACKオプションが有効な場合と無効な場合で2種類のTCPクラスを新たに作成しなければなりません.これも, コピー&ペーストによる複製が増える原因の一つとなります.
そこで,テンプレートクラスを利用して前述したような不必要な複製をできるだけ減らす方法を検討します. 新規TCPを作成する際には,最初に以下のような宣言にしておきます.
template <BaseTcp>
class XXXTcpAgent : public virtual BaseTcp {
...
};
そして,以下のように必要な分だけtypedefしておくことで,不必要な複製を防ぐことができます.
typedef XXXTcpAgent<RenoTcpAgent> XXXRenoTcpAgent; typedef XXXTcpAgent<Sack1TcpAgent> XXXSackTcpAgent;
尚,注意する点として,テンプレートクラスを使用した場合,継承元(BaseTcp)のメンバ変数やオーバーライドしてない メソッドに関しては,this->xxxのようにしてアクセスします.また,このように作成したTCPクラス内で, bindした変数に関しては,typedefしたクラス(に関連付けられているtclクラス)の分だけ,ns-default.tcl に記述する必要があります.
スケルトンクラス
この章では,これまでに述べたの指針を満たしたTCPクラスのスケルトンを示します.
// ヘッダファイル
template <class BaseTcp>
class ExTcpAgent : public virtual BaseTcp {
public:
ExTcpAgent();
virtual ~ExTcpAgent();
virtual void timeout(int tno);
virtual void timeout_nonrtx(int tno);
protected:
virtual void send_idle_helper();
virtual void send_helper(int maxburst);
virtual void output_helper(Packet* pkt);
virtual void recv_helper(Packet* pkt);
virtual void recv_newack_helper(Packet* pkt);
virtual void dupack_action();
virtual void send_one();
// 他,tcp_bind_init_all()など必要なメソッドを追加する
private:
typedef BaseTcp super;
};
// 必要なものだけ,typedefしておく
typedef ExTcpAgent<RenoTcpAgent> ExRenoTcpAgent;
typedef ExTcpAgent<Sack1TcpAgent> ExSackTcpAgent;
// ソースファイル
template <BaseTcp>
void ExTcpAgent<BaseTcp>::send_idle_helper()
{
.....
super::send_idle_helper();
}
template <BaseTcp>
void ExTcpAgent<BaseTcp>::send_helper(int maxburst)
{
.....
super::send_helper(maxburst);
}
.....
おわりに
今回は,TCPクラスのns-2コードを記述する際の指針について述べてきました.コピー&ペーストによるコーディングは, 単純な見にくさの観点以外においても多くの問題点を含んでいます.そのため,できるだけそういったコーディングは控え, 最小かつ完全の原則でクラス設計およびコーディングを行っていくことが重要であると考えられます.
また,せっかくC++で記述されてあるのだから,C++が持つ機能(テンプレートなど)も有効に利用することで, さらにバージョン間の移行が容易なコードが書けるのでは,と思います.