出处:http://blog.csdn.net/kgn28/article/details/5375662
使用IE不会有显示的问题Firefox有的代码显示不出来;
这篇文章适合初学者,关于初学者应该参考的文档:NS by Example、NS2 Beginners Page都有很多实例可以参考。
本文通过实现一个简单的传输协议来说明如何在 ns2 中实现网络协议,当然,这个协议非常简单,但是在 ns2中实现协议(不是修改)的流程大体就是这个样子的了。我们称这个简单的协议做: simple_trans 协议,我们一步一步来,把 simple_trans 这个协议慢慢做的复杂。首先我想要明确一个概念:什么是在 ns2 中实现网络协议,不把这个问题搞明白我们都不知道自己在做什么。网路协议顾名思义网络上运行的协议,网络是由关系(无论什么关系)组成的,在这个网络上运行的规则(无论是优化网络数据传输还是共享网络信息)就叫做协议,所以我觉得把协议理解为强逻辑的规则是没有问题的。我们实现一个网络协议的前提是这个协议被设计出来,所以我们先要想好我们所要实现的协议是要用来做什么事情的;回到 ns2 , ns2 帮我们实现好了一个框架,这个框架给我们提供了数据包初始化,链路连接,数据包传递路由等功能,也就是说我们只要搭建好我们的逻辑就可以完成协议的模拟了,在 ns2 中我们通过对数据包类型、发送数据包逻辑等等进行控制。这就好比于 ns2 给我们提供了一个铁路网,火车需要的电也有了,火车不够了还可以生产,我们在 ns2 中实现协议就是要对火车进行调度,何时到站,到站后如何运行等等就是协议的内容。
下面就从我们的 simple_trans开始 说起,在这个协议里,首先我们要实现的任务非常简单,简单到什么程度了呢,简单到这个协议就是 a 节点对 b 节点说一句话:“ hi!, I’m a ”。不要笑,这也是一个协议。要在 ns2 上完成这个任务,我们首先要给 simple_trans 这个协议起个名字使得 ns2 可以发出这个协议的数据包并且认得这个协议发出的数据包,现在开始就是第一步了。
1, 在 NS_HOME/common/packet.h 的 enum packet中 加入协议数据包名称 PT_SIMPLE_TRANS_PACKET (必须的,注意不要加错地方,最好加在倒数第二的地方),在 class p_info 中加入name_[PT_SIMPLE_TRANS_PACKET] = "simple_trans_packet" (非必须的)。 Packet.cc 就不要动了。
2, 为了我们协议的独立性、好看性,我们在 NS_HOME 根目录下创建一个文件夹,我就叫他 kgn ,好在 kgn目录(也就是 NS_HOME/kgn )目录下给协议的主角: simple_trans.h&simple_trans.cc 。两个空文件没什么用,下面我们添加协议内容。
3, simple_trans.h 内容:
- #ifndef ns_simple_trans_h
- #define ns_simple_trans_h
- #include "agent.h"
- #include "tclcl.h"
- #include "packet.h"
- #include "address.h"
- #include "ip.h"
- #define PROTOCOL_DEFAULT_PORT 1023
- #define PROTOCOL_INIT_SYN 1
首先我们引用一些需要用到的头文件,然后我们定义了两个宏,第一个是我们 simple_trans 协议默认传输的端口(这方面如果有所疑问请参考这里 ),第二个是我们仅有的一条指令:同步指令(类似于 TCP 协议中三次握手的第一步,事实上我们的这个协议最终就是要实现一个简化版的三步握手)。继续看:
- struct hdr_simple_trans {
- int type;
- static int offset_;
- inline static int& offset() {
- return offset_;
- }
- inline static hdr_simple_trans * access(const Packet * p) {
- return (hdr_simple_trans*) p->access(offset_);
- }
- };
这些可以当做领导讲话的开头部分内容。就是定义一个我们协议的头所包括的内容,只有 type 这个是我定义的,其他的内容是 ns2 系统需要的。再继续:
- class simple_trans_agent : public Agent {
- public :
- simple_trans_agent();
- virtual void recv(Packet *, Handler *);
- void send_simple_msg(int type, int target);
- int get_target(){ return simple_target; }
- protected:
- int simple_target;
- int simple_port;
- int command(int argc, const char*const*argv);
- };
这里就是我们定义的负责“调度火车”的功能的类了。继承的是 agent 类,在 ns2 中,这个 agent 不可小觑,他是我们可以产生数据包、发送数据包、接收数据包的地方,包括的 target 变量就是数据包发送给的下一个目标。 recv函数会在仿真的过程中“自动”的收到网络上传输的数据包(更深层次的是经过了地址和端口过滤器);send_simple_msg 函数用来执行创建并发送数据的功能; get_target 就不用解释了(接口保护)。接下来是我们在协议制定过程中经常会用到的 timer 的定义, timer 顾名思义是一个定时器(闹钟)在到时时候会调用一个 expire(超时)函数,这个被执行的超时函数的内容就是我们所感兴趣的,因为通过 timer 我们可以实现很多逻辑。
- class SYNTimer : public TimerHandler {
- public:
- SYNTimer(simple_trans_agent* t) : TimerHandler(), t_(t) {
- }
- inline virtual void expire(Event *);
- protected:
- simple_trans_agent* t_;
- };
我们只要实现 expire 函数即可, timer 的初始和使用见 simpe_trans.cc 文件:
- SYNTimer *syn_timer = new SYNTimer(this);
- syn_timer->resched(1.00);
resched 用来给“闹钟上弦”。
- void SYNTimer::expire(Event *){
- t_->send_simple_msg(PROTOCOL_INIT_SYN, t_->get_target());
- this->resched(1.00);
- }
expire 可以实现我们的“理想”了,譬如,我们到时了就发送我们的 SYN 信息给我们的目标节点(目标节点通过tcl 文件定义,下文中我们会见到)。
4, simple_trans.cc 内容:
- int hdr_simple_trans::offset_;
- static class simple_transHeaderClass : public PacketHeaderClass {
- public:
- simple_transHeaderClass() : PacketHeaderClass("PacketHeader/simple_trans",sizeof(hdr_simple_trans)) {
- bind_offset(&hdr_simple_trans::offset_);
- }
- } class_simple_transhdr;
- static class simple_transClass : public TclClass {
- public:
- simple_transClass() : TclClass("Agent/simple_trans") {}
- TclObject* create(int, const char*const*) {
- return (new simple_trans_agent());
- }
- } class_simple_trans;
- simple_trans_agent::simple_trans_agent() : Agent(PT_SIMPLE_TRANS_PACKET),
- simple_target(-1), simple_port(PROTOCOL_DEFAULT_PORT) {
- bind("simple_target_", &simple_target);
- bind("simple_port_", &simple_port);
- }
这个又是八股文,前面几个类照葫芦画瓢即可,如果想要理解是什么意思可以参考我的文章 ,最后一个我们 bind了几个变量,这几个变量通过绑定就可意思让我们通过 tcl 脚本方便的改变他们的值了(不需要重新编译 c++ 文件)。
5, 在这个文件中我们主要注意这么几点:
a) 在 send_simple_msg 中数据包的生成 Packet* pkt = allocpkt() ;
b) 数据包的访问: hdr_ip *iph = hdr_ip::access(pkt) ;
c) 数据包 ip 地址和端口号的设定(从这里我们可以看出实现的是一个应用层协议);
d) 发送数据包 send( pkt, 0 ) ,我们可以不用去管 0 是什么意思;
e) Command 命令中不要忘记 return (TCL_OK) 这句话,否则会出错的。
f) 在 recv 函数中实现我们的简单逻辑:显示出我们收到了来自对方的一个 simple_trans 的数据包。
6, 看我们这两个宏命令:
- #define NOW Scheduler::instance().clock()
- #define MYNODE Address::instance().get_nodeaddr(addr())
这两个命令给我们编程提供帮助,分别显示系统时间和得到当前节点的地址,也许以后我们会用得着。
7, 在 tcl 脚本中我们需要使用我们的 simple_trans 协议:
- set sT1 [new Agent/simple_trans]
- $sT1 set-target [AddrParams addr2id [$n1 node-addr]]
- $n0 attach $sT1 1023
- set sT2 [new Agent/simple_trans]
- $n1 attach $sT2 1023
- ... ...
- $ns at 1.0 "$sT1 begin"
在 tcl 中 new 一个对象,比如 sT1 之后我们要将其 attach 到所属的节点上,注意最后一个 1023 ,这是我们 attach到节点上的给我们 simple_trans 协议分配的端口(深层次的意思是端口分类器会把目的端口是 1023 的数据包分给sT1 )。 begin 方法是在 command 中实现的,回过头到 simple_trans.cc 中可以看到他的意思,我们可以好好理解一下 command 中函数和 tcl 中的使用关系。
8, 最后一步,就是编译我们整个协议将其键入到 ns 中了,编译前我们要修改 makefile 文件,由于我们是在NS_HOME/kgn 目录中所以, makefile 需要修改的有两个地方:在 INCLUDES = 中加入 -I./kgn ,加入这个的好处就是我们在其他目录使用 simple_trans.h 的时候不用将 kgn 次级目录包含进去;在 OBJ_CC = 中加入kgn/simple_trans.o / 。好了大功告成,下面回到 NS_HOME 目录下 make 一下,如果成功,我们执行一下我们的 tcl 脚本,看看是不是真的可以运行了呢。
小结:到了这里我们已经添加了一个简单的协议了,好了,有的人会说了,这么简单的协议有什么用呢?那好,我们想一想我们有什么可以改进的吗?以上的协议我们叫做 simpe_trans 协议 0.1 版,那么我们看看 0.2 版给我带来了什么新的变化。
|
首先要做的就是协议的复杂化,我们将协议改为三次握手过程如图所示:
这个过程对应以下代码(修改simple_trans.h):
- #define PROTOCOL_INIT_SYN 1
- #define PROTOCOL_INIT_SYN_ACK 2
- #define PROTOCOL_INIT_ACK 3
- #define INTERVAL 0.3
- class simple_trans_agent;
- enum simple_state{
- CLOSED,
- SYN_SENT,
- SYN_RCVD,
- ESTABLISHED
- };
其中 C->CLOSED , SS->SYN_SENT , SR->SYN_RCVD , E->ESTABLISHED 为节点可能处于的状态在发送或接受 SYN 和发送 SYN-ACK 接受 ACK 后的变化,而两个 timer 的作用就是使得没有正确到底目的地的数据包可以被重新发送,当然这些 timer 需要在适当的时机取消比如: ack_timer->cancel() ,取消 timer 使用 cancel 函数即可。具体代码实现参考 0.2 版本的代码。那么现在我们重新 make 编译我们的程序,我们会发现两个节点可以通过三次握手建立起来一个简单的链接了,可以说我们在有这个简单的可以建立连接的程序之后我们马上想到是不是还可以发送数据呢,在 ns2 中,数据的发送,我们常见的如 CBR 或者 FTP ,都可以发送数据但是他们之间有很大的不同 CBR 使用的是 trafficgenerater ,而 FTP 可以看成是一个带发送数据包的 agent ,现在为了让我们的simple_trans 协议可以在建立起连接以后发送数据,我们就有了两种选择,是继承 trafficgenerater 成为数据发送源呢,还是类似 FTP 使用 agent 发送数据,考虑到我们协议的简洁易懂性,我们直接使用一个 timer ,在每次 timer到时的时候都利用 simple_trans 的 send 函数发送一个具有 PROTOCOL_DATA 类型(标识是一个数据)的包给通信对端( CN )。在 sendmsg 函数中的实现如下:
- hdr_rtp* rh = hdr_rtp::access(p);
- hdr_simple_trans *shdr = hdr_simple_trans::access(p);
- hdr_ip* ih = hdr_ip::access(p);
- double local_time = NOW;
- hdr_cmn::access(p)->size() = size;
- hdr_cmn::access(p)->timestamp() =
- (u_int32_t) (SAMPLERATE * local_time);
- rh->seqno() = seqno++;
- ih->daddr() = simple_target;
- ih->dport() = simple_port;
- ih->saddr() = MYNODE;
- shdr->type = PROTOCOL_DATA;
- target_->recv(p);
这里面我们还可以通过 RTP 协议给每一个包设置序列号,当然也可以在 hdr_simple_trans 中添加一个 seq 的属性。当然我们的协议升级到 0.3 版本后的变化并不只是有这些而已。我们还将 simple_trans 协议的数据包的大小以及发送频率设置成可变的等,具体可以参考 0.3 的代码。
小结:通过以上的设计,我们初步有了一个可以建立连接并发送数据的协议,什么?像是 SIP 协议,没错我们也可以将我们的程序叫做一个简单的会话发起协议,当然你可以实现的更加复杂。至此,我们在 ns2 中添加一个基本网络协议的事情已经完成了,我们注意到:不同的协议使用节点上的不同的端口,这样的协议是不能够影响到诸如路由、无线链路等协议的结果的,所以并不是所有的 ns2 中的协议都可以这么添加,我们还可以修改节点数据结构等方法添加我们自己的一些修改进 ns2 达到仿真的目的,所以这篇文章的目的还是介绍如何在 ns2 中实现协议的基础,我们要根据我们自己的仿真需要来设计我们的程序。通过以上的介绍我们应该掌握的是在 ns2 中发送数据的方法、 ns2 中 timer 的使用方法等等技巧。下面我介绍一个比较有意思的利用我们的 simple_trans 做的协议修改实验:添加无线节点丢包模型,在这里主要参考的是柯志亨老师的实现方法,但是在丢包方面我这里做的对原有协议破坏性更多(更不合理吧),我们将演示当两个无线节点距离增大的时候会丢失数据包并且我们的 ACKTimer 以及SYNTimer 的作用。好,下面就是如何修改的过程了:
在 NS_HOME/mac 目录下的 wireless-phy.cc 的 380 行左右,我们添加如下代码:
- //error model.
- hdr_cmn *hdr_err = HDR_CMN(p);
- hdr_simple_trans *sh = hdr_simple_trans::access(p);
- double ratio = Pr/RXThresh_;
- double std = error_modle_lf(ratio);
- //printf("wireless-phy model receive packet ratio=%lf std=%lf/n",ratio,std);
- if (hdr_err->ptype() == PT_SIMPLE_TRANS_PACKET){
- if (!sh->error){
- double tmp=((double)rand())/RAND_MAX;
- if (tmp>std){
- sh->error = false;
- }else{
- sh->error = true;
- //printf("wireless-phy error model set the packet error/n");
- }
- }
- }
- //end of error model.
我们修改的是 WirelessPhy::sendUp(Packet *p) 函数,在发送数据包之前我们检查数据包中 simple_trans 协议的数据包,并将该数据包中在 hdr_simple_trans 中定义好的 error 属性置为 true (说明这个数据包出错),实现数据包出错分布的函数 error_modle_lf ,这是一个拉格朗日差值函数的实现:
- double error_modle_lf(double ratio){
- if(ratio >1.5)return 0;
- double x[6] = {1,1.1,1.2,1.3,1.4,1.5};
- double y[6] = {1,0.5,0.3,0.1,0.02,0};
- double res = 0;
- for(int i = 0; i < 6; i++){
- double temp = 1;
- double temp1 = 1;
- for(int j = 0; j < 6; j++){
- if(i == j)continue;
- temp *= (x[i] - x[j]);
- temp1 *= (ratio - x[j]);
- }
- res += (temp1 / temp) * y[i];
- }
- return res;
- }
显然,我们设计的是无线节点离基站越远对包个数越多。
1, 在 simple_trans.cc 中添加 if( shdr->error )return ,这样错误的包我们就“装作”收不到了
2, 这里补充说明,柯志亨老师的错误模型实现是基于无线层的,出错了就真的不发或者重发,而我的实现可以说是假的,还会造成无线网络的吞吐,但是还是可以演示无线丢包情况的,具体结果可以编译我称作 0.4 版本的程序运行。可以将包的序列号画出来,这样会更形象的展现丢包情况。
3, 我们将包序列号、包收到时间等等信息都通过 printf 函数打印出来,这样我们就可以不用去考虑如何通过 trace文件来分析得到数据,这种方法有的时候更加有效,我们不必去了解 trace 机制,这也算是一个捷径了。
总结:
Ns2 作为一个在科研领域应用广泛的仿真器有着其内在的很多优势的:开源协议修改自如、分裂设计可设计不同的仿真场景而不需要修改协议代码,但是,我们在做网络协议的研究的时候往往会发现 ns2 现有的协议不足以完成我们的仿真,这是就需要自己设计协议或者修改现有的协议,所以通过对这个简单的 simple_trans 协议的实现我们可以更加的有的放矢,知道如何在那里修改 ns2 的协议,虽然 simple_trans 只是一个超级笨的协议,但是它已经展现了基本的协议设计技巧:集成 agent 、 timer 的使用、协议包头设计等等。如果我们能够再将 ns2 有线无线节点结构、路由模块、无线 mac 等等这些代码仔细研读,那么到时你就会发现在 ns2 上面实现一个协议倒不是难事,反而是在协议自身的设计上,这就和我们高级程序语言一样,语言的学习不是难事,而真正熟练的利用语言解决问题才是我们的学习目标。
扩展实验:
1,考虑路由协议的实现和我们这个application的协议的实现有何异同?
2,实现一个单工的停等协议?
3,实现数据流的优先级(同一个节点上安装多个simple_trans协议【端口要改】)以考察QoS的概念?
实例代码下载:http://d.download.csdn.net/down/2123721/kgn28
实例平台:ns-2.31+cygwin+gcc3.4.4