GNE Tutorials

GNE website

Tutorials Home

Tutorials

  1. Creating exhello
  2. Techniques using PacketFeeder

Figures

  1. GNE Mid-Level API Connection Process

Installation Tutorials

  1. MSVC.NET
  2. MinGW32
  3. Linux/UNIX

Tutorial 1: Creating exhello

Applies to GNE version 0.55.

Contents

Introduction

In this example we will walk through the process and the design behind the GNE exhello world example program.

This "hello world" example shows the 'minimum' code to get a fully-functioning and correct program running in GNE. A first look at the example and it looks pretty complicated, but don't let that deceive you! A few reasons simply that is looks deceiving is that most of the code is in comments, but more importantly most of that code follows to be pretty obvious derivations of code that already exists in GNE, and this will be explained in the tutorial.

It is also important to notice that the program is fully functioning and fleshed out. You may look at code using raw sockets or using low-level network libraries and think it is simpler, but most are leaving out complete error checking and handling, and most only work for simple one-client connections. This complete example shows that programming with GNE has a high overhead but once you pass that overhead, adding more functionality is simple. All of this code has the following advantages:

  • Once you write the code for "one" client, you've written it for all clients. GNE handles all of the thread creation for you. This example can handle an unlimited number of clients connecting all at the same time. One client does not have to finish connecting before another can start, as is the typical case in most single-threaded applications.
  • The connection creation, domain name lookup, and connection negotiation are all handled for you, with all errors reported in a simple format that is easy to report. You don't have to write code to check every byte comming in. You should be able to send any random garbage to exhello and it should handle it.
  • The code takes advantage of GNE's parsers, and GNE handles any binary format conversions needed to talk, for example, between Intel machines and Macs.
  • All of the features of GNE such as bandwidth throttling and timeout detection are just a few lines away.

Anyways, enough of the propaganda. I hope you find the extra initial effort worth the power and flexibility you gain over your communications. Let's dive into the code.

Back to Contents

Code Organization

The code is organized a little strangely in the exhello example, because the exhello example and the exsynchello example both implement the same thing, exhello with event-driven logic and exsynchello with syncronous logic. This will be explained later and in later tutorials. But for now you should know that the event-driven logic is the primary way of using GNE. The code that is shared between the two examples is in shared.h. Note that they share the main function.

Back to Contents

Part 1: Initializing GNE

The entry point to this program is a little unorthodox as two examples share the same code. For now just ignore this. The main function calls the doMain functions providing the name of the example logic.

#include <gnelib.h>
#include <iostream>
#include <string>

using namespace std;
using namespace GNE;
using namespace GNE::Console;
using namespace GNE::PacketParser;

int main(int argc, char* argv[]) {
  doMain("Event-Driven");
  return 0;
}

void doMain(const char* connType) {
  if (initGNE(NL_IP, atexit)) {
    exit(1);
  }

  //sets our game info, used in the connection process by GNE to error check.
  //We give exhello and exsynchello the same name because they are compatable
  //with each other.
  setGameInformation("hello examples", 1);

  initConsole(atexit);
  setTitle("GNE Basic Connections Example");
}

The first thing we do is include the gnelib header. You will notice this is different from the actual code in the example, because the example is coded to use the header part of the source to let you compile the examples before installing GNE.

We then import the std namespace (for endl) and the GNE namespaces we will be using, so that we won't need to fully qualify the class names (for example GNE::initGNE is now just initGNE).

The first call we make is to initGNE. This function takes the network type as a parameter, and the atexit function. Use NL_IP for internet, and use NO_NET if you don't want to use the network (for example just to test the console functionality, as in the exinput example). The function returns true on error, and we exit the program if this is the case.

We then call setGameInformation. The name of your game or program and the number is used to UNIQUELY identify your program to any program using GNE. In this way GNE automatically checks version numbers and program types to make sure you are both using the same version of YOUR protocol (GNE also checks against its internal protocol version transparently to you).

The call to initConsole tells GNE we will be using the console code for text drawing and to initialize that. As with initGNE it takes the atexit function so GNE will shutdown automatically when main() ends.

The setTitle call sets the title bar of your applications Window, if it is able to do so (currently MS Windows only).

Back to Contents

Part 2: Interacting with the Console

After GNE is initialized we get input from the user, to determine whether we will run the server or client side of the code. The GNE console code is to be used over the standard C or C++ I/O classes, because of the multithreaded nature of GNE. GNE provides just enough console functionality to write simple text interfaces for servers (and also for simple examples as you can see):

  • Output is multithreaded, so multiple threads can write at the same time to the screen.
  • Ability to get information about the window size, and set the window title.
  • Ability to locate the cursor, and get non-blocking input in a multithreaded environment.
  • Compability with the standard C++ cout and cin objects by replacing just the streambuf.

The gout and gin objects correspond to the C++ objects cout and cin. gout and gin are exactly the same as their counterparts, except they use a custom streambuf. The HTML API reference explains many more features than those we will use in this tutorial. Also, the C-style (like printf) functions are also there, you can find them in the reference (see GNE::Console::mprintf). mprintf is used in this tutorial. Unlike cout and printf, mprintf and gout can be interchanged and used at the same time.

Here is the rest of the code of the doMain function.

  gout << "GNE " << connType << " Basic Connections Example for "
       << GNE::VER_STR << endl;
  gout << "Local address: " << getLocalAddress() << endl;
  gout << "Should we act as the server, or the client?" << endl;
  gout << "Type 1 for client, 2 for server: ";
  int type;
  gin >> type;

  int port;
  if (type == 2) {
    setTitle("GNE Basic Connections Example -- Server");
    gout << "Reminder: ports <= 1024 on UNIX can only be used by the superuser."
         << endl;
    port = getPort("listen on");
    doServer(0, 0, port);
  } else {
    setTitle("GNE Basic Connections Example -- Client");
    port = getPort("connect to");
    doClient(0, 0, port);

    gout << "Press a key to continue." << endl;
    getch();
  }

As you can see, gout works in the same way as cout. All of the calls in this section are pretty self-explainatory -- you can always view them in the reference, though. The getPort function is a simple utility function defined in the example to return an integer port. We set the window title appropriately when the user picks client or server versions. Note that GNE provides the GNE::Console::getch() function -- this is NOT the function by the same name as found in in the popular headers/libraries conio.h or curses.h.

Back to Contents

Part 3: Defining a Packet

Before we start doing anything over the network, let's define the types of data our program will use. Communication in GNE is always done with packets. You create your own packets by inheriting from the GNE::Packet class, and overriding the appropriate methods. Most of the time these methods are very straightforward, the load and write packet methods are the most complicated.

To create a packet, you will need to know what data the packet will hold, and we need to give the packet an ID. GNE IDs range from MIN_USER_ID to MAX_USER_ID, inclusive. It is suggested that give your packets IDs relatlve to MIN_USER_ID (for example MAX_USER_ID + x, where x is some number). For this example, we will only be defining one packet, and we'll be giving it the id MIN_USER_ID + 0, or just MIN_USER_ID. This packet will just contain a string containing the "hello" message. Note that the maximum GNE string length size is 255 bytes. Larger strings should be split up into different packets. Note also the max packet size is a little over 500 bytes. GNE expects you to make the smallest packets possible, and split up your data into pieces (one packet for each game object). Don't worry about sending too many small packets -- this is meant to be as GNE is designed to optimize the overhead of the connection by automatically combining small packets, so making small packets helps this process.

class HelloPacket : public Packet {
public:
  HelloPacket() : Packet(MIN_USER_ID) {}
  HelloPacket(string message2) : Packet(MIN_USER_ID), message(message2) {}
  virtual ~HelloPacket() {}

  Packet* makeClone() const {
    return new HelloPacket(*this);
  }

  int getSize() const {
    return Packet::getSize() + RawPacket::getStringSize(message);
  }

  void writePacket(RawPacket& raw) const {
    Packet::writePacket(raw);
    raw << message;
  }

  void readPacket(RawPacket& raw) {
    Packet::readPacket(raw);
    raw >> message;
  }

