Click here to Skip to main content
15,860,861 members
Articles / Desktop Programming / MFC
Article

CSPServer, State-based Protocol Server Class

Rate me:
Please Sign up or sign in to vote.
4.88/5 (14 votes)
11 Mar 20038 min read 144K   1.4K   71   41
Class framework for creating client/server protocol servers

Introduction

CSPServer is a VC++ class which makes it relatively easy to create solid, multi-threaded client/server state-based protocol servers (SPS).  Common existing standard client/server SPS systems are SMTP, POP3, FTP, NNTP and others systems which millions of people use every day on the internet. 

CSPServer gives you a time-tested, well engineered framework to create your own standard protocol server or a new protocol server that suits your needs.  CSPServer is used in Santronics Software's intranet hosting product, Wildcat! Interactive Net Server (http://www.santronics.com) to provide an  integrated multiple protocol intranet hosting system.   Proprietary virtual communications technology was removed to make a public socket-based only version of CSPServer

This article will explain how to use the CSPServer class with working SPS examples.  This is the author's first CodeProject article subsmission, so all commentators and critics are welcome.

Background

A State-based Protocol Server or SPS is client/server methodology where by a client application connects to a server application to begin a text based "controlled" conversation.  This controlled conversation is often called the "State Machine."  

In a state machine, a connected client will issue a command and then wait for a server response to the command.  In a properly designed state machine, the client can not continue with additional commands until a response was provided by the server for the current command.  It is very important to understand that all conversations in a state machine begins with the client issuing commands. The server will never send data or information to the client unless it was requested or in response to a client command. 

CSPServer offers a framework to create your own client/server state machine conversation for your client/server application. 

If you understand this basic concept, you can skip the next background section which illustrates a SPS using a standard SMTP server.

Example standard SPS - SMTP

If you ever connected to standard SPS such as SMTP, POP3, FTP, NNTP etc, the first line you see is the welcome line.   The best way to see this is to use a standard TELNET client such as the one that comes with Windows.  For example, to connect to the Microsoft SMTP server (port 25) using telnet, type the following:

Telnet maila.microsoft.com 25

If successful, you will see the welcome line.  You can type HELP to see the available commands.  Most standard SPS systems will provide HELP information on the available commands.

However, SPS systems are ultimately designed for automated applications, not human interaction.  Client software are used to automate the process, such as sending an email.   The following illustrates what typically happens when you want to send an email to anyone in the world.

Example SMTP client/server session:

Lets assume the target address for the email is gbush@whitehouse.gov  and lets assume you are using Outlook Express (OE) to create and send the email.  OE has a built-in SMTP client component which is used to send mail to a SMTP server. 

The following are the steps taken to send the email.

  1. OE smtp client obtains the MX record for the domain whitehouse.gov. The MX provides the IP address location of the SMTP server. The client will then connect to the IP address defined by the MX record.
  2. The client waits for the welcome response and then issues the HELO or EHLO command.    The client waits for a positive response.
  3. The client issues the command MAIL FROM: <youraddress> and waits for a positive response. 
  4. The client issues the command RCPT TO: <gbush@whitehouse.gov> and waits for a positive response.  A negative response means the address is invalid or some other error, like mailbox is full.
  5. The client issue the DATA: command and waits for a positive response.
  6. The client begins to send the actual email message line by line, ending it with a "." line.  The client then waits for a positive response indicating the email was successfully received.
  7. The client issues the QUIT command and waits for a positive response.
  8. The client ends.

In summary, the smtp client commands and smtp server responses occur:

SMTP CLIENT COMMANDSSMTP SERVER RESPONSES
 220 Connected to Domain XXX,  Server Ready!
HELO or EHLO <client domain name>250 Hello Client Domain!
MAIL FROM: <your email address>250 <address>.... Sender Ok!
RCPT TO: <gbush@whitehouse.gov>250 <address>.... Receipient ok
DATA354 Start mail input; end with <CRLF>.<CRLF>
email message 
.250 Message received!
QUIT221 Closing connection, Goodbye!

Please note how the SMTP server uses numeric response codes for server responses.   They control how the client will react.  For example, when the client issues the RCPT TO: command,  the positive response code is 250 to indicate the address is acceptable.  However, negative response codes such as 550 can be issued which means the "unknown address."

The point behind this example is to illustrate the "tight" client/server state machine conversation between a state-based protocol server and a state-based protocol client such as in SMTP. Servers like SMTP have specific RFC design guidelines that describe the proper state machine (commands and responses).  The same is true for FTP, NNTP and POP3.

With CSPServer, you can create your own client/server state machine conversation.  You can use a similar response code concept for your own for your particular client/server application.

Understanding the CSPServer State Machine

The following figure 1.0 illustrates the "state machine" in CSPServer:

Figure 1.0
CSPServer State machine

Server Applet
Client Connection Listening Thread

<--connect--

Client Applet

|
Accept
|

Instantiate subclass CSPServer
Object thread

|

Call subclass
Go() Handler

|

call subclass SendWelcome()handler

--response-->

Client waits for welcome response

Command1()handler

<-----------

command1

--response-->

Command2()handler

<-----------

command2

--response-->

.
.

.
.

CommandN()handler

<-----------

commandN

--response-->

run optional subclass Cleanup()handler when client disconnects

When the client first connects , the listening server will start a new CSPServer session which start a new thread to manage client session.  The thread handler will call the subclass Go() handler.

The subclass Go() handler can be used to collect connection information but its main goal is to start the state machine engine by calling the inherited Go() handler.

The inherited Go() handler will then call the virtual "SendWelcome()" function and begin the state machine.  The SendWelcome() override provides the opportunity for the SPS to  introduce itself  and also possibly supply "readiness" information to the client.

Using the CSPServer Class

For those who wish to get started quickly,  the following is a "quick how to use" outline.  For technical class or code details see the source code and examples provided.

At a minimum, to create a SPS using the CSPServer class, you need to do following items (in no particular order):

  1. Create a subclass of CSPServer,
  2. Override the subclass constructor,
  3. Override of the Go() handler in your subclass,
  4. Add a TSPDispatch member variable,
  5. Create a TSPDispatch structure defining the state machine dispatch commands,
  6. Add command dispatch handlers to the subclass, and
  7. Create a Listening Server Thread to answer incoming connections

There are other virtual functions you can override, but the constructor and Go() are the only required overrides to start the CSPServer engine.

The SampleServer.cpp source file contains a complete working example of a SPS.   The following are the basic steps in create an SPS:

Step 1:

Add #include <spserver.h> to your source code, and create a CSPServer subclass (i.e., CMySPServer) such as the one shown below. 

For the sake of an example, we will create a state machine with 5 commands;   "HELLO", "LOGIN", "SHOW", "HELP" and "QUIT".  So for each command a handler is added.

#include <spserver.h>

class CMySPServer: public CSPServer {
    typedef CSPServer inherited;
public:
    CMySPServer(CSocketIO *s);  // REQUIRED
protected:
    virtual void Go(); // REQUIRED
    virtual void SendWelcome();
private:
    static TSPDispatch Dispatch[]; // REQUIRED
    BOOL SPD_HELLO(char *args);
    BOOL SPD_LOGIN(char *args);
    BOOL SPD_SHOW(char *args);
    BOOL SPD_HELP(char *args);
    BOOL SPD_QUIT(char *args);
};

Please note the SendWelcome() override is optional.  However, it is almost always required to send a connection response to the client when the client first connects to the server. 

The class CSocketIO is a simple socket wrapper with formatting functions and a socket input circular buffer.  This class documentation is not within the scope of this article.  See the source file socketio.h/cpp for usage and reference.

Step 2:

Create the TSPDispatch structure for the subclass member Dispatch declaring the commands and the commands dispatch handles as follows

CSPServer::TSPDispatch CMySPServer::Dispatch[] = {
  SPCMD(CMySPServer, "HELLO", SPD_HELLO),
  SPCMD(CMySPServer, "LOGIN", SPD_LOGIN),
  SPCMD(CMySPServer, "SHOW",  SPD_SHOW),
  SPCMD(CMySPServer, "HELP",  SPD_HELP),
  SPCMD(CMySPServer, "QUIT",  SPD_QUIT),
  {0}
};

For each command in the Dispatch structure, declare a dispatch handler in the subclass using the following prototype:

BOOL dispatch_handler_name(char *args);

Advanced Usage: It is possible to have an single handler for call commands.  In this case, you can use the method GetCurrentCommandName() to return the current command issued.

Step 3:

Now begin to add the implementation of the overrides and the dispatch handlers:

//////////////////////////////////////////////////////////////
// Constructor

CMySPServer::CMySPServer(CSocketIO *s)
     : CSPServer(s, Dispatch)
{
    // Initialize all your session variables here.

    // Done is a special BOOL used to exit
    // the inherited::Go() handler. One of the
    // Dispatch commands should set Done = TRUE;
    // i.e., QUIT() command.

    Done = FALSE;

    // start thread, calls Go() handler. If you
    // wish, you can call Start() outside the constructor.

    Start();  
}

//////////////////////////////////////////////////////////////
// Go() is called by start()

void CMySPServer::Go()
{
    // By this point, we have a new thread running. 
    // This is a good point to collect client ip or
    // domain information.

    // To start the state machine, you must call
    // the inherited Go() function.  This will
    // starts the thread's socket command line
    // reader and dispatcher. Go() returns when 
    // the Done is set TRUE or if connection drops
    // or one of the dispatch handlers return FALSE.

    inherited::Go(); // REQUIRED

    // we are done, good place to do session
    // cleanup.

    delete this;  // REQUIRED
}

