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
Greeterinterface we defined earlier in Slicean 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:
#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:
#include "Chatbot.h"
#include <iostream>
#include <sstream>
using namespace std;
What really matters in this file is the implementation of Chatbot::greet.
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 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 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:
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:
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.
Ice::CommunicatorHolder communicatorHolder{communicator};
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.
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();
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:
// 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:
./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:
./build/server --Ice.Trace.Dispatch --Ice.Trace.Network=2