Chat Demo - Graphical Clients

An important objective for any graphical application is to maintain a responsive user interface. The event loops of many common graphical programming tool kits run single-threaded, and any delays in application-specific code have a noticeable and negative impact on the user experience. Applications often have no choice but to create a separate thread for any operation that has the potential to block.

By default, an Ice invocation uses synchronous semantics similar to that of a regular in-process function call: the calling thread blocks until the invocation completes. However, a synchronous Ice invocation carries additional risk of blocking because of the network activity that Ice performs behind the scenes. As an example, the Ice run time may need to establish a connection to the server, which can take some time. Even if the connection is already established due to an earlier invocation, the seemingly simple act of sending a message over an open connection can also block unexpectedly (for example, due to problems in the network).

Applications such as our graphical chat clients that require non-blocking semantics must instead use Ice's asynchronous programming model, called Asynchronous Method Invocation (AMI). Although this model requires some extra effort, the benefit is worth it: Ice guarantees that asynchronous invocations will never block the calling thread. As a result, graphical applications can safely make Ice invocations from the event loop thread without adversely affecting the user interface. To invoke an operation using AMI, the operation's Slice definition must be annotated with metadata, as shown in the following example:

interface ChatSession extends Glacier2::Session {
    void setCallback(ChatRoomCallback* cb);
    ["ami"] long send(string message) throws InvalidMessageException;
};
By annotating send with the ["ami"] metadata, push clients are able to invoke the operation using either synchronous or asynchronous semantics.

An asynchronous invocation differs from its synchronous counterpart in its API as well as its behavior. When an application invokes an operation asynchronously, the application must supply a local callback object (not an Ice object) that the client-side Ice run time invokes once it receives the results of the operation. Graphical applications must be aware of the fact that Ice calls methods on an AMI callback object from a background thread; since some graphical toolkits require that any updates to the user interface be made from the event loop thread, an AMI callback may need to schedule an update from the appropriate thread. Both of the graphical clients in the chat demo use this technique.

Creating a Session

One of the first tasks of a push client is to establish a session with the Glacier2 router. (The code shown below was adapted from the Java graphical client, but the C# code is identical.)

The client begins by obtaining a proxy for the communicator's default router and down-casting it to a proxy for the derived interface Glacier2::Router. This is done using a checked cast, which behind the scenes sends a request to the target object to confirm that it implements the desired interface:

Ice.Communicator communicator = ...
Ice.RouterPrx defaultRouter = communicator->getDefaultRouter();
Glacier2.RouterPrx router = Glacier2.RouterPrxHelper.checkedCast(defaultRouter);

With the router's proxy in hand, the client can create a Glacier2 session. The createSession operation returns a proxy of type Glacier2::Session, and the client narrows this proxy to the derived interface Chat::ChatSession that we saw earlier. In this case, the client uses an uncheckedCast; this avoids sending a confirmation request to the target object and is useful when the client already knows the target object implements the desired interface:

Glacier2.SessionPrx s = router.createSession("user", "password");
Chat.ChatSessionPrx session = Chat.ChatSessionPrxHelper.uncheckedCast(s);

After successfully creating the session, the client must take care of a little house-keeping before it can instantiate its callback object. The first step is to create an object adapter, which is the Ice construct responsible for dispatching incoming requests to Ice objects. Activating the object adapter starts the Ice dispatch loop in a background thread:

Ice.ObjectAdapter adapter =
    communicator.createObjectAdapterWithRouter("ChatDemo.Client", router);
adapter.activate();

Every Ice object requires a unique identity, which is a structure consisting of two strings representing a name and a category. When using callbacks with Glacier2, the client must use a category supplied by Glacier2. The code below prepares the identity for the callback object:

Ice.Identity callbackId = new Ice.Identity();
callbackId.name = Ice.Util.generateUUID();
callbackId.category = router.getCategoryForClient();

Now the client can instantiate its callback object. Since the client activated the object adapter earlier, it is possible for requests to be dispatched to this object as soon as we add it to the object adapter. In turn, the object adapter returns a proxy that we can down-cast to the appropriate interface:

Ice.Object obj = new ChatRoomCallbackI();
Ice.ObjectPrx proxy = adapter.add(obj, callbackId);
Chat.ChatRoomCallbackPrx callback =
    Chat.ChatRoomCallbackPrxHelper.uncheckedCast(proxy);

The last step is to join the chat room, which we accomplish by invoking setCallback on the session:

session.setCallback(callback);

Java Client

The Java chat client uses Swing together with the JGoodies Looks and Forms libraries to construct its user interface. To avoid blocking Swing's event loop, the program uses asynchronous invocations when communicating with the chat server. For example, the code below shows how the client invokes an asynchronous version of the send operation to publish a newly-typed chat message:

ChatSessionPrx session = ...
String message = ...
session.send_async(new AMI_ChatSession_sendI(userName, message), message);

The first argument to send_async is the AMI callback object, whose constructor needs the name of the user sending the message as well as the message itself. As you can see in the following definition of the callback class, the completion of the asynchronous invocation prompts the program to schedule an update to the user interface:

public class AMI_ChatSession_sendI extends Chat.AMI_ChatSession_send {
    public AMI_ChatSession_sendI(String name, String message)
    {
        _name = name;
        _message = message;
    }

    public void ice_response(long timestamp)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            public void run()
            {
                String message = formatTimestamp(timestamp) + " - <" + _name +
                                 "> " + unstripHtml(_message));
                appendMessage(message);
            }
        });
    }

    ...
}

