Distributed Network Object

The Problem

In a network application, state replication of objects across several or different instances of the application is done by custom application protocols. A typical example is this: You want to create a trainer application where a person is supposed to perform some actions at his computer and you want to replicate the effect at other computers connected to it, simulating the same behaviors. For such a task, sometimes, custom structures or different string formats are used, where the object state is packed into these structures/strings, and pass on to the connected side, which upon receiving it, unpack the structure/string, and retrieve information. Eventually the particular object on to the other side is updated with the information passed on, which in turn would be stored in some collection. You’ll have to create a large number of these structures/string formats for each state update.

This model, although used widely, has some issues, especially when it comes to extensibility/flexibility. If one single piece of information that was not earlier being transported has to pass on, the message structures/string has to be updated; this can lead you to make changes in application protocol. Even this minor change can cause a ripple effect and (depending upon the design), one might have to re-engineer several application areas to accommodate the new change—especially, if changes happen frequently, a lot of time, effort, and resources are required to keep things in balance.

The Solution

The “Network Distributed Object Model” offers a simplified way to perform network communication at the object level. This is a network communication framework for applications (client/server) that gives ability to the application programmers to develop a network application without worrying about the underlying network communication details and custom protocols.

The idea is to move network communication at the object level. In a network application, the object at the server side, most likely, will have a corresponding object at the client side. For example, if you are playing a game, and you are controlling a tank, the other players at the network can also see the tank; if you move your tank at your machine, they’ll also see the movement at their machines. Or, if you are designing an application for stock markets, if the symbols (like MSFT, YHOO, GOOG, and so forth) at the server side will also have a corresponding object at the client side; if some attribute of these objects (like bid/ask price, print price, and so on) changes at the server side, all clients should be updated with the change occurred at the server side. In this model, the communication is moved down to these objects, rather than the application. Objects, when updated at the server side, communicate with the connected end, and pass on their state to them, which in turn update their own object that corresponds to the server object.

Figure 1: Typical Network Application Communication Model

Figure 2: Distributed Network Object Application Design

A Network Distributed Object is based upon polymorphism; it offers a class named “CNetworkObject”, from which an application user can inherit their class and over-ride a method named “NetworkSerialize”, which performs serialization. Each network object is assigned a unique ID upon creation; therefore, the order of object creation is important. Two sides that want the information exchange, have to have the same network object creation sequence.

When the objects (inherited from CNetworkObject) are created, they are automatically registered with a network object manager (in other words, CNetworkObjectManager), which maintains a collection of these network objects. In the main application message pump, a method of network object manager (SerializeObjects) has to be called. This acts as a heartbeat to the model which, in one heartbeat, serialize all objects. In the case of the server side, it sends the updated object states, whereas at the client side, it updates the latest object states from the server.

Before you move any further, let me describe the major projects, files, and classes that are part of this model.

The project is split into two sub-projects:

1. Util: This contains utility classes

  1. units.h
  2. SmartPointers.h
  3. globals.h
  4. debug.h
  5. Crc.hpp/.cpp
  6. DataStream.h

units.h has various typedefs, for portability reasons.

#ifdef _MSC_VER
#pragma once

typedef unsigned __int64       uint64_t;
typedef __int64                int64_t;
typedef unsigned long          uint32_t;
typedef long                   int32_t;
typedef short                  uint16_t;
typedef short                  int16_t;
typedef unsigned char          uint8_t;
typedef char                   int8_t;
#endif

#if defined (Linux)
#include <stdint.h>
#endif

#if defined(freebsd) || defined(tru64_alpha)
#include <inttypes.h>
#endif

#if defined(solaris)
#include <sys/int_types.h>
#endif

//typedef int8_t int8;
typedef int16_t int16;
typedef int32_t int32;
typedef int64_t int64;

typedef uint8_t uint8;
typedef uint16_t uint16;
typedef uint32_t uint32;
typedefuint64_t uint64;

SmartPointers.h is a stripped-down version, and contains only a template-based Singleton class. Note that it’s not a thread safe singleton.

template <class T>
class TSingleton
{
public:
   typedef T& reference;
   static reference Instance()
   {
      static T obj;
      return obj;
   }
private:
   TSingleton() {}
   ~TSingleton() {}
}
#define SINGLETON(T) protected:friend class TSingleton< T >;T(){};
#define SINGLETONINSTANCE(T) TSingleton< T >::Instance()

