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

Writing your first TCP server

Probably the easiest way to start writing your own server is to take a look at one of the many example servers that come with The Server Framework code. There are TCP and UDP examples and they start from the simplest echo server and build up slowly towards more advanced servers, such as SSL enabled TCP web servers and include servers that run as Windows Services and servers that expose internal monitoring via perfmon counters.

This guide will assume that you have a copy of the most basic EchoServer example and will explain how the server is put together, how you get it to do the work that you want and how you configure it.

A simple server will usually consist of at least 3 files; a ServerMain.cpp file that puts together the objects required to run the server and configures them, and SocketServer.h and SocketServer.cpp files that provide a link between the framework and the socket server callbacks that you will implement to act on various network events that happen during the lifetime of the connections to your server. We'll start by looking at the SocketServer.h file. For the simplest EchoServer this file could look something like this:

 #include "JetByteTools\SocketTools\StreamSocketServer.h"

 #include "JetByteTools\SocketTools\StreamSocketServerExCallback.h"

 #include <string>

 class CSocketServer :
    public JetByteTools::Socket::CStreamSocketServer,
    private JetByteTools::Socket::CStreamSocketServerExCallback
 {
    public :

       CSocketServer(
          const std::string &welcomeMessage,
          const JetByteTools::Socket::IFullAddress &address,
          const JetByteTools::Socket::ListenBacklog listenBacklog,
          JetByteTools::IO::IIOPool &pool,
          JetByteTools::Socket::IAllocateStreamSockets &socketAllocator,
          JetByteTools::IO::IAllocateBuffers &bufferAllocator,
          JetByteTools::Socket::ILimitConnections &connectionLimiter = JetByteTools::Socket::CConnectionLimiter::NoLimitLimiter);

       ~CSocketServer();

    private :

       // Implement just the bits of IStreamSocketServerCallback that we need

       virtual void OnConnectionEstablished(
          JetByteTools::Socket::IStreamSocket &socket,
          const JetByteTools::Socket::IAddress &address);

       virtual void OnReadCompleted(
          JetByteTools::Socket::IStreamSocket &socket,
          JetByteTools::IO::IBuffer &buffer);

       // Our business logic

       void EchoMessage(
          JetByteTools::Socket::IStreamSocket &socket,
          JetByteTools::IO::IBuffer &buffer) const;

       const std::string m_welcomeMessage;

       /// No copies do not implement
       CSocketServer(const CSocketServer &rhs);
       /// No copies do not implement
       CSocketServer &operator=(const CSocketServer &rhs);
 };
The CSocketServer class derives from the framework's basic TCP server class but it doesn't actually need to except for the convenience of being able to manipulate a single object as your server. The important base class is the CStreamSocketServerExCallback as this allows you to override just the callback functions that you're interested in dealing with rather than having to provide a default implementation for the whole of IStreamSocketServerCallback. We derive from CStreamSocketServerExCallback rather than a CStreamSocketServerCallback class as there's only one 'do nothing' implementation of the stream socket callback interfaces, it derives from the most specific interface, IStreamSocketServerExCallback which, as you'll see, derives from IStreamSocketServerCallback, etc.

