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.

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. 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. Ice provides the Ice.Dispatcher facility to give applications control over the thread in which callbacks are dispatched. Both of the graphical clients in the chat demo use this facility to allow AMI callbacks to update the user interface directly.

Creating a Session

One of the first tasks of a push client is establishing a session with the Glacier2 router. This process is greatly simplified by using the Glacier2 helper classes included with Ice, and our graphical clients make extensive use of these classes. (The code shown below was adapted from the Java graphical client, but the C# code is similar.)

Before we can create a session, we need to configure the dispatcher to ensure that AMI callbacks are executed in the event loop thread:

initData.dispatcher = new Ice.Dispatcher() {
    public void dispatch(Runnable runnable, Ice.Connection connection) {
        // ...
        SwingUtilities.invokeLater(runnable);
    }
};
As you can see, the dispatcher implementation simply delegates to Swing's invokeLater function.

Next the client creates an instance of Glacier2.SessionFactoryHelper and passes it a callback object:

Glacier2.SessionCallback callback = ...;
_factory = new Glacier2.SessionFactoryHelper(initData, callback);
The callback is invoked to notify the application about significant events in the lifecycle of a Glacier2 session.

Now we can use the factory to initiate the creation of a session:

_factory.setRouterHost(...);
Glacier2.SessionHelper session = _factory.connect(userName, password);
The client invokes setRouterHost to configure the name of the host on which the router is running, and then invokes connect with the user-supplied account information. The return value is a new Glacier2.SessionHelper object that is responsible for managing the session and for invoking the client's SessionCallback object when necessary. For example, since the establishment of a session occurs asynchronously to avoid blocking the thread that calls connect, the client cannot begin using the session until its connected callback method is invoked to indicate that the request completed successfully:
public void
connected(SessionHelper session)
    throws SessionNotExistException
{
    // ...
    _chat = Chat.ChatSessionPrxHelper.uncheckedCast(_session.session());

    Chat.ChatRoomCallbackPrx callback =
        Chat.ChatRoomCallbackPrxHelper.uncheckedCast(
            _session.addWithUUID(new ChatRoomCallbackI(...)));

    _chat.begin_setCallback(callback, ...);
}
The SessionHelper method session returns a proxy for the newly-created Glacier2 session object. We down-cast this proxy to the derived interface Chat::ChatSession that we saw earlier.

Next the code creates the callback object that will receive events from the chat room. There's a lot happening in this short snippet of code. First, we instantiate our callback servant class ChatRoomCallbackI and pass it to the addWithUUID method. This method registers our servant with an object adapter intended especially for callback objects, creating that object adapter if it doesn't already exist. addWithUUID also ensures that the object identity associated with the servant includes a UUID and complies with Glacier2's conventions for callback objects. Finally, we narrow the resulting callback proxy to the type Chat::ChatRoomCallback using an unchecked cast.

The last step is to join the chat room, which we accomplish by invoking setCallback on the session. We use an asynchronous invocation here to avoid blocking.

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 = ...
_chat.begin_send(message, new AMI_ChatSession_sendI(_username, message));

The first argument to begin_send is the chat message and the second argument 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 causes the program to update the user interface:

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

    public void response(long timestamp) {
        userSayEvent(timestamp, _name, _message);
    }

    public void exception(Ice.LocalException ex) {
        destroySession();
    }

    public void exception(Ice.UserException ex) {
        if(ex instanceof Chat.InvalidMessageException) {
            Chat.InvalidMessageException e = (Chat.InvalidMessageException)ex;
            appendMessage(" - " + e.reason);
        }
    }

    private final String _name;
    private final String _message;
}

The callback class derives from Slice-generated code and must override several methods. Ice invokes the 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, 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.

Note that the response and exception methods are dispatched in the Swing thread because we installed an Ice.Dispatcher implementation, so it's safe for these methods to modify 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 = ...
_chat.begin_send(message).whenCompleted(cb.response, cb.exception);

The begin_send method accepts the chat message as its only argument and returns an Ice.AsyncResult object, on which we call the whenCompleted method to register our AMI callbacks. The callback class is shown below:

public class AMI_ChatSession_sendI {
    public AMI_ChatSession_sendI(Coordinator coordinator, string name,
        string message) {
        _coordinator = coordinator;
        _name = name;
        _message = message;
    }

    public void response(long timestamp) {
        _coordinator.userSayEvent(timestamp, _name, _message);
    }

    public void exception(Ice.Exception ex) {
        if(ex is Chat.InvalidMessageException) {
            Chat.InvalidMessageException e = (Chat.InvalidMessageException)ex;
            _coordinator.appendMessage(" - " + e.reason + Environment.NewLine);
        }
        else {
            _coordinator.destroySession();
        }
    }
    private Coordinator _coordinator = null;
    private string _name = "";
    private string _message = "";
}

We named our methods response and exception, but you can use any names you like. Ice invokes the 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, 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.

Note that the response and exception methods are dispatched in the event loop thread because we installed an Ice.Dispatcher implementation, so it's safe for these methods to modify the user interface.

Previous: C++ Clients Next: PHP Client

Terms of Use | Privacy © 2011 ZeroC, Inc.