[Dragonfly]

Dragonfly Networking

Extending the Dragonfly Game Engine with Networking


Top | Overview | Details | Hints

Overview

Dragonfly is a text-based game engine, primarily designed to teach about game engine development. While it is a full-featured game engine for stand-alone computer game development, it does not provide any support for networking. This document provides guidlines for designing and implementing network support for Dragonfly from the user level (i.e., as game programmer code).


Details

If you have not already, you might consider working through the Dragonfly tutorial available online through the Dragonfly Web page. Doing so will help you setup your development environment, as well as provide some background on Dragonfly syntax and structure.

Design of Dragonfly Networking

Recommended are three classes in support of Dragonfly networking: 1) a network manager, 2) a network event, and 3) a sentry to support network interaction. While these classes will not technically be part of the Dragonfly engine (the engine is an immutable library, unless you have the source from implementing the engine from scratch), the new network classes can easily be accessed through an additional library (e.g., libnetwork.a) you make and, hence, will behave as if they are part of the engine.

Since the added classes are intended to function as part of the engine, they should be placed in Dragonfly's df namespace. This requires code outside of the namespace (e.g., game code) to use df:: to access elements inside the namespace (e.g., df::NetworkManager::getInstance()). Typically large, 3rd-party libraries (such as a game engine) use namespaces to help developers avoid conflicts in names their own code may use with names the libraries use. The Dragonfly namespace is meant to prevent potential name conflicts with game code.

Network Manager

The Network Manager handles the internals of a game's network connection. As such, both a game client and a game server have a Network Manager. Like other Dragonfly managers, the NetworkManager class inherits from the Manager base class and is a singleton. Its main private attribute is a connected socket. To support a 2-player game (or a cloud-based game engine), only 1 socket is required. The class header file is provided below:

//                                                                                 
// NetworkManager.h                                                                
//                                                                                 
// Manage network connections to/from engine.                                      
//                                                                                 
 
#ifndef __NETWORK_MANAGER_H__ 
#define __NETWORK_MANAGER_H__ 
 
// System includes.                                                                
#include <string.h> 
 
// Engine includes.                                                                
#include "Manager.h" 
 
#define DRAGONFLY_PORT "PICK NUMBER HERE"   // Default port.                                   
 
namespace df { 
 
class NetworkManager : public df::Manager { 

 private:
  NetworkManager();                       // Private since a singleton.            
  NetworkManager(NetworkManager const&);  // Don't allow copy.                     
  void operator=(NetworkManager const&);  // Don't allow assignment.               
  int sock;                               // Connected network socket.             

 public:

  // Get the one and only instance of the NetworkManager.                          
  static NetworkManager &getInstance();

  // Start up NetworkManager.                                                      
  int startUp();

  // Shut down NetworkManager.                                                     
  void shutDown();

  // Accept only network events.                                                   
  // Returns false for other engine events.                                        
  bool isValid(std::string event_type) const;

  // Block, waiting to accept network connection.                                  
  int accept(std::string port = DRAGONFLY_PORT);

  // Make network connection.                                                      
  // Return 0 if success, else -1.                                                 
  int connect(std::string host, std::string port = DRAGONFLY_PORT);

  // Close network connection.                                                     
  // Return 0 if success, else -1.                                                 
  int close();

  // Return true if network connected, else false.                                 
  bool isConnected() const;

  // Return socket.                                                                
  int getSocket() const;

  // Send buffer to connected network.                                             
  // Return 0 if success, else -1.                                                 
  int send(void *buffer, int bytes);

  // Receive from connected network (no more than nbytes).                         
  // If peek is true, leave data in socket else remove.                            
  // Return number of bytes received, else -1 if error.                            
  int receive(void *buffer, int nbytes, bool peek = false);

  // Check if network data.                                                        
  // Return amount of data (0 if no data), -1 if not connected or error.           
  int isData() const;
};

} // end of namespace df                                                           