  static Packet* create() {
    return new HelloPacket();
  }

  string getMessage() {
    return message;
  }

private:
  string message;
};

Note that HelloPacket is defined in shared.h as both the exhello and exsynchello examples share this packet type so they can communicate with each other. Let's look at this class in detail.

  • The constructor passes MIN_USER_ID to Packet, the ID that we selected for our custom packet type.
  • The makeClone method will be straightforward for each packet you write. It should return a copy of the packet. This is so GNE can copy your custom packet internally without ever knowing any specifics.
  • The getSize method returns the size of your serialized packet, used internally by GNE.
  • The write and read packet methods are the core of any Packet implementation. Always call the parent class's write and read before yours! If you derived yet another packet from HelloPacket, you would call HelloPacket::writePacket(raw) as the first line, instead. You only or read write the data for your specific class.
  • The create method, like the makeClone method, is always very straightforward. Return a new instance of your packet -- GNE will create a packet through this function and then call readPacket on it to fill it with data.
  • The getMessage function has nothing to do directly with making a packet type -- it's just simply an accessor for our message variable.

Now that we have made our custom packet, we must register it so GNE knows to parse it. If you try to send a HelloPacket without registering it, it will send correctly, by the other side will report that it received an illegal, unknown packet as a parser error. Add a line to doMain after initializing GNE:

registerPacket(MIN_USER_ID, HelloPacket::create);

We pass the registerPacket function the ID of our packet, and its creation function, so GNE can create our HelloPackets internally. Congratulations, you have just integrated a new packet type into GNE's library of packets!

Back to Contents

Part 4: Creating the Server Listener

Now that we have initialized GNE and have a packet to communicate with, we need to set up three things:

  1. The ServerConnectionListener to listen on a port for new clients, and when an incoming connection comes in, set those parameters.
  2. A ConnectionListener for the server side, to implement the server.
  3. Another ConnectionListener for the client side, to implement the client.

In this part we will create a ServerConnectionListener. To do this we derive from the ServerConnectionListener class:

class OurListener : public ServerConnectionListener {
public:
  OurListener(int iRate2, int oRate2) 
    : ServerConnectionListener(), iRate(iRate2), oRate(oRate2) {
  }

  virtual ~OurListener() {}

  void onListenFailure(const Error& error, const Address& from,
                       ConnectionListener* listener) {
    mprintf("Connection error: %s\n", error.toString().c_str());
    mprintf("  Error received from %s\n", from.toString().c_str());
    delete listener;
  }

  void getNewConnectionParams(ConnectionParams& params) {
    params.setInRate(iRate);
    params.setOutRate(oRate);
    params.setUnrel(true);
    params.setListener(new OurServer());
  }

private:
  int iRate;
  int oRate;
};

This class is usually fairly simple. We have set up an error handler in onListenFailure to report errors if an error occurs before a connection is even created (for example if another non-GNE program tried to connect to yours, or if the versions of the connecting programs differ). The getNewConnectionParams method is the important method, and it sets up important parameters for our connection. There are more parameters we could set up, but we choose the stick with the default setting on those, and instead we set:

  • Our available incoming bandwidth throttle to iRate -- we force the other side never to send more than iRate bytes per second (this is needed to manage low-bandwidth connections, like when accessing the internet with a modem). If we set 0, then GNE does not limit or manage our bandwidth.
  • Our available outgoing bandwidth (oRate). Again, if we set 0, then GNE does not limit or manage our bandwidth.
  • We set to allow unreliable connections. If we pass false, we FORCE all data to be sent reliably. Both sides must use true to allow unreliable data. Note that you can always optinally send reliable data at the time you send it.
  • The last line creates a listener for the new connection's events. The OurServer class we will derive in the next section. This class implements the server.

Note that with this method, there may be many connections going at the same time -- each connection will have its own OurServer instance, as getNewConnectionParams is called for every incoming client.

The ServerConnectionListener class can do a lot more than this. The example expong is currently the only example that uses ServerConnectionListener to its fullest extent. You can use it to distribute bandwidth amongst incoming connections, and you can use it only allow a certain number of connections at a time (expong uses it to only allow one ever enter the game).