Our constructor looks fairly complex but, in reality, it's simply a way of connecting together the objects that we rely on. These objects are separate objects so that our server can be configured in versatile ways. Most of these objects are accessed via interfaces so that we can replace the default implementations with our own if we need to in order to meet performance or functionality requirements that were not anticipated when the framework itself was designed. In summary the need to supply all of these objects to the constructor of our server is a "Good Thing". See Parameterise from Above for more details about this technique. So, what exactly are all these interfaces that the constructor needs? First we pass in a std::string which is simply part of our simple server's "business logic", we'll ignore that for now. Next comes an instance of IFullAddress which is the address that we'll be listening on. There are several concrete addressing classes that we can select from, but for now we'll assume that we're passing in an instance of CAddressIPv4 which is a TCP/IPv4 address. Notice that we could simply pass in an instance of CAddressIPv6 if we wished our server to listen on a TCP/IPv6 address or CFullAddress if we didn't know what kind of address we'd be using (CFullAddress can construct itself from string representations of addresses and can represent any valid address type given a valid construction string). Next comes the listenBacklog which is the maximum length of the queue of pending connections. If set to SOMAXCONN, the underlying service provider responsible for the server's listening socket will set the backlog to a maximum reasonable value. Something small usually works well for most simple servers, and you can increase it if you find that your server is refusing connections. Note that this isn't the number of connections that your server can handle, it's just the number of connections that are queing to be accepted by the server, the server accepts connections very quickly and so this queue of pending connections can usually be quite small. Next comes an instance of IIOPool this is where all of the multi-threading is done. The reason that the pool is passed in rather than part of the server itself is that multiple servers can share the same pool, and, indeed, the pool can be shared with other code that performs asynchronous I/O if required. Next is an instance of IAllocateStreamSockets, our socket allocator. This can often pool sockets for later reuse so that once the server is running with the 'normal' number of clients connecting and disconnecting there's no need to allocate memory. Likewise an instance of IAllocateBuffers does the same for our data buffers. Finally there's an optional instance of ILimitConnections. This is a very important part of a high availability server that needs (or could) service many thousands of concurrent connections. The connection limiter can protect the machine that the server runs on from running out of essential resources, the result of which is often a Blue Screen Of Death, see Limiting Resource Usage for more details...

As we can see from the body of the constructor, we pass most of these things down to the server object.
 CSocketServer::CSocketServer(
    const string &welcomeMessage,
    const IFullAddress &address,
    const ListenBacklog listenBacklog,
    IIOPool &pool,
    IAllocateStreamSockets &socketAllocator,
    IAllocateBuffers &bufferAllocator,
    ILimitConnections &connectionLimiter)
    :  CStreamSocketServer(address, listenBacklog, *this, pool, socketAllocator, bufferAllocator, NoZeroByteRead, connectionLimiter),
       m_welcomeMessage(welcomeMessage)
 {

 }
The most interesting thing about the things we pass to our base class is that we pass a reference to ourselves as the third parameter. This is where we pass in an instance of the server's callback interface.

As you'll see from the documentation for socket server callbacks, how your server reacts to events that occur on your connections is all down to what you do in your callback methods and which ones you implement. Our simple server implements two callbacks, OnConnectionEstablished() and OnReadCompleted(). OnConnectionEstablished() is called, not surprisingly, when a new connection is established to your server.
 void CSocketServer::OnConnectionEstablished(
    IStreamSocket &socket,
    const IAddress & /*address*/)
 {
    Output(_T("OnConnectionEstablished"));

    if (socket.TryWrite(m_welcomeMessage.c_str(), GetStringLength<DWORD>(m_welcomeMessage)))
    {
       socket.TryRead();
    }
 }
Our simple server just sends a message to the newly connected client and then issues a read request. Nothing will now happen on this connection until the read completes.

 void CSocketServer::OnReadCompleted(
    IStreamSocket &socket,
    IBuffer &buffer)
 {
    try
    {
       EchoMessage(socket, buffer);

       socket.Read();
    }
    catch(const CException &e)
    {
       Output(_T("ReadCompleted - Exception - ") + e.GetDetails());
       socket.AbortConnection();
    }
    catch(...)
    {
       Output(_T("ReadCompleted - Unexpected exception"));
       socket.AbortConnection();
    }
 }
