The Ice Programming Model

The Ice programming model consists of four simple steps:

  1. Define types and interfaces with Slice
  2. Compile the Slice definitions into source code for your chosen programming language
  3. Write client-side application code and compile it—together with the code generated by the Slice compiler—into a client program.
  4. Write server-side application code and compile it—together with the code generated by the Slice compiler—into a server program.

The following diagram illustrates this process, using C# as an example. The process for other compiled languages, such as C++, Java, and Objective-C, is very similar. For Ruby and Python, the Slice compilation step is optional. (You can instead have Ice interpret the Slice definitions at run time.) For PHP, Slice definitions are always loaded at run time, so no separate compilation is necessary.

Note that the only thing that links client and server are the Slice definitions. Client and server can be developed by different teams, and can be written in different languages.

The following sections illustrate how to create a complete client and server with C#. As you will see, only a few lines of code are needed to write a fully-functional distributed application.

Our application is very simple: the client passes a string to the server that the server translates into upper case and returns to the client. Despite its simplicity, this application shows all the essential steps that are needed to create a distributed application with Ice. Because the application does very little, most of it is actually boiler-plate code to initialize and finalize the Ice run time—this code needs to be written only once and can be reused for larger applications.

To see how to create a more sophisticated application that communicates through a firewall and requires the server to call back into clients, we suggest that you check our Chat Demo. Despite being considerably more sophisticated, that demo is still very simple because most of the code is application code. This is one of the most useful features of Ice: the middleware stays out of your way and allows you to get on with building your application.

Define Types and Interface with Slice

Slice (Specification Language for Ice) defines the types of data that are exchanged between client and server, as well as the interfaces of the distributed objects that are hosted by the server. For our example, we can define our interface as follows:

// File example.ice
module Example
{
    interface Converter
    {
        string toUpper(string s);
    };
};

The server provides an object of type Converter that clients can access. The object has a single operation, toUpper, that accepts a string as its input and returns the corresponding upper-case string as its output.

Note that the definition is enclosed in a module Example. Slice modules serve the same purpose as namespaces or packages in programming languages: they partition the global namespace to make it less likely that different applications define the same symbols. Modules can be reopened and need not be defined in a single source file because Slice supports separate compilation. This is especially useful to decouple larger development teams; developers can independently modify different parts of Slice definitions without causing "compilation avalanches", where a trivial change forces recompilation of the entire system.

Compile the Slice Definitions

To compile the Slice definition into C# source code, we use slice2cs, the Slice-to-C# compiler:

slice2cs example.ice

This compiles the definitions in the file example.ice into C# source code, in the file example.cs.

Write and Compile Client-Side Application Code

Writing the client takes only a few lines of code:

using Example;
public class Client
{
    public static void Main(string[] args)
    {      
        try
        {
            Ice.Communicator communicator = Ice.Util.initialize();
            ConverterPrx cvt = ConverterPrxHelper.checkedCast(
                communicator.stringToProxy("converter:tcp -p 10000 -h host.domain.com"));
            string upper = cvt.toUpper("hello world");
            System.Console.WriteLine("Server returned: " + upper);
            communicator.destroy();
        }
        catch(System.Exception ex)
        {
            System.Console.Error.WriteLine(ex);
            System.Environment.Exit(1);
        }
    }
}

Here are the steps taken by the client:

  1. The client initializes the Ice run time by calling Ice.Util.initialize. This returns an object of type Communicator, which represents an instance of the Ice run time.
  2. The client converts the string converter:tcp -p 10000 -h host.domain.com into a proxy by calling stringToProxy. The proxy represents the remote object that is provided by the server and, for most intents and purposes, behaves like an ordinary C# reference. The checkedCast converts that proxy from its base type (Ice.ObjectPrx) to the actual type (Example.ConverterPrx).

The converter part of the string used by the client identifies the target object. If the server hosts more than one object, each of the objects has a different identity.

Note that the client code knows a-priori that the server runs at host.domain.com and listens at port 10000 for incoming requests. We have hard-coded the server's details in this way for simplicity only. Ice provides a location service that allows clients to find servers without knowing anything about their physical location, so there is no need to hard-code (or configure) address details.

  1. The client's cvt reference (of type Example.ConverterPrx) represents the remote object in the server; the client can invoke a method via the cvt reference exactly like any other C# reference. Here, the client calls toUpper, which sends "hello world" to the server over the network and returns the upper-case string from the server.
  2. The client calls destroy on the communicator to finalize the Ice run time.

Note that this code could hardly be simpler. Ignoring the initialization and finalization code (which is largely the same for all applications), the client contains only a single line of application code:

string upper = cvt.toUpper("hello world");

That line of code takes care of all the networking chores: it opens a connection to the server, sends a message to the server, reads the response from the server, and returns the converted string to the application. If anything goes wrong (such as the server not responding), the Ice run time informs the client with an appropriate exception.

To compile the client, we compile the code generated by slice2cs (example.cs) and the application code (client.cs) into an executable. The necessary run-time support is provided by the Ice run-time library, Ice.dll:

csc /r:Ice.dll /lib:c:\Ice-3.3.1\bin client.cs example.cs

Write and Compile Server-Side Application Code

For our server, we must implement the toUpper method that is called by the client:

public class ConverterI : Example.ConverterDisp_
{
    public override string toUpper(string s, Ice.Current c)
    {
        return s.ToUpper();
    }
}

Note that we define a class that derives from Example.ConverterDisp_. This base class is generated by the Slice-to-C# converter and takes care of reading the message sent by the client off the network. The implementation of the toUpper method is trivial—it simply returns the converted string.

The remaining task is for the server to initialize and finalize the Ice run time. This code is almost identical to the code for the client. However, instead of initializing a proxy, the server creates an object adapter (which listens for incoming requests), and the server adds a ConverterI object (which processes incoming requests) to the adapter. In addition, the server calls waitForShutdown, which waits until the the Ice run time is shut down (typically, shutdown is initiated by a signal or by code running in another thread in the server).

public class Server
{
    public static void Main(string[] args)
    {
        try
        {
            Ice.Communicator communicator = Ice.Util.initialize();
            Ice.ObjectAdapter adapter = communicator.createObjectAdapterWithEndpoints(
               "converter", "tcp -p 10000");
            adapter.add(new ConverterI(), communicator.stringToIdentity("converter"));
            adapter.activate();
            communicator.waitForShutdown();
            communicator.destroy();
        }
        catch(System.Exception ex)
        {
            System.Console.Error.WriteLine(ex);
            System.Environment.Exit(1);
        }
    }
}

We have hard-wired the port at which the server listens for requests here for simplicity. The Ice location service can instead be used to dynamically assign an unused port to the server (and clients can locate the server without needing to know on which host and port the server runs).

To compile the server, we compile the code generated by slice2cs (example.cs) and the application code (server.cs) into an executable. The necessary run-time support is provided by the Ice run-time library, Ice.dll:

csc /r:Ice.dll /lib:c:\Ice-3.3.1\bin server.cs example.cs

Running Client and Server

To run the client and server, simply start the server in one window, and the client in another:

server.exe

client.exe (in a separate window)

The client prints:

Server returned: HELLO WORLD

Terms of Use | Privacy © 2010 ZeroC, Inc.