Bidirectional Connections

On this page:

Use Cases for Bidirectional Connections

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:


Callbacks in an open network.

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:


Callbacks with a firewall.

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.

Configuring a Client for Bidirectional Connections

A client needs to perform the following steps in order to configure a bidirectional connection:

  1. Create an object adapter to receive callback requests. This adapter does not require a name or endpoints if its only purpose is to receive callbacks over bidirectional connections.
  2. Register the callback object with the object adapter, which returns a proxy for this callback object.
  3. Obtain a connection object by calling 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.
  4. Pass the proxy to the callback object (obtained in step 2) to the server.

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<CallbackPrx>(adapter->addWithUUID(std::make_shared<CallbackI>()));
proxy->ice_getCachedConnection()->setAdapter(adapter);
proxy->addClient(cbPrx);
Ice::ObjectAdapterPtr adapter = communicator->createObjectAdapter("");
CallbackPrx cbPrx = CallbackPrx::uncheckedCast(adapter->addWithUUID(new CallbackI));
proxy->ice_getCachedConnection()->setAdapter(adapter);
proxy->addClient(cbPrx);
var adapter = communicator.createObjectAdapter("");
var cbPrx = CallbackPrxHelper.uncheckedCast(adapter.addwithUUID(new CallbackI()));
proxy.ice_getCachedConnection().setAdapter(adapter);
proxy.addClient(cbPrx);
com.zeroc.Ice.ObjectAdapter adapter = communicator.createObjectAdapter("");
CallbackPrx cbPrx = CallbackPrx.uncheckedCast(adapter.addwithUUID(new CallbackI()));
proxy.ice_getCachedConnection().setAdapter(adapter);
proxy.addClient(cbPrx);
const adapter = await communicator.createObjectAdapter("");
const cbPrx = CallbackPrx.uncheckedCast(adapter.addwithUUID(new CallbackI()));
proxy.ice_getCachedConnection().setAdapter(adapter);
await proxy.addClient(cbPrx);
adapter = communicator.createObjectAdapter("")
cbPrx = CallbackPrx.uncheckedCast(adapter.addwithUUID(CallbackI()))
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 Considerations

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);
proxy->ice_getCachedConnection()->setACM(IceUtil::None, IceUtil::None, Ice::HeartbeatAlways);
proxy.ice_getCachedConnection().setACM(Ice.Util.None, Ice.Util.None, Ice.ACMHeartBeat.HeartbeatAlways);
proxy.ice_getCachedConnection().setACM(null, null, Optional.of(com.zeroc.Ice.ACMHeartbeat.HeartbeatAlways));
proxy.ice_getCachedConnection().setACM(undefined, undefined, Ice.ACMHeartbeat.HeartbeatAlways);
proxy.ice_getCachedConnection().setACM(Ice.Unset, Ice.Unset, 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.

Configuring a Server for Bidirectional Connections

A server needs to take the following steps in order to make callbacks over a bidirectional connection:

  1. Obtain a proxy to the callback object; this proxy is typically supplied by the client.
  2. Convert this proxy into a fixed proxy bound to the connection, by calling the 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_ptr<ClientPrx> client, const Ice::Current& current)
{
    client->ice_fixed(current.con)->notify();
}
void addClient(const ClientPrx& client, const Ice::Current& current)
{
    client->ice_fixed(current.con)->notify();
}
public override void addClient(ClientPrx client, Ice.Current current)
{
    ((ClientPrx)client.ice_fixed(current.con)).notify();
}
public void addClient(ClientPrx client, com.zeroc.Ice.Current current)
{
    client.ice_fixed(current.con).notify();
}
def addClient(self, client, 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.

Fixed Proxies

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.

Limitations of Bidirectional Connections

Bidirectional connections have certain limitations:

  • They can only be configured for connection-oriented transports such as TCP and SSL.
  • Most proxy factory methods are not relevant for a fixed proxy. The proxy is bound to an existing connection, therefore the proxy reflects the connection's configuration. Attempting to change settings such as the proxy's timeout value causes the Ice run time to raise 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.
  • A connection established from a Glacier2 router to a server is not configured for bidirectional use. Only the connection from a client to the router is bidirectional. However, the client must not attempt to manually configure a bidirectional connection to a router, as this is handled internally by the Ice run time.

 

Threading Considerations for Bidirectional Connections

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.

See Also