Sidenote: When high-level GNE is completed, there will be an actual Server class that will handle that functionality for you easily.

Back to Contents

Part 5: Creating the Server

We will create the implementation of the server by deriving a custom class from ConnectionListener. Typical communication in GNE is event-based. Every time an event happens on a connection, one of the methods of your ConnectionListener will be called. You will then respond to the data, and then return from the function, and lie dormant until your next event. Only one event can be occuring for a single connection at any time -- but keep in mind your "main" thread will still continue to run, and other connections may be processing events at the same time in different threads, so you should use mutexes where needed. Fortunately for this sample the code is very simple and data is not shared between threads, so mutexes are not needed. Let's look at the OurServer class:

class OurServer : public ConnectionListener {
public:
  OurServer() : conn(NULL), received(false) {
    mprintf("Server listener created\n");
  }

  virtual ~OurServer() {
    mprintf("Server listener killed\n");
  }

  void onDisconnect() { 
    //Call receivePackets one last time to make sure we got all data.
    //It is VERY possible for data still left unread if we get this event,
    //even though we read all data from onReceive.
    receivePackets();
    mprintf("ServerConnection just disconnected.\n");
    if (!received)
      mprintf("No message received.\n");
    delete conn;
    delete this;
  }

  void onExit() {
    mprintf("Client gracefully disconnected.\n");
  }

  void onNewConn(SyncConnection& conn2) {
    conn = conn2.getConnection();
  }

  void onReceive() {
    receivePackets();
  }

  void onFailure(const Error& error) {
    mprintf("Socket failure: %s\n", error.toString().c_str());
  }

  void onError(const Error& error) {
    mprintf("Socket error: %s\n", error.toString().c_str());
    conn->disconnect();
  }

  //Tries to receive and process packets.
  //This function responds to any requests.  Note that it is called in
  //onDisconnect and it is perfectly valid to send data from onDisconnect --
  //it just won't ever be sent ;), but there is no reason to pass in a param
  //and check for disconnection just so we don't send the data.
  void receivePackets() {
    Packet* message = NULL;
    while ( (message = conn->stream().getNextPacket() ) != NULL) {
      if (message->getType() == MIN_USER_ID) {
        HelloPacket* helloMessage = (HelloPacket*)message;
        mprintf("got message: \"%s\"\n", helloMessage->getMessage().c_str());
        received = true;
        
        //Send Response
        mprintf("  Sending Response...\n");
        HelloPacket response("Hello, client!  I'm the event-driven server!");
        conn->stream().writePacket(response, true);
      } else
        mprintf("got bad packet.\n");
      delete message;
    }
  }

private:
  Connection* conn;
  bool received;
};

This class is pretty long-looking in the example, but a lot of the code is in comments, blank lines, and messages to help us debug. This class includes a LOT of "fluff" code to display what's happening, so I've cut some of it out for this tutorial. You can see the extra code in the actual example file.

It's pretty simple when you break it down, so let's do that now. Let's break it down by the order events in which events will occur for us.

  1. onNewConn: A server-side connection always starts with an onNewConn event. We keep a pointer to our connection. In the actual example, there is some extra code here that displays the client's information.
  2. onReceive: This message is sent when new packets arrive from the client. We take this opportunity to call receivePackets. receivePackets checks the incoming packets, looking for our HelloPacket ID. If we got a HelloPacket we receive and parse it, and then send a response (passing "true" to send it reliably).
  3. onError and onFailure: These events might be received if GNE discovers that "Something Bad" has happened. When an error occurs, the connection is recoverable and is still active, unless we choose to disconnect. For simplicity in this example, we treat errors as failures and disconnect. A failure in GNE terms means that the connection cannot continue, and it was closed. We report these errors.
  4. onExit: The onExit event is sent before onDisconnect if the disconnection was a graceful one.
  5. onDisconnect: Any GNE connection ends with one and only one onDisconnect event, and it is always the last event. When we disconnect we take one last look to see if we've received any packets, then we delete the connection, and delete ourselves. In this way, OurServer is completely stand-alone from the main thread. The main program never has to see the OurServer. In an "actual" program you would probably in onNewConn call a function and keep a list of current connections, and in onDisconnect, call another function to say you are disconnected, and the main program will delete you later.

