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.70. View version for 0.55. Those porting from GNE 0.55 to GNE 0.70 may wish to compare the two tutorials to get a feel for the major API changes between the two versions.

Last updated: Tuesday, August 2, 2005

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 C 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. The parsers also detect buffer underflows and overflows and gracefully handles these errors even when reading and writing your own packets through GNE's Buffer class, without any extra work from you. Typical programs using low-level "C" libraries contain a large number of exploits, and sending them bad data will cause crashes or exploiting. GNE prevents this as much as it possibly can for you.
  • GNE's use of smart pointers and graceful shutdown code prevents memory leaks and gracefully shuts down all connections and threads even in failure cases without any extra work from you. Simple programs using low-level code typically leak memory or leave sockets open in abnormal cases, some even in normal cases. This is impossible in GNE if the operating system does not forcefully terminate it (that is, if "main" exits properly).
  • 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 doMain function which effectively implements "main".

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();
  setTitle( "GNE Basic Connections Example" );
}

The first thing we do is include the gnelib header.

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. Because GNE registers itself with atexit, GNE will automatically be shut down on program exit if you do not do so explicitly.

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.

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 GNE 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). Both mprintf and gout are used in this tutorial. Unlike cout and printf, mprintf and gout can be interchanged and used at the same time without any abnormal behavior, but you must remember that gout is line-buffered, so the output from gout will only be shown on flush or on a newline character.

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 when only a single thread is active. 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.

Note that if multiple threads may be using gout, you need to use the acquire/release manipulators with gout, or use LockObject. mprintf is safe not to acquire/release. If you do not acquire gout the program will not crash, but GNE has no way of keeping a single gout statement together, thus if you use

gout << 530 << " hello " << 5 << endl;

you may see the 3 portions of that output ( 530, " hello ", and 5 ) intermixed with portions from other gout statements. The solution is to use acquire/release, or preferably LockObject, so that all information covered in the lock is treated as a single "output unit".

{
  LockObject lock( gout );
  gout << 530 << " hello " << 5 << endl;
}

When you use mprintf, all text produced by that call is treated as one "output unit." Because of the locking issues and the fact the sometimes C++-style output is better and sometimes C-style is better, it is explicitly allowed in GNE and acceptable to mix gout, mprintf, and the other GNE::Console output functions.

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.

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.

The exception to the smallest packet possible rule is if you are sending something large such as a map data file over a Connection, and that is the only transfer currently taking place -- in this case you want to send as large of packets as possible.

class HelloPacket : public Packet {
public:
  typedef SmartPtr<HelloPacket> sptr;
  typedef WeakPtr<HelloPacket> wptr;

public:
  HelloPacket() : Packet(ID) {}
  //copy ctor not needed, because message is copyable
  HelloPacket(string message2) : Packet(ID), message(message2) {}

  virtual ~HelloPacket() {}

  static const int ID;

  int getSize() const {
    return Packet::getSize() + Buffer::getSizeOf(message);
  }

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

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

  string getMessage() {
    return message;
  }

private:
  string message;
};
  
const int HelloPacket::ID = MIN_USER_ID;

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 two typedefs define smart pointer types for our Packet. You will see this used in server-side code (the client side code uses traditional explicit memory management as was the only option in GNE 0.55).
  • The constructor passes MIN_USER_ID to Packet, the ID that we selected for our custom packet type.
  • Packets must be copyable. Most packets typically contain only simple primitive types and std::string, so often no explicit definition of the copy constructor, operator = or destructor is needed, but they need to be defined if they are needed for proper semantics. Our HelloPacket does not need any of these three.
  • 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. If a parsing error occurs during either of these two methods, your code is allowed to throw GNE::Error or a subclass thereof (specifically you must throw Error&). The Buffer operations GNE provides automatically check for buffer underflow and overflows and throws on error, so all error checking here is handled already by GNE.
  • 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:

defaultRegisterPacket<HelloPacket>();

The default default packet registration template uses the ID member from your packet as its ID. After registration GNE can create our HelloPackets internally. Congratulations, you have just integrated a new packet type into GNE's library of packets!

The default registration assumes the following this about your packet (called P):

  • P::ID is the ID you gave to your packet.
  • You can create new packets of your type with new, as in new P();
  • You delete your packets using operator delete, as in delete ptrToP;
  • You can copy packets using new and the copy constructor, as in new P( otherPacket );

Unless you want to use your memory allocator to replace the standard C++ new and delete operators, you are safe using defaultRegisterPacket. See the GNE documentation if you want to use your own memory allocators for the full registration procedure.

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 {
protected:
  OurListener(int iRate, int oRate) 
    : ServerConnectionListener(), iRate(iRate), oRate(oRate) {
  }

public:
  typedef SmartPtr<OurListener> sptr;
  typedef WeakPtr<OurListener> wptr;

  static sptr create(int iRate, int oRate) {
    sptr ret( new OurListener( iRate, oRate ) );
    ret->setThisPointer( ret );
    return ret;
  }

  virtual ~OurListener() {}

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

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

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.

Many of the major GNE classes are managed by smart pointers. Thus when we inherit from a GNE class we have to accomodate for that. In these cases, we define smart pointer typedefs, make the constructor protected, and provide a static creation function so that the only way to create the class gives you a SmartPtr. SmartPtr is reference counted so you never need to "delete" it. Read the Smart Pointers section of the API changes document (internet link) for more information about smart pointers.

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: //typedefs
  typedef SmartPtr<OurServer> sptr;
  typedef SmartPtr<OurServer> wptr;

protected:
  OurServer() : received(false) {
    mprintf("Server listener created\n");
  }

public:
  static sptr create() {
    return sptr( new OurServer() );
  }

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

  void onDisconnect( Connection& conn ) { 
    //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( conn );
    mprintf("ServerConnection just disconnected.\n");
    if (!received)
      mprintf("No message received.\n");
  }

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

  void onNewConn( SyncConnection& conn2) {
    mprintf("Connection received; waiting for message...\n");
  }

  void onReceive( Connection& conn ) {
    receivePackets( conn );
  }

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

  void onError( Connection& conn, 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( Connection& conn ) {
    //This time we use the SmartPtr version of getNextPacket, to show both
    //ways.  Using the SP version we don't need to call destroyPacket.
    Packet::sptr message;
    while ( (message = conn.stream().getNextPacketSp() ) ) {
      if ( message->getType() == HelloPacket::ID ) {
        HelloPacket::sptr helloMessage = static_pointer_cast<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");
    }
  }

private:
  bool received;
};

This class includes some "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. 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). This version of onReceive uses the smart pointer version of getNextPacket. Notice in this version there is no call to explicitly destroy the packet as that is not needed with smart pointers. In this way you can be sure you do not leak memory, even in the face of early returns or (even more importantly) exceptions.
  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. After onFailure, onDisconnect will result.
  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. In this way, OurServer is completely stand-alone from the main thread. The main program never has to see the OurServer.

Like our listener before, OurServer is to be managed by the smart pointer system in GNE so we don't have to worry about deleting it when the connection is closed -- we can "make and forget". As before we used the typedefs and static create function for this purpose.

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: //typedefs
  typedef SmartPtr<OurClient> sptr;
  typedef SmartPtr<OurClient> wptr;

protected:
  OurClient() {
    mprintf("Client listener created.\n");
  }

public:
  static sptr create() {
    return sptr( new OurClient() );
  }

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

  void onDisconnect( Connection& conn ) { 
    mprintf("Client just disconnected.\n");
  }

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

  void onConnect(SyncConnection& conn2) {
    mprintf("Connection to server successful.\n");
  }

  void onReceive( Connection& conn ) {
    Packet* message = NULL;
    while ( (message = conn.stream().getNextPacket()) != NULL ) {
      if ( message->getType() == HelloPacket::ID ) {
        HelloPacket* helloMessage = (HelloPacket*)message;
        LockObject lock( gout );
        gout << "got message: \"" << helloMessage->getMessage() << '\"'
             << endl;
      } else
        mprintf("got bad packet.\n");
      destroyPacket( message );
    }
  }

  void onFailure( Connection& conn, 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( Connection& conn, 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( Connection& conn, const Error& error) {
    mprintf("Connection to server failed.\n");
    mprintf("GNE reported error: %s\n", error.toString().c_str());
  }
};

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's implementation just prints a message.

The receivePackets method of OurClient uses the old getNextPacket method that returns a Packet*. This method requires a GNE::destroyPacket method to be called on that pointer when you are finished with the Packet. Both ways are available and shown but the smart pointer method used in OurServer is much safer and is the preferred method.

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

//For both types of exhello, starting the server is the same.
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::sptr server = OurListener::create(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();

  //This is not strictly needed, as all listeners will be shutdown when GNE
  //shuts down.
  server->close();
}

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 separate server and client log files. Therefore we missed a couple of lines at the start of the log pertaining to GNE startup, 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. Because we are using smart pointers, the OurListener created will always be deleted, even if we exit the program early with errorExit, without any work on our part.

We then wait for a keypress to shutdown the server. When GNE is shutdown all listeners and connections are disconnected, so although the close call is not technically needed there, we provide it for completeness. Even if connections are active, GNE will wait (by default) up to 10 seconds to gracefully close all connections, end all threads, and stop all timers. Thus shutdowns with GNE are always clean and graceful since GNE internally keeps a list of all connections, threads, and timers. And because of the smart pointers, you don't need to worry about memory allocation either.

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( OurClient::create() );
  params.setUnrel(true);
  params.setOutRate(outRate);
  params.setInRate(inRate);
  ClientConnection::sptr client = ClientConnection::create();
  if (client->open(address, params))
    errorExit("Cannot open client socket.");
    
  client->connect();
  client->waitForConnect();
    
  //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();      
  }

  //client will be destroyed here as its sptr will go out of scope and
  //because it is disconnected.
}

(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 waitForConnect method.

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 the waitForConnect method returns an Error object if the connection process failed. Instead of reporting the error on onConnectFailure, we could have just as easily done so where we called waitForConnect. Note that waitForConnection can be called even after the connection's attempt has completed. In that case, the waitForConnect method returns immediately with the Error that occured (the Error may have a NoError code if the connection was successful).

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. This is performed transparently to you, so you need not worry about the type of connection negotiated.

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

As in the server's case, the disconnect call is not strictly needed as GNE will request all connections to disconnect at shutdown and wait for up to 10 seconds by default for a graceful close.

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 without any missing error handling or memory leaks even in failure cases.

Back to Contents