Click here to Skip to main content
15,881,898 members
Articles / Programming Languages / C#

Network gaming classes and functions

Rate me:
Please Sign up or sign in to vote.
4.56/5 (7 votes)
19 Oct 2009CPOL8 min read 33.6K   1.2K   37   11
A class library designed to provide easy-to-use/easy-to-integrate classes and functions for making network games.

Introduction

In this article, I hope to try to solve and explain the problems I have had and also how to use the classes I have produced. My classes are designed to provide easy-to-use interfaces to make online gaming easy. They provide interfaces that allow fast and easy network connections. I hope to have solved the problem of never being able to find pre-built classes and interfaces that are very easy to integrate with any software.

Background

I wanted to produce an online multiplayer game but discovered that there were no articles or classes that would do all the connection and networking stuff I needed. So I set out to create some useful interfaces and classes that did! And here they are, ready for you to use. The basic idea is that the you can use the interfaces to set up network connections and transfer data quickly and easily. At the moment, the current version of my code doesn't support any form of encryption or security functions, but look out for my next article on the same topic in which I will have got that working. For now, I just wanted people to be able to have the basic code.

Using the code

I will go through all the code from a basic level right up to the complicated stuff at the top. Here goes!

In my classes, I have provided two main base interfaces. They are IHost and IClient. These both do most of the work, with IHost acting as the server and IClient as the clients.

Host class file

In the Host.cs file, first look at the following code :

C#
public Host(bool StartNow, int Port)
{
     TheListener = new TcpListener(Port); 
     ReadThread = new Thread(new ThreadStart(Read));

     if (StartNow)
     {
          Start();
     }
}

