COM/DCOM综述

   COM/DCOM综述      
    1.   分布式组件对象模型  
    多少年来软件的开发过程并没有很大的改变,软件开发过程中需要面对的主要问题如:开发周期长,难于确保程序的正确性,难于维护等还没有得到非常好的解决,尽管出现了如面向对象,框架设计等等的概念和工具。组件对象模型是传统面向对象模型的扩充,传统面向对象模型的重点是源程序,以及系统分析和设计过程。组件的概念则强调大的软件系统如何由不同开发商的小型可执行组件构成。以下首先从面向对象模型开始对各种概念作一番梳理。  
    ·   面向对象编程  
    面向对象是一个被广大编程人员和工业界认同已久的概念。面向对象程序设计语言让开发者按照现实世界里人们思考问题的模式来编写程序,它让开发者更好地用代码直接表达现实中存在的对象,这样开发代码简单并且易于维护。面向对象语言有以下三个最重要的概念:  
    封装(Encapsulation)-   强调隐藏对象的实现细节,对象的使用者仅仅通过定义好的接口使用对象。  
    继承(Inheritance)-   新的对象可以利用旧的对象的功能。  
    多态(Polymorphism)-   根据所使用的对象展现多种不同行为的能力。    
    而从程序的整体来看,面向对象编程提供给用户的最重要的概念则是代码的共享与重用,它对于提高编写程序的效率极为重要。但是代码的共享与重用一旦应用于实践中仍然存在种种问题,如版本的升级、接口的变化、在不同程序设计语言之间共享代码等等。对于这些困难原有的面向对象程序设计方法并没有相应的答案,这就是组件对象模型提出的背景。  
    ·   组件对象模型  
    将工程分解为逻辑组件是面向组件分析和设计的基础,这也是组件软件的基础。组件软件系统由可重用的二进制形式的软件组件模块组成,只需要相当小的改动就可以将这些来自不同开发商的组件模块组合在一起。特别重要的是这样的组合并不需要源代码,也不需要重新编译,组件之间通过基于二进制的规范进行通讯,这被称为二进制重用。组件模块是独立于编程语言的,使用组件的客户程序和组件之间除了通过标准的方法进行通讯以外,彼此不做任何限定。  
    组件可以划分为不同的类型,包括可视化组件如按钮或者列表框;功能组件如打印或者拼写检查。例如一个基于组件的架构可以提供将多个开发商的拼写检查组件插入到另一个开发商的字处理应用程序中的机制,这样用户可以根据自己的喜好方便地选择和替换字处理软件。  
    组件结构中最重要的概念是接口。接口是集合在同一个名称(通常是一个系统唯一的ID值)下的相关方法的的集合。组件之间的通讯是基于接口的,接口是组件和其客户之间严格类型化的契约。实现相同接口的两个对象就被认为是多态的,这里的多态不包含诸如基类指针指向派生类对象的意义,这里是指同一个接口可以由多个对象以不同方法实现。    
    2.   COM/DCOM的基本概念  
    ·   概述:  
    以下将通过程序实例解释COM/DCOM的基本概念。基于微软的一贯作风,虽然COM/DCOM自称为是一个可跨平台支持异构的模型(也确实从根本上说是可以跨平台的),但它也是和Microsoft   Windows系统中的其它概念紧密结合在一起的,而且除了Microsoft   Windows系统以外很少有什么系统支持COM/DCOM,所以在以下概念的介绍中将基于Microsoft   Windows系统。COM/DCOM模型主要包括三方面的内容:(A)程序编写的模式。(B)程序交互时遵循的二进制规范。(C)程序运行的辅助环境。首先通过图1描述COM/DCOM基本机制。  
              
    由图可见COM/DCOM是基于客户机和服务器模型的,客户程序和组件程序是相对的,进行功能请求调用的是客户程序而响应该请求的是组件程序。组件程序也可以作为客户程序去调用其它的组件程序,正是这种角色的转换和相互调用关系使组件程序最终构成一个软件系统。根据COM/DCOM中客户程序和组件程序的交互关系可以将组件分为进程内组件和进程外组件两种。所谓进程内组件是指客户程序和组件程序在同一个进程地址空间内;进程外组件指客户程序和组件程序分别处在不同的进程空间地址中。进程内组件是通过将组件作为动态连接库(DLL)来实现的,客户程序将组件程序加载到自己的进程地址空间后再调用组件程序的函数。对于这两种不同的组件,客户程序和组件程序交互的内在方式是完全不同的。但是对于功能相同的进程内和进程外组件,从程序编写的角度看,客户程序是以同样的方法来使用组件程序的,客户程序不需要做任何的修改。因此以下先通过进程内组件的实现来理解COM/DCOM的编程模式。  
    ·   进程内组件:  
    例子程序:  
    以下是一个用C++语言编写的COM程序实例的主要内容:  
    头文件:component.h  
    interface   DECLSPEC_UUID("10000001-0000-0000-0000-000000000001")  
    ISum   :   public   IUnknown  
    {public:  
      virtual   HRESULT   STDMETHODCALLTYPE   Sum(   int   x,   int   y,   int   __RPC_FAR   *retval)   =   0;  
    };  
    客户程序:  
    #include   "component.h"    
    const   CLSID   CLSID_InsideCOM   =   {0x10000002,0x0000,0x0000,  
      {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01}};  
    void   main()  
    {  
      IUnknown*   pUnknown;  
      ISum*   pSum;  
      HRESULT   hr   =   CoInitializeEx(NULL,   COINIT_APARTMENTTHREADED);  
      hr   =   CoCreateInstance(CLSID_InsideCOM,   NULL,    
           CLSCTX_INPROC_SERVER,   IID_IUnknown,   (void**)&pUnknown);  
      hr   =   pUnknown->QueryInterface(IID_ISum,   (void**)&pSum);  
      if(FAILED(hr))  
        cout   <<   "IID_ISum   not   supported.   "   <<   endl;  
      pUnknown->Release();  
      int   sum;  
      hr   =   pSum->Sum(2,   3,   &sum);  
      if(SUCCEEDED(hr))cout   <<   "Client:   Calling   Sum(2,   3)   =   "   <<   sum   <<   endl;  
      pSum->Release();  
      CoUninitialize();  
    }  
    组件程序:  
    #include   "component.h"    
    const   CLSID   CLSID_InsideCOM   =   {0x10000002,0x0000,0x0000,  
      {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01}};  
    class   CInsideCOM   :   public   ISum{  
    public:  
      //   IUnknown  
      ULONG   __stdcall   AddRef();  
      ULONG   __stdcall   Release();  
      HRESULT   __stdcall   QueryInterface(REFIID   riid,   void**   ppv);  
      //   ISum  
      HRESULT   __stdcall   Sum(int   x,   int   y,   int*   retval);  
   
      CInsideCOM()   :   m_cRef(1)   {}  
      private:  
      ULONG   m_cRef;  
    };    
   
    ULONG   CInsideCOM::AddRef()  
      {    return   ++m_cRef;   }  
   
    ULONG   CInsideCOM::Release()  
      {    if(--m_cRef   !=   0)   return   m_cRef;  
        delete   this;  
        return   0;  
      }  
   
    HRESULT   CInsideCOM::QueryInterface(REFIID   riid,   void**   ppv)  
    {  
      if(riid   ==   IID_IUnknown)  
       {    *ppv   =   (IUnknown*)this;   }  
      else   if(riid   ==   IID_ISum){    
         *ppv   =   (ISum*)this;   }  
      else   {    
         *ppv   =   NULL;  
         return   E_NOINTERFACE;  
       }  
      AddRef();  
      return   S_OK;  
    }  
   
    HRESULT   CInsideCOM::Sum(int   x,   int   y,   int*   retval)  
    {    *retval   =   x   +   y;  
      return   S_OK;  
    }  
   
    class   CFactory   :   public   IClassFactory  
    {  
      public:  
        //   IUnknown  
        ULONG   __stdcall   AddRef();  
        ULONG   __stdcall   Release();  
        HRESULT   __stdcall   QueryInterface(REFIID   riid,   void**   ppv);  
        //   IClassFactory  
        HRESULT   __stdcall   CreateInstance(IUnknown   *pUnknownOuter,   REFIID   riid,   void**   ppv);  
        CFactory()   :   m_cRef(1)   {   }  
      private:  
        ULONG   m_cRef;  
    };  
    ULONG   CFactory::AddRef()  
    {    return   ++m_cRef;   }  
    ULONG   CFactory::Release()  
    {  
      if(--m_cRef   !=   0)   return   m_cRef;  
      delete   this;  
      return   0;  
    }  
    HRESULT   CFactory::QueryInterface(REFIID   riid,   void**   ppv)  
    {    
      if(riid   ==   IID_IUnknown)  
      { *ppv   =   (IUnknown*)this;   }  
      else   if(riid   ==   IID_IClassFactory)  
      {  
        *ppv   =   (IClassFactory*)this;  
      }  
      else{    
          *ppv   =   NULL;  
          return   E_NOINTERFACE;  
         }  
      AddRef();  
      return   S_OK;  
      }  
    HRESULT   CFactory::CreateInstance(IUnknown   *pUnknownOuter,   REFIID   riid,   void**   ppv){  
     CInsideCOM   *pInsideCOM   =   new   CInsideCOM;  
     HRESULT   hr   =   pInsideCOM->QueryInterface(riid,   ppv);  
     return   hr;  
    }  
    HRESULT   __stdcall   DllGetClassObject(REFCLSID   clsid,   REFIID   riid,   void**   ppv){  
      if(clsid   !=   CLSID_InsideCOM)  
        return   CLASS_E_CLASSNOTAVAILABLE;  
      CFactory*   pFactory   =   new   CFactory;  
      if(pFactory   ==   NULL)  
        return   E_OUTOFMEMORY;  
      HRESULT   hr   =   pFactory->QueryInterface(riid,   ppv);  
      return   hr;  
    }Top
    由于COM/DCOM系统组件之间通讯是和位置无关的,也即一个使用组件功能的客户程序在编写时不需要考虑组件的位置,组件的定位和通讯由系统完成。因此不妨将客户程序和组件程序分别加以分析。  
    客户端程序:  
    (1)   调用CoInitializeEx初始化。  
    因为程序的很多辅助功能是由库函数和操作系统中的各种服务自动完成的,如组件的定位和加载,并且这些工作很复杂,所以程序需要首先作一些初始化。  
    (2)调用CoCreateInstance创建对象。  
    第1个参数CLSID_InsideCOM是一个128位的标识-类标识符(CLSID),在程序中定义为   {0x10000002,0x0000, 0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01}},今后这样128位的标识在表示时将省略0x并用"-"代替 ",";第4个参数IID_IUnknown也是一个128位的标识-接口标识符(IID);第5个参数(void**)&pUnknown是一个指针,在返回时它指向一个接口实例的指针。  
    CoCreateInstance()是一个库函数,从语义上说它创建对应类标识(CLSID)的一个COM/DCOM对象实例,并获得该对象的一个接口实例指针。对于进程内组件一个COM/DCOM对象是在一个DLL中实现的,在Windows系统注册表中维护着CLSID和DLL文件路径的对应关系,CoCreateInstance首先查找注册表,然后加载对应的DLL程序,调用该DLL的DllGetClaseObject引出函数(任何作为组件的DLL都必须提供该函数)以及其他的一些操作创建一个对象实例。  
    一个COM/DCOM对象对于客户程序唯一可见的是它所包含的一组接口,每一个接口都由128位的IID标识,在整个COM/DCOM系统中都是唯一的(包括分布在不同机器上的COM/DCOM系统)。任何类型的COM/DCOM对象都必须支持IID_IUknown(标识为00000000-0000-0000-C000-000000000046)接口。不同语言编写的客户程序中基本都有某种机制来标识接口指针,COM/DCOM用对象的IID_IUnknown接口指针的值来区分对象而没有独立的对象引用,任何对象在生命期内返回给客户程序的IID_IUnknown接口指针值必须是相同的。注意COM/DCOM对其他的接口指针值没有如上的要求。  
    COM/DCOM规定IID_IUnknown接口由以下三个函数组成:  
    QueryInterface(const   IID   iid,   void   **ppv);  
    AddRef(   );  
    Release(   );  
    而且其他任何接口也必须包含这三个函数。其中AddRef和Release是用来控制对象生存周期的。   QueryInter-   face则达到通过接口标识查询对象实现的接口,COM/DCOM规定通过对象的任何接口的QueryInterface函数可以获得同一个对象的其余接口指针。  
    (3)pUnknown->QueryInterface(IID_ISum,   (void**)&pSum);获得接口标识符为IID_Isum的另一个接口实例指针pSum。  
    (4)hr   =   pSum->Sum(2,   3,   &sum);通过接口实例指针pSum,调用接口的成员函数Sum。从编程模式的角度来看客户程序向组件程序发送功能请求在源程序中最终体现为调用接口的一个成员函数,并且实际上不论对于进程内组件还是对于进程外组件都是同样的。对于进程内组件这一调用就是通过调用同一个进程中的函数实现的,但是必须强调即使是进程内的调用遵循的仍然是二进制的规范,也即客户程序中的pSum指向的内存格式必须满足COM/DCOM的规范,至于这一规范是怎样的将在后文中讲述。对于一个确定的接口(确定值的IID),它的进程内组件不论用什么程序设计语言实现,生成的目标DLL返回给客户程序的接口指针所指向的内存格式都是一样的。  
    (5)CoUninitialize();调用清理函数。  
    通过以上对客户程序的分析,可见客户程序的编程模式为(a)创建对应CLSID的对象实例;(b)获得对象的初始接口指针;(c)通过接口指针的QueryInterfase函数查询其它接口指针;(d)通过接口指针调用接口的函数。(e)通过接口的AddRef()和Release()控制对象的生命期。客户程序和进程内组件程序遵循的二进制规范则体现在(a)128位的类表识CLSID和接口表识IID;(b)组件程序必须是一个合法的DLL,并且引出若干标准的函数如DellGetClassObject。(c)组件程序返回给客户程序的接口指针所指向的内存必须满足COM/DCOM规范。  
    组件程序:  
    现在分析一下组件程序的编写,了解对象是如何实现的。首先察看DllGetClassObject(const   CLSID   clsid,   const   IID   id,   (void   **)ppv)函数,当客户程序加载该DLL后将首先调用该引出函数。该函数是一个进程内组件提供其服务的最基本的入口,也是进程内组件所遵循的二进制规范的一部分。DllGetClassObject的功能是根据CLSID判断本组件是否支持该类型的对象,一个组件可以支持多种类型的对象。DllGetClassObject根据CLSID生成对应的类厂对象,并根据输入参数const   IID   id将类厂对象的对应接口指针通过ppv返回给客户程序。这里的引入了类厂这个在客户程序中未提及的新概念。根据COM/DCOM规范,组件程序必须为自己支持的每个CLSID提供类厂对象,由类厂对象负责创建对应类型的COM/DCOM对象实例。类厂对象提供通常称为IID_IClassFactory的接口(其值为000001-0000-0000-C000-000000000046),客户程序通过调用该接口的CreateInstance(Iunknown   *pUnknown-Outer,   const   IID   iid,   void   **ppv)函数真正创建对象实例并获得对象的第一个接口指针。由此可见客户程序调用CoCreateInstance库函数实际完成了两个步骤的工作,它首先请求组件创建类厂对象,然后又通过类厂对象创建对应CLSID的对象实例。  
    上面组件程序的实例中,在DllGetClassObject函数中通过new   Cfactory创建了类厂对象,在CFactory:: CreateInstance   中通过new   CInsideCOM创建类对象。COM/DCOM对象是以C++对象的形式实现的,接口指针是以C++中的对象指针的形式返回的。对于进程内组件,组件和客户程序在编写时是分别进行的,而在运行时处于同一个地址空间内又以指针的方式进行交互,那么交互的二进制兼容自然是基于内存格式的。  
    在了解进程内组件的编写后,读者最大的疑惑必然是如何确保客户程序和组件程序在彼此独立的编写的过程中(甚至使用不同的语言)如何确保二进制兼容的。如前所述对于进程内组件二进制兼容包含三个方面的内容,128位标识符的识别以及确保组件DLL程序的合法性是很容易做到的,而如何确保在基于内存的交互时接口指针所指向的内存格式符合规范则显得有些复杂。下一节将介绍IDL语言,它是解决以上问题的重要手段。  
   
   
    3.   IDL语言  
    在上面的例子程序中,不论是客户程序还是组件程序都没有使用任何的辅助手段就达到了COM/DCOM所要求的二进制的规范。不难想象:符合一定结构的一般C++程序经过编译后生成的二进制代码是满足COM/DCOM二进制规范的。同样不难想象:为了达到符合COM/DCOM的二进制规范,一种简单的方法就是对于生成目标代码的源程序的格式作一定的限制,对于例子中的C++程序显然只要对实现对象的C++类定义作限制就可以了。同时考虑到COM/DCOM是和编程语言无关的,使用C++的头文件显然是行不通的,因此必须使用一种独立的语言来描述接口,微软选用的语言就是IDL。  
    IDL语言是开放软件基金会(OSF)为分布式计算环境RPC软件包开发的,IDL帮助RPC程序员保证工程的客户机和服务器都遵守同一接口。为了将IDL语言应用于COM/DCOM系统中,微软对IDL语言的语法进行了扩充。IDL本身不是一种编程语言,它是用来定义接口的一种工具,至于对IDL语言的解释由使用它的系统决定。COM/DCOM对IDL语言的解释和COM/DCOM的二进制规范密切相关,而这样的解释和其它利用IDL的系统毫无关系。  
    COM/DCOM通常并不直接将IDL语言定义的接口翻译成二进制代码。C++语言的用户使用微软提供的MIDL.EXE程序可将IDL语言翻译成对应的C++头文件,上面例子程序中的头文件就是由以下的IDL文件生成的,接口ISum   继承了接口IUnknown。接口定义文件精确地描述了接口所包含的函数,函数的参数及参数的类型。  
    import   "unknwn.idl";  
    [   object,   uuid(10000001-0000-0000-0000-000000000001)   ]  
    interface   ISum   :   IUnknown{  
     HRESULT   Sum([in]   int   x,   [in]   int   y,   [out,   retval]   int*   retval);  
    };  
    其中unknwn.idl是系统预定义的,其内容如下:  
    [local,  
    object,  
    uuid(00000000-0000-0000-C000-000000000046),  
    ]  
    interface   IUnknown{  
          HRESULT   QueryInterface([in]   REFIID   riid,   [out,   iid_is(riid)]   void   **ppvObject);  
          ULONG   AddRef();  
          ULONG   Release();  
    }  
    由IDL生成的C++头文件在客户程序和组件程序中分别通过#include被包含。使用由MIDL.EXE翻译而成的C++头文件一方面确保客户程序和组件程序中接口指针所指的内存结构一致,解决了同一编程语言实现的组件间的互操作性;另一方面也确保了接口指针所指的内存结构符合COM/DCOM的二进制规范,解决不同编程语言实现的组件间的互操作性。不过由于微软的MIDL.EXE没有通过IDL文件直接生成其它语言(如VB,   JAVA)相应头文件的功能,这些语言的用户需要其它的工具才能利用IDL,这里不作叙述。  
   
    4.组件对象的继承  
    COM/DCOM模型作为组件对象模型具有对象模型的基本特性,其中对象的封装性,多态性前已做过论述。但对象模型的另一个重要特性---继承性---还没有涉及。COM/DCOM通过包容和聚合提供类似的特性。  
    包容和聚合有一个共同的特点就是对象包容和聚合后必须使客户相信那是一个对象。如前所述,客户程序区别对象的唯一标志是对象的IID_IUnknown接口指针的值,而且通过同一对象的接口的QueryInterface函数必须能够查询到本对象的其它接口。图2表示包容和聚合的实现,对象B实现接口IID_IB和IID_ISum,其中IID_Isum的功能是通过创建另一个CLSID_InsideCOM类型的对象完成的。  
        
    如图所示当采用包容模式时,对象B简单地创建一个CLSID_InsideCom对象,客户程序所有对对象B接口IID_ISum的调用都可以利用CLSID_InsideCOM对象的IID_ISum接口完成。当采用聚合模式时,对象B仍然创建一个CLSID_InsideCom对象,但对象B自己并不实现Top
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章