Tutorial 3 demonstrates the creation of a simple multi-threaded client/server application using MynahSA's TCP transport mechanism. The process of setting up a multi-threaded server is straight forward, and builds on the basics established in Tutorial 1: Simple Object Persistence.
In this example, the client will transmit a "Ping" message to a server and the server will respond back to the client with a ping response. The behavior is analogous to the unix ping command and measurements of the round trip time are reported. The protocol diagram is:
Ping Protocol
The server in this application has no state, which simplifies the implementation.
To start with, the objects transmitted across the network are defined. The RequestPing object contains a time stamp, and its definition (from examples/tutorial3/requestping.hpp) is as follows:
class RequestPing : public MynahSA::IoBase {
public:
void serialize(MynahSA::Archive& ar) {
ar & _sec;
ar & _usec;
}
RequestPing();
RequestPing(const struct timeval& tv);
virtual ~RequestPing();
virtual std::string ioName() const;
inline unsigned int getSeconds() const { return _sec; }
inline unsigned int getUSeconds() const { return _usec; }
private:
unsigned int _sec;
unsigned int _usec;
};
The ResponePing object that the server will transmit back to the client is similar, however, a boolean status flag named _ok is added to indicate the server's status. The definition for ResposnePing is placed in examples/tutorial3/responseping.hpp.
class ResponsePing : public MynahSA::IoBase {
public:
void serialize(MynahSA::Archive& ar) {
ar & _ok;
ar & _sec;
ar & _usec;
}
ResponsePing(bool ok=false, unsigned int sec=0, unsigned int usec=0);
virtual ~ResponsePing();
virtual std::string ioName() const;
bool isOk() const { return _ok; }
inline unsigned int getSeconds() const { return _sec; }
inline unsigned int getUSeconds() const { return _usec; }
private:
bool _ok;
int _sec;
int _usec;
};
Both of these classes are simple container classes and serve only as a mechanism for MynahSA's archiving system.
The next step is to define the "Server" behavior of the system. The server is nothing more then a mechanism that converts one descendant class of MynahSA::IoBase into another - in this case, reading a RequestPing object and returning a ResponsePing. The server behavior is defined by creating a class that derives from MynahSA::RequestResponseHandler. The following code segment is taken from MynahSA::RequestResponseHandler, and shows the operator() method that must be overridden to handle the RequestPing object. class RequestResponseHandler {
public:
...
virtual SHARED_PTR<IoBase>
operator()(const SHARED_PTR<IoBase>&) = 0;
};
MynahSA's network server classes invoke RequestResponseHandler::operator() every time an object is received. To handle ping objects, an object named PingRequestResponseHandler is derived from MynahSA::RequestResponseHandler, and its operator() is:
SHARED_PTR<IoBase>
PingRequestResponseHandler::operator()(const SHARED_PTR<IoBase>& req) {
SHARED_PTR<IoBase> resp;
if (req->ioName() == RequestPing().ioName()) {
SHARED_PTR<RequestPing> prq = static_pointer_cast<RequestPing>(req);
resp = SHARED_PTR<IoBase>(new ResponsePing(true,
prq->getSeconds(),
prq->getUSeconds()));
} else {
cerr << "PingRequestResponseHandler::operator()"
" : Unknown class received." << endl;
}
return resp;
}
There are a few things of note here:
-
This server is completely stateless. There is no sharing of information between two different connections, and because of this, the implementation of PingRequestResponseHandler is trivial. Should state sharing be desired, the state must be encapsulated in the derived instance of MynahSA::RequestResponseHandler.
-
Each simultaneous connection to a server spawns a new thread. Inside that thread, MynahSA::RequestResponseHandler::operator() is invoked. Therefore, all code placed in implementations of operator() must be thread safe. This is not a problem for PingRequestResponseHandler as it has no state.
Every time a new connection is created with MynahSA's TCP server a new instance of TCPArchiveStream is instantiated to provide archiving and serialization of the objects transmitted over the TCP connection. As explained in Tutorial 1: Simple Object Persistence objects transmitted by shared pointer must provide a constructor object and the constructor object must be registered with all input archivers. MynahSA's network transports operate by transmitting objects by shared pointer, therefore, constructor functions must be provided for all objects that will be transmitted. Constructor objects are created by deriving from base class MynahSA::StreamConstructor:
class PingStreamConstructor : public MynahSA::StreamConstructor {
public:
PingStreamConstructor();
virtual ~PingStreamConstructor();
virtual void operator()(MynahSA::ArchiveStreamBase&;) const;
};
The implementation of operator() from examples/tutorial3/pingstreamconstructor.cpp, shown below, demonstrates the registration of RequestPing and ResponsePing:
The above implementation of operator() takes a bidirectional stream instance (class SSLArchiveStream or TCPArchiveStream), instantiates constructor objects and registers them with the stream. A call to MYNAHSA_REGISTER_CONSTRUCTOR is required for each class transmitted by shared pointer across the network.
All of the components that are shared between client and server have now been demonstrated. The next step is to pull the components together and build the server and client implementations.
The server is implemented in examples/tutorial3/tcpserver.cpp. The steps in creating the server are:
-
Create a PingRequestResponseHandler.
-
Create a PingStreamConstructor.
-
Create a TCPConnectionManager, binding together the PingRequestResponseHandler and the stream constructor object.
-
Create a TCPRPCServer, using the connection manager.
-
Loop forever, waiting for connections.
We show the relevant code bits here from tcpserver.cpp - omitting exception handling and the calculation of the serverPort variable.
saInit();
...
PingRequestResponseHandler prh;
PingStreamConstructor myConstructor;
TCPConnectionManager cm(prh,
myConstructor);
TCPRPCServer server(cm, serverPort);
while (1) {
try {
server.checkClients(5);
} catch (const TCPServerConnectionError& sce) {
cerr << "Connection error detected: " << sce.what() << endl;
}
}
The client implementation is simple, and the steps are similar to the server. The steps are:
-
Determine the IP Address and port of the server
-
Create an instance of PingStreamConstructor
-
Create the TCPRPCClient instance
-
Call TCPRPCClient::rpc( ... ) with request objects and handle the response objects returned.
For brevity, the computation of the server's IP address and port are omitted. Refer to examples/tutorial3/tcpclient.cpp for details.
The creation of a PingStreamConstructor is identical to the server. The setup code looks like this:
PingStreamConstructor myConstructor;
TCPRPCClient myClient(myConstructor, ina, port);
The final step is to loop forever transmitting ping objects and receiving and handling responses. The code for calculating the round trip time has been omitted: while(1) {
...
SHARED_PTR<RequestPing> rpreq(new RequestPing(t));
SHARED_PTR<IoBase> resp(myClient.rpc(rpreq));
if (resp->ioName() == ResponsePing().ioName()) {
SHARED_PTR<ResponsePing> rsp = static_pointer_cast<ResponsePing>(resp);
...
}
}
This tutorial has demonstrated the creation of a simple client/server using MynahSA's TCP transport mechanism. The process of using TCP for creating a client/server system is reasonably simple, however, there are a few things to remember:
-
TCP is an insecure transport mechanism - meaning that all information is transmitted in the clear. For applications used in-house this may be perfectly acceptable, but for Internet applications using the SSL transport is strongly recommended.
-
There are only two differences between a client/server system and archiving to files: The server behavior is encapsulated in an object derived from MynahSA::RequestResponseHandler and a constructor object for MynahSA's TCP transport is required.