This is the main constructor for the Host class. First, it creates a new TcpListener and tells it to listen on the port specified.. (I will assume that you know what the basic objects are!) Then, it creates a new Thread called ReadThread. (I will come onto what this does specifically later.) If the user has said that the Host should 'StartNow' or start listening for connections and incoming data as soon as possible, then the Start() method is called. (If the user hasn't specified it, then he/she must call the Start() method manually.)

Here is the Start() method:

C#
public bool Start()
{
     try
     {
          TheListener.Start();
          TheListener.BeginAcceptSocket(new AsyncCallback(Accept), null);
          ReadThread.Start();
          ReadThread.Name = "Host Read Thread";
          return true;
     }
     catch
     {
          return false;
     }
}

First, the TcpListener, created earlier, is told to start listening for connections, and the BeginAcceptSocket method is called so that an asynchronous callback calls the Accept method when a connection is available. Then, the ReadThread, also created earlier, is started, and given the name "Host Read Thread". If all that is completed successfully, it returns true. If there is an error, it returns false.

Here is the Accept(IAsyncResult Result) method :

C#
private void Accept(IAsyncResult Result)
{
     try
     {
          Socket TheSocket = TheListener.EndAcceptSocket(Result);
          TheListener.BeginAcceptSocket(new AsyncCallback(Accept), null);
          Client TheClient = new Client(TheSocket);
          Clients.Add(TheClient);

          if(OnAccept != null)
          {
               OnAccept.Invoke(new AcceptArgs(TheClient));
          }
     }
     catch
     { 
     }
}

This method is called asynchronously when a connection is waiting. First, we EndAcceptSocket and get the waiting socket. Then, we begin the process of accepting a socket again. Then, we create a new Client class to be used by the server for communication with the new client. (Note: A Client class is used, not a IClient interface. This is because the Host must be able to access some functions that users should not be able to use.) Finally, if the OnAccpet event is not null, we invoke it, and now we have finished AcceptSocket.

Here is the Read() method :

C#
private void Read()
{
    try
    {
        ThreadPool.SetMaxThreads(10, 10);
        ThreadPool.SetMinThreads(0, 0);
        while (!Terminating)
        {
            if (Clients.Count > 0)
            {
                List<Client> RemoveClients = new List<Client>();
                for (int i = 0; i < Clients.Count; i++)
                {
                    Client AClient = Clients[i];
                    try
                    {
                        if (AClient.Connected)
                        {
                            if (AClient.DataAvailable)
                            {
                                byte[] Buffer = AClient.Buffer;
                                if (Buffer.Length > 0)
                                {
                                    OnRead.Invoke(new ReadArgs(Buffer, AClient));
                                }
                            }
                        }
                        else
                        {
                            RemoveClients.Add(AClient);
                        }
                    }
                    catch
                    {
                        RemoveClients.Add(AClient);
                    }
                }
                foreach (Client AClient in RemoveClients)
                {
                    OnClientDisconnect.Invoke(new DisconnectArgs(AClient.ID));
                    Clients.Remove(AClient);
                }
            }
            else
            {
                Thread.Sleep(100);
            }
        }
    }
    catch
    {
    }
}

The Read() method, in essence, just loops round, checking every client for incoming data until the Host instance terminates. Each time it checks a client, it makes sure that that client is still connected, and then sees if data is available. If it is, it reads the data, then calls the OnRead event.

The ClearUp() method simply calls ClearUp() on all the clients, and then ends the read thread and stops the TcpListener.

The Client Class File

In the Client.cs file, first look at the following code:

(The code below has been edited to fit the page)

C#
public Client(bool ShouldConnect, bool StartRead, string HostIPOrName, int Port)
{
    TheSocket = new Socket(AddressFamily.InterNetwork, 
                           SocketType.Stream, ProtocolType.Tcp);

    if (ShouldConnect)
    {
        Connect(HostIPOrName, Port);
    }
    if(StartRead) 
    {
        TheStream.BeginRead(TheBuffer, 0, TheBuffer.Length, 
                            new AsyncCallback(Read), null);
    }
    ID = Guid.NewGuid();
}

This is the main constructor for the Client class; however, there is another one which is used by the Host class for creating clients the server can use. However, the only one you should ever need to use will be this one, so this is the constructor I will explain.

First, a TheSocket (the main socket that the communication is run down) is set up. We create a new socket and then, similar to the Host class constructor, we see if the user wants us to connect straight away. If so, we call Connect(string HostIPOrName, int Port) to connect to the user defined IP and port. Then, if the user wants us to start reading, we set up the read loop. Basically, we call BeginRead, then whenever the Read(IAsyncResult Result) method is called, we call BeginRead again, thus causing a loop. Finally, we set the client's ID to a new GUID, for use that we will see later when we look at the Advanced Client, MainHost, and Game Server classes.

There are only three methods that I will explain. These are Read(), Process(byte[] Bytes) and Send(object Message, bool MessageForServer).

Here is the Send(object Message, bool MessageForServer) method:

(The code below has been edited to fit the page)

C#
public bool Send(object Message, bool MessageForServer)
{
    try
    {
        byte[] SendBuffer = 
          Functions.PackageToByteArray(new Package(Message, MessageForServer, ID));
        byte[] BytesLength = ASCIIEncoding.UTF8.GetBytes(SendBuffer.Length.ToString());
        byte[] FinalBytes = Functions.MergeBytes(BytesLength, SendBuffer);
        return Send(FinalBytes, 0, FinalBytes.Length) ? true : false;
    }
    catch
    {
        return false;
    }
}

This method is the one that you use for sending objects to the server. First, we convert a new Package, containing the Message, MessageForServer, and ID objects, to a byte array. Then, we get the length of the SendBuffer and convert that to a byte array. Then, we merge the bytes and send it off.

Here is the Read(IAsyncResult Result) method:

C#
private void Read(IAsyncResult Result)
{
    try
    {
        TheStream.EndRead(Result);
        byte[] Bytes = (byte[])TheBuffer.Clone();
        TheBuffer = new byte[Settings.Default.BufferSize];
        TheStream.BeginRead(TheBuffer, 0, TheBuffer.Length, new AsyncCallback(Read), null);
        TheStream.Flush();
        Process(Bytes);
    }
    catch
    {
    }
}

This is the first method that deals with reading and processing incoming data. The second is Process(byte[] Bytes). This method gets called by thye asynchronous callback we set up earlier or are about to reset now. First, we call EndRead to finish reading, and so the bytes of data are read into TheBuffer. Then, we clone TheBuffer and put it into a new byte array called Bytes. This allows us to reset TheBuffer and call BeginRead again, starting the process all over again. Then, we call Process(byte[] Bytes) passing it the byte array Bytes.

Here is the Process(byte[] Bytes) method:

C#
public bool Process(byte[] Bytes)
{
    try
    {
        byte[] BytesLength = new byte[] { Bytes[0], Bytes[1], Bytes[2], Bytes[3] };
        string StringLength = ASCIIEncoding.UTF8.GetString(BytesLength).Replace(
            "<", "").Replace("?", "").Replace(
            "x", "").Replace("m", "");
        if (StringLength != "")
        {
            int Length = Convert.ToInt32(StringLength);
            int LengthLength = StringLength.Length;
            Package Data = Functions.GetPackage(Bytes, LengthLength, Length);
            OnRead.Invoke(new ReadArgs(Data, this));

            int BytesAfterPos = 0;
            for (int i = Length + LengthLength; i < Bytes.Length; i++)
            {
                if (Bytes[i].ToString() != "0")
                {
                    BytesAfterPos = i;
                    break;
                }
            }
            if (BytesAfterPos != 0)
            {
                byte[] NewBytes = new byte[Bytes.Length - Length - LengthLength];
                for (int i = Length + LengthLength; i < NewBytes.Length; i++)
                {
                    NewBytes[i - Length - LengthLength] = Bytes[i];
                }
                Process(NewBytes);
            }
            return true;
        }
        else
        {
            return false;
        }
    }
    catch
    {
        return false;
    }
}

In this method, we take the length that we added to the front of the message during the Send function, convert it to an int, and then take the bytes for the length we now have after those first few bytes that contain the length. Then, we deserialize the bytes to a Package instance and invoke the OnRead event, passing it the Package we just created and the current client instance (this). Finally, we check to see if there were any more bytes in the buffer at the end after our Package, and if there are, call Process again to process those bytes.

Advanced Client, Main Host, and Game Server Classes

For these classes, I will not explain all the code; however, I will explain the basic shell of what they do. The order of events is:

  • A client sends a 'Get server' request to a main host.
  • That main host either responds with a Game Server IP or sends 'Become server'.
  • If the main host responds with 'Become server', the client sets up a new Game Server.
  • The server then does some checks with the new Game Server to make sure it works and then sends to the client either 'Test ok' or 'Test fail'.
  • If the client receives 'Test fail', then it closes the Game Server and waits for the main host to send it an IP of a game server.
  • If the client receives 'Test ok', then it just waits for the main host to send it an IP of a game server.
  • Normally, the main host will now end up sending the client its own IP for it to connect to, but this does not matter. Before the main host responds with any IP, it warns the Game Server of the potential client and gives it the client's IP.
  • When the Game Server receives a connection, it checks to see if it is the main host, and if it is, writes to the Console: 'Main Host warn client added'. When testing on one machine, this will always happen; however, if it is not the main host, it checks its list of potential clients, and if that contains the client's IP, then it allows the connection. If it doesn't, then it closes the connection. This stops one person connecting to lots of servers. The Game Server also tells the main host that it has accepted a new client so the main host can stop sending clients to that Game Server if it has too many. The opposite happens when a client disconnects.
  • When the Game Server wants to close, it sends 'Terminating' to the main host and then closes.
  • The main host, on receiving 'Terminating', removes the relevant Game Server.

Conclusion

You can now download the Demo Game 1 source files to see how I have used it in a game. To use the game, run two instances of the game on one machine and another on a different machine. You will have to change MainHostIPAddress in the settings file to the IP address of the machine which my game says is running the main host instance.

Tip: If you are having problems getting the code to work in your code, remember, you have to call the Functions.Init() method to set up the Functions class; otherwise, no communication can happen.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Student
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
NewsNew article.. no really there is a new article this time! XD Pin
Ed Nutting18-Nov-10 6:11
Ed Nutting18-Nov-10 6:11 
QuestionRe: New article about same subject! Pin
The-Great-Kazoo23-Jun-10 8:48
professionalThe-Great-Kazoo23-Jun-10 8:48 
AnswerRe: New article about same subject! Pin
Ed Nutting23-Jun-10 8:59
Ed Nutting23-Jun-10 8:59 
Sorry, my article got veto'd.
GeneralRe: New article about same subject! Pin
The-Great-Kazoo23-Jun-10 9:42
professionalThe-Great-Kazoo23-Jun-10 9:42 
GeneralRe: New article about same subject! Pin
Ed Nutting18-Nov-10 6:13
Ed Nutting18-Nov-10 6:13 
GeneralNew code added! Pin
Ed Nutting19-Oct-09 10:13
Ed Nutting19-Oct-09 10:13 
GeneralAnother code update! Pin
Ed Nutting19-Oct-09 8:06
Ed Nutting19-Oct-09 8:06 
GeneralRe: Another code update! Pin
hth200019-Oct-09 8:26
hth200019-Oct-09 8:26 
GeneralRe: Another code update! Pin
Ed Nutting19-Oct-09 10:12
Ed Nutting19-Oct-09 10:12 
GeneralCode Update! Pin
Ed Nutting19-Oct-09 6:31
Ed Nutting19-Oct-09 6:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.