The callback class derives from Slice-generated code and must override several methods. Ice invokes the ice_response method when the invocation completes successfully; the parameters of this method correspond to the return value and output parameters of the Slice operation. In the example above, ice_response receives the time stamp assigned by the server, which—along with the user name and message stored by the constructor—is all of the information the callback needs to display a new chat message. The implementation of ice_response calls the SwingUtilities.invokeLater method to ensure that the nested Runnable object gets executed from Swing's event loop thread, where it is safe to make changes to the user interface.

.NET Client

The user interface of the .NET chat client is built using Windows Presentation Foundation (WPF), introduced by Microsoft with .NET Framework 3.0. The chat client's graphical elements are arranged using Extensible Application Markup Language (XAML), while the application logic is implemented in C#. To avoid blocking .NET's event loop, the program uses asynchronous invocations to communicate with the chat server. For example, the code below shows how the client invokes an asynchronous version of the send operation to publish a newly-typed chat message:

ChatSessionPrx session = ...
string message = ...
session.send_async(new AMI_ChatSession_sendI(userName, message), message);

The first argument to send_async is the AMI callback object, whose constructor needs the name of the user sending the message as well as the message itself. As you can see in the following definition of the callback class, the completion of the asynchronous invocation prompts the program to schedule an update to the user interface:

using System.Windows.Application.Current;

public class AMI_ChatSession_sendI : Chat.AMI_ChatSession_send {
    public AMI_ChatSession_sendI(String name, String message)
    {
        _name = name;
        _message = message;
    }

    public override void ice_response(long timestamp)
    {
        Dispatcher.BeginInvoke(DispatcherPriority.Normal, (Action)delegate()
        {
            string message = formatTimestamp(timestamp) + " - <" + _name + "> " +
                             unstripHtml(_message) + Environment.NewLine);
            appendMessage(message);
        });
    }

    ...
}

The callback class derives from Slice-generated code and must override several methods. Ice invokes the ice_response method when the invocation completes successfully; the parameters of this method correspond to the return value and output parameters of the Slice operation. In the example above, ice_response receives the time stamp assigned by the server, which—along with the user name and message stored by the constructor—is all of the information the callback needs to display a new chat message. The implementation of ice_response calls the Dispatch.BeginInvoke method to ensure that the nested delegate object gets executed from .NET's event loop thread, where it is safe to make changes to the user interface.

Previous: C++ Clients Next: PHP Client

Copyright © 2008 ZeroC, Inc.