![]() |
Implementation
To implement RakNet all you have to do is get an instance of the client, server, or peer (or all three) in your program. These are the most common headers you'll need: Headers #include "PacketEnumerations.h" #include "RakNetworkFactory.h" #include "RakPeerInterface.h" #include "NetworkTypes.h" PacketEnumerations.h contains a giant enumeration representing the native packet identifiers that RakNet uses to send you messages, such as disconnection notifications. Since you will probably want to define your own packet identifiers, you can add these to this file. RakNetworkFactory.h is an implementation of the factory design pattern, used to get a pointer to RakPeer, RakServer, and RakClient. This is necessary to use the DLL. RakPeerInterface.h is an interface for the RakPeer class. NetworkTypes.h defines the structures used in RakNet, including PlayerID - a unique identifier for systems, and Packet which the API returns to you when you get data or when it needs to send you a message. Instancing RakClientInterface* client = RakNetworkFactory::GetRakClientInterface(); RakServerInterface* server = RakNetworkFactory::GetRakServerInterface(); RakPeerInterface* peer = RakNetworkFactory::GetRakPeerInterface(); That code would give you one instance of the client, one instance of the server, and one instance of the peer. Usually you would only want one of these in a particular exe. The next step is to connect the client or start the server respectively. For example: Client Connection client->Connect(serverIP, serverPort, clientPort, 0, 0); The first parameter to the Connect function, serverIP, is the IP address of the server. If you want to connect to your own system, such as when running two copies of the same program, use "127.0.0.1" or "localhost" which is accepted notation for your own system. If the server is on a different system you'd enter the IP address of the other system. The next parameter is the serverPort. This is the port you want to try to connect to on the server. If you specify a port the server is not expecting data on you won't be able to connect just like if you had entered the wrong IP. The IP and the port always work together in this fashion to form the complete address. How do you know what port to connect to? Well as the programmer you decide this ahead of time and then just hardcode it into your program. How do you choose a port? You can choose whatever you want as long as no one else is using it and its within the range of 2^16 (0 to 65535). However, certain ports are reserved for use by established programs, such as your web browser, telnet, and FTP. You can look up these port assignments on the internet, but the quick and dirty answer is most ports under 32000 are reserved and ports over 32000 are open to whoever wants them. I like to pick from the high ranges, such as over 60000, to be safe. In practice ports are generally set with #define per program and not changed. For example: #define SERVER_PORT 60005 #define CLIENT_PORT 60006 This way the server will always know what port to respond to and the clients will always know what port to connect to. It also saves end-users the trouble of typing the ports in. The next parameter, clientPort, is which port you want to recieve and send data on for the client. This has the same purpose as the serverPort, except that it is for the client instead of the server. You CAN use the same port for both the client and the server but then you wouldn't be able to run two copies of your program on the same computer and have them connect to each other because you cannot use ports that are already in use. The next parameter (set to 0 in this example) is the connectionValidationInteger. It is unused and remains for backwards compatibility. The last parameter (set to 0 in this example) is the thread sleep timer. A value of 0 is good for games that need fast responses, such as a shooter. Otherwise, a value of 30 will give good response times and will require very little CPU usage. Note that connections are asynchronous. The function will return true immediately if it succeeded in the attempt to connect, but it does not mean your connection succeeded. You know your connection succeeded when IsConnected() returns true. 'Friendly' hosts will inform the client of an invalid port and you will get the network message ID_UNABLE_TO_CONNECT_TO_REMOTE_HOST. This way you can stop waiting for the connection immediately. Unfriendly hosts won't and you will just have to stop waiting when you feel you have waited long enough. RakNet connects quickly so if you don't connect within a few seconds it's not going to connect. Starting the server is similar. Server Connection server->Start(2, 0, false, serverPort); The first parameter is how many simultaneous client connections to allow. The second parameter is the connectionValidationInteger (as described above). The third parameter is the thread priority bool (as described above). The fourth parameter is the server port (as described above). The parameter serverPort in the Start method for the server should be assigned the same value as the parameter serverPort in the Connect function for the client. Keep in mind that the actual number of players you could have playing is one more than the number of clients you support if you program your game to allow the server to act as a player. If your server is a dedicated server or if you program your game to have both a client and a server on the same system (not recommended) then obviously the number of people who could play would change accordingly. How do I know if the connection was successful or if anyone connected to me? Keep reading. Peer to peer connections peer->Initialize(10, 60000, 0); peer->SetMaximumIncomingConnections(4); Initialize would start the peer with 10 allowable connections. An allowable connection is either incoming or outgoing. It uses port 60000 to receive data (equivalent to clientPort), and uses priority 0. Priority 0 is regular priority, 1 is high priority, and 2 is ultra high priority. SetMaximumIncomingConnections is necessary if you want to allow other peers to connect to you, but is not necessary if you only plan to connect to others. In this case, it sets the value to 4. This is a maximum value rather than a reserved value, so it is still possible to say connect to 8 peers - you would then only be able to accept 2 incoming connections until you disconnected from one or more of those peers. Reading Packets Packet *packet = client->Receive(); or Packet *packet = server->Receive(); or Packet *packet = peer->Receive(); It's that easy. If packet is 0 then there was nothing to read and you can go on with your game. Otherwise you got some data. Note that this is done automatically by the multiplayer class. ProcessPackets will either call a handler, if it is a system message, or call ProcessUnhandledPacket if it is not. You can get two kinds of data: Messages from the engine Messages from other instances of RakNet, on the same computer or from other computers Both are handled the same way. Lets look at the Packet struct: struct Packet { PlayerID playerId; unsigned long length; unsigned long bitSize; // Same as length but represents bits. Length is obsolete and retained for backwards compatibility char* data; }; PlayerID specifies the origin of the packet. Every connected client has a unique playerID which is assigned automatically. Certain native network messages use this field - for example ID_REMOTE_DISCONNECTION_NOTIFICATION tells you as a client that another client has disconnected. PlayerID in that case specifies which client. UNASSIGNED_PLAYER_ID is a reserved value for "Unknown". length and bitSize tells you how many bits long the data field of the struct is. Now that you got a packet you need to determine what the data means. Usually the first byte of the data is an enum that specifies type (see creating packets for more information). This is not always the case as you'll later learn. To make things easy there is already a function written for you to get the identifier in Multiplayer.cpp . Here it is again: unsigned char GetPacketIdentifier(Packet *p) { if ((unsigned char)p->data[0] == ID_TIMESTAMP) return (unsigned char) p->data[sizeof(unsigned char) + sizeof(unsigned long)]; else return (unsigned char) p->data[0]; } This will return an unsigned char, which corresponds to an enum specified in PacketEnumerations.h. The network engine will return certain messages only for the client, certain messages only for the server, and certain messages for both. For a full explanation of the messages refer to PacketEnumerations.h. The important ones to worry about are ID_NEW_INCOMING_CONNECTION and ID_CONNECTION_REQUEST_ACCEPTED. These mean that the server or a peer got a new incoming client, and the client or a peer has successfully connected respectively. At this point you can send your own messages. If the packet identifier is NOT one of the pre-defined identifiers then you got user data which was sent by another system. You can then decode the data and handle it in your game as appropriate. See creating packets for information on encoding and decoding data. IMPORTANT! When you are done with your data, don't forget to deallocate the packet! Just pass it to DeallocatePacket. This is done automatically by the multiplayer class, so if you use the multiplayer class you don't have to deallocate the packet yourself. All 3 classes have the same interface to do this: server->DeallocatePacket(p); client->DeallocatePacket(p); peer->DeallocatePacket(p); The multiplayer class is nothing more than a class that reads a packet, parses the first byte as the identifier, and deallocates it. It is templated so it can take the client, server, or peer. Even if you don't use it, it is worth looking at because you will probably want to parse packets the same way. It also contains documentation on each packet identifier. Sending Data The best way to illustrate sending data is with an example: const char* message = "Hello World"; For our server: server->Send((char*)message, strlen(message)+1, HIGH_PRIORITY, RELIABLE, 0, UNASSIGNED_PLAYER_ID, true, true); For our client: client->Send((char*)message, strlen(message)+1, HIGH_PRIORITY, RELIABLE, 0); The first parameter is your data and must be a byte stream. Since we're sending a string, and a string is a byte stream, we can send it directly without any casting. The second parameter is how many bytes to send. In this example we send the length of the string and one more for the null terminator. The third parameter is the priority of the packet. This takes one of three values: HIGH_PRIORITY MEDIUM_PRIORITY LOW_PRIORITY High priority packets go out before medium ones, and medium ones go out before low ones do. Simple. The fourth parameter takes one of five values. Lets say you send data 1,2,3,4,5,6. Here's the order and substance of what you might get back: UNRELIABLE - 5, 1, 6 UNRELIABLE_SEQUENCED - 5 RELIABLE - 5, 1, 4, 6, 2, 3 RELIABLE_ORDERED - 1, 2, 3, 4, 5, 6 RELIABLE_SEQUENCED - 5, 6 RELIABLE and RELIABLE_ORDERED are fine for most cases. For more details on this refer to PacketPriority.h The fifth parameter (0 in this example) is which ordering stream to use. This is used for relative ordering of packets in relation to other packets on the same stream. It's not important for now, but for more information on this refer to the Sending Packets section. SERVER ONLY: The sixth parameter for the server (UNASSIGNED_PLAYER_ID in this example) is the playerID (remember how we discussed it in the section on the packet?). UNASSIGNED_PLAYER_ID is a reserved value meaning "no-one in particular". The PlayerID field means one of two things: either who you want to send the packet to, or who you don't want to send the packet to, depending on the value of broadcast, which is the last parameter. The seventh parameter (true in this example) is whether to broadcast to all connected clients or not. This parameter works with the sixth parameter, playerID. If broadcast is true, then playerID specifies who not to send to. If it is false, then it specifies who to send to. If we want to broadcast to everyone, then we just specify UNASSIGNED_PLAYER_ID for playerID, which is pre-defined to mean out of range. This works out quite well when relaying packets, because the packetID field will specify who the sender is. By passing the sender to playerID field of the Send function and specifing broadcast as true, we can relay the packet to everyone BUT the sender, which makes sense since we usually don't want to send the same information back to the person who just sent it to us. The eight parameter (true in this example) is whether or not to spend 20 extra bytes downstream and extra CPU cycles on both systems to provide extra packet security. Packet security is important because otherwise malicious users could modify the contents of packets in order to disrupt games, crash servers, or otherwise cause havok. RakNet automatically provides a certain amount of security even with this parameter set to false. However, setting it to true, which you should do for critical and/or infrequent data, will provide optimum security. Shutting Down Shutting down is easy and nearly instantaneous. Just call Disconnect() For our server: server->Disconnect(); For our client: client->Disconnect(); This also stops the network threads so RakNet will not consume CPU cycles unnecessarily. You can always restart the network with the Start and Connect methods. Obviously, if you shut down the server with clients connected they will stop getting data and eventually drop due to time-out. If you want to be polite you can use the Kick method to drop everyone first so they get a disconnection message. Cleaning up Just pass the instance that the factory gave you to the DestroyRakClientInterface or DestroyRakServerInterface methods of the factory as appropriate. You may want to do this mid-program to free memory but it is not required. For our server: RakNetworkFactory::DestroyRakServerInterface(server); For our client: RakNetworkFactory::DestroyRakClientInterface(client); Firewall and NAT users take note Firewalls are hardware or software utilities intended to only let authorized data pass to and from a computer to the general network. As RakNet is a networking API, a firewall will block RakNet just as indiscriminately as it would any other API or application. To get past this, you have to use certain tricks. One way is to use a well-known port for RakNet, such as the HTTP port 80. This often works for the server, or for fussy clients. Another way is to use port 0 for the client, which will automatically pick an open port. You can use the master server to serve from behind a NAT. When the game server estabilishes a connection to the master server, it can then accept any incoming connections on the broadcast IP / Port (Note this is only true for UDP). For a full list of reasons on why a connection attempt might fail, see the FAQ. Final Word Next page: Step by step tutorial |
![]() |
Index Introduction System Overview Tutorial Compiler Setup |