//////////////////////////////////////////////////////////////
// SendWelcome() is called by the inherited Go(). This is 
// a good place to provide "server readiness" information
// to the connecting client.  Standard SPS use numeric 
// response codes to provide this information. 

void CMySPServer::SendWelcome()
{
    Send("Hello! Server ready\r\n");
}

//////////////////////////////////////////////////////////////
// Dispatch handlers.  
//
// Dispatch handlers have one parameter, char *args. It will 
// contain the string, if any, passed with the command. 
// 
// Return TRUE to continue the state machine. If FALSE is 
// returned, the session ends. NOTE: Returning FALSE is not 
// a good idea in practical designs as it can put the remote 
// client in a irregular state. You should always have a 
// graceful way to complete a session.  Even if you wish to 
// show "error" conditions, you should always return TRUE.

BOOL CMySPServer::SPD_HELLO(char *args)
{
    Send("--> HELLO(%s)\r\n",args);
    return TRUE;
}

BOOL CMySPServer::SPD_LOGIN(char *args)
{
    Send("--> LOGIN(%s)\r\n",args);
    return TRUE;
}

BOOL CMySPServer::SPD_SHOW(char *args)
{
    Send("--> SHOW(%s)\r\n",args);
    return TRUE;
}

BOOL CMySPServer::SPD_HELP(char *args)
{
    Send("--- HELP commands ---\r\n");
    Send("HELLO\r\n");
    Send("LOGIN\r\n");
    Send("SHOW\r\n");
    Send("HELP\r\n");
    Send("QUIT\r\n");
    Send("--- end of help ---\r\n");
    return TRUE;
}

BOOL CMySPServer::SPD_QUIT(char *)
{
    Send("<CLICK> Bye!\r\n");
    Control->Shutdown();   // Disconnects socket
    Done = TRUE;           // Tells Go() to exit
    return TRUE;
}

Step 4:

Finally, now that you have a CSPServer class ready, you need a listening server thread that will answer incoming socket connections and for each new connection, a CSPServer instance is started. 

To create the Listening Server, the CThread class is used:

class CServerThread : public CThread {
    typedef CThread inherited;
public:
   CServerThread(const DWORD port = 4044, const DWORD flags = 0);
   virtual void Stop();
protected:
   virtual void Go();
private:
   SOCKET serverSock;
   DWORD serverPort;
};

The subclass Go() handler is used to create the listening socket server. 

When a new connection is accepted, a new instance of CMySPServer is created passing the peer socket handle as a new  CSocketIO object in the CMySPServer constructor.  The following is done in the CServerThread::Go() handler:

.
.
SOCKET t = accept(serverSock, (sockaddr *)&src, &x);
if (serverSock == INVALID_SOCKET) break; // listening server broken
new CMySPServer(new CSocketIO(t));       // Start new CMySPServer session
.
.

You don't need to work about releasing the objects.  The class themselves will do the cleanup.

Example usage of CServerThread in a console application:

CServerThread server(4044);

while (!Abort) {
  if (kbhit() && getch() == 27) break;  // Escape to Exit
  Sleep(30);
}

server.Stop();

Points of Interest

See the source file SampleServer.cpp for a complete working example.  By default, the example uses port 4044.   To test the server, use telnet like so:

    Telnet LocalHost 4044

History

  • v1.0P - March 4, 2003, Initial Public Release

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Generalredundant check conditions make the code harder to understand Pin
ehaerim9-Jul-06 11:03
ehaerim9-Jul-06 11:03 
Dear Hector Santos,

CSPServer is one of the most valuable article in the codeproject.

Below is excerpted from the source.

I think the two checking conditions (bold ones) identical except that CheckAbort checks "Done" variable first. I think both are effectively identical so that I can remove redundant one. It's really hard to understand why you put two "WaitForSingleObject(terminate, 0) != WAIT_OBJECT_0".

DWORD CSocketIO::WaitForReceivedData(DWORD timeoutms, HANDLE terminate)
{
...

DWORD start = GetTickCount();
do
{
....
....
} while (((timeoutms == INFINITE) || ((GetTickCount() - start) < timeoutms))
&& (Socket != INVALID_SOCKET) && !CheckAbort()
&& ((terminate == NULL) || (WaitForSingleObject(terminate, 0) != WAIT_OBJECT_0)));

return WAIT_TIMEOUT;
}

Wouldn't it be equivalent to the following code?

DWORD CSocketIO::WaitForReceivedData(DWORD timeoutms, HANDLE terminate)
{
...

DWORD start = GetTickCount();
do
{
....
....
} while (((timeoutms == INFINITE) || ((GetTickCount() - start) < timeoutms))
&& (Socket != INVALID_SOCKET) && !CheckAbort());

return WAIT_TIMEOUT;
}


haerim

<ps> BTW, your email codeproject@winserver.com is not reachable. Please let me know if you have one.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.