文章目录
- 1.1 分布式软件的基本概念
- 1.1.1 分布式软件与客户机/服务器模型
- 1.1.2 分布式软件的三层/多层结构
- 1.2 软件构件的基本概念
- 1.2.1 对象到构件的发展
- 1.2.2 软件构件的特性与相关概念
- 1.3 中间件的基本概念
- 1.3.1 中间件的动因
- 1.3.2 中间件提供的支撑
- 1. 提供构件运行环境
- 2. 提供互操作机制
- 3. 提供公共服务
- 1.4 互操作的基本原理与实例
- 1.4.1 互操作的基本原理
- 1.4.2 互操作实例
- 1.4.2.1 数据库设计
- 1.4.2.2 接口定义
- 1.4.2.3 服务端程序
- 1.4.2.4 客户端程序
- 1.4.2.5 编译并运行应用程序
- 1.4.2.6 分布运行
- 1.4.2.7 小结
这里介绍分布式软件的基本概念、软件构件的基本概念、中间件的动因与基本概念;利用 JDK 的远程方法调用 Java RMI
,开发了一个简单的分布式应用程序,通过该例子演示「软件构件与中间件技术为软件开发提供的基本支持」。
1.1 分布式软件的基本概念
1.1.1 分布式软件与客户机/服务器模型
在计算机硬件与网络通信技术的支持下,应用需求驱使计算机软件的规模与复杂度不断增长,软件正变得无处不在,软件所面临的挑战也正在日益加剧,软件开发过程中复杂度高、开发周期长、可靠性保证难等问题日益突出。在这种背景下,软件开发人员不得不在软件开发的过程中寻求更多的支持,以帮助其在特定的开发周期内开发出规模更大、更可靠的软件系统。
这一系列关注在上述背景下大型分布式软件系统的开发支撑。原因主要有两个:
- 随着网络与通信技术的发展,分布式软件的应用越来越广泛,分布式软件在计算机软件应用领域扮演着非常重要的角色。
- 分布式软件一般比集中式软件规模大、复杂,是软件开发复杂性的集中体现。
简单地讲,分布式软件是指运行在网络环境中的软件系统,而网络环境是一群通过网络互相连接的处理系统,每个处理节点由处理机硬件、操作系统及基本通信软件等组成。分布式计算有两种典型的应用途径。
- 第一种应用途径是,将分布式软件系统看作直接反映了现实世界中的分布性,例如:当今许多业务处理流程通常呈现一种分布式运作方式,如某生产制造企业,其负责加工制造的工厂可能位于珠江三角洲一带,而负责销售的部门则可能分别位于北上广,这时负责业务流程的软件系统显然也应做相应的分布式处理。
- 第二种应用途径,主要用于改进某些应用程序的运行性能,使它们比单进程的集中式实现更具有效率,如利用互联网上的大量计算机实现海量数据的科学计算或分析,此时软件系统的分布性并不是现实世界中分布性的映射,而是为利用额外的计算资源而人为引入的。
分布式软件通常基于客户机/服务器 Client/Server
模型。如果一个系统的两个组成部分存在如下关系:其中一方提出对信息或服务的请求(称为客户机),而另一方提供这种信息或服务(称为服务器),那么这种结构即可看作是一种客户机/服务器计算模型。
互联网的许多应用程序都采用客户机/服务器模型,例如 Web 浏览器与 Web 服务器、电子邮件客户程序与服务程序、FTP 客户程序与服务程序等;更一般地,在普通的函数或对象方法调用中,「执行调用语句的子程序」与「实现函数/方法体的子程序或对象」可看作一种客户机/服务器模型,其中实现方是服务器,调用方是客户机。
分布式软件与传统的集中式软件的主要区别在于,强调客户端与服务端在地理位置上的分离,这种分离可带来许多好处:
- 更好的支持平台无关性:客户端和服务端可以运行在不同的硬件(PC、工作站、小型机等)与操作系统(Windows、Unix、Linux等)平台上;
- 更好的可扩展性:可以在服务端功能不变的前提下,对服务端的程序进行改进或扩充,而这种改进或扩充不会影响到客户端的应用程序。
当然,客户端与服务端的分离也使得软件系统更复杂:
- 开发人员不得不分别编写客户端和服务端应用程序,并力求保持两者的一致性;
- 软件系统的调试、部署、维护更加困难;
- 需考虑更多的可靠性、安全性、性能等软件质量因素。
1.1.2 分布式软件的三层/多层结构
早期的分布式系统基于图1-1所示的两层结构。在两层结构中,简要将软件系统划分为服务器层和客户层,服务器层又称为数据层。这时的系统结构比较简单,就是多个客户端程序共享一个数据库。
- 在服务器层,一般放置一个数据库服务器,上面安装一个数据库管理系统,存放系统用到的持久数据。
- 客户层则实现系统的主要业务功能,实现时需要访问数据库中存放的数据,一般会有多个客户端同时访问数据库服务。
两层结构中软件开发的主要工作量在客户层。数据层基本没有什么程序代码,主要就是建好数据库,可能利用存储过程实现一些基本的业务逻辑。开发人员所编写的代码,几乎全部都在客户端,一般可以把客户端的代码分为「用户界面相关的代码」和「业务逻辑相关的代码」,在客户端的代码中要访问数据库中的数据,可以执行一些 SQL 语句或调用存储过程。
两层结构下,客户程序直接访问数据库,并且用户界面代码和业务逻辑代码交织在一起,这些导致两层结构存在以下重要的缺陷:
- 第一,客户端的负担比较重。一般认为,客户端程序只要为使用该系统的用户提供一个人机交互的接口就行了,但在两层结构下,客户端仍然需要进行比较复杂的数据处理,因为客户端从数据库中得到的仅仅是一些原始的数据,必须按照业务逻辑的要求,对这些数据进行一定的处理后才能呈现给用户,所以客户端的负担比较重。
- 第二,客户端的可移植性不好。处理复杂必然牵涉更多的移植性问题。另外,在两层结构下,每个客户端上都要安装数据库驱动程序,移植至少需要重新安装数据库驱动。
- 第三,系统的可维护性不好。因为客户端包含过多的业务逻辑,并且业务逻辑与人机交互界面交织在一起,无论是用户界面需要修改,还是业务逻辑需要修改,都很麻烦。
- 第四,数据的安全性不好。两层结构下,数据库必须为每一个客户端机器开放直接操作数据库的权限,这时就很难防止一个恶意的用户,在某个客户端机器上利用该权限执行其不应该执行的操作。
鉴于以上原因,人们提出了图1-2所示的三层结构:
在 。客户程序与数据库的连接被中间层屏蔽,客户程序只能通过中间层间接地访问数据库,从而降低了客户端的负担、改善了其可移植性,又提高了系统的数据安全性;同时业务逻辑代码与用户界面代码相对独立,也在很大程度上提高了系统的可维护性,较好地解决了两层结构的上述问题。
三层结构中软件开发的主要工作量在中间层,中间层包括除用户界面代码与持久数据之外的几乎所有系统代码,是整个软件系统的核心。
在 层结构中,客户层和数据层已被严格定义,但中间层并未明确定义。中间层可以包括所有「与应用程序的界面和持久数据存储无关的处理」,假定将中间层划分成许多服务程序是符合逻辑的,那么将每一主要服务都视为独立的层,则 层结构就成为了 层结构。典型地,可将业务逻辑层分离出实现数据持久化操作的持久层,用于实现对于持久数据操作的封装,从而形成由客户层、业务逻辑层、持久层与数据层构成的四层结构。
1.2 软件构件的基本概念
本节从分布式对象的角度讨论软件构件的特性。分布式对象是构成分布式系统的软件构件,除了具备一般软件构件的特征外,其重要特征就是分布特性。
1.2.1 对象到构件的发展
按照面向对象的观点,软件系统由若干对象组装而成,将这一观点延伸至分布式系统,分布式系统由若干分布式对象组装而成。面向对象的精髓之一在于,对象维护自身的状态、并通过消息进行通信,这对于分布式计算环境是非常理想的,但是分布式对象与传统对象相比,有其特殊的特性。
首先,分布式系统由于其规模与分布特性等原因,比集中式系统更容易被拆分给不同的人或团队开发,这就很有可能遇到不同的人或团队习惯使用不同的程序设计语言和环境的情况。但是,这些使用不同语言的人(团队)开发的程序要能够方便的交互,换句话说就是,用不同的语言所写的对象要能够方便的交互。而在传统的集中式面向对象系统中,显然对象之间的交互是局限于某种特定的语言的。从语言这个角度我们可以看到,分布式对象比传统对象面临的环境更复杂,这要求分布式对象比传统对象具备更好的封装特性,比如,要把实现所使用的语言屏蔽起来,对象的使用者看到的是一个跨语言的对象。
更进一步讲,分布式系统要求分布式对象可以在任何粒度上透明的使用,也就是说,无需考虑位置与实现,不需要考虑具体在哪——是与使用者在同一个进程内,还是在不同的进程内,如果不同又在哪个进程内,或是在哪个机器上的哪个进程内,或者是运行在什么样的操作系统之上等等。这些具体的位置信息,使用者都不希望去过多的关心,另外也不需要关心对象的具体实现细节——如是用什么语言实现的,是不是用面向对象的语言实现的,还是用一个函数库甚至是一个面向对象数据库实现的,都不用去关心。
传统的面向对象语言中的对象,很难满足上面提到的要求。传统对象的关注点是封装和通过继承对实现进行重用——封装提供了一种「将对象实现细节与其他对象屏蔽开的严格方法」,可以大大缓解在面向过程系统中较突出的维护问题;继承提供了一种重用对象实现的简便方法。而分布式系统要求分布式对象要有更好的可插入性,这个要求仅仅依靠传统面向对象的封装和继承是不可能满足的:
- 首先,要求另一层次上的封装,只需暴露公用接口;
- 其次,从重用的角度来讲,继承局限于程序设计语言,而分布式系统不太关注于直接重用代码,而是要求能够利用远程所实现的服务。这就是说,分布式系统中的分布式对象和传统的对象不一样,实际上是具有良好封装特性的软件构件。
一般地讲,构件指系统中可以明确辨识的构成成分,而软件构件指软件系统中具有一定意义的、相对独立的构成成分,是可以被重用的软件实体,构件提供了软件重用的基本支持。
分析传统工业,如机械、建筑等行业,以及计算机硬件产业,其成功的基本模式是符合标准的零部件(构件)生产与基于标准零部件的产品生产(组装),其中复用是必需的手段,而构件是核心和基础。这种模式是产业工程化、工业化的必由之路,也是软件产业发展的必然途径,这是软件复用与软件构件技术受到高度重视的根本原因。
由于关注分布式系统,对应软件构件也关注分布式构件,因此以后论述中不区分构件与分布式对象这对概念,一般提到的构件均指分布式构件,即分布式对象,反之亦然。
1.2.2 软件构件的特性与相关概念
与对象相比,构件通常具有如下特性:
- 构件是一个严格定义的可插入单元:类似于硬件模块,一旦开发完成,就可以方便的用来组装系统。构件一般是基于对象实现的,但也可以不作为对象实现。
- 构件将封装运用到了极限:构件通过封装来隐藏构件的实现以达到:构件的实现语言是未知的,如一个Java客户端不会感觉到所使用的构件是由C++实现的;构件的物理位置是未知的,一个VB客户端不会感觉到所使用的构件是运行在相同的进程内(使用DLL),还是在同一机器的不同进程内,甚至是位于不同机器上。
- 构件通常在容器中进行管理:按照上面的讨论,构件要屏蔽实现语言、实现方式等很多实现细节,使用一个构件时可以不关心具体实现和位置。这仅是指开发人员不用关心,是不是谁都不用管这些事情,当然不是,如果谁都不去处理不同语言、不同实现方式、不同的物理位置所带来的差异,那用不同语言、不同实现方式实现的、位于不同的物理位置的构件就不可能很好的交互。那具体谁去作这个工作呢?
通常的做法是设置容器来给构件一个运行环境,容器是一种特殊的应用程序,构件在容器中运行和管理。当有客户端要访问某个构件时,实际是向容器发起了一个请求,容器再去判断构件的具体实现语言、实现方式以及构件的具体物理位置,然后帮助请求者与构件进行交互。也就是说,我们所使用的、具备很好特性的构件,实际上是容器和我们用各种语言、方式所编写的程序共同协作的结果。这就要求构件遵循所处的容器的规则,并按照标准的途径向容器发送事件。 - 构件可以从容器中获得属性或服务:如构件可以使用容器的背景色作为自己的背景色。
- 构件允许对所支持的接口进行动态发现和调用:客户程序可以在运行状态下确定一个构件支持何种功能,然后调用该功能。
既然构件具备极好的封装性和可插入性,那么基于构件的系统从可维护性上来讲,就可以达到一种比较理想的状态。如图1-3所示,一个构件化的软件系统在进行维护或升级时,在保持构件接口不变的前提下,可单独对系统中若干个构件进行修改,而不会影响构成系统的其它构件。
下面介绍几个构件相关的概念。
- 接口:接口是系统中用来定义「分布式对象的能力」的约定。由于构件将封装运用到了极限,构件之间相互看到的就只有接口。这里的接口和程序设计语言,如Java中的
interface
所起的作用基本一致,但构件的接口通常是跨语言的。 - 数据类型:分布式对象之间不可避免的要进行交互,而交互的最基本支撑就是要定义在分布式对象之间传输的数据类型。一般说来,现有程序设计语言中已有的数据类型不能直接在分布式系统中使用,因为分布式系统通常涉及多种程序设计语言与多个平台,而不同语言或平台的数据类型往往不能兼容,如整型数在不同的语言或平台上可能字节数或字节顺序不尽相同,因此,一般需要一种独立于语言和平台的数据类型系统。
- 编组与解组
Marshaling and Unmarshaling
:分布式对象之间交互时,由于一般至少跨越了进程边界,因此交换的数据通常要在网络上进行传输,而网上传输的数据只能是串行化的流数据,所以需要把程序员熟悉的有类型的数据、转化成便于网络传输的流数据发送出去,并且需要把网络上接收到的流数据、转化成程序员容易处理的有类型数据,这就是编组与解组的过程。编组过程将数据封装成标准的格式以便于传输,解组过程则负责打开传输来的数据。 - 对象句柄
Object Handle
:在客户程序的编程语言或脚本环境内,对象句柄可用来引用分布式对象的实例。从功能角度上讲,对象句柄类似于面向对象语言中的对象指针或对象引用,但是具体实现上和对象指针或对象引用却存在本质的差别,由于对象句柄是对远端分布式对象的引用,因此不可能像对象指针或对象引用那样,指向对象在内存中的首地址。 - 对象创建
Object Creation
:和面向对象语言中使用一个对象要先new
类似,分布式对象系统必须为创建一个新的分布式对象实例提供一种机制,但是这也不同于简单的new
,要涉及更多的其它工作,分布式系统中经常使用工厂factory
来完成对象创建工作。工厂是一种特殊的分布式对象类型,常用来创建其它的分布式对象。 - 对象调用
Object Invocation
:分布式对象系统必须为分布式对象的调用提供一种机制。通常情况下,开发人员不希望自己写代码编组与解组这些复杂的底层工作,而是希望像使用面向对象语言中的对象那样,实现分布式对象的调用。通过后面的讨论可以发现,有了中间件的支持就可以实现。但是分布式系统的开发人员一定要随时牢记,分布式对象的调用一般会涉及一个多方参与的跨越网络通信的过程,忽略这种区别会给开发的系统带来很大的危害。
1.3 中间件的基本概念
1.3.1 中间件的动因
尽管有了构件技术的支持,但是随着软件系统规模与复杂度的不断提高,软件开发过程中复杂度高、开发周期长、可靠性保证难等突出问题,并没有得到根本缓解;而分布式软件面临更大的挑战,分布式软件所运行的网络环境具有明显的分布性、开放性、演化性、异构性、并发性等特征,因此分布式软件必须解决互操作、数据交换、分布性、可行性等一系列更复杂的问题。
究其本质原因,在于人们控制复杂性的能力相对稳定,但面临的问题却越来越多。在现实生活中,如果遇到一件很复杂的事情要完成,我们往往会寻求工具的支持,很多工具的作用是帮人们完成重复性的、每次手工做起来又很费力费时的工作,在软件开发时解决问题的思路是一致的。
基本的解决思路就是抽取软件的共性成分,抽取的共性成分由系统级软件完成,向开发人员屏蔽系统低层的复杂度,从而在高层保持整体复杂度的相对稳定。在软件领域,这种解决思路往往导致新型系统软件的产生。操作系统与数据库管理系统的产生就是经历了类似这样的过程。
在操作系统出现之前,计算机的初始组成就是“硬件+程序”,即程序直接运行在裸机硬件之上。此时,应用程序直接控制硬件的各种运行细节,应用程序中存在大量的代码用于管理各种物理器件——以访问数据为例,程序必须控制怎样连接磁盘,如何读取其中的数据,如何处理各种异常情况等。这使得程序代码十分庞大,而且正确性难以保证。随着计算机应用的日益广泛,程序的规模不断增大,软件开发变得越来越困难。在这种背景下,人们进行了软件共性的第一次抽取,即抽取出了程序的共性(稳定)成分——计算机资源管理,此次共性的抽取导致了操作系统的产生,分离出了应用程序。初期的操作系统被称为管理程序或监督程序,提供大量的与硬件相关的代码(系统调用)来完成上层应用程序的各种请求,隐藏了与硬件相关的程序执行过程的复杂性,从而简化了应用程序的开发。
操作系统形成之后,计算机的组成变成了“硬件+操作系统+应用程序”。此时,应用程序中访问的数据和应用程序一样以简单文件的方式存储,应用程序的开发人员需要了解数据的具体组织格式,并且需要自己编写程序解决完整性等相关问题。随着应用程序处理的数据规模越来越大,应用程序中数据管理这一共性也越来越明显,即应用程序中普遍存在大量代码实现数据管理功能。于是,人们进行了软件共性的第二次抽取,即抽取出了程序的共性(稳定)成分——数据管理,此次共性的抽取导致了数据库管理系统的产生,分离出了应用软件。数据库管理系统对数据进行统一的管理和控制,并保证数据库的安全性和完整性,为用户屏蔽系统关于数据存储和维护等的细节,从而再次简化了应用程序的开发。
类似的工作仍在继续,在软件系统规模与复杂度不断提高的同时,人们不断从应用软件中提取共性、降低高层复杂性,最终导致了中间件的产生。与操作系统、数据库管理系统类似,中间件是在操作系统(数据库管理系统)与应用系统之间的一层软件,通常为分布式应用的开发、部署、运行与管理提供支持。
1.3.2 中间件提供的支撑
在中间件应用的早期,人们依据所抽取出的、应用软件中的不同共性,设计与实现了多种类型的中间件,一般一种类型的中间件实现一种共性功能,为应用软件提供一种开发支撑。由于所属的具体领域不同,面临的问题差异很大,因此不同开发组织分离、开发出的中间件也不尽相同。以下是几种常见的中间件以及其提供的支持:
- 终端仿真/屏幕转换中间件:用以实现「客户端图形用户接口」与「已有的字符接口方式的服务器应用程序」的互操作。在该种中间件支持下,可以很容易地为原有字符界面的应用程序提供图形用户界面。
- 数据访问中间件:在数据库管理系统的基础上,实现对异构环境下的数据库(或文件系统)联接的中间件,为应用程序访问数据库提供开发支撑。
- 远程过程/方法调用中间件:用以实现远程过程或方法调用的中间件。向应用程序提供远程调用时的底层通信支持,帮助应用程序完成编组与解组等工作,使得程序员方便地编写客户端应用程序,像调用本地过程或对象那样,调用位于远端服务器上的过程或对象方法。
- 消息中间件:为应用程序提供发送和接收异步消息支持的中间件。基于消息的交互方式提供了基本的异步编程模式,即客户端可以通过发送消息来请求某种服务,在服务端处理请求期间,客户端不必等待对方完成,可以执行其它操作,服务端完成后会以消息的形式通知客户端。在消息中间件的支持下,应用程序可以很容易地实现消息的发送和接收,而不必关心消息交换过程中的具体细节。
- 事务(交易)中间件:提供事务控制机制的中间件。事务(交易)管理支持可靠的服务端计算,这在很多关键系统中都是必需的。事务的基本特征是维护一系列操作的原子性,如银行业务系统中转账功能所包含的扣除源账户余额与增加目标账户余额两个操作,这两个操作从业务逻辑上讲应该是原子的,即要么全部都完成,要么全部都不做。基于事务中间件,应用程序可以佷方便地实现事务控制,而不必关心具体事务控制的细节。
- 构件中间件:提供构件化支持的中间件。在分布、异构的网络计算环境中,将各种分布式对象有机地结合在一起,完成系统的快速集成,实现对象重用。
有了各种中间件的支撑,在应用软件中用到中间件对应的功能时,不需要开发人员自己实现,可直接利用中间件,将其已实现好的功能快速集成到应用软件中。
随着中间件应用越来越广泛,又出现了一个新问题:中间件越来越多,开发时需要安装的支撑环境越来越复杂,开发人员不得不花费越来越多的时间,安装与配置需要的各种中间件。因此自然地出现一种中间件集成的强烈需求,在中间件研究的基础上,人们开始考虑将各种中间件的功能集成在一起,现有中间件多以集成中间件的形式出现,集成中间件也称为应用服务器。
现有的集成中间件典型地为三层/多层结构的分布式软件系统提供各种开发支撑,因为三层结构的分布式软件的核心为中间层,因此支撑主要集中在对中间层开发的支撑上。目前应用最广泛的集成中间件有三类:
- 基于OMG
Object Management Group, 对象管理组织
CORBA规范的集成中间件; - 基于Sun JEE
Java Enterprise Edition, Java 企业版
规范的集成中间件; - 基于微软.NET架构的集成中间件
其中前两种所基于的规范均为工业标准,这两种标准得到了产业界众多厂商的广泛支持,因此可供选择的具体中间件产品较多,也是主要的内容关注点。第三种基于微软公司的私有技术,具体的中间件产品基本局限于微软公司的平台,可参考其它相关书籍或资料。为便于论述,在不引起混淆的情况下,用“中间件”一词代表“集成中间件”。
现有中间件为分布式软件系统提供的基本支持,与分布式软件所运行的网络环境密切相关,具体可归为提供构件运行环境、提供互操作机制与提供公共服务三个方面。
1. 提供构件运行环境
现有中间件均提供构件化的基本支持,方便开发与使用符合特定规范的构件(分布式对象)。中间件一般通过构件容器,为构件提供基本的运行环境,具体功能一般包括管理构件的实例及其生命周期、管理构件的元信息等。
2. 提供互操作机制
因为分布式软件跨越了多台计算机,所以需要一种像 TCP/IP 或者 IPX 这样的网络基础设施,来连接应用程序的各节点。现有操作系统(如Unix、Linux、Windows)或高级程序设计语言(如Java、C++),均提供了像套接字 Socket
这样的开发接口,支持编写跨越网络交互的代码。
但是不难发现,基于这些开发接口来实现,需要进行比较复杂的开发与调试,而跨越网络的交互是每个分布式系统必须解决的首要问题。因此,现有的集成中间件,均集成了早期远程过程/方法调用中间件的功能,提供了很强的高层通信协议,以屏蔽节点的物理特性、以及各节点在处理器和操作系统等方面的异构性。基于中间件的互操作支持,开发人员在开发与调用分布式对象时,均不需自己编写处理底层通信的代码。
广泛使用的高层通信协议包括以下几种:
- 远程过程调用
Remote Procedure Call, RPC
:RPC是第一个得到广泛应用的高层通信协议,使用RPC,客户应用程序可以像调用本地过程那样,调用在远程计算机上执行的C语言函数。由于是结构化的,因此目前基本已经被面向对象的通信协议取代。 - IIOP
Internet Inter-ORB Protocol
:IIOP是 CORBA 中使用的一种通信协议,有了它,运行在不同平台上的两个对象可以很方便的进行交互。 - DCOM通信协议:微软在RPC基础上实现的分布式COM构件间使用的通信协议。
- JRMP
Java Remote Messaging Protocol
:特定于 Java 语言的通信协议,支持用Java语言编写的对象之间进行远程交互Java Remote Method Invocation, Java RMI
。 - RMI/IIOP:Java企业版中访问EJB使用的通信协议,基于 IIOP实现。
以上各种协议的共同特征就是,帮助应用程序完成编组与解组等跨越网络通信的底层工作,实现远程过程/方法调用中间件的功能。
3. 提供公共服务
除了互操作的支持外,现有的集成中间件将「早期各种中间件中针对分布式软件的通用支持」集成于一身,以公共服务的形式提供给应用程序。公共服务又称为系统级服务,指由中间件(应用服务器)实现的、应用程序使用的软件系统中共性程度高的功能成分。公共服务有两个基本特征:
- 由中间件而非应用程序实现;
- 应用程序中通常会调用其实现的共性功能
与应用程序中开发人员开发的构件实现的功能不同,公共服务通常不实现应用系统中的具体业务逻辑,而是为具体业务逻辑的实现提供共性的支持,而开发人员开发的构件则实现具体的业务逻辑。
显然,一个中间件平台所提供的公共服务越多,开发者就越容易在更短的时间内开发出高质量的分布式系统。有了中间件提供的公共服务,开发者可以将主要精力集中于系统的具体业务逻辑。以下是几种常见的公共服务:
- 事务服务
Transactions Service
:提供支持事务处理的机制,以保证系统状态与数据的一致性与完整性,支持可靠的服务端计算。 - 安全服务
Security Service
:为系统提供在分布式环境下的安全机制,以防止未授权用户对系统的非法访问。 - 命名服务
Naming Service
:在分布式系统中,命名服务提供了一种定位分布式对象与其它系统资源的机制。 - 持久性服务
Persistence Service
:持久性服务使得分布式对象可以通过持久的数据存储来保存、更新和恢复他的状态。 - 消息服务
Messaging Service
:消息处理服务提供异步编程模式,异步模式在很多应用中都需要。 - 分布式垃圾回收服务
Distributed Garbage Collection Service
:当一个程序不再使用分布式对象时,分布式垃圾回收服务会自动释放分布式对象所占用的存储单元。 - 资源管理服务
Resource Management Service
:一般来说,资源管理器按照使可伸缩性最大化的方式来管理分布式对象,即支持大量的客户程序同分布式对象在短时间内进行交互的能力。
1.4 互操作的基本原理与实例
广义的互操作包括中间层应用构件与数据库、客户层构件与中间层应用构件、中间层应用构件与公共服务构件、中间层应用构件之间的互操作等多个层次,我们主要关注应用构件之间的互操作,即「软件系统开发人员编写的程序」之间的互操作。
1.4.1 互操作的基本原理
上节提到的RPC、IIOP、DCOM通信协议、JRMP、RMI/IIOP等高层通信协议,均可以帮助应用程序完成编组与解组等跨越网络通信的底层工作,实现传统的远程过程/方法调用中间件的功能。
这些高层通信协议尽管具体的实现细节不尽相同,但是在实现方式与开发模式上,均采用了RPC中相同的通信模型与类似的开发模式,它们采用的通信模型称为 Stub/Skeleton
结构,如图1-4所示。
在 Stub/Skeleton
结构中,由客户端桩 Stub
替客户端完成「与服务端程序交互的具体底层通信工作」,客户程序中的远程对象引用实际上是对本地桩的引用;而服务端框架 Skeleton
负责替服务端完成「与客户端交互的具体底层通信工作」。由于客户端桩与服务端框架分别位于客户端与服务端程序的进程内,因此开发人员开发客户端与服务端程序时,只需分别与「本进程内的桩与框架构件」交互,即可实现与远端的交互;而负责底层通信的客户端桩与服务端框架,会在开发过程中自动生成、而非由开发人员编写,从而为开发人员省去底层通信相关的开发工作。
在 Stub/Skeleton
结构的支撑下,客户程序与服务程序按照图中所示的
- ① 客户程序将调用请求发送给客户端桩,对于客户程序来说,桩就是服务程序在客户端的代理。
- ② 客户端桩负责将远程调用请求进行编组,并通过通信总线发送给服务端。
- ③ 调用请求经通信总线传送到服务端框架。
- ④ 服务端框架将调用请求解组,并分派给真正的远程对象实现(服务程序)。
- ⑤ 服务程序完成客户端的调用请求,将结果返回给服务端框架。
- ⑥ 服务端框架将调用结果编组,并通过通信总线发送给客户端桩。
- ⑦ 客户端桩将调用结果解组,并返回给客户程序。
- ⑧ 客户程序得到调用结果。
Stub/Skeleton
结构的支持,使得开发人员从繁杂的底层通信工作中解脱出来,开发人员可以不用编写底层通信代码,即可实现客户端与服务端的远程交互,从而将主要精力集中到业务逻辑的实现上。
1.4.2 互操作实例
下面通过一个具体实例来演示互操作的基本原理。该例子实现远程查询通话记录的功能,客户端远程调用分布式对象实现的查询通话记录功能,分布式对象则通过查询数据库完成具体的查询功能。
该例子程序采用纯Java语言编写,利用 JDBC/ODBC
访问关系数据库,并采用远程方法调用 RMI
实现客户程序与服务程序之间的交互。客户程序只能通过服务程序间接地访问关系数据库,因而该例子程序属于一种典型的三层设计模型,该例子程序的三层结构与每一层实现基本功能如图1-5所示。
本例关注的重点是客户端与中间层分布式对象之间的交互,交互基于 Java RMI
实现,Java RMI
采用 JRMP
协议以支持客户程序访问远端分布式对象。
1.4.2.1 数据库设计
数据库中存放本例中使用的持久数据,即电话用户的通话记录,数据库中的表结构如图1-6所示。其中,TelephoneDirectory
记录了所有用户的电话号码,以 number
为主键;表 CallHistory
记录了所有电话的通话历史,其中 number
是外键,建立该属性的允许重复的索引。表 CallHistory
通过外键 number
与表 TelephoneDirectory
相关联。
本例中利用 JDBC/ODBC
访问数据库,因而支持使用多种不同的数据库管理系统,只要这些数据库管理系统提供了ODBC接口,如Microsoft Access、Microsoft SQL Server、Sybase、Oracle等。
使用ODBC访问数据库之前,必须将数据库配置为ODBC的一个数据源。以采用Microsoft Access数据库为例,主要的步骤为:在Windows的控制面板中打开“数据源(ODBC)”,单击“添加”按钮后选择“Microsoft Access Driver (*.mdb)”驱动程序,然后为数据源命名、并选择相关联的Access数据库文件,如图1-7所示。
为进一步提高程序的数据独立性,还可以利用许多关系DBMS支持的“存储过程”来完成数据的查询与更新操作。数据独立性(逻辑和物理独立性),使得在数据库中表的属性或表之间的关联发生某些变化时,程序员无需对应用程序作任何修改。使用存储过程访问数据库还可带来其他好处,例如提高了SQL语句查询与更新数据库的效率,在网络环境下加强了数据库的安全性等等。在不同的数据库管理系统中,存储过程可能有不同的名字,如保留过程、触发器、查询等。
下面展示了本例中使用的存储过程(即Microsoft Access中的查询),它根据指定电话用户名字的参数(Telephone Subscriber
),查询该用户登记的所有电话的通话记录。本例程序中利用该存储过程实现对数据的查询。
// 查询 QueryCallHistoryWithSubscriber
SELECT TelephoneDirectory.number, CallHistory.startTime, CallHistory.endTime
FROM TelephoneDirectory, CallHistory
WHERE (TelephoneDirectory.number = CallHistory.number) AND
(TelephoneDirectory.subscriber = [Telephone Subscriber])
ORDER BY startTime;
1.4.2.2 接口定义
除了数据库中的持久数据外,本例中需要编写的程序代码包括以下几个主要部分:
- 中间层(服务端程序):
- 实现分布式对象,完成中间层的核心功能,即具体查询数据库、完成查询通话记录功能;
- 服务程序:使用
Java RMI
实现分布式对象时,需要开发人员编写一个服务程序,该服务程序完成「真正提供服务的分布式对象」的创建与注册,服务程序中的「真正提供服务的对象实例」通常又称为伺服对象servant
;
- 客户程序:利用服务程序中伺服对象提供的服务,完成通话记录查询的功能。
在开发服务端程序与客户程序之前,接口是需要首先考虑的问题,因为接口是客户程序与服务端分布式对象之间的约定。在 Java RMI
中分布式对象的接口用Java语言的 interface
定义,并且要求所有分布式对象的远程接口必须继承 java.rmi.Remote
接口,还要求其中的每一个方法必须声明抛出 java.rmi.RemoteException
异常,因为网络通信或服务程序等原因均可能导致远程调用失败。
程序1-1给出了例子程序中通话记录管理器的远程接口定义。定义的接口 CallManagerInterface
约定,服务端的分布式对象提供一个可供远程调用的操作 getCallHistory
,该操作需要一个字符串类型的参数(电话用户的姓名),返回一个 DatabaseTableModel
类型的对象(该用户的通话记录)。
// 程序 1-1 CallManagerInterface.java
// 分布式对象通话记录管理器 CallManager 的远程接口
package Telephone;
public interface CallManagerInterface extends java.rmi.Remote {
// 根据电话用户名字查询通话记录。
// 参数: subscriber - 电话用户的名字
public Database.DatabaseTableModel getCallHistory(String subscriber)
throws java.rmi.RemoteException;
}
1.4.2.3 服务端程序
程序1-2定义的类 CallManager
就是服务端分布式对象的实现,该类实现了远程接口 CallManagerInterface
中约定的 getCallHistory
方法。为防止多个客户程序并发地调用数据库查询操作,方法 getCallHistory()
被定义为同步方法。
在 CallManager
类的实现中,由于 Java RMI
中 Stub/Skeleton
结构的支持,因此在实现 getCallHistory
方法时,并没有因为该方法是被远程调用的方法、而添加任何代码处理底层通信;在下面的实现代码中,除了方法接口处声明抛出 RemoteException
外,该方法的实现与一个普通Java类中方法的实现没有区别。
// 程序 1-2 CallManager.java
// 通话记录管理器(即远程接口 CallManagerInterface 的实现)
package Telephone;
public class CallManager extends java.rmi.server.UnicastRemoteObject implements CallManagerInterface {
// 属性定义
protected Database.DatabaseAccess database;
// 缺省构造方法,必须抛出 RemoteException 异常
public CallManager() throws java.rmi.RemoteException {
database = new Database.DatabaseAccess();
}
// 根据电话用户名字 subscriber 查询通话记录,实现远程接口指定的方法
public synchronized Database.DatabaseTableModel getCallHistory(String subscriber) throws java.rmi.RemoteException {
String sql = ""; // SQL 查询语句
Database.DatabaseTableModel table = null; // 返回的二维表模型
System.out.println("Respond to client request: " + subscriber);
try {
sql = "QueryCallHistoryWithSubscriber('" + subscriber + "')";
java.sql.ResultSet rs = database.callQuery(sql);
table = new Database.DatabaseTableModel(rs);
rs.close();
} catch(java.sql.SQLException exc) {
System.out.println(exc.getMessage());
System.exit(1);
}
return table;
}
}
程序1-3所示 ServerApplication
是服务程序的主程序,它的核心功能是:完成「真正提供服务的分布式对象」的创建与注册。
// 程序 1-3 ServerApplication.java
// 服务程序的主程序
public class ServerApplication {
final static String JDBC_DRIVER = "sun.jdbc.odbc.JdbcOdbcDriver";
public static void main(String args[]) {
// 为 RMI 设置安全管理器
System.setSecurityManager(new java.rmi.RMISecurityManager());
// 加载 JDBC 驱动程序
try {
Class.forName(JDBC_DRIVER);
} catch(ClassNotFoundException exc) {
System.out.println(exc.getMessage());
System.exit(1);
}
// 创建并注册伺服对象
try {
// 创建伺服对象
Telephone.CallManager callManager = new Telephone.CallManager();
// 用名字"CallManagerServant001"注册伺服对象
java.rmi.Naming.rebind("CallManagerServant001", callManager);
} catch(java.rmi.RemoteException exc) {
System.out.println(exc.getMessage());
System.exit(1);
} catch(java.net.MalformedURLException exc) {
System.out.println(exc.getMessage());
System.exit(1);
}
// 提示服务程序就绪
System.out.println("Call manager in the server is ready ...");
}
}
程序1-4定义的 DatabaseAccess
类与程序 1-5 定义的 DatabaseTableModel
类均用于抽象JDBC访问数据库的行为,在实际应用中,通常会设计更完善、更个性化的数据库访问程序包,用来包装JDBC的API。由于本例主要关注客户程序与分布式对象之间的交互,如对Java程序中访问数据库不熟悉或不感兴趣,可略过这两个类的具体实现代码。
DatabaseAccess
主要用于管理服务程序与数据库的连接,并完成数据库的查询与更新操
作。DatabaseTableModel
负责将数据库查询结果转换为二维表数据模型的形式,方便利用 swing
的二维表控件显示查询结果。在基于关系数据库的应用中,二维表控件是最常用的数据表达方式;对于某些具有层次结构的数据,则以树控件表达会更加自然。
// 程序 1-4 DatabaseAccess.java
// 实现 JDBC 与数据库的连接
package Database;
import java.sql.*;
public class DatabaseAccess {
// 常量定义
protected final String DATABASE_NAME = "jdbc:odbc:Telephone";
// 属性定义
protected Connection connection; // 为数据库建立的连接
protected Statement statement; // 将执行的 SQL 语句
protected CallableStatement callable; // 将调用的 SQL 存储过程语句
// 行为定义
// 构造方法,建立与数据库的连接
public DatabaseAccess() {
try {
// 建立与指定数据库的连接
connection = DriverManager.getConnection(DATABASE_NAME);
// 如果连接成功则检测是否有警告信息
SQLWarning warn = connection.getWarnings();
while (warn != null) {
System.out.println(warn.getMessage());
warn = warn.getNextWarning();
}
// 创建一个用于执行 SQL 的语句
statement = connection.createStatement();
callable = null;
} catch(SQLException exc) {
System.out.println(exc.getMessage());
System.exit(1);
}
}
// 析构方法,撤销与数据库的连接。
public synchronized void finalize() {
try {
connection.close();
} catch(SQLException exc) {
System.out.println(exc.getMessage());
System.exit(1);
}
}
// 利用存储过程执行数据库查询操作。
// 参数: procedure - 存储过程名字
// 返回: 查询结果集
public synchronized ResultSet callQuery(String procedure) throws SQLException {
callable = connection.prepareCall("{call " + procedure + "}");
ResultSet rs = callable.executeQuery();
return rs;
}
}
// 程序 1-5 DatabaseTableModel.java
// 根据数据库查询结果构造供 JTable 控件使用的二维表数据模型
package Database;
import javax.swing.*;
import java.sql.*;
import java.util.Vector;
public class DatabaseTableModel extends javax.swing.table.AbstractTableModel {
// 属性定义
protected String[] titles; // 列标题
protected int[] types; // 各列的数据类型
protected Vector data; // 二维表的数据
// 构造方法,根据 SQL 查询结果集 rs 构造二维表
public DatabaseTableModel(ResultSet rs) {
try {
// 取得结果集的元数据
ResultSetMetaData meta = rs.getMetaData();
int columnCount = meta.getColumnCount();
// 取所有列标题与类型名字(注意 JDBC 元数据的下标从 1 开始计数)
titles = new String[columnCount];
types = new int[columnCount];
for (int index = 1; index <= columnCount; index++) {
titles[index - 1] = meta.getColumnName(index);
types[index - 1] = meta.getColumnType(index);
}
// 逐行取结果集中的数据
data = new Vector(1000, 100);
while (rs.next()) {
Vector row = new Vector(30);
for (int index = 1; index <= columnCount; index++)
row.addElement(rs.getObject(index));
row.trimToSize();
data.addElement(row);
}
data.trimToSize();
} catch(SQLException exc) {
System.out.println(exc.getMessage());
System.exit(1);
}
}
// 实现 AbstractTableModel 遗留的抽象方法。
public int getRowCount() {
return data.size();
}
public int getColumnCount() {
return titles.length;
}
public String getColumnName(int col) {
return titles[col];
}
public Object getValueAt(int row, int col) {
return ((Vector) data.elementAt(row)).elementAt(col);
}
public void printData() {
int i, j;
String ss = "";
for(j = 0; j < getColumnCount(); j++)
ss = ss + getColumnName(j) + "\t";
System.out.println(ss);
for(i = 0; i < getRowCount(); i++) {
ss = "";
for(j = 0; j<getColumnCount(); j++)
ss = ss + getValueAt(i, j).toString() + "\t";
System.out.println(ss);
}
}
}
1.4.2.4 客户端程序
本例中,客户程序完成的功能为调用中间层的分布式对象,并将结果显示在客户端界面上。
在 Java RMI
中,客户程序通过 Java RMI
的命名服务,查找希望使用的分布式对象。程序1-6给出的查询程序 Client.java
中,用于解析远程对象的对象标识为 "CallManagerServant001"
,它必须与「服务程序注册远程对象时采用的名字」完全相同,同时,构造远程对象的 URL
时必须包含主机名。
与实现服务端分布式对象时类似,由于 Java RMI
中 Stub/Skeleton
结构的支持,因此在实现客户端程序时同样不需编写(底层通信代码)。注意,程序中远程对象 callManager
必须声明为远程对象接口 CallManagerInterface
的实例,而不是远程对象实现类 CallManager
的实例,因为客户程序只能看到远端分布式对象(构件)的接口。
// 程序 1-6 Client.java
// 客户端程序
package Telephone;
public class Client {
// 实现 main 方法,根据参数调用分布式对象的 getCallHistory 完成通话记录的查询
public static void main(String[] args) {
String name = args.length > 0 ? args[0] : "songshengli";
Database.DatabaseTableModel result = null;
try {
// 从 Applet 取主机名
String host = "//localhost/";
// 远程对象的标识必须与服务程序注册时使用的对象标识完全相同
String objectId = "CallManagerServant001";
// 根据主机名与对象标识解析远程对象
CallManagerInterface callManager = (CallManagerInterface) java.rmi.Naming.lookup(host + objectId);
// 调用远程对象方法查询用户的通话记录
result = callManager.getCallHistory(subscriber);
System.out.println(result.toString());
} catch(Exception exc) {
System.out.println(exc.getMessage());
System.exit(1);
}
}
}
1.4.2.5 编译并运行应用程序
Java语言要求程序包与子目录相对应(类名与源代码文件名相对应),因而在编写Java 程序之前,就必须决定程序包与子目录的名字。表1-1总结了本小节例子程序的完整目录组织与文件清单。
表1-1中的Java字节码,由JDK提供的 javac
编译器生成,而远程对象在客户端的桩与服务端的框架,必须另由 rmic
编译器生成,rmic
的输入是 Java
字节码文件、而不是 Java
源程序文件。具体的编译过程如下:
# 编译 DatabaseAccess 类与 DatabaseTableModel 类
D:\TelephoneExample> javac Database\*.java
# 编译 CallManagerInterface 接口与 CallManager 类
D:\TelephoneExample> javac Telephone\*.java
# 生成客户端桩 CallManager_Stub 与服务端框架 CallManager_Skel
D:\TelephoneExample> rmic Telephone.CallManager
# 编译 ServerApplication 类与 Client 类
D:\TelephoneExample> javac *.java
如果开发者不是直接使用JDK,而是使用某种集成化开发环境(如 NetBean、Borland JBuilder、Eclipse等),则利用项目 Project
机制,可减轻许多编译工作的负担。开发环境会根据项目内容,自动调用相应的编译器生成字节码文件、以及客户程序桩与服务程序框架文件。
完成例子程序的编译与布置后,就可以开始运行例子程序了。首先启动 RMI
远程对象注册表,然后启动服务程序。例如在Windows中可在 MS-DOS
中键入以下命令:
# 启动 RMI 远程对象注册表
D:\TelephoneExample> start rmiregistry
# 启动服务程序
D:\TelephoneExample> start java ServerApplication
# 运行客户端程序
D:\TelephoneExample> java Client
客户端与服务端程序的输出界面如图1-9所示:
1.4.2.6 分布运行
此例子程序可以很容易地将客户端与服务端,分布到两个不同的物理机器上运行,由于Java良好的可移植性,除了Windows 平台,客户端程序还可运行在安装JDK的LINUX、UNIX等其它操作系统之上,由于服务端使用 JDBC/ODBC
访问数据库,因此可让其仍运行在Windows操作系统之上。
跨机器运行时,我们需要修改客户端程序中查找分布式对象的代码,使其查找运行在服务端机器上的分布式对象,而不是本机上的分布式对象。具体的修改就是,在调用命名服务的 lookup
方法中,将对应的主机名改为「服务程序运行的机器的主机名或 IP 地址」,如程序1-7所示中的黑体部分所示:
// 程序 1-7 Client.java
try {
// 从 Applet 取主机名
String host = "//serverhost/"; // 改为服务程序运行机器的主机名或 IP 地址
// 远程对象的标识必须与服务程序注册时使用的对象标识完全相同
String objectId = "CallManagerServant001";
// 根据主机名与对象标识解析远程对象
CallManagerInterface callManager = (CallManagerInterface)
java.rmi.Naming.lookup(host + objectId);
// 调用远程对象方法查询用户的通话记录
result = callManager.getCallHistory(subscriber);
System.out.println(result.toString());
} catch(Exception exc) {
System.out.println(exc.getMessage());
System.exit(1);
}
1.4.2.7 小结
在本例中,我们实现了一个简单的三层结构的分布式应用:
- 数据层:存放系统的持久数据——通话记录,并提供一个存储过程以提高数据独立性。
- 中间层:提供分布式对象,实现系统的核心功能——通话记录查询,并编写了一个服务程序将分布式对象准备好(创建并注册)。
- 客户层:调用分布式对象的功能,将结果呈现给用户。
在上述三层结构中,重点是中间层分布式对象与客户端程序通信的实现,即基于 Java RMI
的 Stub/Skeleton
结构实现的远程交互。在上述开发过程中,rmic
命令的执行,生成了负责客户端底层通信的 CallManager_Stub
与负责分布式对象底层通信的 CallManager_Skel
1,因此,尽管没有编写任何代码、处理跨越机器的通信,我们的程序也能很容易分布到两个物理机器上运行。
在实际的复杂分布式系统中,特别是当要解决的问题变得更庞大、更复杂的时候,除了分布式对象与其使用者之间的基本通信问题外,我们往往需要寻求中间件提供的更多支持,典型的主要包括如下两个方面:
- ① 更复杂的可互操作性:除了跨越机器与操作系统之外,复杂的分布式系统中,基于网络交互的双方经常出现跨越其它异构环境的进行交互的需求,如交互双方采用不同的程序设计语言实现等。而
Java RMI
的实现约束程序员只能使用Java语言,如果需要实现更复杂的互操作,我们需要寻求其它中间件的支持。 - ② 公共服务:大规模的分布式系统经常需要公共服务中共性的功能,如系统中分布式对象数量很多时,我们需要更有效的命名服务来查找定位分布式对象,需要更复杂的伺服对象管理机制来管理大量的分布式对象,需要资源管理机制、以使得关键的系统资源能够在大量的对象中方便、高效的共享;当分布式对象的操作可能修改系统的核心业务数据时,我们需要事务控制机制以保证系统状态的一致性;当应用在网络中发布时,我们需要有效的安全控制,以保证只有合法的授权用户才能执行特定的操作等等。JDK并没有对公共服务提供更多的支持,如果需要,我们也要寻求其它中间件的支持。
后续内容将以 CORBA
与 Java
企业版中间件为核心,重点讨论:
- 如何开发与使用符合对应规范的分布式对象(构件);
- 如何获得更好的可互操作性;
- 如何使用中间件提供的公共服务等内容
- 在新版本的JDK中,可能会发现
rmic
只会生成一个负责客户端底层通信的Stub
类,而不会生成负责服务端底层通信的Skeleton
类,这主要是因为随着研究的进展,技术上已经实现通用的Skeleton
,即一份代码即可为所有的分布式对象实现底层通信的功能,而由于每个分布式对象对客户端提供的接口不尽相同,为保持客户端编程的简单性,每个分布式对象仍需一个Stub
类。 ↩︎