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 :
virtual void OnConnectionEstablished(
JetByteTools::Socket::IStreamSocket &socket,
const JetByteTools::Socket::IAddress &address);
virtual void OnReadCompleted(
JetByteTools::Socket::IStreamSocket &socket,
JetByteTools::IO::IBuffer &buffer);
void EchoMessage(
JetByteTools::Socket::IStreamSocket &socket,
JetByteTools::IO::IBuffer &buffer) const;
const std::string m_welcomeMessage;
CSocketServer(const CSocketServer &rhs);
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 & )
{
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 , char * )
{
try
{
CIOPool pool(
0);
pool.Start();
CStreamSocketAllocator socketAllocator(
10);
CBufferAllocator bufferAllocator(
1024,
10);
const CAddressIPv4 address(
INADDR_ANY,
5050);
const ListenBacklog listenBacklog = 5;
CConnectionLimiter connectionLimiter(
1000);
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: