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

Example Servers - Simple Protocol Server

This example shows you how to build a server which works with a simple, CR LF terminated, line based protocol. This is how lots of standard internet protocols, like POP3 and SMTP work. The basic structure is very similar to the Packet 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 Packet Echo Server example and this example.

This example is shipped with all licensed versions of The Server Framework and it requires the core server framework libraries (see here for licensing options). You can always download the latest version of this example from here; and although you will need the correct libraries to be able to build it you can look at the example code and see how it works and perhaps get ideas from it. A compiled, unicode release, build of this example is available on request if you require it for performance analysis of the framework.

The main difference between this server and the Packet Echo Server is that whereas the Packet Echo Server has a protocol in which the data packet is prefixed with a length indicator and once we have read the length indicator we know how many bytes we need to complete the packet, this server requires that we read bytes until we reach a CR LF pair and then process the line. This style of server can be less efficient than a length prefixed packet protocol as the code that accumulates the protocol command for processing needs to examine every byte of the incoming data stream to look for the command terminating CR LF pair. However, it's much easier to use interactively by humans and so is a common design.

As with the Packet Echo Server example we have a protocol parsing state machine loop. The loop has the same states as in the previous example, too much data, not enough data, one protocol message and one protocol message and more data.

 IBuffer *CSocketServer::ProcessDataStream(
    IStreamSocket &socket,
    IBuffer &buffer) const
 {
    IBuffer *pBuffer = &buffer;

    bool done;

    DEBUG_ONLY(Output(_T("ProcessDataStream:\r\n") + DumpData(buffer.GetMemory(), buffer.GetUsed(), 60, true)));

    do
    {
       done = true;

       const IBuffer::BufferSize used = buffer.GetUsed();

       if (used >= GetMinimumMessageSize())
       {
          const IBuffer::BufferSize messageSize = GetMessageSize(buffer);

          if (messageSize == 0)
          {
             // havent got a complete message yet.

             // we null terminate our messages in the buffer, so we need to reserve
             // a byte of the buffer for this purpose...

             if (used == (buffer.GetSize() - 1))
             {
                Output(_T("Too much data!"));

                const std::string response("-ERR too much data! Go away!\r\n");

                // Write this message and then shutdown the sending side of the socket.
                socket.Write(response.c_str(), GetStringLengthAsDWORD(response));
                socket.Shutdown(ShutdownSend);

                // throw the rubbish away
                buffer.Empty();

                done = true;
             }
          }

As before, the "not enough data" state is handled by falling out of the loop and returning the buffer for more data. We've still got a GetMinimumMessageSize() because there's no point in scanning for terminators if we don't have enough data for at least the smallest legal protocol message. The implementation for this protocol looks like this:

 IBuffer::BufferSize CSocketServer::GetMinimumMessageSize() const
 {
    // The smallest possible command we accept is TOP (plus the crlf terminator,
    // once we have this many bytes we can start with try and work out
    // what we have...
    return 5;
 }

We also have a GetMessageSize() function, but this one is more complex than the version in the packet based protcol server.

 IBuffer::BufferSize CSocketServer::GetMessageSize(
    const IBuffer &buffer) const
 {
    const BYTE *pData = buffer.GetMemory();

    const IBuffer::BufferSize used = buffer.GetUsed();

    for (IBuffer::BufferSize i = 0; i < used; ++i)
    {
       if (pData[i] == '\r')
       {
          if (i + 1 < used && pData[i + 1] == '\n')
          {
             // the end of the message is i+1
             // we actually want a count of characters, not a zero based
             // index, so we have to add 1...
             return i + 1 + 1;
          }
       }
    }

    return 0;
 }

We scan through the buffer contents looking for a terminating CR LF pair and return the length of the protocol message if we find it and 0 if we don't. Note that we're not especially efficient here, we could and probably should, be remembering the point in the buffer that we got to with a failed check, but since that would complicate the example with the use of per connection user data we'll leave that for another example server to demonstrate...

          else if (used == messageSize)
          {
             Output(_T("Got complete, distinct, message"));

             buffer.AddData(0);   // null terminate the command string;

             ProcessCommand(socket, buffer);

             buffer.Empty();

             done = true;
          }

Once we have a complete message we can process it. The code for processing a distinct message is shown above, by distinct we mean a buffer that only contains a single message. To make the message easier to process we null terminate the protocol command in the buffer and then pass it to ProcessCommand() for processing. Unlike the Packet Echo Server we don't use the read buffer for a write operation and so we can empty it and reuse it for the next read.

          else if (used > messageSize)
          {
             Output(_T("Got message plus extra data"));

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

             CSmartBuffer message(buffer.SplitBuffer(messageSize));

             message->AddData(0);   // null terminate the command string;

             ProcessCommand(socket, message.GetRef());

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

             done = false;
          }
 /// 

Once again, if we have a protocol message and more data we need to split the data into a new buffer for processing. This is also less efficient than it could be, we could simply process the command in place in the buffer and then use ConsumeAndRemove() to remove the command from the buffer so that we can continue to read more data into the same buffer.

Our business logic resides in ProcessCommand() and, as you can see, it's simply shows that we can process the string commands in any way we fancy. It seems to be a recurring theme with this example, but there are better ways to process that command than the way we do it here...

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