The C++ framework for developing highly scalable, high performance servers on Windows platforms.

Example Servers - Packet Echo Server

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"));
             // we have a whole, 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"));
             // we have a message, plus some more data

             // allocate a new buffer, copy the extra data into it and try again...

             CSmartBuffer message(buffer.SplitBuffer(messageSize));

             EchoMessage(socket, message.GetRef());

             // loop again, we may have another complete message in there...

             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);

             // throw the rubbish away
             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);

    // not enough data in the buffer, reissue a read into the same buffer to collect more data
    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.

Generated on Sun Sep 12 19:06:45 2021 for The Server Framework - v7.4 by doxygen 1.5.3