## 客户/服务器远程数据传输处理技巧 ### ----在实际的MIS系统中，远程数据库访问大多通过Modem连接，出于通信费用及速度方面的考虑，往往采用先将数据保存在本地，然后集中传送到远端的办法。远程数据传送可以有多种方案，最常见的是先将要传送的数据打包成文件，在利用文件传输形式传送到目的地，在目的地对数据恢复后添加到本地数据库中。这种方法普遍地应用于证券交易系统，其优点是速度快，并且可事先对数据压缩，更大限度地节约传送时间及费用。但这种方案也有其不足之处：由于利用文件传输机制，无法利用数据库本身的特性如完整性约束、数据一致性、回滚机制等，因此在比较复杂的数据库系统中较少采用。另一种方法是直接将两端处理成"客户/服务器"模式，将数据传送看成是向Server提交数据。由于这种方案充分利用了数据库服务器的特性，并且实际操作基本与局域网方式一致，因此本文将详细介绍这种方案。另外本文的部分内容是基于Delphi/CBuilder的。 ----由于传输速度的原因，当传送大量数据时绝对不赞成逐条记录地向服务器提交数据，而应批量地向Server提交，Delphi/CBuilder中提供了一个TBatchMove控件专门用于批量传送数据，利用它可极大减少网络负担，提高传送速度。遗憾的是，TBatchMove控件只提供了简单的错误控制功能，没有提供显示传送进度、用户终止传送等重要功能。然而TBatchMove所依赖的BDE却提供了一种"回调机制"可以完成上述两个功能。所谓"回调"过程是这样的：当BDE执行某种操作时，比如从一张表向另一张表拷贝大量数据的过程中，每过一段时间（如需要显示拷贝进度时），BDE会调用一段你自己写的函数（回调函数），以帮助你更完全地控制程序。这种做法有点想DLPHI中的Event（事件）及事件处理函数--某个具体的操作动作会让VCL触发某个事件，从而调用一段你写好的事件处理函数，不同的事件会触发不同的处理函数。 ----为了让BDE能正确地与你的函数协同工作，你必须事先"注册"你的函数，让BDE知道某个事件发生时应调用（回调）你的某段代码。BDE提供了一个DbiRegisterCallBack注册函数，不幸的是，BDE的联机帮助中的说明不能适合于Delphi/CBuilder，按照该说明编写的程序根本不能通过编译！笔者通过实践找到了正确使用BDE回调函数的方法，下面将详细介绍该机制的使用。BDE回调机制包含以下几个步骤： ----1）按BDE的预定格式编写你的回调函数 ----2）调用DbiRegisterCallBack函数注册你的回调函数，这样当你执行相关数据库操作时就自然地触发你的回调函数。 ----3）执行相关数据库操作，比如BatchMove1->Exectue(); ----4）注销该回调函数 ----其中最关键的是正确注册你的回调函数，因此先介绍第二步。(注册与注销都调用同一函数，只是最后一个参数略有不同) ----首先你应知道在哪类"事件"发生时调用你的回调函数，其次你应明白与该事件相关的参数及数据结构--这一切都发生在调用DbiRegisterCallBack函数注册时，所以下面先介绍DbiRegisterCallBack的正确用法及说明： ----在原BDE帮助中该函数的原形（C）是这样的
    DBIResult DBIFN DbiRegisterCallBack (hCursor, ecbType, iClientData, iCbBufLen, pCbBuf, pfCb);
----要使用该函数必须include头文件，问题是Delphi/CBuilder中根本没有提供该文件，取而代之的是"BDE.HPP"，但是在包含进该文件后程序仍然不能编译通过，因为该文件中没有DBIFN等的说明。一个简单的方法是在代码中去掉DBIFN。函数中各参数解释如下：hCursor是一个BDE中对象的句柄，如果这个参数为NULL，则表示注册的回调函数适合于所有BDE任务；第二个参数ecbType是指回调函数的触发条件的类别，有很多种类型可以选择，其中cbGENPROGRESS表示当需要显示一个长操作的进度时触发这个回调函数；第三个参数iClientData是传递给回调函数的某个数据结构的指针，在我们的例子中为NULL；第四个参数iCbBufLen是指回调Buffer的大小，该大小随第二个参数的不同而不同，比如sizeof(CBPROGRESSDesc)；第五个参数pCbBuf是回调Buffer的指针，该指针类型随第二个参数变化，比如cbGENPROGRESS的数据结构是CBPROGRESSDesc；最后一个参数是回调函数的地址指针，当该参数为NULL时表示注销该类型的回调函数。关于回调函数将在稍后详细介绍。下面是注册执行长操作时显示进度的回调函数的格式：
    int rst=   DbiRegisterCallBack (NULL,

    //适合于任何进程

    cbGENPROGRESS, //回调类型：显示长操作的进度

    NULL,	//没有数据

    sizeof(CBPROGRESSDesc), //数据结构的大小

    &aCBBuf, 	//数据的内存地址

    ApiCallBackFun  //回调函数的地址

    );
