Writing a Greeter Server

10 min read

6 min read

7 min read

6 min read

7 min read

This page provides a step-by-step guide to writing the server-side of our C++ Greeter application.

The server consists of:

  • a C++ class that implements the Greeter interface we defined earlier in Slice
  • an Ice object adapter that accepts TCP connections from clients, and later routes requests received over these connections to our Greeter implementation

You can find the complete source code for this example in the ice-demos repository. The code below is lightly simplified: it leaves out the demo’s error-reporting helper and the include guard in Chatbot.h.

The first step when writing a C++ application with Ice is to compile the Slice definitions for this application with the Slice to C++ compiler (slice2cpp).

Here, we compile the Greeter.ice Slice file we wrote earlier:

Shell
slice2cpp Greeter.ice

This produces two files: a header file, Greeter.h, and a C++ source file, Greeter.cpp. The header file provides the Greeter abstract base class we implement in the code below, and Greeter.cpp is compiled into the server like any other source file. See Using the Slice Compiler for the options slice2cpp accepts.

In a real project you don’t run slice2cpp by hand. We recommend that you include this Slice compilation step in your build project, like we demonstrate for the C++ demo programs.

We implement the Greeter abstract base class with a C++ class, Chatbot. We call this implementation a servant class, or servant for short. Chatbot is declared in Chatbot.h:

C++
#include "Greeter.h"
namespace Server
{
/// Chatbot is an Ice servant that implements Slice interface Greeter.
class Chatbot : public VisitorCenter::Greeter
{
public:
// Implements the pure virtual function in the base class
// (VisitorCenter::Greeter) generated by the Slice compiler.
std::string greet(std::string name, const Ice::Current&) override;
};
}

Since the Greeter Slice interface only has one operation (greet), there is only one function that Chatbot needs to implement.

Notice that this function takes one more parameter than the Slice operation: a trailing const Ice::Current&. The Slice compiler adds this parameter to every operation it maps onto a servant. It describes the request being dispatched — the identity of the target Ice object, the operation name, the request context, and more. Our implementation doesn’t need any of this information, so we leave the parameter unnamed. See operations for the full mapping.

Next, let’s look at Chatbot.cpp, which provides the concrete implementation.

It starts out with some boilerplate #include statements and a using namespace std:

C++
#include "Chatbot.h"
#include <iostream>
#include <sstream>
using namespace std;

What really matters in this file is the implementation of Chatbot::greet.

C++
string
Server::Chatbot::greet(string name, const Ice::Current&)
{
cout << "Dispatching greet request { name = '" << name << "' }" << endl;
ostringstream os;
os << "Hello, " << name << "!";
return os.str();
}

You can see it takes a name parameter, and returns a greeting based on the provided name, matching both our header file, and indirectly, what was specified in our Slice file.

Our main server code is placed in its own file, Server.cpp, to keep it separate from the servant implementation. We start this file by including the following:

  • Chatbot.h: This is the header we just wrote for our Greeter servant.
  • Ice/Ice.h: This header provides definitions that are necessary for accessing the Ice runtime.
  • iostream: So the server can access cout to print to the console.

We also add a using declaration for the std namespace to reduce clutter:

C++
#include "Chatbot.h"
#include <Ice/Ice.h>
#include <iostream>
using namespace std;

Next, we define the main function which runs the server.

We start by creating a CtrlCHandler object. It must come before anything else, and we come back to what it does in step 4:

C++
int
main(int argc, char* argv[])
{
// CtrlCHandler is a helper class that handles Ctrl+C and similar signals.
// It must be constructed at the beginning of the program,
// before creating an Ice communicator or starting any thread.
Ice::CtrlCHandler ctrlCHandler;
// ...

Putting this aside, the interesting parts of this application can be broken down into four parts:

First, we create a Communicator with Ice::initialize:

C++
Ice::CommunicatorPtr communicator = Ice::initialize(argc, argv);

The communicator is our main entry point into the Ice runtime. In this simple server application, we need this communicator to create an object adapter and nothing else (see next step).

We immediately place this communicator in a CommunicatorHolder. This stack-allocated helper calls destroy on the communicator in its destructor. Always call destroy on a communicator when you’re done with it, either explicitly or through a helper class. destroy closes connections gracefully and performs other cleanups.

C++
Ice::CommunicatorHolder communicatorHolder{communicator};

Next, we create an object adapter using our communicator:

C++
auto adapter = communicator->createObjectAdapterWithEndpoints("GreeterAdapter", "tcp -p 4061");

An object adapter serves two purposes in Ice:

  • it accepts connections from clients
  • it receives requests over these connections and routes them to servants based on the object identities carried by these requests

In this example, we create an object adapter named GreeterAdapter which listens for TCP connections on port 4061, and later receives requests over these connections.

We then register our Chatbot servant with this adapter under the identity greeter:

C++
// Register the Chatbot servant with the adapter.
adapter->add(make_shared<Server::Chatbot>(), Ice::Identity{"greeter"});

Later on, when the object adapter receives a request with identity greeter, it will route this request to our Chatbot instance. It is therefore essential that the client uses the same identity in its proxy.

At this point, our object adapter does not accept connections yet. The endpoint is already bound, so the operating system completes the TCP handshake, but Ice never finishes establishing the connection — a client attempting to connect just waits, until it gives up with a ConnectTimeoutException.

We call activate to start accepting connections:

C++
adapter->activate();
cout << "Listening on port 4061..." << endl;

Our server is now active, waiting for connections and requests from clients, and dispatching requests for greeter to our Chatbot servant.

It is essential to keep the server running, and not let main return prematurely. We use the following technique to achieve this goal:

  • wait in the main thread until an event occurs
  • catch Ctrl+C signals in a background thread and trigger this event upon Ctrl+C

The event we chose is “the communicator was shut down”. But you could pick any other event; for example, you could implement the same logic with a std::promise.

The Ctrl+C handling is courtesy of the CtrlCHandler object we created earlier. We call its setCallback function to specify what to do when it catches a signal. In this case, we want to shut down the communicator:

C++
// Shut down the communicator when the user presses Ctrl+C.
ctrlCHandler.setCallback(
[communicator](int signal)
{
cout << "Caught signal " << signal << ", shutting down..." << endl;
communicator->shutdown();
});

Next, we block the main thread until the communicator is shut down:

C++
communicator->waitForShutdown();

Finally, once the communicator is shut down, we reach the end of main and return:

C++
return 0;

At this point, the destructor of the CommunicatorHolder destroys the communicator and indirectly our object adapter, all incoming connections are closed, and various other cleanups take place.

After building the server (see the demo’s README for instructions), running it is as simple as running any other executable:

Linux and macOS:

Shell
./build/server

Windows:

Shell
build\server

The server prints its listening message, then waits for clients:

Listening on port 4061...

Leave it running and start the Greeter client in a separate terminal. Each request the client sends shows up in the server’s output:

Dispatching greet request { name = 'alice' }
Dispatching greet request { name = 'bob' }
Dispatching greet request { name = 'carol' }

Press Ctrl+C to stop the server.

You can optionally set Ice properties on the command line. This works because Ice::initialize parses the arguments we handed it: it applies every --Ice.* option it recognizes as a configuration property, and removes it from argv. For example, this command sets Ice.Trace.Dispatch to 1 and Ice.Trace.Network to 2:

Shell
./build/server --Ice.Trace.Dispatch=1 --Ice.Trace.Network=2

This page provides a step-by-step guide to writing the server-side of our C# Greeter application.

The server consists of:

  • A C# class that implements the Greeter interface we defined earlier in Slice
  • an Ice object adapter that accepts TCP connections from clients, and later routes requests received over these connections to our Greeter implementation

You can find the complete source code for this example in the ice-demos repository.

The first step when writing a C# application with Ice is to compile the Slice definitions for this application with the Slice to C# compiler (slice2cs).

Here, we compile the Greeter.ice Slice file we wrote earlier. We recommend that you include this Slice compiler step in your build project, like we demonstrate for the C# demo programs.

This generates a single file named Greeter.cs which provides the GreeterDisp_ abstract base class we implement in the code below.

We implement the GreeterDisp_ abstract base class with a C# class, Chatbot. We call this implementation a servant class, or servant for short. We define our Chatbot in Chatbot.cs:

C#
using VisitorCenter;
namespace Server;
internal class Chatbot : GreeterDisp_
{
public override string Greet(string name, Ice.Current current)
{
Console.WriteLine($"Dispatching greet request {{ name = '{name}' }}");
return $"Hello, {name}!";
}
}

Since the Greeter Slice interface only has one operation (greet), there is only one method that Chatbot needs to implement. You can see our implementation takes a name parameter, and returns a greeting based on the provided name, matching what was specified in our Slice file.

We’ll write our main server code in a file named Program.cs. The logic in this file can be broken down into four pieces:

First, we create a Communicator using its constructor:

C#
await using var communicator = new Ice.Communicator(ref args);

The communicator is our main entry point into the Ice runtime. In this simple server application, we need this communicator to create an object adapter and nothing else (see next step).

It is important to make sure that your communicator is properly disposed when no longer needed. This ensures that network connections are gracefully closed, threads are joined, and other important clean-up occurs. The easiest way to do this is with an await using like we do here.

Next, we create an object adapter using our communicator:

C#
Ice.ObjectAdapter adapter = communicator.createObjectAdapterWithEndpoints(
"GreeterAdapter",
"tcp -p 4061");

An object adapter serves two purposes in Ice:

  • it accepts connections from clients
  • it receives requests over these connections and routes them to servants based on the object identities carried by these requests

In this example, we create an object adapter named GreeterAdapter which listens for TCP connections on port 4061, and later receives requests over these connections.

We then register our Chatbot servant with this adapter under the identity greeter:

C#
adapter.add(new Server.Chatbot(), new Ice.Identity { name = "greeter" });

Later on, when the object adapter receives a request with identity “greeter”, it will route this request to our Chatbot instance. It is therefore essential that the client uses the same identity in its proxy.

At this point, our object adapter does not accept connections yet. A client attempting to connect would get a ConnectTimeoutException.

We call activate to start accepting connections:

C#
adapter.activate();
Console.WriteLine("Listening on port 4061...");

Our server is now active, waiting for connections and requests from clients, and dispatching requests for “greeter” to our Chatbot servant.

It is essential to keep the server running and not fall off main prematurely. We use the following technique to achieve this goal:

  • wait in main until an event occurs
  • register a callback that runs upon Ctrl+C and triggers this event

The event we chose is “the communicator was shut down”. But you could pick any other event.

The Ctrl+C handling is courtesy of Console.CancelKeyPress. We add a callback function which runs when it intercepts a signal. In this case, we want to shut down the communicator:

C#
Console.CancelKeyPress += (sender, eventArgs) =>
{
eventArgs.Cancel = true; // don't terminate the process
Console.WriteLine("Caught Ctrl+C, shutting down...");
communicator.shutdown(); // starts shutting down
};

Next, we block until the communicator is shut down:

C#
await communicator.shutdownCompleted;

Finally, after the communicator is shut down, the communicator is disposed (because we used await using), and then our application exits.

After building the server (see the demo’s README for instructions), you can run it with dotnet:

Shell
cd Server
dotnet run

This page provides a step-by-step guide to writing the server-side of our Java Greeter application.

The server consists of:

  • A Java class that implements the Greeter interface we defined earlier in Slice
  • an Ice object adapter that accepts TCP connections from clients, and later routes requests received over these connections to our Greeter implementation

You can find the complete source code for this example in the ice-demos repository.

The first step when writing a Java application with Ice is to compile the Slice definitions for this application with the Slice to Java compiler (slice2java).

Here, we compile the Greeter.ice Slice file we wrote earlier. We recommend that you include this Slice compiler step in your build project, like we demonstrate for the Java demo programs.

The Slice compiler produces 4 files in a folder named com/example/visitorcenter:

  • Greeter.java provides the Greeter interface we implement in the code below.
  • AsyncGreeter.java provides the AsyncGreeter interface, which a servant implements instead of Greeter to dispatch requests asynchronously.
  • GreeterPrx.java provides a Greeter proxy used by the client-side of this application.
  • _GreeterPrxI.java contains internal code for GreeterPrx.

We implement the Greeter interface with a Java class: Chatbot. We call this implementation a servant class, or servant for short. Chatbot is defined in Chatbot.java:

Java
package com.example.ice.greeter.server;
import com.example.visitorcenter.Greeter;
import com.zeroc.Ice.Current;
class Chatbot implements Greeter {
@Override
public String greet(String name, Current current) {
System.out.println("Dispatching greet request { name = '" + name + "' }");
return "Hello, " + name + "!";
}
}

Since the Greeter Slice interface only has one operation (greet), there is only one method that Chatbot needs to implement. You can see our implementation takes a name parameter, and returns a greeting based on the provided name, matching what was specified in our Slice file.

We write our main server code in a file named Server.java that starts out looking like:

Java
package com.example.ice.greeter.server;
import com.zeroc.Ice.Communicator;
import com.zeroc.Ice.Identity;
import com.zeroc.Ice.ObjectAdapter;
class Server {
public static void main(String[] args) {
// ...
}
}

Our main function can be broken down into 4 pieces:

First, we create a Communicator using its constructor:

Java
public static void main(String[] args) {
try (Communicator communicator = new Communicator(args)) {
// ...
}
}

The communicator is our main entry point into the Ice runtime. In this simple server application, we need this communicator to create an object adapter and nothing else (see next step).

It is important to make sure that your communicator is properly closed when no longer needed. This ensures that network connections are gracefully closed, threads are joined, and other important clean-up occurs. The easiest way to do this is with a try-with-resources statement like we do here.

Next, we create an object adapter using our communicator:

Java
ObjectAdapter adapter = communicator.createObjectAdapterWithEndpoints(
"GreeterAdapter", "tcp -p 4061");

An object adapter serves two purposes in Ice:

  • it accepts connections from clients
  • it receives requests over these connections and routes them to servants based on the object identities carried by these requests

In this example, we create an object adapter named GreeterAdapter which listens for TCP connections on port 4061, and later receives requests over these connections.

We then register our Chatbot servant with this adapter under the identity greeter:

Java
adapter.add(new Chatbot(), new Identity("greeter", ""));

Later on, when the object adapter receives a request with identity “greeter”, it will route this request to our Chatbot instance. It is therefore essential that the client uses the same identity in its proxy.

At this point, our object adapter does not accept connections yet. A client attempting to connect would get a ConnectTimeoutException.

We call activate to start accepting connections:

Java
adapter.activate();
System.out.println("Listening on port 4061...");

Our server is now active, waiting for connections and requests from clients, and dispatching requests for “greeter” to our Chatbot servant.

It is essential to keep the server running and not fall off main prematurely. We use the following technique to achieve this goal:

  • wait in the main thread until an event occurs
  • register a shutdown hook that will run upon Ctrl+C and trigger this event

The event we chose is “the communicator was shut down”. But you could pick any other event.

We register a shutdown hook that calls shutdown on the communicator, and then waits for the main thread to finish, to ensure a clean shutdown. The hook runs in its own thread, so we capture the main thread before we register the hook:

Java
Thread mainThread = Thread.currentThread();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Caught Ctrl+C, shutting down...");
communicator.shutdown();
try {
mainThread.join(); // Wait until the main thread completes.
} catch (InterruptedException ignored) {}
}));