#endif // __NETWORK_MANAGER_H__                                                    

Upon startup(), the NetworkManager initializes the socket to -1 (indicating the NetworkManager is not yet connected). Upon shutdown(), if the socket is connected (greater than 0), it is closed via the system close() call.

The method isValid() is used by the Manager class to determine if the derived manager (the NetworkManager, in this case) accepts the event in question. It should return true only for network events (see the EventNetwork class below).

The accept() method undertakes setting up and then waiting for a TCP/IP socket connection from a client, somewhere else on the Internet. Typical of most Internet servers, the NetworkManager listens on a "well-known" port (for most network games, this is defined by the game developers) that the client uses when connecting. The default can be specified via DRAGONFLY_PORT. The assumption is also that an external naming service provides the IP address and/or hostname where the server (game host) is running. Inside accept(), the following steps are done:

  1. Create an unconnected socket to which a remote client can connect. A relevant system call is socket().
  2. Bind to the server's local address and port so a client can connect. A relevant system call is bind().
  3. Put the socket in a state to accept the other "half" of an Internet connection. A relevant system call is listen().
  4. Wait (block) until the socket connected. A relevant system call is accept().

The complimentary method, connect(), is invoked by the client to connect to the server. As such, it takes as parameters the server's hostname and port as strings. When invoked, connect() does the following steps:

  1. Lookup the host name, translating it into the necessary system structure for connections. A relevant system call is getaddrinfo().
  2. Create a socket used by the client to connect to the server. A relevant system call is socket().
  3. Connect to the server. A relevant system call is connect().

The method isConnected() can be used by either the client or the server to tell if the NetworkManager has a connected socket. It returns true if the socket (sock) is greater than 0. The method getSocket() actually returns the socket.

The method send() sends data through the connected socket (using the send() system call).

The method receive() receives data from the connected socket, using the recv() system call). Receive is non-blocking, so if their is no data pending, the method does not block but returns. The receive() method has a "peek" option to read the data but leave the data in the socket.

The method isData() returns the amount of data (in bytes) that is waiting in the network socket, leaving any data there intact. If there is no data, zero is returned.

Network Event

The network event is a "message" that is communicated by the NetworkManager to all Objects that have registered interest in a network event. Implementation of the EventNetwork class is fairly straightforward as the class is mostly a container. The proposed design, shown below, does not include the actual network data (although it could) since that is assumed to be pulled from the NetworkManager by any interested game Objects.

//                                                                                 
// A "network" event, generated when a network packet arrives.                     
//                                                                                 
  
#ifndef __EVENT_NETWORK_H__
#define __EVENT_NETWORK_H__

#include "Event.h"

namespace df {

const std::string NETWORK_EVENT = "df::network";

class EventNetwork : public Event {

 private:
  int bytes;                    // Number of bytes available                       

 public:
  // Default constructor.                                                          
  EventNetwork();

  // Create object with initial bytes.                                             
  EventNetwork(int initial_bytes);

  // Set number of bytes available.                                                
  void setBytes(int new_bytes);

  // Get number of bytes available.                                                
  int getBytes() const;
};

} // end of namespace df                                                           

#endif // __EVENT_NETWORK_H__                                                      

The constructors should set the event type (via setType() in the base Event class) to NETWORK_EVENT.

The other methods are to set and get the number of bytes, respectively.

Sentry

If the NetworkManager was actually implemented in the engine, the engine would provided notification of all network events - i.e., when network data arrived, the engine would generate an EventNetwork for all Objects that had registered. However, for this project, since the NetworkManager is implemented in game code, there needs to be a game object running on both the client and host that generates network events. This can be most easily done by a "sentry" object (called Sentry) that is derived from Object and registers for step events. Every step, Sentry polls the network manager and when network data has arrived since the last step generates network events. Since the Host/Client had registered interest in network events, it is then notified that network data has arrived.

