ns-3中广泛应用的回调机制

前言

回调机制是ns-3中应用非常广泛的机制,但是回调机制不仅仅是ns-3独有的机制,在C语言和C++中都有回调机制。那你又要问了既然C++中已经有了回调机制,那ns-3为什么还要多此一举,在内核中实现回调API供用户使用。答案当然是,ns-3提供回调机制可以方便用户。要想弄明白就要弄清楚一下问题。

First:关于回调的普遍意义的讨论:

1.为什么有回调这样的骚操作(What?&Why?)
2.C语言和C++中的回调是如何实现的(How?)

Second:关于回调在ns-3中的讨论:

3.ns-3为什么要专门在内核中实现回调API(What?&Why?)
4.如何使用ns-3中的回调API(How? )

1. 为什么有回调这样的操作?

回调的出现实际上为了解耦各个模块之间的错综复杂的包含和调用关系,它可以将调用函数和被调用的函数分离开来,使得被调用函数不需要考虑不同的调用方的特性。这样讲非常的虚无缥缈,非常的不让人理解。我们从例子出发来理解回调函数的到底是什么,有啥用:
例子1:带回调函数的快排函数
某程序员小王实现了一个快速排序函数供其他人使用,使用该排序函数的程序员有不同的需求,有的是需要对整型数进行排序,有的是需要对浮点数进行排序,有的是需要对字符串进行排序。那么问题来了,小王的程序要兼容这么多的数据类型,显然工作量非常大。小王就想:不管什么类型的排序都需要涉及比较大小,数据类型不同造成的大工作量也是在大小比较上,那我就通知每个调用我函数的调用方,每次遇到比较大小的时候,我就把需要比较大小的数返回给调用方,让调用方自己比较。也就是说每个调用方在调用的时候需要给被调函数提供一个对应数据类型的大小比较函数。
这里写图片描述
如上图所示,每个调用方在调用快速排序函数的时候,都会自己提供一个适合自己数据类型的大小比较函数,这样使得程序员小王写的快速排序函数可以有更好的兼容性和适用性。如果你还不明白为什么要提供大小比价函数,和大小比较函数提供给被调用的快排函数有什么用,可以看下面更加清晰的图。在快速排序过程中,凡是遇到需要比较大小的时候,快速排序函数就会调用传进来的大小比较函数,让调用方自己提供的函数去完成大小比较的工作,而快排函数只实现快速排序的逻辑。其中需要注意的调用方在提供自己的大小比较函数的时候,其实提供的是一个函数指针。
这里写图片描述
例子2:带回调函数的商店
你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。回答完毕。来源知乎:常溪玲

例子3:酒店叫醒服务
有一家旅馆提供叫醒服务,但是要求旅客自己决定叫醒的方法。可以是打客房电话,也可以是派服务员去敲门,睡得死怕耽误事的,还可以要求往自己头上浇盆水。这里,“叫醒”这个行为是旅馆提供的,相当于库函数,但是叫醒的方式是由旅客决定并告诉旅馆的,也就是回调函数。而旅客告诉旅馆怎么叫醒自己的动作,也就是把回调函数传入库函数的动作,称为登记回调函数(to register a callback function)。来源于知乎:no.body

编程分为两类:系统编程(system programming)和应用编程(application programming)。所谓系统编程,简单来说,就是编写库;而应用编程就是利用写好的各种库来编写具某种功用的程序,也就是应用。系统程序员会给自己写的库留下一些接口,即API(application programming interface,应用编程接口),以供应用程序员使用。所以在抽象层的图示里,库位于应用的底下。当程序跑起来时,一般情况下,应用程序(application program)会时常通过API调用库里所预先备好的函数。但是有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为回调函数(callback function)。如下图所示(图片来源:维基百科):
这里写图片描述

What?
经过以上两个例子,初步对回调函数有了基本的认识。回调函数就是调用方提供给被调函数回调的函数,比如例子1当中的大小比较函数,比如例子2当中的电话号码,而登记回调函数就是调用方把回调函数的函数指针作为参数传递给被调函数的过程。当有回调关联的事件发生的时候就触发调用回调函数,回调函数执行相关操作就是相应回调事件。

Why?
以上两个例子体现了使用回调函数的一点点优点,在不同的应用场景下有不同的优点。此处提高被调函数的适用性,提高商店的服务和销量是使用回调函数的优点之一。下面在讲解ns-3中回调机制再讲解ns-3应用场景的优势。

2. C语言和C++中的回调是如何实现的

通过上一节的了解,其实回调机制就是将回调函数的函数指针作为参数传递给被调函数,当有关联时间触发时候,被调函数可以通过函数指针去调用回调函数。其重中之重的基础就是回调函数的函数指针
.
下面就讲解C语言和C++中的函数指针是如何实现的,又是如何通过函数指针去使用(指针所指向的)函数的。

C语言的回调机制是回调问题的鼻祖,所以首先弄清楚C语言中的回调机制。在上一节的例子1中提到,要把回调函数(大小比价函数)的函数指针作为参数传递到被调函数(快速排序函数)中。其实,回调函数的函数指针就是回调函数的地址,函数与函数指针之间的关系就像对象与对象的指针一样,可以通过函数指针访问函数。

2.1 C语言的回调机制

在C语言中声明函数指针按照如下的语法规则进行:

返回值类型  (* 函数指针的名字)(形参列表); //返回值类型和形参列表必须对所指向函数的形参列表一致

以上声明函数指针得语法注意与定义返回指针类型的函数区分开:

返回指针类型 * 函数名(形参列表);    //注意区分:此处返回值类型为指针的函数定义 

在声明函数指针之后,就可以对函数指针进行赋值,在赋值时,可以直接将函数指针指向函数名(函数名即代表该段代码的首地址)。但是再次强调:函数指针和它指向的函数的参数个数以及类型必须一致。函数指针的返回值类型与函数的返回值类型必须一致。
完成了函数指针得声明和赋值,下面就可以使用函数指针,在使用过程中,指针(*f)和函数function使用方式相同,下面通过一个例子进行了解:

int findMax(int a, int b) {…;}//比较整型数a和整型数b,返回更大的整型数
int (*f)(int a,int b); //声明函数指针
f = findMax; //为函数指针赋值
int Bigger=0;
Bigger=(*f)(1,2); //通过函数指针f调用findMax函数来计算
Bigger=findMax(1,2); //直接调用findMax函数来计算,与上一行效果相同

//该段代码说明了函数指针的作用,也表明了调用方将回调函数的函数指针传递给被调函数是可能的。

2.2 C++中的回调机制

在了解C语言中的回调机制之后,再来看稍微复杂一点的C++中的回调机制。由于C++中有了类和对象的概念,对类的成员函数和普通函数,它们的函数指针有些差别,所以C++中的回调机制更加复杂一些。

对于C++中的普通函数,其函数指针与C语言中的函数指针得定义是一样的。下面主要看成员函数的函数指针。贴一个很棒的参考资料C++中的成员函数指针

其实,成员函数又分为静态成员函数和非静态成员函数。类的静态成员不属于任何对象,因此无须特殊的指向静态成员的指针,指向静态成员的指针与普通指针没有什么区别。与普通函数指针不同的是,非静态成员函数指针不仅要指定目标函数的形参列表和返回类型,还必须指出成员函数所属的类。因此,我们必须在*之前添加classname::以表示当前定义的指针指向classname的成员函数:

返回值类型 (类名称::*函数指针名称)(形参列表);//中文描述
int (A::*pf)(int, int);   // 声明一个非静态成员函数的函数指针pf,且该指针指向类A的非静态成员函数  

同理与C语言中的语法规则一样,这里A::*pf两端的括号也是必不可少的,如果没有这对括号,则pf是一个返回A类数据成员(int型)指针的函数。完成函数指针的声明之后,对函数指针进行赋值:

pf = &A::add;   // 正确:必须显式地使用取址运算符(&)  
pf = A::add;    // 错误 

当我们初始化一个成员函数指针时,其指向了类的某个成员函数,但并没有指定该成员所属的对象——直到使用成员函数指针时,才提供成员所属的对象。下面是一个成员函数指针的使用示例:

class MyClass
{
public:
    int MyMethod(int arg); //非静态成员函数
};
int (MyClass::*pmi)(int arg)=0; //声明一个返回值类型、形参列表都一样的函数指针
pmi = &MyClass::MyMethod;//对函数指针进行赋值
MyClass dragon;
dragon.*pmi(521); //通过函数指针调用MyMethod函数计算
dragon.MyMethod(521); //直接调用MyMethod函数计算,与上一行代码效果相同

//该段代码说明了函数指针的作用,也表明了调用方将回调函数的函数指针传递给被调函数是可能的。

HOW?
通过以上在C语言和C++中使用函数指针来使用函数的讲解和例子,相信你对回调函数实现的基础有了理解。既然理解了函数指针,那么回调函数的实现,就是把回调函数的指针传递给被调函数供其在合适的时候使用。通过下图加深对其工作模式的理解吧。
这里写图片描述

3. ns-3为什么要专门在内核中实现回调API

要讨论ns-3为什么要在内核中实现回调API,其实可以分为两个方面:1.为什么ns-3要用回调机制,2.为什么使用回调机制非要实现底层API,本来自己可以定义一个函数指针即可实现回调,为什么非要在内核中实现一个支持回调的API。下面来进行一一的解答:

3.1 ns-3中为什么要应用回调机制?

在编写仿真代码的时候,不可避免的希望2个或多个模块(类)之间可以进行通信(调用),在ns-3的实现语言C++中,常用的方法就是在B模块中声明A模块的对象,然后通过函数调用来实现他们之间的通信。例如:

class A
{
public:
    void ReceiveInput();
...
};

class B
{
public:
    void Dosomething();
private:
    A *a_instance;
}

