This example shows you how to build a server which works with a simple length prefixed block protocol. The basic structure is very similar to the
Basic Echo Server example and you should go and read about that first and have a good understanding of how everything fits together. This document will only cover the differences between the
Basic Echo Server example and this example.
The main difference between this server and the
Basic Echo Server is that whereas the
Basic Echo Server has no concept of data boundaries and simply treats the TCP data stream as a stream of bytes that should be echoed in order, this server works in terms of an artificial structure imposed on the TCP data stream by both client and server. This is quite common and the artificial structure is generally called a protocol... The key point to remember is that the protocol is imposed on the TCP byte stream by the client and server, the TCP layer itself has no interest or knowledge in how the client and server are treating that data stream that it provides.
The protocol that we implement is simple, a packet is a block of data which has a one byte length indicator as the first byte. The length indicator holds the length of the packet and includes the length of the indicator itself. The rest of the packet is data. Packets should only be echoed when they are complete.
The first place that this server differs from the other example servers is in its implementation of
OnReadCompleted() which is shown below. The difference being that we pass a pointer to a buffer to our call to
Read(). This pointer is returned to us by the
ProcessDataStream() function and will either be
null or a pointer to the buffer that was passed in to
ProcessDataStream(). The reason for this is that we need to accumulate a complete packet in the server before we echo the packet (or, in a server with slightly more complex business logic, act on the contents of the packet).
ProcessDataStream() implements our protocol and if it doesn't have enough bytes for a complete packet it returns a pointer to the buffer so that it will be used in the next call to
Read(), this call will read more data from the TCP stream into the same buffer, appending it to the existing buffer contents.
void CSocketServer::OnReadCompleted(
IStreamSocket &socket,
IBuffer &buffer)
{
try
{
IBuffer *pBuffer = ProcessDataStream(socket, buffer);
socket.Read(pBuffer);
}
catch(const CException &e)
{
Output(_T("ReadCompleted - Exception - ") + e.GetDetails());
socket.AbortConnection();
}
catch(...)
{
Output(_T("ReadCompleted - Unexpected exception"));
socket.AbortConnection();
}
}
ProcessDataStream() is where the protocol itself is managed, since our protocol is so simple all we're actually doing is breaking the TCP byte stream into our packet structure. We do this by using a very simple state machine. Note that the state machine runs inside a processing loop as we may be dealing with 0, 1 or many protocol packets in a single buffer.
IBuffer *CSocketServer::ProcessDataStream(
IStreamSocket &socket,
IBuffer &buffer) const
{
IBuffer *pBuffer = &buffer;
bool done;
do
{
done = true;
const IBuffer::BufferSize used = buffer.GetUsed();
if (used >= GetMinimumMessageSize())
First we determine if we have enough data to work out if we have enough data for a complete packet, this sounds a bit circular and it is. Since our protocol has a 1 byte packet length indicator as the first byte of the packet we need to check that we have at least 1 byte of data to work with, if we do we can work out how large the packet is and see if we have a complete packet. A more complex protocol might have a multi-byte packet length indicator which would make this state in the state machine slightly more likely to be a state that we actually spend some time in... We're calling a function to get the minimum message size so that it's easy to see what we're doing and it's easy to change what the function returns to support protocols which have a larger leading length indicator (as is the case in the large packet echo server example. And, remember, TCP is a byte stream, and a read can return any number of bytes from that byte stream. When designing the processing loop for incoming data you must always assume that your data could arrive one byte at a time.
if (used >= GetMinimumMessageSize())
{
const IBuffer::BufferSize messageSize = GetMessageSize(buffer);
if (used == messageSize)
{
Output(_T("Got complete, distinct, message"));
EchoMessage(socket, buffer);
pBuffer = 0;
done = true;
}
Once we know that we have more bytes than the minimum size of the message we can work out what the size of the message actually is. Again this work has been factored into a function with a helpful name that can be replaced if we change the protocol to use a different sized packet length indicator.
Now that we know how both how many bytes we have in the buffer and how many bytes we need for a complete packet we can work out if we have a complete packet. There are three options here; no we don't yet have a complete packet
(used < messageSize), yes we have a packet and that's all we have
(used == messageSize) and yes we have a packet and we have more data to process once we're done with that packet
(used > messageSize). The incomplete packet state is dealt with by exiting the processing loop and returning a pointer to the buffer to the caller to add more data to.
If
used == messageSize then we have a single protocol packet in the buffer and we can pass the buffer off to our business logic,
EchoMessage(), break out of our processing loop and return a
null to our caller to tell it to read into a new buffer.
else if (used > messageSize)
{
Output(_T("Got message plus extra data"));
CSmartBuffer message(buffer.SplitBuffer(messageSize));
EchoMessage(socket, message.GetRef());
done = false;
}
If we find that we have more than one message in the buffer, as can be the case if the client is allowed to send multiple protocol packets before it receives a reply to one, then we need to break the buffer into two (of course we could elect to process the complete packet in place if we wanted). We use
SplitBuffer() for this; it takes
x bytes from the front of buffer 'A' and returns a new buffer (B) with those
x bytes in it. It then moves the remainder of the bytes in 'A' to the front of 'A'. We can then pass buffer 'B' to our business logic and continue to loop and process the remaining bytes in 'A'.
else if (messageSize > buffer.GetSize())
{
Output(_T("Error: Buffer too small\nExpecting: ") + ToString(messageSize) +
_T("Got: ") + ToString(buffer.GetUsed()) + _T("\nBuffer size = ") +
ToString(buffer.GetSize()) + _T("\nData = \n") +
DumpData(buffer.GetMemory(), buffer.GetUsed(), 40));
socket.Shutdown(ShutdownSend);
buffer.Empty();
done = true;
}
}
The final state in our protocol processing state machine is one that exists purely due to our data reading design. Since we accumulate protocol packets in an instance of a data buffer the protocol packet must be able to fit into a single data buffer. We check for that in our state machine though it would require a buffer allocator which allocates buffers of less than 256 bytes for it to be possible for this state ever to be reached.
}
while (!done);
return pBuffer;
}
The processing loop continues until all complete protocol packets in the buffer have been processed by the business logic and then either returns
null if the buffer contained a complete packet or a pointer to the buffer if it contained a partial packet.
The
EchoMessage() function is the same as in the
Basic Echo Server and could, of course, be replaced with something that actually processed the contents of our protocol's packets.