Writing a Greeter Server in C++
This page presents how to create a C++ server that implements our greeter interface and hosts it for clients to call.
Our server is comprised of 3 files:
Chatbot.h
: This file defines a servant that implements Slice interface Greeter…Chatbot.cpp
: … and this file provides the concrete implementation for it.Server.cpp
: This file contains the main server program.
You can see the fully put together application on GitHub in the ice-demos repository.
Servant Implementation
First, we need to define our Chatbot
servant that implements the Greeter
Slice interface. Because the servant is so small and simple, we’ll look at the complete file, then explain it piece by piece:
#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;
};
}
At the top, notice that we include Greeter.h
; this is the header file that the Slice compiler generated from Greeter.ice
. Then, we open a namespace named Server
. This isn’t necessary for everything to work, but is considered a best practice.
More importantly, it’s here that we define the Chatbot
class. There are two things that matter here:
It inherits from
VisitorCenter::Greeter
. This is a ‘servant skeleton’ class that the Slice compiler generated from theGreeter
Slice interface which (among other things) declares pure virtual functions corresponding to each mapped Slice operation for theGreeter
interface.Chatbot
provides concrete implementations for each of the mapped Slice operations. It’s permissible (and normal) for servants to contain other helper functions and fields as well, but at a minimum, it must implement functions whose signatures exactly match those in the skeleton.
Next, let’s look at Chatbot.cpp
, which provides those concrete implementations.
It starts out with some includes
and a using namespace std
, but this is just boilerplate.
#include "Chatbot.h"
#include <iostream>
#include <sstream>
using namespace std;
What really matters in this file is that it provides an 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.
That’s all it takes to implement our servant!
Main Server Program
Server.cpp
starts by including a few files:
Chatbot.h
: Like we said above, this contains our servant definition.Ice/Ice.h
: This header provides definitions that are necessary for accessing the Ice runtime.iostream
: So the server can usecout
to 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 will run the server:
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;
// ...
Unlike clients, we don’t want our server to exit until it’s explicitly told to shutdown. While there’s a variety of ways to accomplish this, for this example, our server will continue running until interrupted with Ctrl-C or a similar signal. To do this, we create a CtrlCHandler object to let us catch Ctrl-C and similar signals. When the server receives such a signal, it will shut down it’s communicator (more on that in a bit), which in turn makes waitForShutdown
return. You can see this at the bottom of the main
function:
// ...
// Shut down the communicator when the user presses Ctrl+C.
ctrlCHandler.setCallback(
[communicator](int signal)
{
cout << "Caught signal " << signal << ", shutting down..." << endl;
communicator->shutdown();
});
// Wait until the communicator is shut down.
// Here, this occurs when the user presses Ctrl+C.
communicator->waitForShutdown();
This is largely just boilerplate though. The interesting part happens in between these two sections.
First, we create a Communicator and place it in a CommunicatorHolder, which ensures it will be properly destroyed when it goes out of scope:
// Create an Ice communicator.
// We'll use this communicator to create an object adapter.
Ice::CommunicatorPtr communicator = Ice::initialize(argc, argv);
// Make sure the communicator is destroyed at the end of this scope.
Ice::CommunicatorHolder communicatorHolder{communicator};
Next we create an object adapter to register our servant with. In this example, our adapter is named GreeterAdapter
and we have it listen on the following endpoint: tcp -p 4061
, i.e. our adapter will listen for TCP requests on port 4061.
// Create an object adapter that listens for incoming requests
// and dispatches them to servants.
auto adapter = communicator->createObjectAdapterWithEndpoints(
"GreeterAdapter", "tcp -p 4061");
Then we 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"});
And finally, we activate on our object adapter to start accepting incoming connections and dispatching requests from them:
// Start dispatching requests.
adapter->activate();
cout << "Listening on port 4061..." << endl;