|
Dragonfly NetworkingExtending the Dragonfly Game Engine with Networking |
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).
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.
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.
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:
socket()
.
bind()
.
listen()
.
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 string
s. When
invoked, connect()
does the following steps:
getaddrinfo()
.
socket()
.
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.
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.
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 int
doStep() generates a network event by calling
the NetworkManager's onEvent()
method
(note onEvent()
is actually defined in the base Manager
class).
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
// 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()
).
Send all questions to the TA mailing list (cs4513-staff at cs.wpi.edu).