1. Ice的连接管理
Ice运行时代表应用程序透明地创建和关闭连接,因此作为应用程序开发人员,你通常可以忽略Ice如何管理连接。但是,了解Ice如何处理连接并选择连接是非常有用的,特别是如果服务器为Ice对象提供多个端点。
1.1. 客户端连接
当客户端通过TCP或SSL联系服务端时,Ice需要建立两者之间的连接。连接始终由客户端启动,并由服务端接受。客户端可以查询代理来获取其Connection对象,该对象描述了代理的底层连接。(即使对于数据报代理,也可以获得连接对象,也就是通过UDP与服务端联系的代理。)Connection对象提供诸如close和createProxy之类的方法,以及许多其他方法。
有两种方法从代理获取Connection对象:
- ice_getConnection
此代理方法返回与代理关联的Connection对象。如果还没有到目标的连接,Ice运行时将首先建立一个连接,然后返回新连接的Connection对象。如果运行时无法建立连接,则操作引发异常;如果代理引用的Ice对象是collocated,则该方法返回null。
- ice_getCachedConnection
如果代理已经绑定到连接,则此代理方法返回与代理关联的Connection对象;如果尚未绑定连接,则该方法返回null。
下面是一个简单的例子,说明如何获得一个Connection对象:
shared_ptr<Communicator> communicator = ...;
shared_ptr<HelloPrx> hello = Ice::uncheckedCast<HelloPrx>(
communicator->stringToProxy("hello:tcp -h remote.host.com -p 10000"));
shared_ptr<Connection> conn = hello->ice_getConnection();
对ice_getConnection的调用在端口10000建立与remote.host.com的连接,并返回关联的Connection对象。 对比下面的例子:
shared_ptr<Communicator> communicator = ...;
shared_ptr<HelloPrx> hello = Ice::uncheckedCast<HelloPrx>(
communicator->stringToProxy("hello:tcp -h remote.host.com -p 10000"));
shared_ptr<Connection> conn = hello->ice_getCachedConnection();
在这种情况下,对ice_getCachedConnection的调用返回null,因为之前没有为hello代理建立连接。
如你所想,连接并不便宜。特别是,连接使用文件描述符,并使用内存来跟踪未决的请求。由于连花费昂贵,连接重用是Ice运行时的一个组成部分。了解客户端如何确定是建立一个新的连接,还是重新使用现有连接是非常重要的。
1.2. 连接生命周期
Ice运行时维护一个现有的连接池(在每个通信的基础上);运行时将这些连接绑定到代理,作为客户端通过该代理进行远程调用的副作用。运行时会按需透明地创建新的连接。
例如,请考虑以下代码:
shared_ptr<Communicator> communicator = ...;
shared_ptr<HelloPrx> h1 = Ice::uncheckedCast<HelloPrx>(
communicator->stringToProxy("hello:tcp -h remote.host.com -p 10000"));
h1->sayHello(); // Connection creation and binding occurs here
当客户端通过代理h1调用sayHello时,Ice运行在端口10000处创建到remote.host.com的连接,并将此连接绑定到代理。请注意,前面的示例使用uncheckedCast,它不会进行远程调用,因此永远不会建立连接。另一方面,如果代码使用checkedCast,则连接建立将作为checkedCast的一部分进行,因为checked cast需要远程调用,来确定目标对象是否支持指定的接口。(关于代理的更多信息,请参阅代理的基础知识。)
连接的生命周期与代理的生命周期无关。 例如:
void
doit(shared_ptr<Communicator> communicator)
{
shared_ptr<HelloPrx> h1 = Ice::uncheckedCast<HelloPrx>(
communicator->stringToProxy("hello:tcp -h remote.host.com -p 10000"));
h1->sayHello(); // Connection creation and binding occurs here
}
一旦doit函数返回,C++运行时就会销毁代理h1。但是,绑定到该代理对象的连接依然存在:连接的生命周期,和绑定到该连接的代理的生命周期是完全独立的。这引出了Ice如何以及何时关闭连接并释放其相关资源的问题。Ice运行时关闭和破坏在各种情况下的连接:
- 销毁
communicator关闭并销毁communicator的连接。 - 如果启用了主动连接管理(ACM),则会关闭空闲时间超过指定超时的连接。你可以在代理的
Connection对象上调用close来显式关闭连接。 - 如果连接超时,那么运行时会在超时到期时关闭连接。(这被认为是不可恢复的错误。)
- 如果运行时遇到不可恢复的错误(如套接字错误),或者接收到违反Ice协议或编码的错误,则会关闭相应的连接。
代理可以在其生命周期中绑定到不同的连接。例如,代理服务器可能有一段连接空闲的时间,并由ACM关闭;下次使用代理进行调用时,运行时会透明地为代理建立新的连接。同样,可以为代理建立一个新的连接,因为之前的连接由于任何上述原因而关闭,或者由于连接缓存被禁用。(我们很快会回到这个话题。)
如果要将代理永久绑定到特定的连接,可以通过调用Connection::createProxy来创建一个固定的代理。
Ice运行时尽可能重用现有的连接。例如,考虑:
shared_ptr<Communicator> communicator = ...;
shared_ptr<HelloPrx> h1 = Ice::uncheckedCast<HelloPrx>(
communicator->stringToProxy("hello:tcp -h remote.host.com -p 10000"));
h1->sayHello();
shared_ptr<HelloPrx> h2 = Ice::uncheckedCast<HelloPrx>(
communicator->stringToProxy("hello2:tcp -h remote.host.com -p 10000"));
h2->sayHello();
在这种情况下,Ice运行时将两个代理h1和h2绑定到相同的连接,因为这两个代理都引用同一个端点处的对象(远程端口10000处的remote.host.com)。相反,请考虑:
shared_ptr<Communicator> communicator = ...;
shared_ptr<HelloPrx> h1 = Ice::uncheckedCast<HelloPrx>(
communicator->stringToProxy("hello:tcp -h remote.host.com -p 10000"));
h1->sayHello();
shared_ptr<HelloPrx> h2 = Ice::uncheckedCast<HelloPrx>(
communicator->stringToProxy("hello2:tcp -h remote2.host.com -p 8000"));
h2->sayHello();
在这个例子中,hello对象驻留在remote.host.com的端口10000上,而hello2对象驻留在remote2.host.com的8000端口上。由于这两个代理有不同的端点,Ice运行时建立了一个独立的连接为每个代理。
如果代理包含多个端点,则情况会变得更加复杂。例如,考虑:
shared_ptr<Communicator> communicator = ...;
shared_ptr<HelloPrx> h1 = Ice::uncheckedCast<HelloPrx>(
communicator->stringToProxy(
"hello:tcp -h remote.host.com -p 10000:tcp -h remote2.host.com -p 8000"));
h1->sayHello();
shared_ptr<HelloPrx> h2 = Ice::uncheckedCast<HelloPrx>(
communicator->stringToProxy(
"hello2:tcp -h remote.host.com -p 10000:tcp -h remote2.host.com -p 8000"));
h2->sayHello();
在这种情况下,hello和hello2对象既可以在remote.host.com上的端口10000上访问,也可以在remote2.host.com上的端口8000上访问。问题是这两个代理将共享相同的连接还是使用不同的连接。答案是,代理共享一个单一的连接,为什么?我们需要更详细地探讨,Ice如何将绑定连接到代理上。
1.3. 端点选择
在绑定期间,Ice运行时查看代理的端点,并从该端点列表中产生候选端点的有序列表。创建候选端点列表和绑定连接的默认算法如下:
- 删除任何不可用或不兼容的端点。
- 随机播放端点,并在洗牌之后,将安全端点移动到列表的末尾。这建立了一个端点首选顺序。
- 检查是否存在任何候选端点的兼容连接。如果是的话,重新使用该连接。
- 否则,不存在兼容的连接。对于候选列表中的每个端点,尝试建立到该端点的连接,并在成功时使用该连接;否则,尝试候选列表中的下一个端点,直到可以建立连接或者不再有候选端点为止。
代理设置可以修改这个算法,下面是Ice如何选择端点并建立连接的细节。
1.3.1. 删除不可用和不兼容的端点
第一步是删除满足以下条件之一的任何端点:
未知端点。例如,如果未安装IceSSL插件,则SSL端点是未知端点。
不兼容端点,这意味着它不匹配代理的调用模式。例如,从数据报代理中删除所有非UDP端点。
不安全端点,但代理被配置为需要安全连接,或设置
Ice.Override.Secure。
如果一旦运行时已经移除了不可用和不兼容的端点,如果没有剩余端点,则调用将向客户端抛出NoEndpointException的异常。 考虑下面的例子来说明。(请注意,为简洁起见,示例中没有在端点中指定-h选项;省略此选项会导致Ice将主机设置为127.0.0.1,或者如果设置了Ice.Default.Host的属性,则使用它的的值。
// 服务端: IceSSL插件已经安装
shared_ptr<ObjectPrx>
SomeServantI::getObj(const Current& current)
{
return current.adapter->getCommunicator()->
stringToProxy("obj:tcp -p 8000:udp -p 9000:ssl -p 10000");
}
// 客户端: IceSSL插件没有安装
shared_ptr<ObjectPrx> obj = someServant->getObj();
obj->ice_ping();
在这个示例中,没有IceSSL插件的客户端,通过线路接收包含TCP、SSL和UDP端点的代理。即使客户端不能使用端点,Ice运行时也会保留客户端代理中的SSL端点。这允许客户端稍后通过线路发送代理,而不会丢失SSL端点。另外请注意,客户端无法通过调用stringToProxy直接创建带有SSL端点的代理,因为如果没有IceSSL插件,stringToProxy会为SSL端点抛出EndpointParseException的异常。
客户端运行时删除不合适的SSL和UDP端点后,只有TCP端点保持合格。Ice删除了SSL端点,因为IceSSL没有安装在客户端,并且删除UDP端点,因为它只能用来进行数据报调用。
// IceSSL插件已经安装
shared_ptr<ObjectPrx> obj = communicator->stringToProxy("obj:tcp -p 8000:udp -p 9000:ssl -p 10000");
obj->ice_ping();
在这个例子中,TCP和SSL端点都保留 如果客户端定义了Ice.Override.Secure,Ice也会删除TCP端点。
// IceSSL插件已经安装
shared_ptr<ObjectPrx> obj = communicator->stringToProxy("obj:tcp -p 8000:udp -p 9000:ssl -p 10000");
obj = obj->ice_datagram();
obj->ice_ping();
在这种情况下,唯一剩下的端点是UDP端点。如果客户端定义了Ice.Override.Secure,Ice也会移除UDP端点(因为UDP不能用于安全调用),并且调用ice_ping会引发NoEndpointException异常。
// IceSSL插件已经安装
shared_ptr<ObjectPrx> obj = communicator->stringToProxy("obj:tcp -p 8000:udp -p 9000:ssl -p 10000");
obj = obj->ice_secure();
obj->ice_ping();
在这个例子中,因为代理是安全的,所以只有SSL端点保持连接建立的资格。
1.3.2. 端点顺序
Ice运行时删除不合适的端点后,将确定端点将用于连接尝试的顺序。这样做分两步进行:
运行时间根据端点选择策略,对端点列表进行排序(可以使用
ice_endpointSelection代理方法)。默认情况下,端点选择策略是随机的,这意味着运行时将端点筛选为随机顺序。否则,选择策略是Ordered,Ice保留代理中列出端点的顺序。如果
PreferSecure为false(默认值),则运行时将所有安全端点移动到列表的末尾。相反,如果PreferSecure为true,则运行时将所有安全端点移动到列表的开始位置。(你可以使用ice_preferSecure代理方法来设置PreferSecure。)
考虑下面的例子:
// IceSSL插件已经安装
shared_ptr<ObjectPrx> obj = communicator->stringToProxy("obj:tcp -p 8000:ssl -p 10000:tcp -p 9000");
obj->ice_ping();
在这种情况下,端点列表可以是<tcp -p 8000,tcp -p 9000,ssl -p 10000>或<tcp -p 9000,tcp -p 8000,ssl -p 10000>。 由于端点选择策略有默认值,因此TCP端点的顺序是随机的。但是,由于PreferSecure是错误的,所以SSL端点保证在最后。
// IceSSL已经安装
shared_ptr<ObjectPrx> obj = communicator->stringToProxy("obj:tcp -p 8000:ssl -p 10000:tcp -p 9000");
obj = obj->ice_endpointSelection(Ordered);
obj->ice_ping();
在这种情况下,端点列表是<tcp -p 8000,tcp -p 9000,ssl -p 10000>,因为选择策略是Ordered的,所以两个TCP端点保持原来的顺序。SSL端点出现在最后,因为PreferSecure是错误的。
// IceSSL已经安装
shared_ptr<ObjectPrx> obj = communicator->stringToProxy("obj:tcp -p 8000:ssl -p 10000:tcp -p 9000");
obj = obj->ice_endpointSelection(Ordered);
obj = obj->ice_preferSecure(true);
obj->ice_ping();
在这种情况下,端点列表是<ssl -p 10000,tcp -p 8000,tcp -p 9000>。 同样,端点选择策略是Ordered,所以两个TCP端点保持原来的顺序。但是,由于PreferSecure为true,则SSL端点先出现。
1.3.3. 连接创建和绑定
如果启用了连接缓存(默认情况下),运行时首先检查是否已经建立连接到任何代理的端点。如果是这样,它会重用这个连接。否则,它建立一个新的。换句话说,运行时只有在不存在与任何端点的兼容连接的情况下,才建立新的连接。
如果连接缓存被禁用,则运行时按顺序遍历端点列表,并且对于每个端点,确定是否存在与该端点的兼容连接,在这种情况下连接被绑定;否则,它会尝试建立到该端点的新连接。这意味着即使存在与列表中稍后出现的端点兼容的现有连接,运行时也可能建立新的连接。
如果连接的端点匹配代理的端点,并且连接匹配代理的配置,则可以重新使用连接。具体而言,连接的超时设置必须与代理的配置超时一致。(如果代理配置了连接ID,则连接ID也必须匹配。)
连接超时是Ice的一个非常重要且经常被误解的方面。简而言之,每个连接都有一个关联的超时值。超时值是从最初导致建立连接的代理复制的。如果通过该连接发送的请求超时,则该连接上所有未完成的请求也会超时,并强制关闭连接。因此,具有不同超时值的两个代理不能共享连接。例如:
shared_ptr<Communicator> communicator = ...;
shared_ptr<ObjectPrx> o = communicator->stringToProxy("hello:tcp -h remote.host.com -p 10000");
shared_ptr<HelloPrx> h1 = Ice::uncheckedCast<HelloPrx>(o->ice_timeout(1000));
h1->sayHello();
shared_ptr<HelloPrx> h2 = Ice::uncheckedCast<HelloPrx>(o->ice_timeout(2000));
h2->sayHello();
在这种情况下,h1和h2绑定到不同的连接,因为两个代理的超时是不同的。我们再回到前面的例子:
shared_ptr<Communicator> communicator = ...;
shared_ptr<HelloPrx> h1 = Ice::uncheckedCast<HelloPrx>(
communicator->stringToProxy(
"hello:tcp -h remote.host.com -p 10000:tcp -h remote2.host.com -p 8000"));
h1->sayHello();
shared_ptr<HelloPrx> h2 = Ice::uncheckedCast<HelloPrx>(
communicator->stringToProxy(
"hello2:tcp -h remote.host.com -p 10000:tcp -h remote2.host.com -p 8000"));
h2->sayHello();
考虑通过h1的第一个调用。端点列表将根据端点是如何混洗而变为<tcp -p 10000,tcp -p 8000>或<tcp -p 10000,tcp -p 8000>。(因为端点选择策略的默认值为Random,所以端点是混洗的。)假定服务端在每个端点上运行,客户端创建一个到候选列表中首先碰到的任何端点的连接,并将该连接绑定到h1代理。现在考虑通过h2的第二次调用。在这种情况下,和以前一样,有两个可能的端点列表。但是,由于启用了连接缓存,因此Ice运行时优先重新使用现有的连接,从而绑定到通过h1初始调用建立的任何连接。
1.3.4. 连接建立重试
如果尝试建立连接失败,则运行时基于Ice.RetryIntervals的值重试。该属性的默认值是零,它表示运行时为每个端点重试一次连接建立,没有中间延迟。如果不能通过任何端点建立连接,则运行时会引发一个异常,表示最终失败连接尝试的原因,例如ConnectionRefusedException。
1.4. 连接缓存
默认情况下,在通过该代理的第一次远程调用期间,连接被绑定到代理;此后,只要代理保持打开状态,代理就会继续使用此连接。换句话说,代理缓存连接。如果连接在某个时间点被关闭,则通过代理的下一个远程调用,透明地使用我们前面介绍的算法建立一个新的连接。对于大多数应用程序来说,这是正确的行为,因为它最大限度地减少了远程调用的开销。
但是,对于某些应用程序,需要在每个远程调用上重新绑定代理的连接。特别是,默认算法不适合每个请求的负载平衡。在这种情况下,代理包含副本组中每个副本的端点。但是,默认算法确实是错误的,因为一旦建立了任何一个副本的连接,所有未来的请求都将通过同一个连接发送,所以只有一个副本被使用:
shared_ptr<Communicator> communicator = ...;
shared_ptr<HelloPrx> h1 = Ice::uncheckedCast<HelloPrx>(
communicator->stringToProxy(
"hello:tcp -h remote.host.com -p 10000:tcp -h remote2.host.com -p 8000"));
h1->sayHello();
h1->sayHello();
在这种情况下,第二个sayHello调用是通过第一个调用建立的任何连接发送的。要改变这种行为,你必须通过调用ice_connectionCached(false)来创建一个新的代理:
shared_ptr<Communicator> communicator = ...;
shared_ptr<ObjectPrx> o = communicator->stringToProxy(
"hello:tcp -h remote.host.com -p 10000:tcp -h remote2.host.com -p 8000");
shared_ptr<HelloPrx> h1 = Ice::uncheckedCast<HelloPrx>(o->ice_connectionCached(false));
h1->sayHello();
h1->sayHello();
通过禁用h1的连接缓存,第二次调用sayHello会导致绑定算法再次执行:
在第一次调用期间,端点被混洗,并且运行时建立到其中一个端点的连接,例如
tcp -p 10000。在第二次调用期间,选择算法再次运行。如果端点洗牌的结果与第一次调用的结果相同,则请求将通过已经存在的连接发送。但是,如果混洗导致的顺序相反,则第二个调用会导致第二个连接打开到
tcp -p 8000。
最终,经过多次调用,两个连接都将建立;为客户端进行的每个调用选择两个现有连接之一是非常有效的,并且导致每个请求的随机负载均衡。
现在,假设我们禁用连接缓存,并将选择策略设置为Ordered。假设服务器在第一个端点上运行,客户端所做的所有调用都将被单独绑定,并且每次调用都会先尝试第一个端点。这种行为对于主从关系中的服务器非常有用:首先列出主端点,除非主端口处于关闭状态,否则将始终使用主端点,此时会尝试后续端点标识的从站。但是,请注意,目前这种方法开销很大,因为在主服务器关闭时,运行时会尝试在每次调用时创建一个到第一个端点的新连接,主服务器重新联机之前,每个这样的尝试都会失败。
1.5. 活动连接管理
活动连接管理(ACM)通过关闭空闲连接来提高应用程序的可伸缩性。Ice运行时会定期检查每个现有连接,如果连接闲置的时间超过Ice.ACM.Client(或Ice.ACM.Server)的秒数,则会正常关闭连接。这些属性的默认值对于客户端是60秒,对于服务器是0(即禁用)。客户端通过连接关闭的代理进行下一次调用,会导致连接自动重新建立,所以ACM对于应用程序代码是透明的。
请注意,在服务端,默认情况下禁用ACM,因为服务器端ACM会导致单向调用被默默的丢弃。仅当客户端使用双向连接时,才需要在客户端禁用ACM。要禁用ACM,请将相应的属性设置为零。
在ACM的上下文中,“空闲”意味着在超时期间内,没有请求和调用通过连接发送,并且没有批处理消息被添加到连接发送。例如:
shared_ptr<Communicator> communicator = ...;
shared_ptr<HelloPrx> h1 = Ice::uncheckedCast<HelloPrx>(
communicator->stringToProxy("hello:tcp -h remote.host.com -p 10000"));
h1->sayHello();
sleep(70);
h1->sayHello();
在这种情况下,第一次调用sayHello建立的连接在60秒(客户端的默认空闲超时)之后关闭。第二个调用创建到同一端点的新连接,并将该连接绑定到代理。然而:
shared_ptr<Communicator> communicator = ...;
shared_ptr<HelloPrx> h1 = Ice::uncheckedCast<HelloPrx>(
communicator->stringToProxy("hello:tcp -h remote.host.com -p 10000"));
h1->sayHello(); // Takes 70 seconds
h1->sayHello();
在这种情况下,连接没有关闭,因为在第一次调用完成后的70秒内,连接上的应答未完成。
请注意,在客户端禁用ACM不能保证连接将保持打开状态,因为ACM可能在服务器端处于活动状态。如果要确保连接保持打开状态,则必须为客户端和服务端同时禁用ACM。