Introduction
Game development is one of the most common areas for home developers to have a go at, which is exactly what I am doing. However, though it is fairly easy to set up a simple, standalone game in XNA, it is very difficult to set up an online game. The reason is that XNA only supports the Windows Live system for network gaming. This is a fine system to use, but only if you are developing for an XBox and have one yourself. So far as I can tell, it is very difficult to get a Windows Live account working without an XBox and to use the network gaming classes is not as easy as it should be. So for these reasons, I decided to make my own Network Gaming Library that would allow me to produce reliable, fast multiplayer games.
Using the Code
The code contains 4 main classes. The most simple class is GlobalMethods
which in short, has two static
methods for converting an object to and from a byte array. Note, if the object is a class you have defined, it must have the [Serializable]
attribute. The serializer methods use BinaryFormatter
and MemoryStream
to convert the object/byte array. (BinaryFormatter
is from System.Runtime.Serialization.Formatters.Binary
and MemoryStream
from System.IO
.)
The 3 remaining classes are the interesting ones. They are:
Client
GameServer
ManagerServer
GameServer
is a server that allows a maximum number of connections to it and allows clients to send data through it to other players or just to the server itself if specified. ManagerServer
does the same thing, but also has the built-in ability to keep track of GameServer
IP addresses and then pass those out to clients who request them.
Client
has two modes, GameClient
and ManagerClient
. GameClient
should be used when the client is connecting to a GameServer
, ManagerClient
should be used when the client is connecting to a ManagerServer
. This is because the base message object types used to transfer data quickly and efficiently between the Client
and two types of server are different.
I will explain the classes by going through the test application line by line and explaining what the different method calls do and the code they run.
To begin, there are a number of variables defined at the start of the console test application. They are:
static int GSMaxConnections = 2;
static int SMMaxConnections = 100;
static int GSPort = 56789;
static int MSPort = 56788;
static int LoopDelay = 500;
public static int BufferSize = 9999999;
static bool Terminate = false;
static List<IPAddress> ServerAddresses = null;
static ManualResetEvent AddressesReceivedEvent = new ManualResetEvent(false);
static Client MSClient2 = new Client(ClientModes.ManagerClient,
new Client.MessageReceivedDelegate(MSClient_OnMessageRecieved));
static ManagerServer TheMS = new ManagerServer(
new MaxConnectionsDelegate(TheMS_OnMaxConnections),
new ManagerServer.MessageReceivedDelegate(TheMS_OnMessageRecieved));
GSMaxConnections
defines the maximum number of connections that the GameServer
(created later) will be allowed to accept. SMMaxConnections
defines the maximum number of connections that the ManagerServer
(created later) will be allowed to accept.GSPort
is the port number the GameServer
will listen on.MSPort
is the port number the ManagerServer
will listen on.LoopDelay
is used later to define how quickly a while
loop should send test messages. It is set in milliseconds. The minimum number it should be set to is 10
if sending small messages or 50
if sending larger amounts of data to ensure maximum reliability. If you find that messages appear to be lost, then send messages less frequently and you will probably find they get through.BufferSize
is the maximum size of the send and receive buffers, used by the underlying sockets can be. The maximum this number can is 9999999 as any bigger and the underlying socket throws an error.Terminate
is used to stop the test message while loop (see later).ServerAddresses
is used to store the list of addresses returned by the ManagerServer
.AddressesRecievedEvent
is used to wait for the addresses to be received (see later).MSClient2
is a ManagerServerClient
that is used to get the server addresses and remove the GameServer
from the ManagerServer
address list.TheMS
is the ManagerServer
. It is used to keep track of existing GameServers
and to display how many GameServers
it knows about.
All the main code runs in Main
which is also the entry point for the application (as required by C# standards).
Thread TerminateThread = new Thread(new ThreadStart(TerminateThreadRun));
TerminateThread.Start();
These first two lines of code start a background thread that waits for the user to press the escape key and then sets Terminate
to true
. This allows the user to exit at any suitable time.
Continue = TheMS.Start(MSPort, SMMaxConnections);
This calls the Start
methods on the ManagerServer
. This method starts the ManagerServer
listening for connections. The code it calls is:
if (!Initialised)
{
OK = Initialised = Initialise(Port);
}
if (OK)
{
MaxConnections = MaximumConnections;
TheListener.Start(MaxConnections);
TheListener.BeginAcceptSocket(AcceptSocketCallback, null);
}
This initialises the ManagerServer
which creates a new TcpListener
that listens for TCP connections on the specified port. It sets up the maximum connections the ManagerServer
is allowed to accept, starts the TcpListener
and finally calls BeginAcceptSocket
which allows the ManagerServer
to asynchronously accept connections.
The next lines of code in Main are:
Client MSClient1 = new Client(ClientModes.ManagerClient,
new Client.MessageReceivedDelegate(MSClient_OnMessageRecieved));
MSClient1.OnErrorMessage += new ErrorMessageDelegate(MSClient_OnErrorMessage);
MSClient1.OnDisconnected += new Client.DisconnectDelegate(MSClient_OnDisconnected);
Continue = MSClient1.Connect(MSPort, Dns.GetHostAddresses(Dns.GetHostName()).First());
This creates a new Client
that is set to ManagerClient mode
, i.e., it will be connected to a ManagerServer
. If the mode is incorrect for the server type you try to connect to, the connection will still be accepted but most if not all messages sent across the connection will be unhandled at the other end. The key line of code here is MsClient1
.Connect
that attempts to connect the client to the local machine (which the ManagerServer
is running on) on the MSPort
(defined earlier). The code it calls looks like:
TheSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
TheSocket.Connect(TheAddress, Port);
byte[] Buffer = new byte[BufferSize];
TheSocket.BeginReceive(Buffer, 0, Buffer.Length,
SocketFlags.None, ReceiveCallback, Buffer);
This creates a new socket which is set to use the TCP protocol and is set to type stream, meaning data will be sent both ways across the connection the same way you would with a stream. It then tries to connect the socket to the specified IP address on the specified socket. The code does not need to check if the socket is connected afterwards because if the socket fails to connect, an error is thrown which is handled by a try
/catch
block. Finally, BeginReceive
is called which allows the client to receive data asynchronously.
If the MSClient
connects, it then sends an AddServer
request which is a default ManagerServerMessageType
which causes the ManagerServer
to add the IP address of the client it receives this type of message from, to its list of servers.
Next, the code sets up a GameServer
. In theory, the above line of code should be called after the game server is set up but for testing purposes, it is easier to do it this way round. The game server setup code looks like:
GameServer.BufferSize = BufferSize;
GameServer TheGS = new GameServer(new MaxConnectionsDelegate(TheGS_OnMaxConnections),
new GameServer.MessageReceivedDelegate(TheGS_OnMessageRecieved));
TheGS.MessageHiding = false;
TheGS.OnError += new ErrorMessageDelegate(TheGS_OnError);
TheGS.OnClientDisconnect += new GameServer.ClientDisconnectDelegate(
TheGS_OnClientDisconnect);
Note that the BufferSize
is a static
variable that can be set anywhere during the code. However, the GameServer BufferSize
and Client BufferSize
should be the same (providing no data greater than the smaller buffer size is sent across the connection, no error occurs as the buffer size is a maximum size) and also the ManagerServer BufferSize
must be the same as the Client BufferSize
.
if (TheGS.Start(GSPort, GSMaxConnections))
The above line of code does the same thing as TheMS
.Start
except that it starts the GameServer
listening on the GSPort
not the MSPort
(defined earlier).
Client.BufferSize = BufferSize;
MSClient2.OnErrorMessage += new ErrorMessageDelegate(MSClient_OnErrorMessage);
MSClient2.OnDisconnected += new Client.DisconnectDelegate(MSClient_OnDisconnected);
if (MSClient2.Connect(MSPort, Dns.GetHostAddresses(Dns.GetHostName()).First()))
The above code again does the same as the previous code for MSClient1
except note the first line now defines the Client BufferSize
. This should, ideally, be defined earlier but in this simple test application it makes no difference to the running or outcome of the program.
MSClient2.MSSend(null, ManagerServerMessageType.GetServers, false);
AddressesReceivedEvent.WaitOne(1000);
The above two lines of code send a request to the ManagerServer
for its list of server addresses and then waits for them to be sent back. It will wait a maximum of 1000 milliseconds (1 second) for the response. This timeout ensures that the program doesn't wait indefinitely if anything goes wrong. Here we must jump to MSClient_OnMessageReceived
.
ManagerServerMessageObject TheClass = (ManagerServerMessageObject)
(GlobalMethods.FromBytes(e.TheBytes));
if (TheClass != null)
{
if (TheClass.TheMessageType == ManagerServerMessageType.ServersResponse)
{
ServerAddresses = (List<IPAddress>)(GlobalMethods.FromBytes(TheClass.TheBytes));
Console.WriteLine("");
if (ServerAddresses != null)
{
Console.WriteLine("Server addresses received.
Addresses count : " + ServerAddresses.Count);
}
else
{
Console.WriteLine("Server addresses received. Addresses were null!");
}
AddressesReceivedEvent.Set();
}
}
The above code handles any message received by either of the MSClients
. It gets the ManagerServerMessageObject
(which is the underlying message object) using the GlobalFunctions
.FromBytes
and then tests to see if it is null
. (N.B. It would only be null
if the GlobalFunctions
.FromBytes
encountered an error.) The code then tests to see if the message was a ServersResponse
, i.e. the data is a list of server IP addresses. If it is, it gets the list, sets ServerAddresses
and then releases the thread that is waiting on the AddressesReceivedEvent
, i.e. the main thread.
Finally, the code sets up two game Clients
and repeatedly sends test messages over one of the connections.
Client GSClient1 = new Client(ClientModes.GameClient,
new Client.MessageReceivedDelegate(GSClient_OnMessageRecieved));
GSClient1.OnErrorMessage += new ErrorMessageDelegate(GSClient_OnErrorMessage);
GSClient1.OnDisconnected += new Client.DisconnectDelegate(GSClient_OnDisconnected);
bool Connected = GSClient1.Connect(GSPort, ServerAddresses[0]);
Console.WriteLine("Attempted to connect game server client 1.
Connected : " + Connected.ToString());
Client GSClient2 = new Client(ClientModes.GameClient,
new Client.MessageReceivedDelegate(GSClient_OnMessageRecieved));
GSClient2.OnErrorMessage += new ErrorMessageDelegate(GSClient_OnErrorMessage);
GSClient2.OnDisconnected += new Client.DisconnectDelegate(GSClient_OnDisconnected);
Connected = GSClient2.Connect(GSPort, ServerAddresses[0]);
Console.WriteLine("Attempted to connect game server client 2.
Connected : " + Connected.ToString());
while (!Terminate)
{
DataClass NewDataClass = new DataClass
("Your random number is : ", new Random().Next());
GSClient1.GSSend(GlobalMethods.ToBytes(NewDataClass), false, false);
Thread.Sleep(LoopDelay);
}
The very last thing the code does is call disconnect on the two GameServer Clients
, disconnects MSClient2
(MSClient1
has already been disconnected) and stops the GameServer
and ManagerServer
. These are done using Client
.Disconnect()
and Server
.Stop()
.
The output of the program looks like this (number of test messages depends on when you decide to exit!) :
Points of Interest
This being the second time I have made a networking library, I have learnt from past errors, i.e., this library is more reliable, faster and more flexible!
History
I have added two demo applications that run together to demonstrate the network structure I intended the classes to be used for. The first application is the Manager Server console application that simply displays what the ManagerServer
class does. The second is a Client console application that demonstrates what you would do with a game. In brief, the program loads, connects to the manager server, gets the list of existing servers, if the list has no servers in it, it becomes a server, if it does have some IP addresses in it, it tries to connect to each IP address in turn until it gets a connection. If no connection can be made, the application ends. If the game becomes a game server, it tells the manager server and then connects to the game server it has just created, i.e., itself. If the program is now connected to a game server, it will allow the user to send test messages to all other clients of the game server. Both programs should be exited by pressing the Escape key as this closes all connections and cleans-up without the risk of leaving open connections behind. The image below shows the output of an instance of a manager server, and two instances of the console game application. One instance is running as the game server and game client, the other just as a game client.
New update: I have updated the code so it is more reliable. I found that if I sent messages immediately after each other, they were lost. At first, I thought the packets were being lost but I was assured by several sources that this wasn't possible. I therefore came to the conclusion that as the packets were being sent immediately after each other, TCP was probably grouping the packets into one. I have therefore updated my code so that it can separate the messages when they are received. I did this by placing a six digit number at the front of every message that contained how many bytes long the message was. This allows my program to separate out the messages at the other end. The code changes are compatible with the previous versions of the library, so please re-download the files if you have already started using it.
New update: I have added OnConnected
events to ManagerServer
, GameServer
and Client
classes. For ManagerServer
and GameServer
, the event is fired when a client connects to them. For client, the event is fired immediately after it connects.