A Reusable Windows Socket Server Class With C++ - Some example servers
(Page 4 of 6 )
We now have a framework for creating servers. The user needs to provide a worker thread class that is derived from CSocketServer::WorkerThread and a socket server that's derived from CSocketServer. These classes could look something like this:
class CSocketServerWorkerThread : public CSocketServer::WorkerThread
{
public :
CSocketServerWorkerThread(
CIOCompletionPort &iocp);
private :
virtual void ReadCompleted(
CSocketServer::Socket *pSocket,
CIOBuffer *pBuffer);
};
class CMySocketServer : CSocketServer
{
public :
CMySocketServer (
unsigned long addressToListenOn,
unsigned short portToListenOn);
private :
virtual WorkerThread *CreateWorkerThread(
CIOCompletionPort &iocp);
virtual SOCKET CreateListeningSocket(
unsigned long address,
unsigned short port);
virtual void OnConnectionEstablished(
Socket *pSocket,
CIOBuffer *pAddress);
}; Implementations for CreateListeningSocket() and OnConnectionEstablished() have already been presented. CreateWorkerThread() is as simple as this:
CSocketServer::WorkerThread *CMySocketServer::CreateWorkerThread(
CIOCompletionPort &iocp)
{
return new CSocketServerWorkerThread(iocp);
} Which leaves us with the implementation of our worker thread's ReadCompleted() method. This is where the server handles incoming data and, in the case of a simple Echo server ;) it could be as simple as this:
void CSocketServerWorkerThread::ReadCompleted(
CSocketServer::Socket *pSocket,
CIOBuffer *pBuffer)
{
pSocket->Write(pBuffer);
} A complete echo server is available for download in SocketServer1.zip. The server simply echos the incoming byte stream back to the client. In addition to implementing the methods discussed above the socket server and worker thread derived classes also implement several 'notifciation' methods that the server and worker thread classes call to inform the derived class of various internal goings on. The echo server simply outputs a message to the screen (and log file) when these notifications occur but the idea behind them is that the derived class can use them to report on internal server state via performance counters or suchlike. You can test the echo server by using telnet. Simply telnet to localhost on port 5001 (the port that the sample uses by default) and type stuff and watch it get typed back at you. The server runs until a named event is set and then shuts down. The very simple Server Shutdown program, available in ServerShutdown.zip, provides an off switch for the server.
More complex servers Servers that do nothing but echo a byte stream are rare, except as poor examples. Normally a server will be expecting a message of some kind, the exact format of the message is protocol specific but two common formats are a binary message with some form of message length indicator in a header and an ASCII text message with a predefined set of 'commands' and a fixed command terminator, often "rn". As soon as you start to work with real data you are exposed to a real-world problem that is simply not an issue for echo servers. Real servers need to be able to break the input byte stream provided by the TCP/IP socket interface into distinct commands. The results of issuing a single read on a socket could be any number of bytes up to the size of the buffer that you supplied. You may get a single, distinct, message or you may only get half of a message, or 3 messages, you just can't tell. Too often inexperienced socket developers assume that they'll always get a complete, distinct, message and often their testing methods ensure that this is the case during development.
Chunking the byte stream One of the simplest protocols that a server could implement is a packet based protocol where the first X bytes are a header and the header contains details of the length of the complete packet. The server can read the header, work out how much more data is required and keep reading until it has a complete packet. At this point it can pass the packet to the business logic that knows how to process it. The code to handle this kind of situation might look something like this:
void CSocketServerWorkerThread::ReadCompleted(
CSocketServer::Socket *pSocket,
CIOBuffer *pBuffer)
{
pBuffer = ProcessDataStream(pSocket, pBuffer);
pSocket->Read(pBuffer);
}
CIOBuffer *CSocketServerWorkerThread::ProcessDataStream(
CSocketServer::Socket *pSocket,
CIOBuffer *pBuffer)
{
bool done;
do
{
done = true;
const size_t used = pBuffer->GetUsed();
if (used >= GetMinimumMessageSize())
{
const size_t messageSize = GetMessageSize(pBuffer);
if (used == messageSize)
{
// we have a whole, distinct, message
EchoMessage(pSocket, pBuffer);
pBuffer = 0;
done = true;
}
else if (used > messageSize)
{
// we have a message, plus some more data
// allocate a new buffer, copy the extra data into it and try again...
CIOBuffer *pMessage = pBuffer->SplitBuffer(messageSize);
EchoMessage(pSocket, pMessage);
pMessage->Release();
// loop again, we may have another complete message in there...
done = false;
}
else if (messageSize > pBuffer->GetSize())
{
Output(_T("Error: Buffer too smallnExpecting: ") + ToString(messageSize) +
_T("Got: ") + ToString(pBuffer->GetUsed()) + _T("nBuffer size = ") +
ToString(pBuffer->GetSize()) + _T("nData = n") +
DumpData(pBuffer->GetBuffer(), pBuffer->GetUsed(), 40));
pSocket->Shutdown();
// throw the rubbish away
pBuffer->Empty();
done = true;
}
}
}
while (!done);
// not enough data in the buffer, reissue a read into the same buffer to collect more data
return pBuffer;
} The key points of the code above are that we need to know if we have at least enough data to start looking at the header, if we do then we can work out the size of the message somehow. Once we know that we have the minimum amount of data required we can work out if we have all the data that makes up this message. If we do, great, we process it. If the buffer only contains our message then we simply process the message and since processing simply involves us posting a write request for the data buffer we return 0 so that the next read uses a new buffer. If we have a complete message and some extra data then we split the buffer into two, a new one with our complete message in it and the old one which has the extra data copied to the front of the buffer. We then pass our complete message to the business logic to handle and loop to handle the data that we had left over. If we dont have enough data we return the buffer and the Read() that we issue in ReadCompleted() reads more data into the same buffer, starting at the point that we're at now.
Next: Chunking the byte stream (Contd.) >>
More C++ Articles
More By Len Holgate