Introduction Manual Class Reference Header Reference

The Server

Overview

This chapter shows how to implement a server class and how it is used. The server will be capable of accepting connections and getting notified when a connection goes down.

Servers (and clients) are implemented by deriving from ZCom_Control. ZCom_Control provides connection related information through virtual methods which are used as callbacks. Callbacks, in a more general sense, are functions defined by the application that get called by some other part of the application when a specific events kicks in. That is why they are also sometimes referred to as 'event handlers'. The C-function defined in Custom Logging is such a callback for instance.

ZCom_Control, upon detection of connection related events such as timeouts, will call the respective callback method on itself. Since the callback methods are defined as 'virtual', deriving classes that define the same methods will override the previous method in ZCom_Control. So it will call the method in the derived class instead. What we have to do in order to get notified about ZCom_Control's events is to override all provided callback methods in ZCom_Control with our own versions.

There are two flavours of callbacks: Those that expect a return value and those that do not. The callbacks without return value are informational only and not necessarily needed to run a server or client. They tell you when a connection goes down or when data arrived and other things. It is possible to make use of this information or totally ignore it. The callbacks expecting a return value are of higher importance to Zoidcom, as they control how Zoidcom will react to the event in question. For example, when someone tries to connect, the ZCom_cbConnectionRequest() callback gets called. When the callback returns true, the connection will be accepted by Zoidcom, when it returns false, the connection will be denied instead.

Attention:
Due to an important design decision, it is necessary to provide implementations for all callbacks, otherwise the derived class can not be instantiated.

Step 1: Derive From ZCom_Control

The first step in getting a functional server class is to derive from ZCom_Control, so that we have a subclass that will later hold our callback implementations.

class Server : public ZCom_Control {
};

For people without prior OOP experience, this code tells the compiler that we are declaring a new class, which is a subclass of ZCom_Control. Subclassing means inheriting, so class Server now has the same functions available as the class ZCom_Control.

Step 2: Copy The Callback Block

The easiest way to get a compilable class is to copy the callback block from the bottom of zoidcom_control.h into the derived class. It provides empty implementations for all of ZCom_Control's callback methods. Callbacks expecting a return value all return 'false' there, that is, they deny all incoming requests.

class Server : public ZCom_Control {
  void ZCom_cbConnectResult( ZCom_ConnID _id, eZCom_ConnectResult _result, ZCom_BitStream &_reply ) {}
  bool ZCom_cbConnectionRequest( ZCom_ConnID  _id, ZCom_BitStream &_request, ZCom_BitStream &_reply ){return false;}
  void ZCom_cbConnectionSpawned( ZCom_ConnID _id ) {}
  void ZCom_cbConnectionClosed( ZCom_ConnID _id, eZCom_CloseReason _reason, ZCom_BitStream &_reasondata ) {}
  bool ZCom_cbZoidRequest( ZCom_ConnID _id, zU8 _requested_level, ZCom_BitStream &_reason ) {return false;}
  void ZCom_cbZoidResult( ZCom_ConnID _id, eZCom_ZoidResult _result, zU8 _new_level, ZCom_BitStream &_reason ) {}
  void ZCom_cbNodeRequest_Dynamic( ZCom_ConnID _id, ZCom_ClassID _requested_class, ZCom_BitStream *_announcedata,
                                   eZCom_NodeRole _role, ZCom_NodeID _net_id ) {}
  void ZCom_cbNodeRequest_Tag( ZCom_ConnID _id, ZCom_ClassID _requested_class, ZCom_BitStream *_announcedata, 
                               eZCom_NodeRole _role, zU32 _tag ) {}
  void ZCom_cbDataReceived( ZCom_ConnID _id, ZCom_BitStream &_data ) {}
  bool ZCom_cbDiscoverRequest( const ZCom_Address &_addr, 
                               ZCom_BitStream &_request, ZCom_BitStream &_reply ) {return false;}
  void ZCom_cbDiscovered( const ZCom_Address & _addr, ZCom_BitStream &_reply )  {}
};

These empty implementations will be used as starting point for the server implementation.

Step 3: Implement The Relevant Callbacks