Back to Contents

Getting and Sending Packets

You will notice the usage of "conn->stream()" in the server code. This returns the GNE::PacketStream instance which represents a stream of packets. All of the data communication will happen through this class. Refer to the documentation to see what methods are available to you and how to use them.

Back to Contents

Part 6: Creating the Client

Now we need a client to connect to our newly created server class. First thing you will notice is that the client and server-side code is mostly the same. Sometimes it is so similar that in some examples there is only one ConnectionListener which does both client and server. I will only describe the differences between the client and server rather than break down the whole class again. Some of the code has been cut out to simplify the view:

class OurClient : public ConnectionListener {
public:
  OurClient() : conn(NULL) {
    mprintf("Client listener created.\n");
  }

  ~OurClient() {
    mprintf("Client listener destroyed.\n");
  }

  void onDisconnect() { 
    mprintf("Client just disconnected.\n");
    delete this;
  }

  void onExit() {
    mprintf("Server gracefully disconnected.\n");
  }

  void onConnect(SyncConnection& conn2) {
    conn = conn2.getConnection();
  }

  void onReceive() {
    Packet* message = NULL;
    while ( (message = conn->stream().getNextPacket()) != NULL ) {
      if (message->getType() == MIN_USER_ID) {
        HelloPacket* helloMessage = (HelloPacket*)message;
        mprintf("got message: \"");
        mprintf(helloMessage->getMessage().c_str());
        mprintf("\"\n");
      } else
        mprintf("got bad packet.\n");
      delete message;
    }
  }

  void onFailure(const Error& error) {
    mprintf("Socket failure: %s\n", error.toString().c_str());
    //No need to disconnect, this has already happened on a failure.
  }

  void onError(const Error& error) {
    mprintf("Socket error: %s\n", error.toString().c_str());
    conn->disconnect();//For simplicity we treat even normal errors as fatal.
  }

  void onConnectFailure(const Error& error) {
    mprintf("Connection to server failed.\n");
    mprintf("GNE reported error: %s\n", error.toString().c_str());
  }
private:
  Connection* conn;
};

Again I will explain the client in terms of the order of events called on it:

  1. When a client connects, it will receive either onConnect or onConnectFailure depending on the success of the connection. We store the connection if it was successful, else we report an error and return. When onConnectFailure is called, onDisconnect will never be called, as the connection was never in a connected state.
  2. onReceive: Similar to the server, except we just simply display whatever data is sent to us.
  3. onError, onFailure, and onExit are written just like the server's.
  4. onDisconnect is a little different. This time we don't delete the connection, because we aren't the "owner" of it. In order to know this we have to know the nature of the calling code. When we see the implementation of doClient I will explain this more in detail.

Back to Contents

Part 7: Using Our New Classes

Now that we have a listener, server, and client setup and ready to go, it's time for us to use them. We have two functions, doServer and doClient. We'll go over doServer first, then look at doClient.

Back to Contents

Implementation of doServer

void doServer(int outRate, int inRate, int port) {
#ifdef _DEBUG
  //Generate debugging logs to server.log if in debug mode.
  initDebug(DLEVEL1 | DLEVEL2 | DLEVEL3 | DLEVEL4 | DLEVEL5, "server.log");
#endif
  OurListener server(inRate, outRate);
  if (server.open(port))
    errorExit("Cannot open server socket.");
  if (server.listen())
    errorExit("Cannot listen on server socket.");

  gout << "Server is listening on: " << server.getLocalAddress() << endl;
  gout << "Press a key to shutdown server." << endl;
  getch();
  //When the server class is destructed, it will stop listening and shutdown.
}

