1. NS 的整体的实现
固定网络的仿真是通过下面三层合作来实现的。
Application这个层是实现数据流的层次。 Agent 这个层是实现所有各层协议的的层次。 Node 这个部分由多个分类器( Classifier )实现了所有接收数据包进行判断是否进行转发或接收到 Agent 的部分。 Link 实现了队列、时延、 Agent 、记录 Trace 等一系列的仿真问题。
2.
TclObject |
Handler |
ParentNode |
Process |
NsObject |
Node |
Application |
Connector |
Classifier |
Delay |
Queue |
Agent |
Trace |
AddressClassifier |
图表2 NS 内部类的继承关系图 |
此图为NS 内部类的继承关系,可以看出一些类是由那些类继承来的,这样相同的属性调用的函数就可以很方便的找到出处。
3. NS 中函数调用的分层
在用 gdb 跟踪 NS发送一个 cbr 数据包的过程可以看到一下顺序:
CBR_Traffic::start
TrafficGenerator::timeout
Application::send
UdpAgent::sendmsg
Classifier::recv
Connect::recv
Connect::send
Trace::recv 写一条数据包加入队列的记录
Connector::send
Queue::recv
DropTail::enque
DequeTrace::recv 写一条数据包弹出队列的记录
Connector::send
LinkDelay::recv 插入事件到 scheduler
Scheduler::dispatch
Scheduler::run
从上面的顺序可以看出,数据包在发送以后先通过应用层( Application::send)进行发送;然后通过 Agent 层( UdpAgent::sendmsg), UdpAgent 是在初始化 Agent 的时候确定的。 UdpAgent 还有一个作用是生成相应的数据包;然后进入 Node 部分的分类器 Classifier ( Classifier::recv),通过 find ()函数返回下一跳地址。这个函数是通过读取 Packet 的内容得到下一跳地址的,返回给 recv 函数后调用 node->recv()进入 connector ;经过 connector::recv和 connector::send后确定数据包可发,进入 Trace::recv,记录这个数据包加入队列的记录;之后通过connector::send,进入 Queue::recv函数,将数据包正式加入发送队列,再根据已经设定好的方法确定加入队列是否成功,是否要被丢弃;再调用DequeTrace::recv记录数据包弹出队列的记录;再通过 connector::send进入 LinkDelay::recv,先判断目的节点是否可达,根据不同的结果将事件写入scheduler ,等待按序执行。
上述的过程只是一个数据包从生成到发送出去的过程,因为 NS 是一个根据一个一个离散事件调度执行的,后面的过程用 gdb 跟不进去。但可以看出,数据包是发送给下一跳节点,可知数据包是通过每个中间节点的。
4. NS 中主要函数的分析
4.1. CBR 数据源
开始从下面函数进入CBR 数据源发送:
void CBR_Traffic::start()
{
init(); // 初始化
running_ = 1;
timeout(); // 进入发送数据的循环
}
voidTrafficGenerator::timeout()
{
if (! running_) // 判断是否要发送数据
return;
send(size_); // 发送一个设定好大小的数据包
nextPkttime_ =next_interval(size_);
if (nextPkttime_ > 0)
timer_.resched(nextPkttime_);
else
running_ = 0;
}
4.2. 中间几个函数只是体现分层
void Application::send(intnbytes)
{
agent_->sendmsg(nbytes);
}
void Agent::sendmsg(int , AppData* , const char*)
{
fprintf(stderr,
"Agent::sendmsg(int, AppData*, const char*) notimplemented/n");
abort();
}
上述两个函数其实并没有什么实质的操作,只是这样可以看出其经过了应用层和Agent 层。
4.3. Classifier的函数
void Classifier::recv(Packet* p,Handler*h)
{
NsObject* node = find(p); // 查找目的节点
if (node == NULL) { // 只要返回了目的节点就调用节点的 recv 函数
Packet::free(p);
return;
}
node->recv(p,h);
}
NsObject* Classifier::find(Packet*p)
{
NsObject* node = NULL;
int cl = classify(p); // 根据发送的 packet 的记录找到 slot
if (cl < 0 || cl >= nslot_ || (node = slot_[cl]) == 0) {// 根据 slot 得到下一跳 node
if (default_target_)
return default_target_;
Tcl::instance().evalf("%s no-slot %ld", name(),cl);
if (cl == TWICE) {
cl = classify(p);
if (cl < 0 || cl >= nslot_ || (node = slot_[cl]) ==0)
return (NULL);
}
}
return (node); //返回给 classifier::recv得到的 node 的值
}
这个地址分类器就是根据数据包的内容,通过偏移查找接收的节点,然后调用接收节点的recv 函数。而 find 函数是根据数据包的内容得到 slot 的值从而查询出谁是接收方的 node 。
4.4. Connector 的函数
void Connector::recv(Packet* p, Handler*h)
{
send(p, h);
}
inline void send(Packet* p, Handler* h) {target_->recv(p, h); }
connector 的 recv 和 send 函数是一个接口。这个函数中最重要的是 target_ 这个值,这个值的不同会不同的调用 Trace::recv、 Queue::recv、 LinkDelay::recv等等,但是这个值在那确定还没有看出来。
4.5. Queue 的函数
void Queue::recv(Packet* p,Handler*)
{
double now =Scheduler::instance().clock();
enque(p); // 根据规定的规则加入队列
if (!blocked_) {
p = deque();
if (p != 0) {
utilUpdate(last_change_, now,blocked_);
last_change_ = now;
blocked_ = 1;
target_->recv(p, &qh_); // 调用 dequetrace
}
}
}
Queue::recv这个函数调用 DropTail 规则将数据包加入队列,然后判断是否堵塞,如果没有则发送一个数据包,之前判断是认定这个包是否要被发送出去。这里也使用了target_->recv(),这里调用的是 DequeTrace::recv函数,将记录一个数据包出队列的记录。
4.6. LinkDelay 的函数
void LinkDelay::recv(Packet* p, Handler*h)
{
double txt = txtime(p);
Scheduler& s =Scheduler::instance();
if (dynamic_) { //这个是动态链路的标志,判断这个值确定链路是否为动态链
Event* e = (Event*)p;
e->time_= txt + delay_;
itq_->enque(p); //用一个队列来储存数据包
s.schedule(this, p, txt + delay_);
} else if (avoidReordering_) {
// 预防重新安排带宽或时延改变
double now_ =Scheduler::instance().clock();
if (txt + delay_ < latest_time_ - now_ && latest_time_> 0) {
latest_time_+=txt;
s.schedule(target_, p, latest_time_ - now_ );// 在 schedule 里面加入事件
} else {
latest_time_ = now_ + txt + delay_;
s.schedule(target_, p, txt + delay_); // 在 schedule 里面加入事件
}
} else {
s.schedule(target_, p, txt + delay_); // 在 schedule 里面加入事件
}
s.schedule(h, &intr_, txt); // 在 schedule 里面加入事件
}
这个函数非常重要,这个是数据包最后离开这个节点的出口,由这个函数写一个事件加入schedule ,在一定的时间后调用。