Global.h is another stripped version and has some macros being used in the implementation. FREE_POINTER and FREE_POINTER_ARRAY are just to delete the pointer and pointer arrays, whereas CANONIC is for making a copy constructor and assignment operator of any class as private. They’ll be used later in the CNetworkObject class.

#ifndef FREE_POINTER
#define FREE_POINTER(x)      if(x) { delete (x); (x) = NULL; }
#endif

#ifndef FREE_POINTER_ARRAY
#define FREE_POINTER_ARRAY(x) if(x) { delete[] (x); (x) = NULL; }
#endif

#defineCANONIC(x)               \
      private:                  \
      x(const x& obj);          \
      x& operator= (const x& obj);

Debug.h contains some assertions.

Crc.hpp/.cpp contains a trivial class that can calculate a 16 bit CRC.

Crc.hpp

class CCalcCRC
{
public:
   static uint16 CalcCRC( const void *pData, const uint32 uSize,
                          uint16 uiCRC =0 );
};

CDataStream plays a vital role in this model. It manages the byte stream of data and can be used in various situations. In this model, it is used in network object serialization and also in NetComm APIs, where network messages are queued. The internal buffer of CDataStream grows and shrinks itself when needed. Moreover, you can mark certain memory locations, to which you can refer later. Operators for POD types also are overloaded. All these features of this class make us able to implement a distributed network object.

#pragma once

#include "globals.h"
#include <algorithm>
#include<vector>

using std::vector;
using std::advance;

class CDataStream
{
public:
   CDataStream( uint32 uSize = 0 );
   CDataStream( );

   void SetBuffer( char* szBuffer, uint32 uBufSize );
   void SetSize( const uint32 uSize );
   void TrimUnreadSize();
   void Clear( );
   void Rewind( bool bRewindWrite = true );

   uint32 ReserveWriteCell( uint32 uSize );

   void ReleaseWriteCell( uint32 uCellID );
   void WriteToCell( uint32 uCellID, char* szData, uint32 uSize );

   uint32 AddWriteMarker( );
   uint32 AddSizeMarkerCell( );
   uint32 WriteSizeMarkerCell( uint32 uCellID );

   void ReleaseWriteMarker( uint32 uMarkerID );

   uint32 GetWriteSizeSinceMarker( uint32 uMarkerID );

   void SeekBackWritePtr( int32 lByteCount );

   const char * GetMarkedDataPtr( uint32 uMarkerID );
   const char* SeekBackReadPtr( int32 lByteCount );
   const char* SeekForwardReadPtr( int32 lByteCount );
   const char* SeekForwardWritePtr( int32 lByteCount );

   uint32 CheckWriteSize( uint32 uSize );
   uint32 CheckReadSize( uint32 uSize );

   void Write( FILE* phFile, uint32 uDataSize )
   const char* GetDataPtr( int32 lStartIndex = 0) const;
   const char* GetReadPtr( int32 lOffset = 0 ) const;
   uint32 GetDataSize( ) const;
   uint32 GetBufSize( ) const;
   int32 GetReadBytesRemaining( ) const;
   void Write ( const char* pSource, const uint3 uDataSize );
   void WriteString( const char* szSource );

   void Read ( char* pDest, const uint32 uDataSize );
   const char* ReadString( );

   void operator >> ( bool&   rbFlag );
   void operator >> ( uint32& ruInt );
   void operator >> ( int32&  ruInt );
   void operator >> ( int16&  iInt );
   void operator >> ( uint16& uiInt );
   void operator >> ( uint8&  ubVal );
   void operator >> ( float&  rfNum );

   void operator << ( bool   bFlag );
   void operator << ( uint32 uInt );
   void operator << ( int32  uInt );
   void operator << ( int16  iInt );
   void operator << ( uint16 uiInt );
   void operator << ( uint8  ubVal );
   void operator << ( float  fNum );

   void Copy( const CDataStream& cArg );
   CDataStream& operator = (const CDataStream& cArg );

private:
   char*  m_szDataBuf;
   char*  m_szWritePtr;
   char*  m_szReadPtr;
   uint32 m_uBufSize;

   struct SReservedCell
   {
      uint32  uSize;
      uint32  uOff;
   };

   typedef std::vector< SReservedCell > TReservedCell;
   typedef std::vector< uint32 > TMarkerArray;

   TReservedCell  m_cResCellArray;
   TMarkerArray   m_cMarkerArray;

   void CheckSize( uint32 writeDataSize );
};

More by Author

Must Read