The first thing we do is if we are compiling in debug mode, we initialize the debug file. GNE will print out very verbose and helpful messages that will greatly aid in viewing the flow of your program. You can customize the contents of the logs by specifing which message levels you want. In this case, we have chosen to display ALL debugging content. For normal programs you should leave out DLEVEL4 and DLEVEL5 as they generate lots of text -- DLEVEL4 will generate a line of text for every event generated.

Note that we had to start our logs late, so we can create server and client logs. Therefore we missed a couple of lines at the start of the log, but we won't worry about that in this tutorial.

Now the important part is that we create an instance of our listener, to listen for incoming connections on our given port. The errorExit function is a small utility function in shared.h to display an error message and exit the program.

We then wait for a keypress to shutdown the server. When the server object is destroyed at the end of the function, it will automatically close the ports and shutdown.

While this is simple, this isn't the best way to program. If there are current connections running we will forcefully terminate them in a non-graceful and ditry way when the program exits. Since our connections are so short, the user will have opportunity to press a key at an appropriate time. A more robust server would keep track of current connections, and then handle them gracefully. This is about the only error checking code that was left out of this program to make it completely robust.

Back to Contents

Implementation of doClient

void doClient(int outRate, int inRate, int port) {
#ifdef _DEBUG
  initDebug(DLEVEL1 | DLEVEL2 | DLEVEL3 | DLEVEL4 | DLEVEL5, "client.log");
#endif
  string host;
  gout << "Enter hostname or IP address: ";
  gin >> host;

  Address address(host);
  address.setPort(port);
  if (!address)
    errorExit("Invalid address.");
  gout << "Connecting to: " << address << endl;

  ConnectionParams params(new OurClient());
  params.setUnrel(true);
  params.setOutRate(outRate);
  params.setInRate(inRate);
  ClientConnection* client = new ClientConnection();
  if (client->open(address, params)) {
    delete client;
    errorExit("Cannot open client socket.");
  }
  
  client->connect();
  client->join();
  
  //Check if our connection was successful.
  if (client->isConnected()) {

    //Send our information
    HelloPacket message("Hello, server!  I'm the event-driven client!");
    client->stream().writePacket(message, true);
    client->stream().writePacket(message, false);
    
    //Wait a little for any responses.
    gout << "Waiting a couple of seconds for any responses..." << endl;
    Thread::sleep(2000);
    client->disconnectSendAll();
  }
    
  delete client;
}

(Some code and comments cut out for visibility). Working with clients is much different -- it's a lot "closer" and personal than the server, as the client is proactive in making a connection while a server just "sits there." We first ask the user to input an address to connect to, then we validate it (note that not only can address take raw IP address such as 129.21.135.193, it can take names like "www.yahoo.com").

We set up a ConnectionParams object in the same way we did so in our implementation of the ServerConnectionListener. We then create the connection and open the port, and then connect. The connect operation is done in a separate thread, so we could go off an do other work, waiting for onConnect to fire and tell us to come back, but in this simple example we have nothing else to do, so we wish to wait to connect. To do this we call the join method. If instead we didn't want to wait, we would have called client->detach(false) instead. (Note: once you detach a thread, you can't join on it, and you must either detach or join on a thread before the objects are destroyed).

After waiting for the connection thread to complete, we check to see if we connected. If we didn't connect, onConnectFailure was called and already reported an error message, so we just exit. If we did connect, we create our HelloPacket. For testing we send one packet reliably with true, and send one unreliably with false. Unreliable packets require less overhead, less bandwidth, and decrease your ping times, but have the disadvantage that might get lost, or arrive in a different order than you sent them in. Reliable packets always arrive and arrive in the same order as sent.

Note that if the server refused our unreliable connection (by setting unreliable in their params to be false), both packets will be sent reliably instead.

We then wait for responses, then shut down the connection gracefully, sending any unsent buffered data, and exit.

Note that because we call "delete client" we don't need "delete conn" in the OurClient class.

Back to Contents

Conclusion

That was quite a lengthy tutorial, but the code that you have created in this example can serve as a template for future GNE code. In this tutorial we actually covered every basic aspect of the library, and created a complete, robust program with the exception of the server shutdown.

Back to Contents