----接下来就应该完成第一步：编写回调函数 ----在C中，回调函数应如下声明：
    CBRType__stdcallApiCallBackFun(

         CBTyp   eecbType,//回调类型

         int  iClientData,//回调数据（指针）

         void  *pCbInfo//回调数据结构指针

    )
----第一个参数是回调类型；第二个参数是回调数据，其解释同DbiRegisterCallBack的第三个参数；第三个是回调数据的指针，该数据的结构随回调类型的不同而不同。比如进度指示cbGENPROGRESS的数据结构是CBPROGRESSDesc，其定义如下：
    struct CBPROGRESSDesc {

     short iPercentDone;	//进度的百分比

     char szMsg[128];  //进度的文本信息

    };
----该结构的两个域同时只有一个起作用，第一个表示操作的进度百分比，当其为-1时表示第二个域起作用。第二个域用字符串表示进度信息，其格式为<String><:><Value>，比如：RecordsCopied:125 ----本文主要在回调函数中完成两个工作： ----1）显示数据拷贝（BatchMove）进度 ----2）提供让用户终止长时间拷贝的机制 ----显示拷贝进度的代码如下：
    CBRType __stdcall ApiCallBackFun(

    CBType       ecbType,    // Callback type

    int       iClientData,  // Client callback data

    void *        pCbInfo   // Call back info/Client)

    {   AnsiString str;

       if(ecbType==cbGENPROGRESS)

          {

           int j=  StrToInt( ((CBPROGRESSDesc*)

           pCbInfo)- >iPercentDone);

           if(j< 0)

           //如果iPercentDone为-1，则分析szMsg的信息

           {

           str=((CBPROGRESSDesc*)pCbInfo)- >szMsg;

           int pos=str.AnsiPos(":")+1;

           //提取出拷贝的记录数

           //下面的代码用来在一个Form中显示拷贝进度及拷贝数量

           Form1- >Label2- >Caption= str.SubString(pos,100);

           Form1- >Label2- >Update();

           Form1- >ProgressBar1- >Position=

           int((str.SubString(pos,100).

           ToDouble()/Form1- >TransNum)*100);

           Form1- >ProgressBar1- >Update();

           }

        else

           {Form1- >ProgressBar1- >Position=j;

           Form1- >ProgressBar1- >Update();

           }

       return cbrCONTINUE;

      //必须返回cbrCONTINUE以便让BatchMove继续

        //若返回cbrABORT则终止拷贝

    }
----一切完成以后，每当调用长时间BDE操作（比如BatchMove1->Exectue()）时都会触发该回调函数，注意在不需要时应"注销"这个回调函数。 ----如果批量传送数据时间很长，则必须为用户提供终止该操作的机会，前面提到，若回调函数返回cbrABORT，则BatchMove过程立即终止。可以在Form上加上一个"停止"按钮和一个全局布尔变量isContinue，当开始拷贝时设该变量为true，当按钮按下后，设该变量为false，每次调用回调函数时检查isContinue的值，若为true则回调函数返回cbrCONTINUE让拷贝继续，否则返回cbrABORT终止拷贝。但是问题在于一旦拷贝过程开始，该进程内所有消息将被阻塞，应用程序在拷贝结束之前没有机会响应键盘、鼠标等一切消息，连屏幕刷新都不能完成，因此必须找到一种避免消息阻塞的方法。 ----大家知道，Windows是靠事件（消息）驱动的，在WIN32系统中有两种消息队列：系统队列和应用程序队列，当一个程序进行一个长时间操作时，系统分配给该程序的时间片将完全用于处理该操作，换句话说，应用程序没有从它的应用程序队列中取出消息并处理的机会，这样该程序将停止一切对外部事件的响应直到该操作完成为止。具体到本文中就是程序必须等到BatchMove1->Execute()执行完毕后才能响应用户操作，因此用户将完全没有机会终止拷贝过程。 ----解决的办法是：在回调函数中取出消息队列中的消息，并后台处理它们，这样用户将有机会按下终止按钮。实现的代码很简单，在回调函数中最后加入以下代码即可
    CBRType __stdcall ApiCallBackFun(…)

    {

    ……

       MSG amsg;

       while(PeekMessage(&amsg,NULL,0,0,PM_REMOVE))

        //从队列中取消息

           {

           TranslateMessage(&amsg); //翻译消息

           DispatchMessage(&amsg); //分发消息

           }

    if (isContinue)

    return cbrCONTINUE;

    else

    return cbrABORT;

    }
----以上的代码虽然都用CBuilder编写，但是其原理同样适用于DELPHI。