The next step is to fill the callbacks with useful code, so that the Server class can actually react to any of the events. But before this is done, we should decide which of the callbacks are interesting to a server.

Note:
ZCom_Control can act as both server and client simultaneously. The difference is made by you, that is, by how the callbacks are implemented and by how the class is used afterwards.
As this is going to be a server, we choose to implement only server related callbacks. A basic server typically needs to:

In this chapter the server will only be able to accept connection and notice when a connection breaks down. When you take a look at the callbacks in ZCom_Control, these fit to the above items:

The selected callbacks will get their special implementations, all others stay as they are.

Step 3.1 Handling Connection Requests

ZCom_cbConnectionRequest() is the most important callback for a server implementation. It gets called everytime another ZCom_Control tries to initiate a connection. When the callback returns 'true', the connection will be accepted by Zoidcom, otherwise it is denied. In both cases, data written to the _reply bitstream will get sent back to where the request came from.

class Server : public ZCom_Control {
  bool ZCom_cbConnectionRequest(ZCom_ConnID  _id, ZCom_BitStream &_request, ZCom_BitStream &_reply ) {
    printf("A client requested connection - the new id is [%d].\n", _id);
    return true;
  }
  // all other callback methods omitted here
};

When clients connect they can attach data to the connection request, which is made available through the _request parameter here. It could for example contain a username and a password.

Inside the callback, data can be written into the _reply bitstream. Whatever has been stored in there will get sent back to the client, for example a 'invalid password' message.

Step 3.2 Handling Established Connections

When a connection has been established, ZCom_cbConnectionSpawned() is called. This is the place where usually applications create a new ingame Player for the connection, store the connection in some container or whatnot. It is purely informational and doesn't necessarily need to be implemented to get a working server.

class Server : public ZCom_Control {
  void ZCom_cbConnectionSpawned( ZCom_ConnID _id ) {
      printf("Connection [%d] has been established.\n", _id);
  }
  // all other callback methods omitted here
};

Step 3.3 Handling Closing Connections

Whenever a connection closes, be it on request or by timeout, ZCom_cbConnectionClosed() gets called. The application is expected to clean up application data related to this connection at this point, but it can as well do nothing. Zoidcom will clean up all internal data related to this connection after the callback returns to ZCom_Control.

class Server : public ZCom_Control {
  void ZCom_cbConnectionClosed( ZCom_ConnID _id, eZCom_CloseReason _reason, ZCom_BitStream &_reasondata ) {
    printf("Connection with id [%d] closed", _id);
  }
  // all other callback methods omitted here
};

When the connected client disconnected through a call to ZCom_Disconnect(), the disconnection reason can be read from the _reasondata bitstream. In all other cases this bitstream will be empty.

Callbacks Done

The selected callbacks now have a proper implementation. For a very, very simple server, only ZCom_cbConnectionRequested() needs to have an implementation that returns 'true' all the time.

Step 4: Instantiate The Server

The Server class is ready for duty and can be instantiated:

// <- insert complete server class here

int main()
{
  // initialize Zoidcom - write all output to zoidcom.log
  ZoidCom* zcom = new ZoidCom("zoidcom.log");
  if (!zcom || !zcom->Init())
    exit(255);

  // create instance of our Server class
  Server *server = new Server();

  // more soon
  
  return 0;
};

Step 5: Initialize The Sockets

Next step on the list is to actually initialize the network sockets. Without sockets, there is no communication. Zoidcom provides two kinds of sockets:

We want the real thing, so we initialize UDP sockets only:

  if (!server->ZCom_initSockets(true /* use udp */, 10000 /* udp port */, 0 /* no internal socket */))
    // if false returned, there was a problem
    exit(255);

The server opens an UDP socket on port 10000 and waits for data to arrive.

Step 6: Run The Server

Running the server essentially consists of continously calling ZCom_processInput() and ZCom_processOutput() in turn. Former checks the sockets for arrived network packets, fetches and handles them. Latter checks if there is anything to send for any of the connected clients and sends it if that is the case.

  while ( true )
  {
    // tell the server to process incoming data
    server->ZCom_processInput();
    // tell the server to process outgoing data
    server->ZCom_processOutput();
    // give up remaining cpu time
    zcom->Sleep(1);
  }

