An Ice connection normally allows requests to flow in only one direction. If an application's design requires the server to make callbacks to a client, the server usually establishes a new connection to that client in order to send callback requests, as shown below:
Unfortunately, network restrictions often prevent a server from being able to create a separate connection to the client, such as when the client resides behind a firewall as shown here:
In this scenario, the firewall blocks any attempt to establish a connection directly to the client.
For situations such as these, a bidirectional connection offers a solution. Requests may flow in both directions over a bidirectional connection, enabling a server to send callback requests to a client over the client's existing connection to the server.
There are two ways to make use of a bidirectional connection. First, you can use a Glacier2 router, in which case bidirectional connections are used automatically. If you do not require the functionality offered by Glacier2 or you do not want an intermediary service between clients and servers, you can configure bidirectional connections manually.
The remainder of this section discusses manual configuration of bidirectional connections. An example that demonstrates how to configure a bidirectional connection is provided in the directory demo/Ice/bidir
of your Ice distribution.
A client needs to perform the following steps in order to configure a bidirectional connection:
ice_getConnection
(or ice_getCachedConnection
) on the proxy, and invoke setAdapter
on the connection, passing the callback object adapter. This associates the object adapter with the connection and enables callback requests to be dispatched.The object adapter remains a regular object adapter, unaware of the connection(s) associated with it through setAdapter
. These connections have no effect on the endpoints and other properties of proxies created by this object adapter.
However, calling activate
on this object adapter is optional if it's used only for bidirectional and collocated dispatches.
The code below illustrates these steps:
auto adapter = communicator->createObjectAdapter(""); auto cbPrx = Ice::uncheckedCast(adapter->addWithUUID(std::make_shared ())); proxy->ice_getCachedConnection()->setAdapter(adapter); proxy->addClient(cbPrx);
The proxy to the callback object (cbPrx
in the code above) is a regular proxy, with no endpoints. Even if it had endpoints, we would not want the server to use these endpoints–we want the server to reuse the already established connection. As we will see below, the server will convert this callback proxy into a new fixed proxy bound to the connection.
Active Connection Management (ACM) automatically and transparently closes idle connections. As far as the Ice run time is concerned, an outgoing connection is a client connection regardless of whether it's also used as a bidirectional connection, therefore the client-side ACM properties govern the ACM behavior for bidirectional connections in a client. Also note that the client's outgoing connection is the only channel on which the server can send callback invocations to the client, therefore prematurely closing the connection (such as allowing the ACM facility to close it automatically) introduces the risk that the client might unknowingly fail to receive callbacks.
It is not necessary to disable client-side ACM when using bidirectional connections. If you leave ACM enabled, you simply need to ensure that the connection remains active to prevent the ACM facility from closing it. The easiest way to accomplish this is to enable connection heartbeats:
Ice.ACM.Client.Heartbeat=Always
Alternatively, you can enable heartbeats on a bidirectional connection by calling setACM
:
proxy->ice_getCachedConnection()->setACM(Ice::nullopt, Ice::nullopt, Ice::ACMHeartbeat::HeartbeatAlways);
The server's ACM configuration also plays an important role here. For more information, refer to our discussion of ACM configurations for bidirectional connections.
A server needs to take the following steps in order to make callbacks over a bidirectional connection:
ice_fixed
proxy method with the connection object. The connection object is accessible as a member of the Ice::Current
parameter supplied to an operation implementation.These steps are illustrated in the C++ code below:
void addClient(shared_ptrclient, const Ice::Current& current) { client->ice_fixed(current.con)->notify(); }
If the client forgot to call setAdapter
on the connection, the notify
call on the fixed proxy fails with an ObjectNotExistException
raised by the client and propagated to the server.
Nested call
With this example, the client thread pool of the client can have only one thread unless the implementation of notify
makes a remote call and waits for its result.
The server's server thread pool must have at least two threads if notify
is a two-way call: one thread that dispatches addClient
, and another thread that receives the (void) result of the notify
call. A single thread in the server thread pool would result in a thread-starvation deadlock.
The proxy returned by the proxy method ice_fixed
is called a fixed proxy. It cannot be marshaled; attempts to do so raise FixedProxyException
. The connection's createProxy
operation also returns a fixed proxy.
A fixed proxy is bound to the connection that created it, and ceases to work once that connection is closed. If the connection is closed prematurely, either by active connection management (ACM) or by explicit action on the part of the application, the server can no longer make callback requests using that proxy. Any attempt to use the proxy again usually results in a CloseConnectionException
.
Many aspects of a fixed proxy cannot be changed. For example, it is not possible to change the proxy's endpoints or timeout. Attempting to invoke a method such as ice_timeout
on a fixed proxy raises FixedProxyException
.
A fixed proxy created by ice_fixed
inherits all relevant aspects of the parent proxy, such as its context and its compression and secure settings. A fixed proxy created by createProxy
on a connection won't use compression unless Ice.Override.Compress=1
is set. To enable compression without Ice.Override.Compress=1
, you can call ice_compress(true)
on the fixed proxy to create a new fixed proxy with compression enabled.
Bidirectional connections have certain limitations:
FixedProxyException
. Note however that it is legal to configure a fixed proxy for using oneway or twoway invocations. You may also invoke ice_secure
on a fixed proxy if its security configuration is important; a fixed proxy configured for secure communication raises NoEndpointException
on the first invocation if the connection is not secure.
The Ice run time normally creates two thread pools for processing network traffic on connections: the client thread pool manages outgoing connections and the server thread pool manages incoming connections. All of the object adapters in a server share the same thread pool by default, but an object adapter can also be configured to have its own thread pool. The default size of the client and server thread pools is one.
The client thread pool processes replies to pending requests. When a client configures an outgoing connection for bidirectional requests, the client thread pool also becomes responsible for dispatching callback requests received over that connection. Similarly, the server thread pool normally dispatches requests from clients. If a server uses a bidirectional connection to send callback requests, then the server thread pool must also process the replies to those requests.
You must increase the size of the appropriate thread pool if you need the ability to dispatch multiple requests in parallel, or if you need to make nested twoway invocations. For example, a client that receives a callback request over a bidirectional connection and makes nested invocations must increase the size of the client thread pool.