free web stats RakNet & Irrlicht - Step 2

A Primer For RakNet Using Irrlicht

By Dave Andrews (http://www.daveandrews.org)



Intro | Step 1 | Step 2 | Step 3 | Conclusion

Step 2: The Server

The server is a separate program from the client, and therefore should be created as a separate project, with its own main.cpp, and linker settings. Configure the linker settings just as you did the client, aside from the fact that the server does not need or use Irrlicht. It's just a basic C++ console app, linked with the RakNet library.
The server for our little chalkboard program is very, very simple. It receives packets from each connected client, and forwards them to every other connected client. All of this is done with only 3 functions!
  1. SendLineToClients. This function receives as parameters 4 ints which represents the 2 endpoints of a line. It will then send that data to every client connected to the server, except for the client who originally sent the line it's forwarding.
  2. HandlePacket. This function will decode a packet that is sent to it. It will check the the type of packet, and if it is a packet which represents a line it will decode that packet and send it out to all the clients.
  3. main. That's right, main() is one of our 3 functions! The main function sets up the server, begins listening, and then enters the main server loop of listening for incoming packets. After the main loop it closes down the server. Just that simple.
Remember, RakNet deals in UDP. This means we don't have to worry about listening for connections and spawning threads or all this other crazy business that books about TCP teach. An incoming connection is just an incoming packet to RakNet, it will handle all the threads and connections/disconnections for us in the background. This is what makes it so simple to use, and so clean!

Let's start with just the includes.

// headers for RakNet
#include <PacketEnumerations.h>
#include <RakNetworkFactory.h>
#include <NetworkTypes.h>
#include <RakServerInterface.h>

#include <windows.h> // for Sleep()
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <conio.h>

These are almost exactly like the includes for the client program, except the RakClientInterface.h has been changed to RakServerInterface.h. In this program, we will actually be dealing with RakNet classes, most important the RakServerInterface, Packet, and BitStream classes.
  1. RakServerInterface is what we will use to start up the server and send/receive packets to and from the connected clients.
  2. Packet is the class which holds data transmitted from one system to another.
  3. BitStream is the (brilliant!) class we will use to encode and decode data into and from a stream of bytes.
First, a bit of information on the packets that these programs will be throwing around the network. The first byte of the data in a packet is the packet identifier. RakNet itself handles packets such as incoming connections and disconnections, by first examining their packet identifier. There are several of these that are predefined in RakNet. There is documentation and declarations of the predefined identifiers in the file PacketEnumerations.h that comes with RakNet.

For this program, we will need to define our own packet identifier. We want our programs to know when they are receiving a packet that represents a 2d line. So we will define our own packet identifier, and it's just this easy:

const unsigned char PACKET_ID_LINE = 100;

It is set to 100 in order to absolutely avoid any confusion with the predefined identifiers. Basically, any packet that is received by the server (and later, the client) where the first byte in its data is equal to 100, it means we are receiving data to represent a line.

We're going to put this to immediate use, with the first and simplest function of our server, SendLineToClients.

void SendLineToClients(RakServerInterface * server, PlayerID clientToExclude, int x1, int y1, int x2, int y2)
{
RakNet::BitStream dataStream;

dataStream.Write(PACKET_ID_LINE);
dataStream.Write(x1);
dataStream.Write(y1);
dataStream.Write(x2);
dataStream.Write(y2);

server->Send(&dataStream, HIGH_PRIORITY, RELIABLE_ORDERED, 0, clientToExclude, true);
}

And there you have it, our first complete network-enabled function. This function introduces several important ideas, the BitStream, sending data through the RakServerInterface, and the PlayerID.

The BitStream is one of the best features of RakNet, in my opinion. It will manage an internal array of data, and allow you to write anything into that stream of data. Secondly, you can also define a BitStream to use a pre-existing array of data (which we'll do later) and then you can use the BitStream class to "decode" data into any type you want.

In this function, an empty BitStream is created. We then write a single byte into the data, our own custom packet identifier (remember that in a packet, the FIRST byte is the packet identifier.) After that first byte, we write into the stream 4 integers, the points (x1, y1) and (x2, y2). All of this data is basically compiled into a stream of bytes by the BitStream class. That data is then passed into the RakServerInterface's Send function.

The RakServerInterface is our window to the clients. We will define the server object later on in this program. It is what controls all of RakNet's server functions, such as automatically accepting connections from incoming clients and also send data to clients.

In this function, we use a type called PlayerID. The PlayerID is used by RakNet to distinguish between connections. If you were to look into the PlayerID, you'd see that it contains the IP address of the client it handles and the port that client is connected through. Think of the PlayerID objects as being individual connections, and it is the way that you distinguish between your connections.

In the case of this function, the PlayerID passed in is a client that we do not want to send this line to. Remember in the client program, once a line was drawn by the user, it would add the line locally, and then send it to the server? Well when we program in the networking code for the client, it will blindly add all lines received from the server into its line list. This is why we want to exclude a certain client when we send the line out to all the clients--if we just sent it to all the clients connected, it would also send the line to the client who supplied the line, and he would end up with 2 copies of the same line, eventually degrading performance.

Check out the documentation for RakServerInterface->Send. If the bool broadcast (the last parameter) is true, then it will send to all clients except the one supplied in the playerID parameter. If the broadcast bool is false, it will send to only the PlayerID supplied. This is how you decide to send data to every client, or only one client.

One advantage of RakNet: notice how we didn't have to manually build a Packet object. The server's send function built a packet from the BitStream supplied, filled it with the pertinent information, and sent it! No questions asked.

void HandlePacket(RakServerInterface * server, Packet * p)
{
unsigned char packetID;

RakNet::BitStream dataStream((const char*)p->data, p->length, false);

dataStream.Read(packetID);

switch(packetID) {
case PACKET_ID_LINE:
int x1, y1, x2, y2;

dataStream.Read(x1);
dataStream.Read(y1);
dataStream.Read(x2);
dataStream.Read(y2);

SendLineToClients(server, p->playerId, x1, y1, x2, y2);
break;
default:
printf("Unhandled packet (not a problem): %i\n", int(packetID));
}
}

Here is the function which handles a packet that has come in from a client. It receives a pointer to a server interface, in the case that a line is received that would need to be sent by the server to connected clients. The Packet p is a packet that has been received from some client.

The first thing the function does is create a BitStream out of the packet's data. The first parameter to BitStream's constructor is the data from the packet (cast into a const char,) then the length of the data stream in the packet, and the last parameter tells it whether to make a copy of the data or use the original data. By passing false we are telling the BitStream to use the original copy of the data instead of creating its own copy.

The next thing the function does it read in the packet identifier. Remember how it's the first byte in the packet?

We are only interested in the packet if the identifier is PACKET_ID_LINE, the one we created above. Therefore we check to see if this is the case, and if it's not we just spit out a message with the packet identifier. Don't worry about unhandled packets, RakNet is still taking care of connections and incoming data.

If we receive a line notification, then we will try to read further from the BitStream. As you can see, we just read the integers from the points and pipe them into the SendLineToClients function. Also of note, we give SendLineToClients the packet's playerID, so it will know which playerID to exclude.

So now let's move into the final server function, main().

int main()
{
RakServerInterface * server = RakNetworkFactory::GetRakServerInterface();
Packet * packet = NULL;

int port = 10000;

Here is just the definition and the local variables our main will use. The most important here, of course, is the server object. The RakNetworkFactory class is there for easy use, it creates and destroys server and client interfaces. We will be creating a client interface for Step 3, but here we want to grab a pointer to a new server interface. Our server will be running on port UDP 10000, which is just an arbitrary number I picked out.

The Packet is the class which handles incoming data, we used that class earlier in the HandlePacket() function.

    if(server->Start(32, 0, 0, port)) {
printf("Server started successfully.\n");
printf("Server is now listening on port %i.\n\n", port);
printf("Press a key to close server.\n");
}
else {
printf("There was an error starting the server.");
system("pause");

return 0;
}

Here is the code that starts the server listening for connections. The Start() function is what we're interested in here.

The first parameter to Start() is the number of allowed simultaneous connections. We are going to allow up to 32 connections to this server. The maximum possible for RakNet (as of this writing) is 65535 connections. The second parameter is deprecated. The third parameter is the number of milliseconds for each connection thread to sleep between updates. We set it to 0 here, because we will be calling Sleep() in our main loop. After that is the port number to listen on.

And so now the server will listen for incoming connections! Let's move on to the main loop.

    while(kbhit() == false) {
Sleep(1);
packet = server->Receive();

if(packet != NULL) {
HandlePacket(server, packet);

server->DeallocatePacket(packet);
}
}

This while loop will continue until a key on the keyboard is pressed. Its purpose is only to check for packets coming in to the server, and then handling any such packets. As you can see, the first thing we do is call the Receive() function which will return a pointer to a packet if there is one.

If there is a packet, we want to pass it straight into our HandlePacket function, which we created above. When that is finished, we must call the DeallocatePacket function, as that will properly dispose of all the data in the packet. If we did not properly dispose of each packet, the server's performance would suffer after time.

We also call Sleep() in the loop, because an uninterrupted while loop would grab 100% of the processor, so we share to be nice.

    server->Disconnect(300);

RakNetworkFactory::DestroyRakServerInterface(server);

printf("Server closed successfully.\n");

system("pause");
}

And here is the end of our server program. This is after the main loop, meaning the administrator has pressed a key on the server to kill it (a real server app would want a more secure way to close the server, of course!)

An important line is the DestroyRakServerInterface function. This makes sure the server object has been destroyed nicely.

The Disconnect() function will disconnect all of the clients connected to the server. The 300 that we passed in was basically a time in milliseconds that the server will wait before closing down, to make sure all packets are taken care of and everything is closed nicely.

And that's it for the server! Compile this application and you have become an official Server Developer. ;)

Back To Step 1 | On To Step 3...


The content, code, and images of this page is copyright (c) Dave Andrews. Any reproduction or redistribution of this material is prohibited without consent from the author.