ZCom_processInput() is the method where most callbacks are generated from. If someone connects to the server, the connection packet is waiting in an internal queue until ZCom_processInput() fetches and processes it. The type of packet is detected and ZCom_cbConnectionRequest() called. After the callback returned, ZCom_processInput() will prepare a new packet for the requester and put it into the outqueue. As soon as ZCom_processOutput() is called next, the reply packet will be sent. It should be obvious that it is essential to keep calling processInput/Output as often as possible to achieve low latency.

zcom->Sleep(1) gives up remaining CPU time to the OS. This can be removed if you don't need it.

Done: The Complete Server

Just for reference, here is the complete server code put together. Almost exactly the same code can be found in example 0, the simplest of all samples, so there is no compelling reason to copy the code from here.

#include <stdlib.h>
#include <stdio.h>
#include <zoidcom.h>

class Server : public ZCom_Control 
{
  // someone tries to connect
  bool ZCom_cbConnectionRequest(ZCom_ConnID _id, ZCom_BitStream &_request, ZCom_BitStream &_reply) {  
    return true;  
  }

  // someone has connected
  void ZCom_cbConnectionSpawned( ZCom_ConnID _id ) {
    printf("New connection with id [%d]", _id);
  }

  // someone has disconnected
  void ZCom_cbConnectionClosed( ZCom_ConnID _id, eZCom_CloseReason _reason, ZCom_BitStream &_reasondata ) {
    printf("Connection with id [%d] closed", _id);
  }

  // currently irrelevant callbacks have empty bodies
  void ZCom_cbConnectResult( ZCom_ConnID _id, eZCom_ConnectResult _result, ZCom_BitStream &_reply ) {}
  void ZCom_cbDataReceived( ZCom_ConnID _id, ZCom_BitStream &_data ) {}
  bool ZCom_cbZoidRequest( ZCom_ConnID _id, zU8 _requested_level, ZCom_BitStream &_reason ) {return false;}
  void ZCom_cbZoidResult( ZCom_ConnID _id, eZCom_ZoidResult _result, zU8 _new_level, ZCom_BitStream &_reason ) {}
  void ZCom_cbNodeRequest_Dynamic( ZCom_ConnID _id, ZCom_ClassID _requested_class, ZCom_BitStream *_announcedata,
                                eZCom_NodeRole _role, ZCom_NodeID _net_id ) {}
  void ZCom_cbNodeRequest_Tag( ZCom_ConnID _id, ZCom_ClassID _requested_class, ZCom_BitStream *_announcedata,
                               eZCom_NodeRole _role, zU32 _tag ) {}
  bool ZCom_cbDiscoverRequest( const ZCom_Address &_addr, ZCom_BitStream &_request, 
                               ZCom_BitStream &_reply ) {return false;}
  void ZCom_cbDiscovered( const ZCom_Address & _addr, ZCom_BitStream &_reply )  {}
};

int main()
{
  // initialize Zoidcom
  ZoidCom* zcom = new ZoidCom("zoidcom.log");
  if (!zcom || !zcom->Init())
    exit(255);

  // make instance of our Server class
  Server *server = new Server();
  server->ZCom_setDebugName("Server");

  // this creates and initializes the network sockets
  // true = use udp socket, 10000 = the UDP port to use, 0 = no internal socket
  bool result = server->ZCom_initSockets(true, 10000, 0);
  // if result is false, Zoidcom had problems while initializing
  if (!result)
    exit(255);

  while ( true )
  {
    // tell the server to process incoming data
    server->ZCom_processInput();
    // tell the server to process outgoing data
    server->ZCom_processOutput();
    // let the program sleep for 0 msecs
    zcom->Sleep(1);
  }

  delete server;
  delete zcom;

  return 0;
};

This file is part of the documentation for Zoidcom. Documentation copyright © 2004-2008 by Jörg Rüppel. Generated on Sat Aug 16 15:26:51 2008 for Zoidcom by doxygen 1.4.6-NO