from:http://www.ibm.com/developerworks/cn/linux/l-callback/index.html
級別: 初級
陳家朋 (japen@vip.sina.com), 系統(tǒng)架構(gòu)師和技術(shù)顧問, 杭州邁可行通信技術(shù)有限公司
2003 年 3 月 01 日
軟件模塊之間總是存在著一定的接口,從調(diào)用方式上,可以把他們分為三類:同步調(diào)用、回調(diào)和異步調(diào)用。同步調(diào)用是一種阻塞式調(diào)用,調(diào)用方要等待對方執(zhí)行完畢才返回,它是一種單向調(diào)用;回調(diào)是一種雙向調(diào)用模式,也就是說,被調(diào)用方在接口被調(diào)用時(shí)也會調(diào)用對方的接口;異步調(diào)用是一種類似消息或事件的機(jī)制,不過它的調(diào)用方向剛好相反,接口的服務(wù)在收到某種訊息或發(fā)生某種事件時(shí),會主動(dòng)通知客戶方(即調(diào)用客戶方的接口)。回調(diào)和異步調(diào)用的關(guān)系非常緊密,通常我們使用回調(diào)來實(shí)現(xiàn)異步消息的注冊,通過異步調(diào)用來實(shí)現(xiàn)消息的通知。同步調(diào)用是三者當(dāng)中最簡單的,而回調(diào)又常常是異步調(diào)用的基礎(chǔ),因此,下面我們著重討論回調(diào)機(jī)制在不同軟件架構(gòu)中的實(shí)現(xiàn)。
1 什么是回調(diào)
軟件模塊之間總是存在著一定的接口,從調(diào)用方式上,可以把他們分為三類:同步調(diào)用、回調(diào)和異步調(diào)用。同步調(diào)用是一種阻塞式調(diào)用,調(diào)用方要等待對方執(zhí)行完畢才返回,它是一種單向調(diào)用;回調(diào)是一種雙向調(diào)用模式,也就是說,被調(diào)用方在接口被調(diào)用時(shí)也會調(diào)用對方的接口;異步調(diào)用是一種類似消息或事件的機(jī)制,不過它的調(diào)用方向剛好相反,接口的服務(wù)在收到某種訊息或發(fā)生某種事件時(shí),會主動(dòng)通知客戶方(即調(diào)用客戶方的接口)。回調(diào)和異步調(diào)用的關(guān)系非常緊密,通常我們使用回調(diào)來實(shí)現(xiàn)異步消息的注冊,通過異步調(diào)用來實(shí)現(xiàn)消息的通知。同步調(diào)用是三者當(dāng)中最簡單的,而回調(diào)又常常是異步調(diào)用的基礎(chǔ),因此,下面我們著重討論回調(diào)機(jī)制在不同軟件架構(gòu)中的實(shí)現(xiàn)。
對于不同類型的語言(如結(jié)構(gòu)化語言和對象語言)、平臺(Win32、JDK)或構(gòu)架(CORBA、DCOM、WebService),客戶和服務(wù)的交互除了同步方式以外,都需要具備一定的異步通知機(jī)制,讓服務(wù)方(或接口提供方)在某些情況下能夠主動(dòng)通知客戶,而回調(diào)是實(shí)現(xiàn)異步的一個(gè)最簡捷的途徑。
對于一般的結(jié)構(gòu)化語言,可以通過回調(diào)函數(shù)來實(shí)現(xiàn)回調(diào)。回調(diào)函數(shù)也是一個(gè)函數(shù)或過程,不過它是一個(gè)由調(diào)用方自己實(shí)現(xiàn),供被調(diào)用方使用的特殊函數(shù)。
在面向?qū)ο蟮恼Z言中,回調(diào)則是通過接口或抽象類來實(shí)現(xiàn)的,我們把實(shí)現(xiàn)這種接口的類成為回調(diào)類,回調(diào)類的對象成為回調(diào)對象。對于象C++或Object Pascal這些兼容了過程特性的對象語言,不僅提供了回調(diào)對象、回調(diào)方法等特性,也能兼容過程語言的回調(diào)函數(shù)機(jī)制。
Windows平臺的消息機(jī)制也可以看作是回調(diào)的一種應(yīng)用,我們通過系統(tǒng)提供的接口注冊消息處理函數(shù)(即回調(diào)函數(shù)),從而實(shí)現(xiàn)接收、處理消息的目的。由于Windows平臺的API是用C語言來構(gòu)建的,我們可以認(rèn)為它也是回調(diào)函數(shù)的一個(gè)特例。
對于分布式組件代理體系CORBA,異步處理有多種方式,如回調(diào)、事件服務(wù)、通知服務(wù)等。事件服務(wù)和通知服務(wù)是CORBA用來處理異步消息的標(biāo)準(zhǔn)服務(wù),他們主要負(fù)責(zé)消息的處理、派發(fā)、維護(hù)等工作。對一些簡單的異步處理過程,我們可以通過回調(diào)機(jī)制來實(shí)現(xiàn)。
下面我們集中比較具有代表性的語言(C、Object Pascal)和架構(gòu)(CORBA)來分析回調(diào)的實(shí)現(xiàn)方式、具體作用等。
2 過程語言中的回調(diào)(C)
2.1 函數(shù)指針
回調(diào)在C語言中是通過函數(shù)指針來實(shí)現(xiàn)的,通過將回調(diào)函數(shù)的地址傳給被調(diào)函數(shù)從而實(shí)現(xiàn)回調(diào)。因此,要實(shí)現(xiàn)回調(diào),必須首先定義函數(shù)指針,請看下面的例子:
void Func(char *s);// 函數(shù)原型
void (*pFunc) (char *);//函數(shù)指針
|
可以看出,函數(shù)的定義和函數(shù)指針的定義非常類似。
一般的化,為了簡化函數(shù)指針類型的變量定義,提高程序的可讀性,我們需要把函數(shù)指針類型自定義一下。
typedef void(*pcb)(char *);
|
回調(diào)函數(shù)可以象普通函數(shù)一樣被程序調(diào)用,但是只有它被當(dāng)作參數(shù)傳遞給被調(diào)函數(shù)時(shí)才能稱作回調(diào)函數(shù)。
被調(diào)函數(shù)的例子:
void GetCallBack(pcb callback)
{
/*do something*/
}
用戶在調(diào)用上面的函數(shù)時(shí),需要自己實(shí)現(xiàn)一個(gè)pcb類型的回調(diào)函數(shù):
void fCallback(char *s)
{
/* do something */
}
然后,就可以直接把fCallback當(dāng)作一個(gè)變量傳遞給GetCallBack,
GetCallBack(fCallback);
|
如果賦了不同的值給該參數(shù),那么調(diào)用者將調(diào)用不同地址的函數(shù)。賦值可以發(fā)生在運(yùn)行時(shí),這樣使你能實(shí)現(xiàn)動(dòng)態(tài)綁定。
2.2 參數(shù)傳遞規(guī)則
到目前為止,我們只討論了函數(shù)指針及回調(diào)而沒有去注意ANSI C/C++的編譯器規(guī)范。許多編譯器有幾種調(diào)用規(guī)范。如在Visual C++中,可以在函數(shù)類型前加_cdecl,_stdcall或者_(dá)pascal來表示其調(diào)用規(guī)范(默認(rèn)為_cdecl)。C++ Builder也支持_fastcall調(diào)用規(guī)范。調(diào)用規(guī)范影響編譯器產(chǎn)生的給定函數(shù)名,參數(shù)傳遞的順序(從右到左或從左到右),堆棧清理責(zé)任(調(diào)用者或者被調(diào)用者)以及參數(shù)傳遞機(jī)制(堆棧,CPU寄存器等)。
將調(diào)用規(guī)范看成是函數(shù)類型的一部分是很重要的;不能用不兼容的調(diào)用規(guī)范將地址賦值給函數(shù)指針。例如:
// 被調(diào)用函數(shù)是以int為參數(shù),以int為返回值
__stdcall int callee(int);
// 調(diào)用函數(shù)以函數(shù)指針為參數(shù)
void caller( __cdecl int(*ptr)(int));
// 在p中企圖存儲被調(diào)用函數(shù)地址的非法操作
__cdecl int(*p)(int) = callee; // 出錯(cuò)
|
指針p和callee()的類型不兼容,因?yàn)樗鼈冇胁煌恼{(diào)用規(guī)范。因此不能將被調(diào)用者的地址賦值給指針p,盡管兩者有相同的返回值和參數(shù)列
2.3 應(yīng)用舉例
C語言的標(biāo)準(zhǔn)庫函數(shù)中很多地方就采用了回調(diào)函數(shù)來讓用戶定制處理過程。如常用的快速排序函數(shù)、二分搜索函數(shù)等。
快速排序函數(shù)原型:
void qsort(void *base, size_t nelem, size_t width, int (_USERENTRY *fcmp)(const void *, const void *));
二分搜索函數(shù)原型:
void *bsearch(const void *key, const void *base, size_t nelem,
size_t width, int (_USERENTRY *fcmp)(const void *, const void *));
|
其中fcmp就是一個(gè)回調(diào)函數(shù)的變量。
下面給出一個(gè)具體的例子:
#include <stdio.h>
#include <stdlib.h>
int sort_function( const void *a, const void *b);
int list[5] = { 54, 21, 11, 67, 22 };
int main(void)
{
int x;
qsort((void *)list, 5, sizeof(list[0]), sort_function);
for (x = 0; x < 5; x++)
printf("%i\n", list[x]);
return 0;
}
int sort_function( const void *a, const void *b)
{
return *(int*)a-*(int*)b;
}
|
2.4 面向?qū)ο笳Z言中的回調(diào)(Delphi)
Dephi與C++一樣,為了保持與過程語言Pascal的兼容性,它在引入面向?qū)ο髾C(jī)制的同時(shí),保留了以前的結(jié)構(gòu)化特性。因此,對回調(diào)的實(shí)現(xiàn),也有兩種截然不同的模式,一種是結(jié)構(gòu)化的函數(shù)回調(diào)模式,一種是面向?qū)ο蟮慕涌谀J健?
2.4.1 回調(diào)函數(shù)
回調(diào)函數(shù)類型定義:
type
TCalcFunc=function (a:integer;b:integer):integer;
|
按照回調(diào)函數(shù)的格式自定義函數(shù)的實(shí)現(xiàn),如
function Add(a:integer;b:integer):integer
begin
result:=a+b;
end;
function Sub(a:integer;b:integer):integer
begin
result:=a-b;
end;
|
回調(diào)的使用
function Calc(calc:TcalcFunc;a:integer;b:integer):integer
|
下面,我們就可以在我們的程序里按照需要調(diào)用這兩個(gè)函數(shù)了
c:=calc(add,a,b);//c=a+b
c:=calc(sub,a,b);//c=a-b
|
2.4.2 回調(diào)對象
什么叫回調(diào)對象呢,它具體用在哪些場合?首先,讓我們把它與回調(diào)函數(shù)對比一下,回調(diào)函數(shù)是一個(gè)定義了函數(shù)的原型,函數(shù)體則交由第三方來實(shí)現(xiàn)的一種動(dòng)態(tài)應(yīng)用模式。要實(shí)現(xiàn)一個(gè)回調(diào)函數(shù),我們必須明確知道幾點(diǎn):該函數(shù)需要那些參數(shù),返回什么類型的值。同樣,一個(gè)回調(diào)對象也是一個(gè)定義了對象接口,但是沒有具體實(shí)現(xiàn)的抽象類(即接口)。要實(shí)現(xiàn)一個(gè)回調(diào)對象,我們必須知道:它需要實(shí)現(xiàn)哪些方法,每個(gè)方法中有哪些參數(shù),該方法需要放回什么值。
因此,在回調(diào)對象這種應(yīng)用模式中,我們會用到接口。接口可以理解成一個(gè)定義好了但是沒有實(shí)現(xiàn)的類,它只能通過繼承的方式被別的類實(shí)現(xiàn)。Delphi中的接口和COM接口類似,所有的接口都繼承與IInterface(等同于IUnknow),并且要實(shí)現(xiàn)三個(gè)基本的方法QueryInterface, _AddRef, 和_Release。
- 定義一個(gè)接口
type IShape=interface(IInterface)
procedure Draw;
end
|
- 實(shí)現(xiàn)回調(diào)類
type TRect=class(TObject,IShape)
protected
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
public
procedure Draw;
end;
type TRound=class(TObject,IShape)
protected
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
public
procedure Draw;
end;
|
- 使用回調(diào)對象
procedure MyDraw(shape:IShape);
var
shape:IShape;
begin
shape.Draw;
end;
|
如果傳入的對象為TRect,那么畫矩形;如果為TRound,那么就為圓形。用戶也可以按照自己的意圖來實(shí)現(xiàn)IShape接口,畫出自己的圖形:
MyDraw(Trect.Create);
MyDraw(Tround.Create);
|
2.4.3 回調(diào)方法
回調(diào)方法(Callback Method)可以看作是回調(diào)對象的一部分,Delphi對windows消息的封裝就采用了回調(diào)方法這個(gè)概念。在有些場合,我們不需要按照給定的要求實(shí)現(xiàn)整個(gè)對象,而只要實(shí)現(xiàn)其中的一個(gè)方法就可以了,這是我們就會用到回調(diào)方法。
回調(diào)方法的定義如下:
TNotifyEvent = procedure(Sender: TObject) of object;
TMyEvent=procedure(Sender:Tobject;EventId:Integer) of object;
|
TNotifyEvent 是Delphi中最常用的回調(diào)方法,窗體、控件的很多事件,如單擊事件、關(guān)閉事件等都是采用了TnotifyEvent。回調(diào)方法的變量一般通過事件屬性的方式來定義,如TCustomForm的創(chuàng)建事件的定義:
property OnCreate: TNotifyEvent read FOnCreate write FOnCreate stored IsForm;
|
我們通過給事件屬性變量賦值就可以定制事件處理器。
用戶定義對象(包含回調(diào)方法的對象):
type TCallback=Class
procedure ClickFunc(sender:TObject);
end;
procedure Tcallback.ClickFunc(sender:TObject);
begin
showmessage('the caller is clicked!');
end;
|
窗體對象:
type TCustomFrm=class(TForm)
public
procedure RegisterClickFunc(cb:procedure(sender:Tobject) of object);
end;
procedure TcustomFrm..RegisterClickFunc(cb:TNotifyEvent);
begin
self.OnClick=cb;
end;
|
使用方法:
var
frm:TcustomFrm;
begin
frm:=TcustomFrm.Create(Application);
frm.RegisterClickFunc(Tcallback.Create().ClickFunc);
end;
|
3 回調(diào)在分布式計(jì)算中的應(yīng)用(CORBA)
3.1 回調(diào)接口模型
CORBA的消息傳遞機(jī)制有很多種,比如回調(diào)接口、事件服務(wù)和通知服務(wù)等。回調(diào)接口的原理很簡單,CORBA客戶和服務(wù)器都具有雙重角色,即充當(dāng)服務(wù)器也是客戶客戶。
回調(diào)接口的反向調(diào)用與正向調(diào)用往往是同時(shí)進(jìn)行的,如果服務(wù)端多次調(diào)用該回調(diào)接口,那么這個(gè)回調(diào)接口就變成異步接口了。因此,回調(diào)接口在CORBA中常常充當(dāng)事件注冊的用途,客戶端調(diào)用該注冊函數(shù)時(shí),客戶函數(shù)就是回調(diào)函數(shù),在此后的調(diào)用中,由于不需要客戶端的主動(dòng)參與,該函數(shù)就是實(shí)現(xiàn)了一種異步機(jī)制。
從CORBA規(guī)范我們知道,一個(gè)CORBA接口在服務(wù)端和客戶端有不同的表現(xiàn)形式,在客戶端一般使用樁(Stub)文件,服務(wù)端則用到框架(Skeleton)文件,接口的規(guī)格采用IDL來定義。而回調(diào)函數(shù)的引入,使得服務(wù)端和客戶端都需要實(shí)現(xiàn)一定的樁和框架。下面是回調(diào)接口的實(shí)現(xiàn)模型:
3.1.1 范例
下面給出了一個(gè)使用回調(diào)的接口文件,服務(wù)端需要實(shí)現(xiàn)Server接口的框架,客戶端需要實(shí)現(xiàn)CallBack的框架:
module cb
{
interface CallBack;
interface Server;
interface CallBack
{
void OnEvent(in long Source,in long msg);
};
interface Server
{
long RegisterCB(in CallBack cb);
void UnRegisterCB(in long hCb);
};
};
|
客戶端首先通過同步方式調(diào)用服務(wù)端的接口RegistCB,用來注冊回調(diào)接口CallBack。服務(wù)端收到該請求以后,就會保留該接口引用,如果發(fā)生某種事件需要向客戶端通知的時(shí)候就通過該引用調(diào)用客戶方的OnEvent函數(shù),以便對方及時(shí)處理。
本文源碼 下載。
關(guān)于作者
 |
|
 |
陳家朋,系統(tǒng)架構(gòu)師和技術(shù)顧問,目前擔(dān)任杭州邁可行通信技術(shù)有限公司MPS2000業(yè)務(wù)交換平臺的首席設(shè)計(jì)人員。擅長架構(gòu)設(shè)計(jì)、提供技術(shù)解決方案等工作,熟知計(jì)算機(jī)和通信領(lǐng)域的各種前沿技術(shù)。可以通過 japen@vip.sina.com跟他聯(lián)系。
|