Skip to main content
Skip table of contents

Writing a Greeter Server in C++

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.

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 (slice2cpp).

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

You can easily integrate Slice compilation with most build projects. The C++ demo programs use cmake.

The Slice compiler produces two files from Greeter.ice: 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.

Implement 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:

CPP
#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.

It is normal for servants like Chatbot to contain fields and other functions in addition to the needed ones from the generated base class. Due to our application’s simplicity, we don’t here though.

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:

CPP
#include "Chatbot.h"

#include <iostream>
#include <sstream>

using namespace std;

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

CPP
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.

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 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:

CPP
#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 an CtrlCHandler object, that we’ll discuss later. It’s important to create this object before anything else; just keep it in the back of your head for now:

CPP
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 4 parts:

Create a Communicator

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

CPP
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).

Before that though, 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.

CPP
Ice::CommunicatorHolder communicatorHolder{communicator};

Create an Object Adapter

Next, we create an object adapter using our communicator:

CPP
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:

CPP
// 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.

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:

CPP
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.

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”. 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:

CPP
// 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:

CPP
communicator->waitForShutdown();

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

CPP
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:

BASH
./build/server

You can optionally set Ice properties on the command line, for example, you can set Ice.Trace.Dispatch to 1 and Ice.Trace.Network to 2 with the following command:

BASH
./build/server --Ice.Trace.Dispatch --Ice.Trace.Network=2

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.