Go to the first, previous, next, last section, table of contents.


Usage example

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.

Receiving raw data

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.

Attaching data to a connection

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.

Receiving line-by-line data

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.

Detecting loss of link

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

Initializing new connections

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.

Detecting inactivity

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();
    }
}

Putting it all together

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.