Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Fast Networking Library 2

4.79/5 (13 votes)
20 Nov 2010CDDL9 min read 70.6K   2K  
A library for easy, fast network gaming

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:

C#
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).

C#
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.

C#
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:

C#
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:

C#
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:

C#
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:

C#
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.

C#
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).

C#
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.

C#
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.

C#
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.

C#
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!) :

Output_Screenshot.PNG

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.

Click to enlarge image - Demo_Apps_Output.PNG

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.

License

This article, along with any associated source code and files, is licensed under The Common Development and Distribution License (CDDL)