Next, after registering this hook, we block the main thread until the communicator is shut down:

Java
communicator.waitForShutdown();

Once the communicator is shut down, we reach the end of our try-with-resources block, the communicator gets closed, and the application exits.

After building the server (see the demo’s README for instructions), you can run it with the launcher script that the build generates:

Shell
./server/build/install/server/bin/server

This page provides a step-by-step guide to writing the server-side of our Python Greeter application.

This simple server application is divided into two sections:

  1. Greeter Implementation: First we need to create a class that “implements” the Greeter interface we defined earlier in Slice. An instance of this class is called a servant.
  2. Main Server Program: Next, we instantiate this class and register the servant with the Ice runtime through an object adapter.

You can find the complete source code for this example in the ice-demos repository on GitHub.

The first step when writing a Python application with Ice is to compile its Slice definitions using the Slice to Python compiler (slice2py).

Here we compile the Greeter.ice Slice file we wrote earlier.

This compilation generates a Python package named VisitorCenter, which matches the Slice module name. Inside this package, you’ll find the generated Greeter module corresponding to the Greeter interface defined in Slice. This module defines the Greeter abstract base class we implement in the code below.

An abstract base class is generated for each Slice interface. The package and class of this abstract base class matches the Slice module and interface name—for this example, VisitorCenter.Greeter.

