In order to understand how MUSES works, we shall examine the demo program that comes with the library. In just a few lines, it builds an internet server that listens to two ports, accepts incoming connections, filters incoming data according to the telnet protocol, and parses it in two different ways.
We will build the various components needed to make such a server work, and in the end we will see how to put them together.
We begin by implementing a ReceiveHandler
: an object whose task is to
interpret whatever arrives from a client.
#include<muses/various_files> using namespace muses; // The entire library is in its own namespace class MyReceiveHandler : public ReceiveHandler { public: MyReceiveHandler(); virtual ~MyReceiveHandler(); virtual unsigned int operator () (Connection& connection, unsigned int len); }; MyReceiveHandler::MyReceiveHandler() : ReceiveHandler() { ; } MyReceiveHandler::~MyReceiveHandler() { ; } unsigned int MyReceiveHandler::operator () (Connection& connection, unsigned int len) { cout << "Receiving " << len << " bytes of data on socket " << connection.socketNumber() << ", port " << connection.port()->portNumber() << "." << endl; connection << "You sent me " << len << " bytes of data." << endl; return len; // Throw it all away }
As you can see, the handler only needs to override operator()
.
It is passed the Connection
from which the data arrived, and the
number of incoming bytes. Among other things, the Connection
knows
its socket and the port to which the client connected; it can access the
input buffer (not seen in this example); it can be used as an ostream to
send a reply to the client.
In many cases, we will be able to interpret the entire input in one pass. Sometimes, though, we may want to wait for more data. The handler is expected to return the number of characters that have been examined and can be removed from the input buffer.
In the example above, the handler is really minimal and stateless. In a real
application, the handler might contain some data about the current session;
as each Connection
can be given a different ReceiveHandler
,
the ReceiveHandler
is a good candidate to act as interface between
MUSES and your program's backend.
Most interesting server programs need to remember something about the previous history of each connection: who is on the other end, what commands were previously issued, and so on. MUSES provides a standard way to attach custom data to each connection.
Per-connection data should be stored in a class derived from
ConnectionData
, as in this example:
class MyData : public ConnectionData, public std::string { public: MyData(const std::string& s) : std::string(s) {}; virtual ~MyData(); }; MyData::~MyData() { ; }
The ConnectionData
class is an empty class, which does not define any
method (apart from constructor and virtual destructor) and does not contain
any data. Therefore, when using it as a mix-in class, as in the example above,
one does not incur in most of the disadvantages of multiple inheritance.
Pointers to ConnectionData objects can be attached to connections; retrieving them requires a dynamic cast, as can be seen in the next part of the example.
In several applications, a server is interested in line input, not in raw
input like before. MUSES provides a ready-made ReceiveHandler
that
splits input into lines and then parses each independently of the others.
Let's see an example:
class MyLineHandler : public LineHandler { public: MyLineHandler(); virtual ~MyLineHandler(); virtual void operator () (Connection& connection, std::string& line); }; MyLineHandler::MyLineHandler() : LineHandler() { ; } MyLineHandler::~MyLineHandler() { ; } void MyLineHandler::operator () (Connection& connection, std::string& line) { cout << "Socket " << connection.socketNumber() << " sent the following to port " << connection.port()->portNumber() << ":" << endl << line << endl; connection << "You sent me " << line.length() << " chars." << endl; }
This is even simpler than the ReceiveHandler
. Just like before,
you can store per-session data in a LineHandler
: each Connection
can be given (via a RH_LineSplitter
) a different LineHandler
.
To make the example more realistic, MyLineHandler
could store and
retrieve data from a MyData
object attached to each connection, and
could interpret some commands - even shutting down the whole server or a
single port as result:
void MyLineHandler::operator () (Connection& connection, std::string& line) { // Retrieve custom data from the Connection. // Return zero if there is no data of the correct kind. MyData *data = dynamic_cast<MyData*>(connection.data()); // Do something cout << "Socket " << connection.socketNumber() << " sent the following to port " << connection.port()->portNumber() << ":" << endl << line << endl; connection << "You sent me " << line.length() << " chars." << endl; if (data) connection << "The previous line you sent was: " << *data << endl; if (line == std::string("DISCONNECT")) connection.disconnect(); // Only the current Connection else if (line == std::string("CLOSE PORT")) connection.port()->shutdown(); // Only the current ServerPort else if (line == std::string("SHUTDOWN")) connection.port()->server()->shutdown(); // The whole server else { // Change Connection data delete data; data = new MyData(line); connection.setData(data); } }
Needless to say, the handler could limit itself to changing the contents of the current connection's custom data object, instead of deleting it and installing a new one.
If a client loses link, yet another handler is invoked. Each Connection
can be given its (optional) DisconnectHandler
, and will invoke it upon
detecting loss of link.
In our example, MyLineHandler
can allocate a MyData
object that
is only referenced by the connection. It should be released on link loss:
class MyDisconnectHandler: public DisconnectHandler { public: MyDisconnectHandler(); virtual ~MyDisconnectHandler(); virtual void operator () (Connection& deadConnection); }; MyDisconnectHandler::MyDisconnectHandler() : DisconnectHandler() { ; } MyDisconnectHandler::~MyDisconnectHandler() { ; } void MyDisconnectHandler::operator() (Connection& deadConnection) { cout << "Closing connection on socket " << deadConnection.socketNumber() << endl; // Retrieve custom data from the Connection and release it MyData *data = dynamic_cast<MyData*>(deadConnection.data()); if (data) { cout << "Last line sent was: " << *data << endl; delete data; deadConnection.setData(0); // Clear deleted pointer } }
The next step is to decide how to handle connection requests. When someone
connects to a MUSES port, a new Connection
is made; MUSES then asks
you to initialize it, set up appropriate handlers for it, and possibly say
hello to whoever connected. This is done via a ConnectHandler
.
class MyConnectHandler: public ConnectHandler { public: MyConnectHandler(ReceiveHandler *rec, DisconnectHandler *disc); virtual ~MyConnectHandler(); virtual void operator () (Connection& newConnection); private: // Here we suppose we can use a single global handler. // A different application may need to allocate a different // handler for each connection. ReceiveHandler *_rec; DisconnectHandler *_disc; }; MyConnectHandler::MyConnectHandler(ReceiveHandler *rec, DisconnectHandler *disc) : ConnectHandler(), _rec(rec), _disc(disc) { ; } MyConnectHandler::~MyConnectHandler() { ; } void MyConnectHandler::operator() (Connection& newConnection) { cout << "New connection on socket " << newConnection.socketNumber() << endl; newConnection.setReceiveHandler(_rec); newConnection.setDisconnectHandler(_disc); newConnection << "Welcome to the MUSES demo." << endl << "At the moment there are " << newConnection.port()->connections().size() << " active connections on port " << newConnection.port()->portNumber() << endl; }
Before doing anything else, the ConnectHandler
installs some handlers
on the current Connection
. If this is not done, the corresponding
events are silently ignored: any data sent by the user is thrown away, and
if the client drops link the connection is silently deleted.
By calling the same methods later on, you can replace a Connection
's
handlers on the fly; this may be useful in several circumstances.
Once told to run, MUSES will listen to its sockets until something happens. This is fine for a "passive" server, which only reacts to user input; however you may want your server to do something even without being told to. This can be achieved via a TimeoutHandler.
Let's assume we want the server to count down from 100 every second it doesn't hear from its clients; upon reaching zero, the server will shut down. Here is a possible implementation:
class MyTimeoutHandler: public TimeoutHandler { public: MyTimeoutHandler(int n); virtual ~MyTimeoutHandler(); virtual void operator () (Server& server); private: int _remaining; }; MyTimeoutHandler::MyTimeoutHandler(int n) : TimeoutHandler(), _remaining(n) { ; } MyTimeoutHandler::~MyTimeoutHandler() { ; } void MyTimeoutHandler::operator() (Server& server) { --_remaining; cout << "Nothing happened, counter is " << _remaining << endl; if (!_remaining) { cout << "Shutting down." << endl; server.shutdown(); } }
We have defined the behaviour of the demo server; now it is time to assemble the parts in a functioning whole. Let's start by building some of the handlers we defined:
int main(void) { const unsigned int MY_PORT_RAW = 1234; const unsigned int MY_PORT_LINE = 2345; MyReceiveHandler myReceive; MyLineHandler myLine;
A Connection
cannot handle a LineHandler
directly; it must be
wrapped with a RH_LineSplitter
, which is a ReceiveHandler
provided by MUSES.
RH_LineSplitter mySplitter; mySplitter.setLineHandler(&myLine);
As you may guess, mySplitter can be told later on to switch to a
different LineHandler
.
Users will log on via the telnet
program, which expects the peer to
obey certain conventions. Let's wrap our ReceiveHandler
objects within
a telnet
filter:
RH_Telnet myTelnet1(&myReceive), myTelnet2(&mySplitter);
RH_Telnet
is a ReceiveHandler
which implements a very minimal
version of the telnet
protocol - it refuses all optional features.
Child classes of RH_Telnet
may extend upon this and provide some more
complex negotiation.
The telnet protocol has some requirements on server output as well: in order
to obey them, let's add the following line to MyConnectHandler
.
newConnection.setSendFilter(sf_telnet);
sf_telnet is a globally available instance of SF_Telnet
, which
is an output filter. In most cases, attaching it to each new Connection
is all you need to do to get a telnet-compliant output.
Let's continue building basic blocks:
MyDisconnectHandler myDisconnect; MyTimeoutHandler myTimeout(100); MyConnectHandler myConnectR(&myTelnet1, &myDisconnect); MyConnectHandler myConnectL(&myTelnet2, &myDisconnect);
It is now time to set up the server and tell it to listen to the ports we are
interested in. Each port is given a ConnectHandler
to be called when
someone connects. Ports are added within a try
block: MUSES will
throw an exception if it is unable to set up the server.
Server myServer; cout << "Setting up server on ports " << MY_PORT_RAW << " (raw) and " << MY_PORT_LINE << " (line-based)." << endl; try { myServer.addPort(MY_PORT_RAW, &myConnectR); myServer.addPort(MY_PORT_LINE, &myConnectL);
Lastly, run the server. The while
loop will iterate as long as
there is an open port; in our case, until someone calls
myServer.shutdown()
. If no data arrives within a million microseconds,
that is one second, call myTimeout. In case some runtime error occurs,
print the appropriate error message and quit.
while (myServer.ports().begin() != myServer.ports().end()) myServer.run(1000000, &myTimeout); // Poll each second } catch (const std::exception& e) { cerr << e.what() << endl; } return 0; }
Compile, link, and your server is ready to run.
Go to the first, previous, next, last section, table of contents.