//                                                                                 
// Sentry                                                                          
//                                                                                 
// Poll NetworkManager for incoming messages, generating network                   
// events (onEvent()) when there are complete messages available.                   
//                                                                                 

#ifndef __SENTRY_H__ 
#define __SENTRY_H__

#include "Object.h"

namespace df {

class Sentry : public Object {

 private:
  void doStep();

 public:
  Sentry();
  int eventHandler(const Event *p_e);
};

} // end of namespace df                                                           

#endif // __SENTRY_H__                                                             

The constructor registers for step events (via registerInterest()). It should also be spectral (SPECTRAL) and invisible (setVisible(false)).

The eventHandler() checks if the event is a step event and, if so, invokes the doStep() method.

The doStep() method checks the socket to see if there is at least an int worth of data. If so, it expects this int to reflect the number of bytes in a complete message (including this intdoStep() generates a network event by calling the NetworkManager's onEvent() method (note onEvent() is actually defined in the base Manager class).


Hints

For asking questions on Dragonfly, you are encouraged to use the Dragonfly Q&A forum. There, you can ask and answer questions, comment and vote for the questions of others and their answers. Both questions and answers can be revised and improved. Questions are tagged with the relevant keywords to simplify future access and organize the accumulated material - you might be able to find the answer to your question before you ask it!

Dragonfly has a built-in logfile that can be helpful for development and debugging. Printing to the Dragonfly logfile is done with the LogManager's writeLog() method. The writeLog() method has printf()-style variable argument formatting. Verbosity of logging can also be controlled using setLogLevel() to set the log level and an initial integer to writeLog() to indicate the level of the message. The LogManager is a singleton, so you need to call getInstance() to use it.

Sockets are by default blocking, meaning if a process reads from them and there is no data available, the process blocks until there is data. On Linux and Mac, recv() call can be made non-blocking behavior with the MSG_DONTWAIT flag and the socket made non-blocking with an fcntl() call with O_NONBLOCK. On Windows, a socket can be made non-blocking with an ioctlsocket() call with FIONBIO.

On Linux and Mac, the ioctl() call can be used to see the number of bytes available in a socket before reading using the FIONREAD parameter as the second argument (e.g., ioctl(sock, FIONREAD, &bytes)). On Windows, the equivalent call is with ioctlsocket() and FIONBIO.

By default, a recv() call that retrieves data from a socket removes it from the operating system buffer such that subsequent reads do not get the same data. The MSG_PEEK can be used to retrieve the data but leave it in the buffer for subsequent reads.

Unlike many system calls, socket code works nearly the same on Windows, Mac and Linux. However, if trying to make the NetworkManager work on all three platforms (optional, not required), the following are Linux/Mac specific:

// Header files
#include <netdb.h>
#include <netinet/in.h>
#include <sys/ioctl.h>          // for ioctl()                                                                    
#include <unistd.h>             // for close()                                                                    
#include <sys/socket.h>

// System calls
close()
ioctl() 
sigaction()

// Flags
MSG_DONTWAIT

// Types
socklen_t

And the following are Windows specific:
// Header files
#include <WinSock2.h>
#include <WS2tcpip.h>

// System calls
closesocket()
ioctlsocket()

For namespace reasons, the syntax :: may be needed in front of system calls (e.g., ::send(...)).

Compilation errors such as "Redeclaration of class ClassName..." (with the actual class name instead of ClassName) typically occur if the header file, say ClassName.h, has been included from multiple source files. This error occurs most often for utility-type classes (and functions) that are used by multiple other classes (e.g., the NetworkManager). The fix is typically to use conditional compilation directives for the compiler pre-processor. If conditional compilation directives are in use, they should be checked that the names used in the #ifndef and #define statements are identical.

Remember to error check all system calls (e.g., send()). Where appropriate, messages should be written to the Dragonfly logfile (using the LogManager writeLog()).


Top | Overview | Details | Hints

Return to 4513 Home Page

Send all questions to the TA mailing list (cs4513-staff at cs.wpi.edu).