We implement this abstract base class with a Python class, Chatbot, declared in chatbot.py:

Python
import Ice
import VisitorCenter
class Chatbot(VisitorCenter.Greeter):
def greet(self, name: str, current: Ice.Current) -> str:
print(f"Dispatching greet request {{ name = '{name}' }}")
return f"Hello, {name}!"

Since the Greeter Slice interface only has one operation (greet), there is only one function that Chatbot needs to implement.

You can see it takes a name parameter, and returns a greeting based on the provided name, matching both the generated abstract class, and indirectly, what was specified in our Slice file.

Our main server code is placed in its own file, main.py to keep it separate from the servant implementation. We start this file by importing the following:

  • sys: For accessing command line arguments
  • chatbot: This contains our servant implementation.
  • Ice: For accessing the Ice runtime.
Python
import sys
import chatbot
import Ice

Next, we define the main function which runs the server. This application can be broken down into 4 parts:

First, we create a Communicator using its constructor:

Python
with Ice.Communicator(sys.argv) as communicator:

The communicator is our main entry point into the Ice runtime. In this simple server application, we need this communicator to create an object adapter and nothing else (see next step).

You should always ensure that destroy is called when you’re done with a communicator so that connections are gracefully closed and other clean-up is performed. The simplest way to do that is to use the with statement like we do here.

