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
Greeterinterface 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.
Compile Slice File with Slice Compiler
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:
slice2cpp Greeter.iceThis 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.
Implement the Servant Class
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:
#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:
#include "Chatbot.h"
#include <iostream>#include <sstream>
using namespace std;What really matters in this file is the implementation of Chatbot::greet.
stringServer::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.
Main Server Program
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 ourGreeterservant.Ice/Ice.h: This header provides definitions that are necessary for accessing the Ice runtime.iostream: So the server can accesscoutto print to the console.
We also add a using declaration for the std namespace to reduce clutter:
#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:
intmain(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:
1. Create a Communicator
First, we create a Communicator with Ice::initialize:
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.
Ice::CommunicatorHolder communicatorHolder{communicator};2. Create an Object Adapter
Next, we create an object adapter using our communicator:
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:
// 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.
3. Activate the Object Adapter
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:
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.
4. Keep Running Until Ctrl+C
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:
// 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:
communicator->waitForShutdown();Finally, once the communicator is shut down, we reach the end of main and return:
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.
Running the Server
After building the server (see the demo’s README for instructions), running it is as simple as running any other executable:
Linux and macOS:
./build/serverWindows:
build\serverThe 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:
./build/server --Ice.Trace.Dispatch=1 --Ice.Trace.Network=2This 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
Greeterinterface 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.
Greeter Implementation
Compile Slice File with Slice Compiler
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.
Implement Servant Class
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:
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.
Main Server Program
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:
1. Create a Communicator
First, we create a Communicator using its constructor:
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.
2. Create an Object Adapter
Next, we create an object adapter using our communicator:
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:
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.
3. Activate the Object Adapter
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:
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.
4. Keep Running Until Ctrl+C
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:
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:
await communicator.shutdownCompleted;Finally, after the communicator is shut down, the communicator is disposed (because we used await using), and then our application exits.
Running the Server
After building the server (see the demo’s README for instructions), you can run it with dotnet:
cd Serverdotnet runThis 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
Greeterinterface 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.
Greeter Implementation
Compile Slice File with Slice Compiler
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.javaprovides theGreeterinterface we implement in the code below.AsyncGreeter.javaprovides theAsyncGreeterinterface, which a servant implements instead ofGreeterto dispatch requests asynchronously.GreeterPrx.javaprovides aGreeterproxy used by the client-side of this application._GreeterPrxI.javacontains internal code forGreeterPrx.
Implement Servant
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:
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.
Main Server Program
We write our main server code in a file named Server.java that starts out looking like:
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:
1. Create a Communicator
First, we create a Communicator using its constructor:
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.
2. Create an Object Adapter
Next, we create an object adapter using our communicator:
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:
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.
3. Activate the Object Adapter
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:
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.
4. Keep Running Until Ctrl+C
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:
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:
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.
Running the Server
After building the server (see the demo’s README for instructions), you can run it with the launcher script that the build generates:
./server/build/install/server/bin/serverThis 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:
- Greeter Implementation: First we need to create a class that “implements” the
Greeterinterface we defined earlier in Slice. An instance of this class is called a servant. - 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.
Greeter Implementation
Compile Slice File with Slice Compiler
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.
Implement Servant Class
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:
import Iceimport 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.
Main Server Program
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 argumentschatbot: This contains our servant implementation.Ice: For accessing the Ice runtime.
import sysimport chatbotimport IceNext, we define the main function which runs the server. This application can be broken down into 4 parts:
1. Create a Communicator
First, we create a Communicator using its constructor:
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.
2. Create an Object Adapter
Next, we create an object adapter using our communicator:
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.
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.
3. Activate the Object Adapter
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:
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.
4. Keep Running Until Ctrl+C
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:
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.
Running the Server
After building the server (see the demo’s README for instructions), you can run it with:
uv run main.pyThis 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
Greeterinterface 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.
Greeter Implementation
Compile Slice File with Slice Compiler
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.
.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:
{ "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.
Implement Servant
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.
import IceNext we implement the Greeter protocol.
/// 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.
Main Server Program
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.
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:
1. Create a Communicator
First, we create a Communicator with Ice.initialize:
var args = CommandLine.argumentslet 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().
2. Create an Object Adapter
Next, we create an object adapter using our communicator:
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:
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.
3. Activate the Object Adapter
Next, we call activate on our object adapter to start accepting incoming connections and dispatch requests to our Chatbot servant:
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.
4. Keep Running Until Ctrl+C
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:
// 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:
// 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.
Running the Server
To run the server, execute the following command (the executable will be compiled if necessary):
swift run Server