class xServer

This class forms the server proper. It is responsible for connecting to the IRC network, parsing incoming messages, distributing commands, and updating the internal data structures. All communication with the network by internal clients is "routed" through the xServer instance.

Network I/O is handled by an instance of ClientSocket. Both input and output is buffered, using class Buffer. The Buffer class is an unbounded signed char buffer, capable of concatenation and deletion from the front of the buffer. It also provides a ReadLine method, which painlessly removes a '\n' terminated line from the buffer and returns it to the caller through an argument passed to the method call:

string tmp ;
if( !inputBuffer.ReadLine( tmp ) )
{
return false ;
}

This removes a lot of the complexity in creating servers and clients alike. The Buffer class is a welcomed addition to GNUWorld.

Input

Data from the network is obtained with the method DoRead(). The Socket class hierarchy uses blockign sockets. Therefore, before calling DoRead, it is very advisable to first check ReadyForRead(). Otherwise, the entire process could block indefinitely waiting for input. With a heavy weight server such as GNUWorld, this isn't normally a problem due to the amount of network traffic generated. However, hard and soft limits of timing events could be blocked should DoRead() block.

DoRead attempts a read from the network connection of SOCKETREADSIZE bytes (config.h). If the read fails, the _connected data flag is set to false, and -1 is returned. Otherwise, the data input is null terminated and appended to the input buffer. The number of bytes read is then returned.

Ouput

xServer provides various Write() method signatures:

virtual bool Write( const string& ) ;
virtual bool Write( const char*, ... ) ;

The first argument takes a const reference to std::string, and the second a printf() style variable argument list. This should provide sufficient flexibility for writing to the network: remember that const char* is implicitly converted to std::string in C++.

Neither of these methods does any actual I/O. If the server is not presently connected, false is returned. Otherwise, the buffer to be written is concatenated to the output buffer and true is returned.

To force a write to the network, use the flushBuffer() method.

Message Processing

The message handling distribution in GNUWorld is unique among the services servers Ive seen. First of all, a brief description of the command map.

Command Map

The command map is a VectorTrie, custom written like many objects, specifically for GNUWorld. This is a rather complicated structure, with a large overhead (though not so much in this case), and extremely fast Insert/Delete/Search. In fact, each of these operations has complexity O(m), where m is the length of the key. This is scorching fast, faster than any other structure I know of, including hash tables.

The command map is a templated structure, storing offset pointers into class xServer:

typedef VectorTrie< string, int (xServer::*)( xParameters& ) > commandMapType ;
commandMapType *commandMap ;

The offset pointer is actually a pointer into an xServer instance for a non-static command handler method. This structure is keyed by std::string, which is expanded out to const string& in calls to the Trie's methods.

Note the argument and return type of the command handlers. Each of these methods returns int: 0 for satisfactory completion, and -1 for error. Each of these methods receives a reference to an xParameters instance. The xParameters instance will have the command in question removed. Any arguments following a ':' (excluding if the colon is the first character) are stored into a single element at the end of the object.

Here is an example:

QAA P AUCBA :hello, how are you?
 

The xParameters instance passed to the handler method will be structured as follows:

xParameters[ 0 ]: "QAA"
xParameters[ 1 ]: "AUCBA"
xParameters[ 2 ]: "hello, how are you?"

Again, note that the command, 'P' (PRIVMSG) is removed, and that arguments following the colon (if the colon is not the first character of the entire line of input) are stored into a single element.

When a line of input is obtained from the input buffer, it is parsed, and an xParameters instance created. The command is then looked up in the command map. If it is found, the method located at the offset within the current (xServer) object is invoked:

commandMapType::pair_type* pairPtr =
commandMap->Search( Command, strlen( Command ) ) ;
if( pairPtr != NULL )
{
// parse the command ...
// Arguments are set.
// Go ahead and call the handler method
(this->*pairPtr->second)( Param ) ;
}

Note that the length of the command string was passed to Search. For an explanation of this (class std::string has a size() method of its own), see the documentation for GNUWorld's VectorTrie.

Just as an aside, this is the first time I've ever had to use the ->* operator.

Command Map Registration Macros

For the server to handle particular commands, each of these command tokens/keys must be loaded into the command map, and a handler method must be created. This is done via command "registration."

Commands that are to be handled must first have a method to perform action on behalf of the server. This requires declaring a prototype and defining the function body for the handler method. The prototype declaration is performed by using the DECLARE_MSG macro:

#define DECLARE_MSG( handlerFunc ) \
virtual int MSG_##handlerFunc( xParameters& ) ;

This macro must be placed in the class header file, preferrably in a protected section. This prohibits any outside classes/methods from calling the handler methods on their own. This command handler system is internal to the xServer, and should remain so.

This macro receives as argument the name of the command handler method. The actual name of the method will be MSG_ appended with the name given the this macro. For example, to prototype a method handler which will handle a command called FOO, use the following:

DECLARE_MSG( Foo )

A command handler prototype will created for FOO, which is equivalent to this:

int MSG_Foo( xParameters& ) ;

This is a suitable C++ forward method declaration, and it follows the format required of all command handler methods.

To actually associate a command token/string with a command handler, use the REGISTER_MSG macro:

#define REGISTER_MSG( key, handlerFunc ) \
if( !commandMap->Insert( key, &xServer::MSG_##handlerFunc, \
string( key ).size() ) ) \
{\
std::cout << "Unable to register function: "\
<< key << std::endl ;\
exit( 0 ) ; \
}

This macro must be placed in the xServer constructor, anywhere after the instantiation of the command map itself. If the registration for some reason fails, an error message will be output to the standard output stream, and the process will terminate.

Note that there is no macro for defining the handler methods themselves. This is left to the programmer. Macros are ugly, and should be avoided where possible. GNUWorld uses them here because they provide an easy way to generate compile time code.