Next, we create an object adapter using our communicator:

Python
adapter = communicator.createObjectAdapterWithEndpoints(
"GreeterAdapter",
"tcp -p 4061")

An object adapter serves two purposes in Ice:

  • it accepts connections from clients
  • it receives requests over these connections and routes them to servants based on the object identities carried by these requests

In this example, we create an object adapter named GreeterAdapter which listens for TCP connections on port 4061, and later receives requests over these connections.

Python
adapter.add(chatbot.Chatbot(), Ice.Identity(name="greeter"))

Later on, when the object adapter receives a request with identity “greeter”, it will route this request to our Chatbot instance. It is therefore essential that the client uses the same identity in its proxy.

At this point, our object adapter does not accept connections yet. A client attempting to connect would get a ConnectTimeoutException.

We call activate to start accepting connections:

Python
adapter.activate();
print("Listening on port 4061...")

Our server is now active, waiting for connections and requests from clients, and dispatching requests for “greeter” to our Chatbot servant.

It’s essential to keep the server running and not fall off main prematurely.

We use waitForShutdown to wait until the communicator is shut down. In this server, this never happens - no code shuts down the communication. The call only returns when the user hits Ctrl+C which raises KeyboardInterrupt exception:

Python
try:
communicator.waitForShutdown()
except KeyboardInterrupt:
print("Caught Ctrl+C, exiting...")

Once the exception handler finishes, the communicator goes out of scope and is automatically destroyed (because we used the with statement). At that point, the application exits cleanly.

After building the server (see the demo’s README for instructions), you can run it with:

Shell
uv run main.py

This page provides a step-by-step guide to writing the server-side of our Swift Greeter application.

The server consists of:

  • a Swift struct that implements the Greeter interface we defined earlier in Slice
  • an Ice object adapter that accepts TCP connections from clients, and later routes requests received over these connections to our Greeter implementation

You can find the complete source code for this example in the ice-demos repository.

To write a Swift application with Ice, first configure SwiftPM to compile the Slice definitions. The Ice package contains a plugin for this purpose, CompileSlice, which can be added to the executableTarget in Package.swift.

Swift
.executableTarget(
name: "Server",
dependencies: [.product(name: "Ice", package: "ice")],
plugins: [.plugin(name: "CompileSlice", package: "ice")]
),

The CompileSlice plugin compiles the .ice files in the target's sources, and the files listed in a slice-plugin.json file in the target's sources. In this example, Greeter.ice is in the slice directory at the root of the package, outside Sources/Server, so Sources/Server/slice-plugin.json lists it, with a path relative to the directory that contains slice-plugin.json:

json
{
"sources": ["../../slice/Greeter.ice"]
}

The plugin compiles Greeter.ice into Greeter.swift and adds it as a source file of the Server target. The generated code provides the APIs that we’ll need in our server code, so it’s an essential step of the development process.

A Swift protocol is generated for each Slice interface. In our example, Greeter.

We implement the Greeter protocol with a Swift struct, Chatbot, defined in Chatbot.swift.

To get started we first need to import the Ice module. This is required for the Ice.Current parameter.

Swift
import Ice

Next we implement the Greeter protocol.

Swift
/// Chatbot is an Ice servant that implements Slice interface Greeter.
struct Chatbot: Greeter {
// Implements the method greet from the Greeter protocol generated by
// the Slice compiler.
func greet(name: String, current _: Ice.Current) -> String {
print("Dispatching greet request { name = '\(name)' }")
return "Hello, \(name)!"
}
}

Since the Greeter Slice interface only has one operation (greet), there is only one method that Chatbot needs to implement.

You can see that the implementation of greeter is simple: it takes a name parameter, and returns a greeting based on the provided name.

Our main server code is placed in its own file, main.swift. First, we need to import the Ice module and create a CtrlCHandler object.

Swift
import Ice
// CtrlCHandler is a helper struct that handles Ctrl+C and similar signals.
// It must be constructed at the beginning of the program, before creating
// an Ice communicator or starting any thread.
let ctrlCHandler = CtrlCHandler()

We’ll discuss this more later. It’s important to create this object before anything else; just keep it in the back of your head for now.

The server application can now be broken down into four pieces:

First, we create a Communicator with Ice.initialize:

Swift
var args = CommandLine.arguments
let communicator = try Ice.initialize(&args)
defer {
communicator.destroy()
}

The communicator is our main entry point into the Ice runtime, handling the creation and caching of outgoing connections, among many other responsibilities.

It is important to properly clean up the communicator when done, which we do with the defer block that calls destroy().

Next, we create an object adapter using our communicator:

Swift
let adapter = try communicator.createObjectAdapterWithEndpoints(
name: "GreeterAdapter", endpoints: "tcp -p 4061")

An object adapter serves two purposes in Ice:

  • it accepts connections from clients
  • it receives requests over these connections and routes them to servants based on the object identities carried by these requests

In this example, we create an object adapter named GreeterAdapter which listens for TCP connections on port 4061, and later receives requests over these connections.

We then register out Chatbot servant with this adapter under the identity greeter:

Swift
try adapter.add(servant: Chatbot(), id: Ice.Identity(name: "greeter"))

Later on, when the object adapter receives a request with identity “greeter”, it will route this request to our Chatbot instance. It is therefore essential that the client uses the same identity in its proxy.

Next, we call activate on our object adapter to start accepting incoming connections and dispatch requests to our Chatbot servant:

Swift
try adapter.activate()
print("Listening on port 4061...")

Our server is now active, waiting for connections and requests from clients, and dispatching requests for “greeter” to our Chatbot servant.

It is essential to keep the server running and not fall off main prematurely. We use the following technique to achieve this goal:

  • wait in the main thread until an event occurs
  • catch Ctrl+C signals in a background thread and trigger this event upon Ctrl+C

The event we chose is “the communicator was shut down”.

The Ctrl+C handling is courtesy of the CtrlCHandler object we created earlier. We call its setCallback method to specify what to do when it catches a signal. In this case, we want to shut down the communicator:

Swift
// Shutdown the communicator when the user presses Ctrl+C.
ctrlCHandler.setCallback { signal in
print("Caught signal \(signal), shutting down...")
communicator.shutdown()
}

Next, we wait until the communicator is shut down:

Swift
// Wait until the communicator is shut down.
// Here, this occurs when the user presses Ctrl+C.
await communicator.shutdownCompleted()

Finally, once the communicator is shut down by Ctrl+C, we reach the end of main and return. The communicator destruction destroys the object adapter, close all incoming connections, and perform various other clean-ups.

To run the server, execute the following command (the executable will be compiled if necessary):

Shell
swift run Server