When the read completes our OnReadCompleted() handler is called and we are given a buffer which contains the bytes that were read from the TCP stream. Now, remember, TCP connections are an unstructured stream of bytes, so this buffer may contain one or one hundred bytes, it doesn't matter that the client at the other end sent a "packet" of exactly 100 bytes in a single call to send, we can recieve any number of those bytes as the result of our read completing. We may, or may not, get the rest of the bytes later on; we probably will and, when testing on a LAN in your office you're unlikely to see too much packet fragmentation, but, you have to assume that every lump of data that is sent to you will arrive one byte at a time, each as the result of a separate call to OnReadCompleted() (in fact, there's scope for someone to write a TCP stream filter that can be used during development and that ensures that you get fragmented packets when your reads complete...)

It's good practice to protect the framework from exceptions that you throw whilst working in a callback method, you don't have to, the framework will catch anything that comes blasting out of your handlers but it's generally better to do your own cleanup work. We simply issue a debug message and shutdown the connection.

Since we're just a simple server our business logic doesn't care about what data has been sent to us, we don't care about any implied packet structure or protocol, we just pass the bytes that we've been given to the EchoMessage() function and, well, this is what it does with them:
 void CSocketServer::EchoMessage(
    IStreamSocket &socket,
    IBuffer &buffer) const
 {
    DEBUG_ONLY(Output(_T("Data Echoed -\r\n") + DumpData(buffer.GetMemory(), buffer.GetUsed(), 60, true)));

    socket.Write(buffer);
 }
All we do is display what we're echoing (in debug builds only), and then write it back to the client.

Note that both the call to Read() and the call to Write() could fail and throw exceptions, possibly due to the connection being closed by the client or due to a network problem. If we wanted to be able to cleanly deal with these failures then we might choose to use TryRead() and TryWrite() instead and deal with the failures directly.

So, that's all that's required to write a simple server, but how do we configure it and set up all of the other objects that we need? ServerMain.cpp for this server might look like this:
 int main(int /*argc*/, char * /*argv[ ]*/)
 {
    try
    {
       CIOPool pool(
          0);                                   // Number of threads, 0 = 2 x number of CPU

       pool.Start();

       CStreamSocketAllocator socketAllocator(
          10);                                 // Number of sockets kept in the pool

       CBufferAllocator bufferAllocator(
          1024,                                // Size of the data buffers
          10);                                 // Number of buffers kept in the pool

       const CAddressIPv4 address(
          INADDR_ANY,                          // Accept connections on all interfaces
          5050);                               // Accept connections on port 5050

       const ListenBacklog listenBacklog = 5;

       CConnectionLimiter connectionLimiter(
          1000);                               // Allow, at most, this many connections

       CSocketServer server(
          "Welcome to echo server\r\n",
          address,
          listenBacklog,
          pool,
          socketAllocator,
          bufferAllocator,
          connectionLimiter);
As you can see, although we're doing the Parameterise From Above thing to construct most of the objects it's not actually too complex and it's quite easy to see what's going on. The objects are as detailed in our explaination of the server object's constructor". <br > <br > So what do we do once we've created our server object?
       server.Start();

       server.StartAcceptingConnections();
Well, that's enough to get the server up and running and have it start accepting connections and dealing with them. All of the work happens on the server object's own thread (for accepting) and the I/O pool's threads (for the actual work of dealing with events that happen on the connections). There's nothing else that this main thread needs to do, except hang around until the server should be shutdown. In the example servers we use an external 'off switch' in the shape of a simple MFC application with a 'stop' button on it. This application simply sets an event to shut the server down. The code for dealing with this, and the ability to pause the acceptance of new connections, lives in CSimpleServerShutdownHandler in the ServerCommon library and is shown below:
       CManualResetEvent shutdownEvent(CGlobalName(_T("JetByteToolsServerShutdown")));
       CManualResetEvent pauseResumeEvent(CGlobalName(_T("JetByteToolsServerPauseResume")));

       HANDLE handlesToWaitFor[2];

       handlesToWaitFor[0] = shutdownEvent.GetWaitHandle();
       handlesToWaitFor[1] = pauseResumeEvent.GetWaitHandle();

       bool accepting = true;
       bool done = false;

       while (!done)
       {
          DWORD waitResult = ::WaitForMultipleObjects(2, handlesToWaitFor, false, INFINITE);

          if (waitResult == WAIT_OBJECT_0)
          {
             done = true;
          }
          else if (waitResult == WAIT_OBJECT_0 + 1)
          {
             if (accepting)
             {
                server.StopAcceptingConnections();
             }
             else
             {
                server.StartAcceptingConnections();
             }

             accepting = !accepting;
          }
          else
          {
             throw CException(
                _T("CSimpleServerShutdownHandler::WaitForShutdownRequest()"),
                _T("Unexpected result from WaitForMultipleObjects - ") + ToString(waitResult));
          }
       }
A real server would probably do things differently, possibly allowing a shutdown request over the network, and possibly still using events, even if only internally to the process, to do the communication...

Once the server has been asked to shutdown we simply ask the server and I/O pool to shutdown their theads:
       server.WaitForShutdownToComplete();

       pool.WaitForShutdownToComplete();
And then, for the sake of slightly easier debugging, force the allocators to clean up...
       bufferAllocator.Flush();

       socketAllocator.ReleaseSockets();
That's all there is to it. The complete ServerMain.cpp file is shown below:
 #include "JetByteTools\Admin\Admin.h"

 #include "JetByteTools\SocketTools\WinsockWrapper.h"

 #include "JetByteTools\Win32Tools\Exception.h"
 #include "JetByteTools\Win32Tools\Utils.h"
 #include "JetByteTools\Win32Tools\ManualResetEvent.h"
 #include "JetByteTools\Win32Tools\GlobalName.h"

 #include "SocketServer.h"

 #include "JetByteTools\IOTools\IOPool.h"
 #include "JetByteTools\IOTools\BufferAllocator.h"

 #include "JetByteTools\SocketTools\AddressIPv4.h"
 #include "JetByteTools\SocketTools\StreamSocketAllocator.h"

 #include "ServerCommon\SimpleServerShutdownHandler.h"

 #pragma hdrstop

 ///////////////////////////////////////////////////////////////////////////////
 // Using directives
 ///////////////////////////////////////////////////////////////////////////////

 using JetByteTools::Win32::_tstring;
 using JetByteTools::Win32::CException;
 using JetByteTools::Core::Output;

 using JetByteTools::IO::CIOPool;
 using JetByteTools::IO::CBufferAllocator;

 using JetByteTools::Socket::CAddressIPv4;
 using JetByteTools::Socket::CStreamSocketAllocator;
 using JetByteTools::Socket::CConnectionLimiter;

 ///////////////////////////////////////////////////////////////////////////////
 // Program entry point
 ///////////////////////////////////////////////////////////////////////////////

 int main(int /*argc*/, char * /*argv[ ]*/)
 {
    try
    {
       CIOPool pool(
          0);                                   // Number of threads, 0 = 2 x number of CPU

       pool.Start();

       CStreamSocketAllocator socketAllocator(
          10);                                 // Number of sockets kept in the pool

       CBufferAllocator bufferAllocator(
          1024,                                // Size of the data buffers
          10);                                 // Number of buffers kept in the pool

       const CAddressIPv4 address(
          INADDR_ANY,                          // Accept connections on all interfaces
          5050);                               // Accept connections on port 5050

       const ListenBacklog listenBacklog = 5;

       CConnectionLimiter connectionLimiter(
          1000);                               // Allow, at most, this many connections

       CSocketServer server(
          "Welcome to echo server\r\n",
          address,
          listenBacklog,
          pool,
          socketAllocator,
          bufferAllocator,
          connectionLimiter);

       server.Start();

       server.StartAcceptingConnections();

       CSimpleServerShutdownHandler shutdownHandler(server);

       shutdownHandler.WaitForShutdownRequest();

       server.WaitForShutdownToComplete();

       pool.WaitForShutdownToComplete();

       bufferAllocator.Flush();

       socketAllocator.ReleaseSockets();
    }
    catch(const CException &e)
    {
       Output(_T("Exception: ") + e.GetDetails());
    }
    catch(...)
    {
       Output(_T("Unexpected exception"));
    }

    return 0;
 }

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