TheONE消息转发流程分析

     TheONE全称为The Opportunistic Network Environment simulator,是一款Java平台的容迟网络仿真软件,本文采用一条纵线分析了TheONE中消息转发的流程。

   1. 消息转发的入口

跟踪发包流程最主要的是找到入口,ONENS2最大的区别在于:NS2是离散事件驱动的,而ONE采用的是周期更新即Update,如果把网络中的事件比喻成出门坐车,那么NS2像是打车,招手即停,而ONE是坐公交,公交车每隔一段时间来一班,这个时间间隔就是Update的周期,在default_setting.txt中定义为0.1秒。于是最先要找到的就是update函数。ONE核心的功能可以看作由两大部分组成,一是移动,二是消息转发,简而言之,前者根据移动模型确定下一时段各节点的位置坐标,后者根据接口和路由来决定节点是否应该收到一个消息。本文讨论的是消息转发流程,因此忽略节点移动部分,直接找到消息转发的入口。

DTNHostONE中最基本的类,它定义了一个节点(后文将节点与Host等同)的基本属性和基本功能,在DTNHost.java中的update()函数负责更新网络接口和路由。

 

    

    public void update(boolean simulateConnections) {

       if (!isActive()) {   //如果节点处于非激活状态,则不会执行后面的语句

           return;       //可以将非激活状态想象成一个自私节点或者没电的传感器

       }

       if (simulateConnections) {

           for (NetworkInterface i : net) {

              i.update();       //更新所有的接口

           }

       }

       this.router.update();    //更新路由

    }

可以看出在update()中先更新了接口,后更新了路由,原因是路由的工作需要建立在接口的基础之上。本文也先说接口,后说路由。

2. 维护有效的连接

首先要解释ONE中的一个最基本的概念——连接(Connection/Connect)。由于ONE并不侧重于底层信道的仿真,因此空中接口的概念已经淡化了,取而代之的是“连接”。简单的理解,当两个节点在彼此通信范围之内,连接就认为是建立的,ONE习惯中用UP表示,反之用DOWNONE中提供了一系列函数用于维护所有节点之间的连接。详细情况可以读一下connection.java。一个最基本的连接包括了:

 protected DTNHost toNode;                 //连接的目的节点

    protected NetworkInterface toInterface;   //连接的目的接口

    protected DTNHost fromNode;               //链接的源节点

    protected NetworkInterface fromInterface; //链接的源接口

    protected DTNHost msgFromNode;            //消息的源节点

    private boolean isUp;                     //最重要的是连接的当前状态

    protectedMessage msgOnFly;               //连接正在传输的消息

实际上,仿真中通常使用的是CBRConnection类,这个类继承自Connection,扩展了:

 

    private int speed;                     //CBR流的传输速度

    private double transferDoneTime;       //传输完成的时间

回到update的问题上,更新接口执行的是网络接口的更新函数,在NetworkInterface类中update()是一个空函数,实际上更新的是它子类中的update()函数。在实际仿真中,经常采用SimpleBroadcastInterface作为接口类型(实际上ONE也没给我们太多的选择),因此SimpleBroadcastInterface.java中的更新函数是维护连接的关键。

    public void update() {

       // First break the old ones

       optimizer.updateLocation(this);

       for (int i=0; i<</span>this.connections.size(); ) {      //遍历所有连接

           Connection con = this.connections.get(i);

           NetworkInterface anotherInterface = con.getOtherInterface(this);

           // all connections should be up at this stage

           assert con.isUp() : "Connection " + con + " was down!";

 

           if (!isWithinRange(anotherInterface)) {       //删除已经断开的连接

              disconnect(con,anotherInterface);

              connections.remove(i);

           }

           else {

              i++;

           }

       }

       // Then find new possible connections

       Collection interfaces =

           optimizer.getNearInterfaces(this);

       for (NetworkInterface i : interfaces) {       //建立新的连接

           connect(i);

       }

    }

 

这个函数的思想很简单,首先检索所有已经建立好的连接,将其中已经断开的连接删除,再找到新的可以建立的连接。这路重点需要看的是两个函数:

isWithinRange()目的是判断两个接口所在的节点是否在彼此的通信范围之内,这个函数是路由与移动模型交互的地方,在仿真脚本中设置的接口传输距离、移动模型都会最这个函数的返回值产生影响。

    connect()函数是建立连接的函数,在SimpleBroadcastInterface类中,建立连接并不简单,需要满足一系列条件才能建立,这些条件主要是:确保接口处于扫描状态、确保节点处于活跃状态、确保两接口所在节点彼此能通信、确保两接口不是已经连接的、确保两接口不是同一接口:

 

public void connect(NetworkInterface anotherInterface) {

           if (isScanning()

                  && anotherInterface.getHost().isActive()

                  && isWithinRange(anotherInterface)

                  && !isConnected(anotherInterface)

                  && (this != anotherInterface)) {

    满足这些条件的两个接口能够成功建立连接,连接建立的过程包括:将连接con添加到本节点和对方节点的connections中、将本节点和对方节点con的状态设置为UP、通知Listener连接con已经建立。在ONE中,多种Listener用于统计网络性能,生成Reports,刚开始看代码的时候,可以将所有与Listener有关的语句都忽略。

 

    protected void connect(Connection con, NetworkInterface anotherInterface) {

       this.connections.add(con);

       notifyConnectionListeners(CON_UP, anotherInterface.getHost());

 

       // set up bidirectional connection

       anotherInterface.getConnections().add(con);

 

       // inform routers about the connection

       this.host.connectionUp(con);

       anotherInterface.getHost().connectionUp(con);

    }

    连接维护的实际过程要比上面所说的稍复杂,但核心思想很简单:节点周期检查自己与周围节点的位置,与满足传输条件的节点的对应接口建立连接。需要补充的是,连接的建立实际上是以接口为单位的,不是与节点为单位的,因为一个节点可以有多个接口,而不同种类接口的传输距离不同。

3.消息的转发流程

连接的建立是为消息传输服务的,当一个连接处于UP状态,连接两端的节点就可以传输消息。消息的传输过程同样是通过update函数来驱动的,在DTNHost中更新当前节点的路由的update()函数。MessageRouter类是所有路由的父类,但在ONE中几乎是个空类,其最关键的子类是ActiveRouter,几乎所有的路由都是ActiveRouter的子类。

ActiveRouter()之所以称之为主动的路由,就体现在对连接的处理上,它维护着正在发送消息的连接的列表,一旦在update()函数中发现连接已经中断,则采用积极的措施来善后。这些善后措施包括:终止在该连接上的数据传输、清理该消息所占用的额外存储空间。

    protectedArrayList sendingConnections;

 但是ActiveRouter仍然不能实现消息的转发,因为在update函数中没有涉及到消息的处理,这个功能实际上是通过其各种子类来实现的,由于ONE中继承ActiveRouter的路由众多,这里只以最简单的EpidemicRouter为例来说明消息转发的流程。

EpidemicRouter 类的 update 函数中真正实现了节点通过连接进行消息交互。如果本节点正在发送数据或者不能发送数据,则不进行消息转发。通过调用 exchangeDeliverableMessages() 函数查看是否有到达目的节点的消息可以发送,现在这个事件优先级是最高的。最后在所有的连接上尝试发送所有的消息。

    @Override

    public void update() {

       super.update();

       if (isTransferring() || !canStartTransfer()) {

           return; // transferring, don't try other connections yet

       }

       // Try first the messages that can be delivered to final recipient

       if (exchangeDeliverableMessages() != null) {

           return; // started a transfer, don't try others (yet)

       }

       // then try any/all message to any/all connection

       this.tryAllMessagesToAllConnections();

    }

 

(1)如果消息能够直接发送到目的节点

exchangeDeliverableMessages() 的作用是发送能够到达目的节点的消息,即当前的节点遍历所有的消息和所有 UP 状态的连接,一旦发现某个消息能够到达目的节点,立即发送这个消息。 exchangeDeliverableMessages 的返回值是一个 Connection ,如果没有可以发送的消息,返回值为空,如果能找到一个(注意只要稍到一个就可以)可以到达目的节点的消息,则返回发送这个消息所对应的连接。

    protected Connection exchangeDeliverableMessages() {

       List connections = getConnections();     //得到所有连接

       if (connections.size() == 0) {     //没有连接,返回为空

           return null;

       }

       @SuppressWarnings(value = "unchecked")

       Tuple t =

    tryMessagesForConnected(sortByQueueMode(getMessagesForConnected()));

       if (t != null) {

           return t.getValue(); // started transfer

       }

       // didn't start transfer to any node -> ask messages from connected

       for (Connection con : connections) {

           if (con.getOtherNode(getHost()).requestDeliverableMessages(con)) {

              return con;

           }

       }

       return null;

    }

函数中调用了getMessagesForConnected()函数,其返回值是一个List,组成它的元组格式为,这些元组中都满足条件:(1Message是当前节点缓存中的消息;(2Connection是当前节点维护的UP连接;(3Message的目的节点就是Connection的另一端。

    protected List> getMessagesForConnected() {

       if (getNrofMessages() == 0 || getConnections().size() == 0) {

          

           return new ArrayList>(0);

       }      //如果当先节点根本没有消息或者根本没有可用的连接,返回的是一个空List,而不是NULL

 

       List> forTuples =     //用于储存临时的元组

           new ArrayList>();

       for (Message m : getMessageCollection()) {       //遍历所有的Message

           for (Connection con : getConnections()) {     //遍历所有的Connection

              DTNHost to = con.getOtherNode(getHost()); //提取Connection的另一端

              if (m.getTo() == to) {                    //消息m是发往con的另一端的

                  forTuples.add(new Tuple(m,con));

              }

           }

       }

       return forTuples;

    }

getMessagesForConnected()的返回值传递给sortByQueueMode函数,这个函数根据消息的优先级对消息进行排序,ONE中定义了两种优先级策略,一是随机排序(默认为0),二是先入先出策略(值为1),这个值可以在配置脚本中进行设置,语句为Group.sendQueue = 1。如果需要对缓存机制进行扩展,显然需要修改这个函数。由于在这里并没有涉及缓存策略,因此不赘述。sortByQueueMode()函数返回的是排序之后的List,保持元组格式不变。

得到了排序之后的 ,传递给 tryMessagesForConnected 函数,这个函数的作用是将元组中优先级最高的那个(最上面的那个消息)在对应的 connection 上发送出去。要强调的是,这里只发送一个消息,返回的一个元组,而不是元组的 List 。当然优先级最高的那个消息可能不会发送成功,那么则会尝试发送优先级次之的消息,如果 List 中所有的消息都尝试过了,那只好返回 NULL 了。

    protected TupletryMessagesForConnected(

           List> tuples) {

       if (tuples.size() == 0) {   //元组为空,返回NULL

           return null;

       }

       for (Tuple t : tuples) {    //遍历所有的元组

           Message m = t.getKey();                       //提取出消息m  

           Connection con = t.getValue();                //提取出连接con

           if (startTransfer(m, con) == RCV_OK) {        //将消息mcon上发送

              return t;         //如果消息发送成功,直接跳出tryMessagesForConnected

           }

       }

       return null;

    }

怎样判断消息m能否在con上发送成功?需要调用startTransfer函数。这个函数很简单,但是其返回值很关键。当函数返回TRY_LATER_BUSY=1时,说明该连接正忙,稍后再试,当返回RCV_OK=0时,说明发送成功,以上还很容易理解。但retVal还有其他的取值来表明消息发送的情况,DENIED_OLD = -1表示该消息发送失败,原因是已经处理过(已经接收过或者已经转发过),DENIED_NO_SPACE = -2表示该消息发送失败,原因是缓存中没有空位置,DENIED_TTL=-3表示消息发送失败,原因是该消息超时。

    protected int startTransfer(Message m, Connection con) {

       int retVal;

      

       if (!con.isReadyForTransfer()) {       //连接正忙,返回1

           return TRY_LATER_BUSY;

       }

       retVal = con.startTransfer(getHost(), m);     //调用连接的startTrransfer函数

       if (retVal == RCV_OK) { // started transfer

           addToSendingConnections(con);

       }

       else if (deleteDelivered && retVal == DENIED_OLD &&

              m.getTo() == con.getOtherNode(this.getHost())) {    

           this.deleteMessage(m.getId(), false);     //清理因DENIED_OLD被拒绝的消息

       }

       return retVal;

    }

在这里涉及到缓存的清理机制,如果在配置脚本中设置Group.deleteDelivered = 1,则开启了已发送消息的清理机制,这样,一旦节点发现消息mDENIED_OLD被拒绝,则会清除本地缓存中的m

    在路由的 startTransfer 函数中调用了连接的 startTransfer ,这个函数在 CBRConnection 类中被重载。在 CBRConnection startTransfer 中,首先调用对方节点的 receiveMessage 函数,看对方节点能否接收这个消息。如果能接收,则进行以下操作:( 1 )设置正在发送的消息 msgOnFly ;( 2 )计算消息预计完成时间;( 3 )返回值 RCV_OK=0

 

    public int startTransfer(DTNHost from, Message m) {

 

       assert this.msgOnFly == null : "Already transferring " +

       this.msgOnFly + " from " + this.msgFromNode + " to " +

       this.getOtherNode(this.msgFromNode) + ". Can't "+

       "start transfer of " + m + " from " + from;

 

       this.msgFromNode = from;

       Message newMessage = m.replicate();

       int retVal = getOtherNode(from).receiveMessage(newMessage, from);

 

       if (retVal == MessageRouter.RCV_OK) {

           this.msgOnFly = newMessage;

           this.transferDoneTime = SimClock.getTime() +

           (1.0*m.getSize()) / this.speed;

       }

       return retVal;

    }

在上面的函数中,调用的是节点的receiveMessage函数(在DTNHost.java定义),进而进入路由的receiveMessage函数,在MessageRouter.java中的receiveMessage函数进行了下面的几个工作:(1)将消息m放进IncomingBuffer;(2)当前节点存入消息mPath中,Path是一系列DTNHost组成的列表,记录着这个消息所走过的路径;(3)通知所有的Listener发生了messageTransferStarted事件;(4)返回RCV_OK=0。

 

    public int receiveMessage(Message m, DTNHost from) {

       Message newMessage = m.replicate();

             

       this.putToIncomingBuffer(newMessage, from);     

       newMessage.addNodeOnPath(this.host);

      

       for (MessageListener ml : this.mListeners) {

           ml.messageTransferStarted(newMessage, from, getHost());

       }

      

       return RCV_OK; // superclass always accepts messages

    }

    至此消息 m 被成功的发送到了目的节点,实际上工作才完成了一半,因为还有一些消息不能被发送到目的节点,需要一些中间节点进行转发。


(2) 当消息不能直接到达目的节点时 

         在update函数中还有一个函数tryAllMessagesToAllConnections()专门负责处理类事件,他的优先级是要低于exchangeDeliverableMessages()的,也就是说当前节点如果有消息能够直接发送到目的节点,就应该优先发送。否则才会执行exchangeDeliverableMessages()函数,首先对所有的消息重新排序,排序的函数仍是sortByQueueMode,在前面介绍过。最后,调用tryMessagesToConnections函数遍历排序之后的所有消息和所有连接,寻找能够发送的消息。

 

    protected Connection tryAllMessagesToAllConnections(){

       List connections = getConnections();

 

       if (connections.size() == 0 || this.getNrofMessages() == 0) {

           return null;

       }

 

       List messages =  new ArrayList(this.getMessageCollection());

       this.sortByQueueMode(messages);    //将所有的消息重新排序

 

       return tryMessagesToConnections(messages, connections);

    }

 

tryMessagesToConnections中,遍历所有的连接,然后调用tryAllMessages(con, messages)依次在每个连接上发送所有的消息。注意区别:在tryMessagesToConnections(messages, connections)中,messages指的是很多消息,connections指的是很多连接;而在tryAllMessages(con, messages)中,messages指的是很多消息,con指的是某个连接;然而在startTransfer(m, con)函数中,m是指某个消息,con是指某个连接。

        在tryAllMessages函数中,遍历所有的消息,依次尝试在con上能否发送成功,只要找到一个能发送的消息m,就终止该函数,返回消息m。否则尝试所有的消息都不能在con上发送,返回NULL

 

 

    protected Message tryAllMessages(Connection con, List messages) {

       for (Message m : messages) {

           int retVal = startTransfer(m, con);

           if (retVal == RCV_OK) {

              return m;  // accepted a message, don't try others

           }

           else if (retVal > 0) {

              return null; // should try later -> don't bother trying others

           }

       }

      

       return null; // no message was accepted      

    }

 

在发送消息的函数中,仍然执行的是startTransfer函数,startTransfer函数在前面已经介绍过了。

        到这里,EpidemicRouter中发送消息的流程已经介绍清楚了,EpidemicRouter路由中鼓励节点尽可能将所有的消息都转发给自己的邻居,因此只需要在ActiveRouter父类上做简单的扩展即可。接下的部分以其他路由为例研究一下路由协议是怎么扩展ActiveRouter的。


 

4.  路由扩展

 

(1)DirectDeliveryRouter

       这个路由的改法是最简单的,只需要在EpidemicRouter的基础上删除this.tryAllMessagesToAllConnections()函数,保留exchangeDeliverableMessages()函数即可,那么消息的转发流程中只剩下了第一部分。

 

(2)FirstContactRouter

EpidemicRouter是最简单的洪泛消息,其他路由协议与它最大的区别就在于:并不是所有的消息都照单全收。回忆上面的消息传输过程,无论是到目的节点还是到中间节点,都需要执行startTransfer,它负责在某个连接con上发送某个消息m。在这个函数中,首先找到con的另一端DTNHost,然后调用receiveMessage函数查看对方能不能接收消息mreceiveMessage是一个不断被各种路由重载的函数,在MessageRouter中,实现了最基本的功能:

l  将消息m放进IncomingBuffer

l  当前节点存入消息mPath中;

l  通知所有的Listener发生了messageTransferStarted事件;

ActiveRouter中增加了功能:

l  调用checkReceiving函数看对方节点能不能接收消息m,很多种可能都会导致消息不能被接收,在ActiveRouter中主要有:(1)对方节点正在发送消息,返回TRY_LATER_BUSY=1;(2)对方节点缓存中已经有该消息了,返回DENIED_OLD=-1;(3)消息超时,返回DENIED_TTL=-2;(4)对方节点缓存已满,返回ENIED_NO_SPACE=-3

从上面的分析发现,要想消息按某种条件被转发,在checkReceiving函数中进行一些过滤是很合理的。

                  FirstContactRouter路由中,区别在于节点把数据转发给第一次遇到的节点,然后就会将转发过的消息从缓存中删除。这里重载了checkReceiving函数。每次节点转发某个消息,都会将自己写在消息的path中,当节点发现自己已经处理过某个消息了,则会用DENIED_OLD的理由将其拒绝。需要指出的是顺序问题,将当前节点加入消息路径Path的操作是在receiveMessage中,这个操作是在checkReceiving之后,所以在检索path的时候当前节点还没有将自己写进Path

 

@Override

    protected int checkReceiving(Message m) {

       int recvCheck = super.checkReceiving(m);

      

       if (recvCheck == RCV_OK) {

          

           if (m.getHops().contains(getHost())) {

              recvCheck = DENIED_OLD;

           }

       }

       return recvCheck;

    }

        除此之外,FirstContactRouter还重载了transferDone函数。节点将消息发送成功之后就会清理缓存。这个函数在ActiveRouter中为空,在EpidemicRouter中也没有将其重载,所以在这里是第一次出现。

 

    @Override

    protected void transferDone(Connection con) {

      

       this.deleteMessage(con.getMessage().getId(), false);

    }

       在这里只是给出了两个路由协议最基本的例子,本文重点在于解释TheONE中消息转发的流程,因此其他的路由协议将在其他文章中进行分析。

 

                     (本文完)


你可能感兴趣的:(java,the,网络仿真,NE,容迟网络,Opportunistic)