void Dosomething()
{
    a_instance->ReceiveInput();
}

这样的方法使得A和B之间存在包含关系,带来了两个问题:1.编译时2个模块要一起进行,否则就编译失败;2.如果要增加实现B与C模块之间的通信,B的源代码就需要增加C_instance,操作麻烦复杂,扩展性不够好。

在网络仿真当中,常常遇到有需要扩展的情况,例如一个用户考虑在TCP和IP层之间添加一个IPSec安全协议子层,如下图所示
这里写图片描述
如果TCP层和IP层之间的通信采用之前的相互包含的方式进行,在扩展IPSec层的是将就会及其的麻烦:首先需要解耦TCP层和IP层之间的互相包含的关系,然后新建TCP层和IPSec层之间相互包含关系,新建IPSec层和IP层之间的关系,显然这是非常繁琐的。如果他们不使用相互包含的通信关系,各个模块之间相对独立,扩展就会变得相对容易。

于是,回调机制就出现了,实现“让一个模块来调用另一个模块中的一个函数,通知模块之间相对独立”。

3.2为什么要实现回调API来“帮助”用户

ns-3在内核中特地实现了回调API来供用户使用,我百思不得其解。回调函数不就是声明和定义一个函数指针,然后把函数指针登记到被调动的函数中吗?不用底层实现的API我自己完全可以完成啊。

悲催的是,直到我写这一篇复习文档时,仍未理解为什么ns-3底层要实现回调API来“帮助”用户。但是!它既然实现了,说明自己来操刀回调函数的函数指针会有坑,用底层实现的API更安全更快捷。

4.如何使用ns-3中的回调API

ns-3提供了Callback类API接口来为用户提供服务,过程大致分为两步:回调类型声明;回调函数实例化。在ns-3中,函数包含静态函数和类成员函数两大类,在C++回调机制小节中提到这两类的函数指针有所区别,故此处API的使用也不太相同,下面一一做介绍。

4.1静态函数使用回调API

针对静态函数如何使用回调API,由于回调API是由Callback类来实现的,所以问题也就是针对静态函数如何声明和实例化Callback类型。

static double Cbone(double a, double b)
{
    std::cout<<"Cbone a= "<<a<<" | b= "<<b<<std::endl;
    return a;
}

int main(int argc, char *argv[])
{
    Callback<double,double,double> one; //实例化回调
    one = MakeCallback(&Cbone); //将回调与签名匹配的函数Cbnoe绑定
    NS_ASSERT(!one.IsNull());  //检查回调“one”是否为空
    Double retOne;
    retOne = one(10.0,20.0); //调用回调“one”,结果与直接调用Cbone()返回的结果一样
}

本例通过Callback类模板来实例化,这里需要注意的是通过Callback类模板实例化时,至少一个参数对应回调函数的返回值类型,最多5个参数,除了第一个参数外,后面4个分别对应自己声明回调函数的参数,如果自己定义的函数加上返回值超过5个,那么久必须自己通过扩展回调来处理。

在上面的代码中,首先声明了一个叫“one”的回调,该回调保留了一个函数指针,它保留的函数指针指向的函数满足这样的特征:返回值必须是double类型的;并且支持2个double参数。如果声明的回调函数的签名(函数签名值:函数参数的个数和类型)与所声明的回调匹配,那么编译就会失败。

只有回调函数的签名与所声明的回调相匹配,才可以将回调与签名匹配的函数进行绑定。具体的绑定有MakeCallback(&函数名)函数来进行。

完成绑定之后代码,利用检查函数IsNull()确保该会断掉不是NULL,即该回调的背后却是存在一个函数调用。

4.2类成员函数使用回调API

在C++面向对象编程中,被回调的函数有可能是类的成员函数。在这种情况下,MakeCallBack函数就要额外地增加一个参数,该参数告诉系统在那个对象上调用该回调函数。以下面的代码为例:

class MyCb()
{
public:
    int CbTwo(double a)
    {
        std::cout<<" CbTwo a= "<<a<<std::endl;
        return -5;
    }
}

Callback<int,double> two; //实例化一个叫“two”的回调
MyCb cb;
two = MakeCallback(&MyCb::CbTwo,&cb); //将回调与签名匹配的函数绑定,同时指定调用该函数的对象
NS_ASSERT(!two.IsNull()); //判断回调不为空

int retTwo;
retTwo = two(10.0);
return 0;
}

以上的代码传递了一个额外的指针给函数MakeCallback<>(),即当函数two()被调用时,调用的是&cb所指向的对象函数CbTwo,也就是类MyCb中的CbTwo函数.

有时候可能会需要构建空回调,ns-3提供MakeNUllCallback<>()函数来实现这一功能。调用空回调就相当于调用了一个函数指针不指向任何函数。

two = MakeNullCallback<int,double>();  //构建Null回调
Int retTwoNull = two(20.0);
NS_ASSERT(two.IsNull());

猜你喜欢

转载自blog.csdn.net/loongkingwhat/article/details/79114164