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.
- 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.
- The client waits for the welcome response and then issues the HELO or EHLO command. The client waits for a positive response.
- The client issues the command MAIL FROM: <youraddress> and waits for a positive response.
- 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.
- The client issue the DATA: command and waits for a positive response.
- 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.
- The client issues the QUIT command and waits for a positive response.
- The client ends.
In summary, the smtp client commands and smtp server responses occur:
SMTP CLIENT COMMANDS |
SMTP 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 |
DATA |
354 Start mail input; end with <CRLF>.<CRLF> |
email message |
|
. |
250 Message received! |
QUIT |
221 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):
- Create a subclass of
CSPServer
,
- Override the subclass constructor,
- Override of the Go() handler in your subclass,
- Add a TSPDispatch member variable,
- Create a TSPDispatch structure defining the state machine dispatch commands,
- Add command dispatch handlers to the subclass, and
- 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);
protected:
virtual void Go();
virtual void SendWelcome();
private:
static TSPDispatch Dispatch[];
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:
CMySPServer::CMySPServer(CSocketIO *s)
: CSPServer(s, Dispatch)
{
Done = FALSE;
Start();
}
void CMySPServer::Go()
{
inherited::Go();
delete this;
}
void CMySPServer::SendWelcome()
{
Send("Hello! Server ready\r\n");
}
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();
Done = TRUE;
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;
new CMySPServer(new CSocketIO(t));
.
.
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;
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