The Internet Communications Engine(Ice)是一个面向对象的RPC框架,它可帮助您以最小的工作量构建分布式应用程序。其负责与底层网络编程接口的所有交互,使得开发者能够将精力集中在应用程序的逻辑处理上,而无需关心诸如打开网络连接、网络传输数据序列化与反序列化、失败重连等细节的实现。
Ice的主要设计目标是:
简单来说,Ice的设计目标就是“让我们构建一个强大的中间件平台,让开发人员的生活更轻松。”
Ice是一个面向对象的中间件平台。从根本上说,这意味着Ice为构建面向对象的客户端 - 服务器应用程序提供了工具、API和库支持。Ice应用程序适用于以下异构环境下的开发:
无论部署环境如何,这些应用程序的源代码都是可移植的。
client(客户端)与server(服务器)不是应用程序特定部分的名称; 它们表示的是在请求期间应用程序的某些部分所扮演的角色:
在这里,服务器不是只响应请求而从不发出请求的“纯”服务器。相反,其通常充当某个客户端的服务器,但反过来,又充当了另一个服务器的客户端,以满足这个客户端的请求。
同样的,客户端通常也不是一个只请求服务的“纯”客户端。相反,客户端通常会是客户端+服务器的混合体。例如,客户端可能会在服务器端长时间的进行操作; 在操作开始时客户端可以向服务器提供一个callback回调,这样服务器在操作完成时就可以使用该callback通知此客户端。在这种情况下,客户端在启动操作时充当客户端,在通知操作完成时充当服务器。
这种角色转换在许多系统中很常见,所以通常情况下,Client - Server系统可以更准确地描述为P2P(peer-to-pper)系统。
Ice Object(Ice对象)是一种抽象概念。其通常有以下几个特点:
实际上,您不需要使用这些全局唯一的对象标识(例如UUID),只需使用那些不与其他标识冲突的标识即可。然而,这就是使用全局唯一标识符所具有的架构上的优势,详细细节我们会另起篇幅讲述。
要使客户端能够联系Ice对象,客户端必须拥有Ice对象的一个proxy代理。代理运行于本地客户端的地址空间;它代表了客户端的(也可能是远程的)Ice对象。
代理充当Ice对象的本地特使一样的角色,当客户端调用代理的某个方法时,运行过程如下:
代理封装了要执行的这一系列步骤的所有必要信息。特别是,代理包含:
代理中的信息可以表示为字符串。例如,字符串:
SimplePrinter:default -p 10000
上述是proxy的可读表示。Ice run time提供了API允许您将代理转换为其字符串形式,反之亦然。正因此,这些API可用于将代理存储在数据库表或文本文件中。
如果客户端知道Ice对象的标识信息及寻址信息,则可以通过这些信息“凭空”创建一个代理。换句话说,代理中的任何信息对开发者而言都是透明可见的; 客户端只有知道了一个Ice对象的标识信息、寻址信息和对象类型才能联系上这个Ice对象。
直接代理是嵌入了Ice对象标识的代理,其运行于server地址空间。地址完全由以下内容指定:
要联系直接代理表示的Ice对象,Ice run time使用了代理中的寻址信息来联系这个服务器; 客户端发出的每个服务请求都会将这个Ice对象的标识发送给服务器。
间接代理有两种类型。它可以仅提供对象的标识,或者它可以与Ice对象适配器标识符一起指定标识。仅使用其标识可访问的对象称为已知对象,相应的代理是众所周知的代理。例如,字符串:
SimplePrinter
上述就是一个已知Ice对象的有效代理,其标识为SimplePrinter。
间接代理包含了一个具有字符串形式的对象适配器标识符。
SimplePrinter@PrinterAdapter
无论该对象是否是一个已知的对象,都可以使用像上述这样的代理来访问对象的适配器。
需要注意的是,间接代理并不包含寻址信息。要找到要连接的服务器,客户端需要将间接代理的信息传递给定位器。这样,定位器就会使用对象标识或对象适配器标识作为键去包含服务器地址的表中查找对应的服务器地址,并将当前服务器地址作为结果返回给客户端。经此以后,客户端就知道如何联系服务器并像往常一样向其发起服务请求。
整个过程类似于域名与其IP地址在DNS服务器中的映射那样:当我们使用域名(例如 www.zeroc.com)浏览网页时,主机名首先被解析为IP地址,一旦知道了对应的IP地址后就用于连接我们要浏览的服务器。对于Ice而言,这种映射关系就变成从对象标识或对象适配器标识符到协议地址对,但在其他方面与上述非常相似。客户端知道如何通过配置联系定位器(就像Web浏览器知道如何通过配置使用哪个DNS服务器一样)。
将代理中的信息解析为协议地址对的过程被称之为binding(绑定)。显然,直接绑定用于直接代理,间接绑定用于间接代理。
间接绑定的主要优点是它允许我们移动服务器(即更改其地址),而不会使客户端持有的现有代理无效。换句话说,直接代理可以避免额外查找服务器的开销,但如果将服务器移动到其他机器上则之前的直接代理就会失效。反之对于间接代理,即使我们移动(或迁移)了服务器,其也还会继续工作。
固定代理是指绑定到专用连接(不包含寻址信息或适配器名称,而是包含一个连接句柄)的代理。只要连接保持打开,连接句柄就会保持有效,因此,一旦连接关闭,代理将不再起作用(并且将永远不会再次工作)。固定代理不能被封装,也就是说,它不能作为方法调用的参数传递。固定代理用于允许双向通信,因此服务器可以在不必打开新连接的情况下对客户端进行回调。
路由代理把所有方法调用转发到一个特殊的目标对象,而非直接发送给实际目标。路由代理对于实现Glacier2等服务非常有用,它使客户端能够与防火墙后面的服务器进行通信。
在Ice中,Replication使对象适配器(及其对象)在多个地址可用。Replication的目标通常是为分布式服务器环境提供冗余机制。如果环境中某台计算机碰巧发生故障,则其他计算机仍然可用。
Replication的使用意味着应用程序是为它而设计的。这意味着客户端通过某地址访问对象而获得的结果与通过任何其他地址访问获取的结果相同。当然,这并不是说这些对象是无状态的,而是说它们为了保持状态上的一致,被设计成要与数据库进行状态上的同步。
当代理为对象指定多个地址时,Ice支持有限形式的Replication。Ice运行库为其初始化连接尝试随机选择其中一个地址,并在出现故障时尝试所有地址。例如以下这个代理:
SimplePrinter:tcp -h server1 -p 10001:tcp -h server2 -p 10002
此代理指明具有标识符SimplePrinter的对象可通过两个地址之一进行TCP通信,一个是服务器server1,另一个是服务器server2。用户或系统管理员只需要确保这些服务器的指定端口运行了相关的服务即可。
除了上面描述的基于代理的复制之外,Ice还支持一种更有用的复制形式,称为复制组,需要使用位置服务。
复制组具有唯一标识符,由任意数量的对象适配器组成。一个对象适配器至多可以是一个复制组的成员; 这样的适配器被认为是复制的对象适配器。
复制组被建立之后,复制组的标识符可以在间接代理中用来代替对象适配器的标识符。例如,PrinterAdapters标识符就能作为一个代理像如下所示那样被使用:
SimplePrinter@PrinterAdapters
复制组被位置服务视为“虚拟对象适配器”。当解析一个包含复制组的间接代理时。例如,位置服务可以决定是否返回复制组中所有对象适配器的地址,在这种情况下,客户端Ice运行库可以选择使用了前面讨论的有限复制形式地址之一随机返回。而当使用了探索式算法时位置服务只会返回一个地址。
无论位置服务解析复制组的方式如何,关键优势都是间接性:位置服务作为中间人可以使绑定的过程变得更加智能。
正如我们所提到的,Ice Object是具有类型、标识符和寻址信息的概念实体。然而,客户端请求最终必须由可以供具体方法调用的服务器来处理。换句话说,客户端请求最终必须在服务器内由代码执行,该代码以特定编程语言编写并在特定处理器上执行。
提供了具体方法调用行为的服务器模块就被称为Servants。Servants为Ice对象提供了实现基础。实际上,Servants只是由服务器开发者编写的类的实例,就类似于注册在服务器Ice运行库上的Ice对象的雇员一样(顾名思义了)。Servants的方法即实现自Ice对象的接口,并提供具体实现。
一个Servants可以化身为一个Ice对象或者同化身为多个Ice对象。如果是前者,这个Ice对象的标识符就隐式的包含在了这个Servants中。如果是后者,则每次请求都要提供Ice对象的标识符,因此,它才可以知道其在请求期间要化身为哪个Ice对象。
相反的,单个Ice对象可以有多个Servants。例如,我们可以为包含不同机器不同地址的Ice对象创建一个代理。在这种情况下,我们将有两个服务器,每个服务器的Servants都指向了同一个Ice对象。当客户端在这样的Ice对象上调用方法时,客户端运行库将请求发送到其中一个服务器。换句话说,单个Ice对象的多个Servants允许您构建冗余系统:客户端运行时尝试将请求发送到一个服务器,如果尝试失败,则将请求发送到第二个服务器。仅当第二次尝试也失败时,才会向客户端应用程序代码报告错误。
Ice请求具有至多一次语义:Ice运行库尽最大努力将请求传递到正确的目标,并且根据具体情况,可能会重试失败的请求。。Ice保证它将发送请求,或者,如果在无法发送请求给服务器时,则以适当的异常通知客户端; 在任何情况下请求都不会发送两次,也就是说,只有在知道先前的尝试确实失败时才会重试。
此规则的一个例外是UDP传输上的数据报调用。对于这些重复的UDP数据包可能导致其违反了至多一次的语义。
至多一次的语义很重要,因为它们保证可以安全地使用非幂等的操作。幂等操作是这样一种操作,如果执行两次,则具有与执行一次相同的效果。例如,x = 1;是一个幂等操作:如果我们执行两次操作,最终结果就像我们执行一次一样。另一方面,x++就不是幂等的:如果我们执行两次操作,那么最终结果与我们执行过一次的结果不一样。
如果没有至多一次语义,我们可以构建在出现网络故障时更健壮的分布式系统。然而,现实系统需要非幂等操作,所以至多一次语义是有必要的,即使它们在存在网络故障时系统不那么健壮。Ice允许您将单个方法标记为幂等操作。对于此类方法,Ice运行库将使用比非幂等操作更积极的错误恢复机制。
默认情况下,Ice使用的请求调度模型是一个同步的RPC模型:方法调用的行为类似于本地调用,也就是说,客户端线程在调用期间被挂起,并在调用完成时被唤醒(并且其结果可用)。
Ice还支持异步方法调用(AMI):客户端可以异步调用方法,这意味着客户端的调用线程在等待调用完成过程中不会被阻塞。客户端传递正常参数,并且根据语言映射,还可能传入一个Callback,当服务器调用完成时回调给客户端运行库;或者此次调用返回一个客户端最终可以获取结果的future任务。
服务器无法区分异步调用和同步调用 - 无论哪种方式,服务器只是看到客户端调用了Ice对象的方法实现。
异步方法调度(AMD)是AMI的服务器端等价物。对于同步调度(默认),服务器端运行库上行调用服务器中的应用程序代码以响应操作调用。当操作正在执行(或者正在休眠,例如,因为它正在等待数据)时,执行的线程被锁在在服务器中; 该线程仅在操作完成时被释放。
使用异步方法调度,服务器端应用程序代码被告知操作调用何时来到。但是,服务器端应用程序可以选择延迟处理请求,而不是强制立即处理请求,并在通知后释放请求的执行线程。此后服务器端应用程序代码就可以随心所欲地执行其他操作。最终,一旦操作结果出来了,服务器端应用程序代码就会进行API调用,以下行通知服务器端Ice运行库先前调度的请求现已完成; 此时,操作结果将被返回给客户端。
如果服务器需要在某段时间内阻止客户端的操作,则AMD很有用。例如,服务器可能有一个带有get操作的对象,该操作需要从外部异步数据源返回数据,此刻就需要阻塞客户端调用直到此数据可用为止。通过同步调度,正在等待数据到达的客户端会占用服务器中的执行线程。显然使用同步调度方式的客户端不会太多(最多十几二十个)。使用异步调度,则服务端可因某调用而阻塞住成百上千的客户端,而这些被阻塞的客户端不会占用服务器中的任何线程。
同步和异步方法调度对客户端来说都是透明的,也就是说,客户端无法判断服务器是选择同步还是异步处理请求。
客户端可以单向调用一个方法。单向调用具有“尽力而为”的语义。对于单向调用,客户端运行库将此次调用交给本地传输,并且一旦本地传输出去此次调用,就意味着在客户端调用完成。然后,操作系统异步发送此实际调用。但服务器并不回复单向调用,即调用流程仅从客户端流向服务器,反之亦然。
单向调用是不可靠的。例如,目标对象可能不存在,在这种情况下,调用仅仅是丢失了。类似地,此次调用可能被分发给服务器中的某Servants,但其操作可能失败(例如,因为参数值无效); 如果是这样,客户端不会收到任何出错的通知。
单向调用仅仅存在于那些没有返回值、出参、异常抛出的方法中。
对于服务器端的应用程序来说,单向调用是透明的,也就是说,服务器无法区分来自客户端的双向调用和单向调用。
仅当目标对象提供面向流的传输(例如TCP / IP或SSL)时,单向调用才可用。
请注意,即使单向调用是通过面向流形式的传输,它们也可能在服务器中无序处理。这可能发生的,因为每次调用都是在自己的线程中调度:即使调用时按顺序初始化并到达服务器,这并不意味着它们将按顺序处理 - 线程调度的变幻莫测可能导致某单向调用比其他更早到达的调用先完成。
每次单向调用都会向服务器发送一次单独的消息。对于一系列短消息,这样做的开销很大:客户端和服务器端的运行库必须为每个消息在用户模式和内核模式之间切换,并且在网络传输来说,每个消息都会产生流量的开销,如额外的消息控制和确认。
批处理单向调用允许您将一系列单向调用作为单个消息发送:每次调用批处理单向操作时,方法调用就缓存在客户端运行库。一旦缓存中累积够了你想要发送的所有单向调用,你就可以调用一个可以将这些调用发送出去的单独的API。然后,客户端运行库就将所有缓冲的调用以单条消息形式发送出去,服务器接收这条包含所有调用的消息。这避免了为客户端和服务器反复进入内核的开销,并且在网络上也更容易处理,因为一个大消息可以比许多小消息更有效地传输。
在单向批处理消息中的各个调用在服务器中由单个线程负责调用,这些单向调用将依照其被放入的顺序处理。这保证了服务器可以按顺序处理批量单向消息中的各个操作。
批量单向调用对于消息服务(如IceStorm)非常有用,同时对于那些提供密集set属性方法的接口而言也是有用的。
数据报调用也具有类似于单向调用那样的“尽力而为”的语义。但是,数据报调用要求对象提供UDP协议作为传输基础(而单向调用需要TCP/IP协议)。
与单向调用一样,只有那些没有返回值、出参或异常的方法能进行数据报调用。数据报调用使用UDP协议来调用方法。一旦本地UDP堆栈接收了该消息,该方法就返回; 而实际方法调用则由幕后的网络堆栈异步发送完成。
与单向调用一样,数据报调用是不可靠的:目标对象可能不存在于服务器中,服务器也可能未运行,或者此调用可能在服务器中执行了但由于客户端发送的参数无效而失败。类同单向调用,客户端不会收到此类错误的通知。
但是,与单向调用不同,数据报调用有以下几类错误情形:
数据报调用非常适合LAN上的小消息,其中丢失的可能性很小。它们还适用于低延迟比可靠性更重要的情况,例如快速、交互式的互联网应用。最后,数据报调用可用于同时向多个服务器多播消息。
同批量单向调用一样,批量数据报调用允许您在缓冲区中累积多个调用,然后通过API调用来刷新缓冲区,将整个缓冲区内容作为单个数据报发送。批量数据报减少了重复系统调用的开销,并使得底层网络更有效率地运行。但是,批量数据报调用仅对总大小基本上不超过网络PDU限制的批处理消息有用:如果批量数据报的大小太大,UDP分段会使一个或多个片段丢失的可能性更大,这将导致丢失整个批量消息。但是,您可以保证批量中的所有调用都将被传递,或者都不被传递。批量消息中的单个调用不可能单独丢失。
批量数据报在服务器中也是使用单个线程来调度各个单向调用的。这保证了调用按照它们发送的顺序进行 - 调用不会服务器中重新排序。
任何方法的调用都可能引发运行时异常。运行时异常由Ice运行库预定义,并涵盖常见错误类型,例如连接失败、连接超时或资源分配失败。运行时异常是作为本地异常出现在应用程序中,因此,其可以与那些支持异常处理的语言的异常处理功能巧妙集成。
服务器通过向客户端引发用户异常来指示这个客户端应用程序的错误原因 用户异常可以携带任意数量的复杂数据,并且可以安排到异常继承层次结构中,这使得客户端可以通过捕获继承层次结构中的异常来轻松地处理错误类别。与运行时异常一样,用户异常映射为客户端本地异常。
Ice运行库多数时间都是通过属性来进行配置的。属性是键值对,例如Ice.Default.Protocol=tcp。属性通常存储在文本文件中,并由Ice运行库解析,以配置各种选项,例如线程池大小,跟踪级别和各种其他配置参数。
每个Ice对象都有至少一个包含许多方法的接口。这些用于在客户端和服务器之间通信的接口、方法和数据类型都是使用Slice语言定义的。Slice允许您以独立于特定编程语言(如C ++,Java或C#)的方式定义客户端 - 服务器契约。这些定义好的Slice由编译器编译为特定编程语言的API,也就是说,这部分由特定的接口和类型组成的API是由编译器自动生成的。
Slice如何被翻译成特定编程语言的规则称为语言映射。例如,对于C ++语言映射而言,一个Slice序列以 std::vector的形式出现,而对于Java映射来说,Slice序列表现为Java数组。为了明确Slice对不同的语言表征,您需要了解Slice及其语言映射的知识与定义。当然,这些映射规则十分简单而且足够常规,您无需阅读生成的代码就可了解如何使用它们。
当然,您也可以研读自动生成的代码。但是,这些生成的代码不一定适合阅读,而是阅读起来也是一个低效的过程。我们建议您熟悉语言映射的规则;这样,您几乎可以忽略掉生成的代码,除非您对代码的某些特定细节感兴趣。
目前,Ice已经为C ++,C#,Java,JavaScript,Python,Objective-C以及客户端PHP和Ruby提供了语言映射。
客户端和服务器都由应用程序代码、lib库和从Slice定义生成的代码混合而成:
就进程角度而言,此处只涉及了两个进程:客户进程和服务进程。Ice库提供了对分布式通信的所有运行支持,当然,自动生成的代码是由Slice定义的。(对于间接代理,需要定位器解析传输端点以获得对应的代理。)
Ice提供了一种RPC协议可以用于各种基础传输。最常见的例子是TCP和UDP传输,但Ice也支持Websocket,蓝牙和Apple的iAP。此外,Ice还允许您使用 SSL来加密客户端和服务器之间的所有通信数据。
Ice协议定义如下:
Ice还支持线路压缩:通过设置配置参数,您可以商定网络流量的压缩以节省带宽。如果您的应用程序在客户端和服务器之间需要交换大量数据,这将非常有用。
Ice协议适用于构建高效的事件转发机制,因为它允许我们在不知道消息的内容细节的情况下转发消息。这意味着消息的传递交换并不需要对消息进行任何解编和重新编组 - 而是简单地将这些消息视为不透明的字节缓冲区来转发。
ICE协议还支持双向操作:如果服务器想向客户端发送消息,则可以通过客户端最初创建连接时传入的callback进行。当客户端位于允许传出连接但不允许传入连接的防火墙之后时,此功能尤其重要。
Ice core为分布式应用程序提供了一个复杂的客户端 - 服务器开发平台。然而,实际的应用程序需要的不仅仅是远程处理能力:通常,您还需要诸如按需启动服务器、给客户端分发代理、异步事件的分发、配置应用程序和为应用程序分发补丁等能力。
Ice提供了许多各具特色的服务。这些服务以Ice服务器的形式出现,而您的应用程序相对而言充当客户端的角色。这些服务并非使用了开发人员不知晓的Ice内部隐藏API实现,因此理论上这些服务您可以自己开发出来。但是,将这些服务作为平台的一部分提供,可以让您专注于应用程序开发,而不必首先构建大量基础架构。此外,构建这样的服务并不是一项简单的工作,因此了解什么可以拿之即用而不是重复造轮子是值得的。
IceGrid是Ice定位服务的一种实现,它将间接代理中的符号信息解析为间接绑定的协议地址对。当然,IceGrid所具有的功能众多远不止Ice定位服务一个。
IceGrid的特点如下:
IceStorm是一种可以使客户端与服务端解耦的发布订阅服务。从根本上说,IceStorm充当了事件的分发器角色。发布者将事件发送到IceStorm,然后IceStorm将事件传递给订阅者。通过这种方式,发布者发布的单个事件可以发送给多个订阅者。事件按主题分类,订阅者指定他们感兴趣的主题,只有与订阅者主题匹配的事件才会发送到它。IceStorm服务允许选择不同的服务质量标准,使得开发者可以在应用程序的可靠性和性能之间利弊权衡。
如果您需要将信息分发给大量应用程序组件,那么IceStorm特别有用:典型的例子是拥有大量订户的股票行情应用程序。IceStorm将信息发布者与订阅者分离,并负责待发布事件的重新分配。此外,IceStorm可以作为联合服务运行,也就是说,可以在不同的机器上运行多个服务实例,以将负载的处理分散到多个CPU上。
IcePatch2是一个软件修补服务。它允许您轻松地将软件更新包分发给客户端。客户端只需连接到IcePatch2服务器并请求特定应用程序的更新。该服务自动检查客户端软件的版本,并以压缩格式下载应用程序的组件更新包以节省带宽。还可以使用Glacier2服务加强软件补丁安全性,以确保只有经过授权的客户端才能下载软件更新包。
Glacier2是Ice防火墙穿越服务:它允许客户端和服务器通过防火墙进行安全通信,而不会影响安全性。客户端-服务器的通信使用公钥证书进行SSL双向加密。Glacier2支持相互身份验证以及安全会话管理。
IcerBridge充当一个或多个客户端和服务器之间的桥梁的角色,并尽可能做到透明。
Ice架构为应用程序开发人员提供了许多好处: