Click here to Skip to main content
15,860,972 members
Articles / Programming Languages / C#
Article

Genesis UDP Server and Client

Rate me:
Please Sign up or sign in to vote.
4.97/5 (46 votes)
21 Dec 20059 min read 236.5K   8.2K   134   48
An article that shows the implementation of a lightweight UDP server and client with optional reliable channel.

Sample Image

Introduction

The Genesis UDP project is a class library that implements a lightweight UDP server and client using the .NET sockets functionality. It uses UDP to keep the amount of data being sent across the network low, and has many features such as basic encryption, sequenced packets, and a reliable channel.

Genesis communicates via "command packets" - a command packet is one or more UDP packets that have a 2 byte opcode, and a variable number of string fields. Unreliable packets can be up to 512 bytes, reliable packets can be longer but are split up by Genesis and sent in sequence. There are a few internal opcodes used by Genesis, but apart form that, how packets, opcodes and fields are handled is totally the responsibility of the host application developer.

There is also an optional encryption system, it is not too advanced but if enabled, will generate a random 320 bit key for each connecting client and use that key in an XOR encryption algorithm. This is quite secure as no two clients share the same key, however it relies on the initial connection packets not being sniffed. Adding public/private key encryption etc. is a possible enhancement to the library. It is possible to be selective over which connections are encrypted and which are not.

Servers vs. Clients

Genesis works on the basis that every instance of Genesis can be both a client and a server. The line between the client and the server is blurred, as any application that uses Genesis can both connect to servers and accept connections from clients. Of course, clients can't just connect by default - the appropriate events must be hooked in the host application to enable the functionality. A server is defined as the remote host that accepted the connection, whilst a client is the remote host that initiated the connection. As an instance of Genesis can do both, it can be both a server and client to other Genesis instances. Servers and clients are known collectively simply as hosts.

Image 2

The diagram above shows how the idea of clients and servers works in Genesis. Each blue box represents an instance of the Genesis library - none are specifically designated servers or clients as both can potentially accept and initiate connections. The definition of server and client is only valid in the context of a single Genesis instance. Let us consider "Genesis 1", it is connected to servers 2 and 3, and 4 is connected as a client, this is determined by the directions of the arrows (the arrows represent which box initiated the connection). If we look at the perspective of "Genesis 2", we see there are just two clients, 1 and 3. However, if we look at the perspective of "Genesis 4", there is one server, "Genesis 1". Notice how "Genesis 1" can be both a client or server depending upon the context.

Background

The idea for Genesis was based on looking at the network protocols of the various online FPS games such as Quake and Half-Life, which use UDP to allow fast communication between the game server and its connected clients. These implementations have the option to send reliable and sequenced packets - abilities which are also incorporated into Genesis. The original intention for Genesis was to be the start of an underlying network API for a game engine written in .NET, however its potential is so great it has been given to the community as a standalone project.

Using the code

There are three interfaces that must be used to implement the Genesis functionality. These are:

  • IConnection
  • ICommand
  • IGenesisUDP

The IConnection objects hold information about each connection to a remote host, whether that host is a server or a client. ICommand holds information about a single command packet, including the opcode and data fields. IGenesisUDP is the actual communications class that controls all of the functionality.

Included with the source are two projects "GenesisChatServer" and "GenesisChatClient". These are a pair of projects that implement a chat system similar to IRC. Using these projects as a reference point should help with understanding the Genesis classes.

Creating A Server

Let's look at the GenesisChatServer - this project shows how Genesis can act as a server to serve other instances of Genesis (the clients).

First, we need to declare and instantiate the Genesis object in the host application.

C#
private GenesisCore.IGenesisUDP m_UDP;

... 

m_UDP = GenesisCore.InterfaceFactory.CreateGenesisUDP("ChatServer");

Notice how the class InterfaceFactory was used to create an instance of Genesis.

The GetLocalAddresses method is used to return a list of local IP addresses on the current machine - and is used to populate a combo box in the chat server application.

C#
string[] addresses = m_UDP.GetLocalAddresses( );
...

In order to handle the Genesis communication events, they must be hooked as per the code below:

C#
//Hook genesis core events
m_UDP.OnListenStateChanged += new ListenHandler(m_UDP_OnListenStateChanged);
m_UDP.OnConnectionAuth += new ConnectionAuthHandler(m_UDP_OnConnectionAuth);
m_UDP.OnCommandReceived += new IncomingCommandHandler(m_UDP_OnCommandReceived);
m_UDP.OnConnectionStateChanged += new 
   ConnectionStateChangeHandler(m_UDP_OnConnectionStateChanged);

OnListenStateChanged is called when the state of the Genesis communication changes, it can be in one of two states: "Listen" or "Closed", which is just effectively like enabling or disabling the communication. When Closed, Genesis will drop all remote connections and close the socket.

OnConnectionAuth is called when a client has sent authorization information - this is where the client can be rejected if, for example, the logon credentials are not accepted. In the chat server example, the connection is rejected if the nickname is too short or if the server password is incorrect. Notice how a rejection reason can be sent back to the client. Everything is controlled by modifying the ConnectionAuthEventArgs object sent with the event. The command containing the authorization information can be accessed by the AuthCommand property of the event arguments.

C#
private void OnConnectionAuth(object o, ConnectionAuthEventArgs e)
{
    ... 
 
    if(e.AuthCommand.Fields[1].Length < 3)
      {
        e.AllowConnection = false;
        e.DisallowReason = "Nickname too short.";
        return;
      }
      else if(e.AuthCommand.Fields[1].Length > 15)
      {
        e.AllowConnection = false;
        e.DisallowReason = "Nickname too long.";
        return;
        }

    ...
 }

OnCommandRecieved is called when a remote host sends a command to Genesis. The eventargs for this event allows access to the ICommand object interface (via the SentCommand property) that contains the information relating to the command sent, and allows access to the data fields and opcode. There is also the ability to get the object corresponding to the remote host that sent the command (via the Sender property). This event is only fired from authorized hosts. The chat server uses this event to handle incoming chat messages and user list requests.

OnConnectionStateChanged is fired when a remote host connects or disconnects from the Genesis instance. The eventargs contain information regarding the host, whether it connected or disconnected, and a disconnection reason if the latter. The chat server uses this event to send a connected user list to the newly connected clients.

Creating A Client

Let's look now at the client side of the chat project, "GenesisChatClient". This is similar to the server application in that it instantiates an instance of Genesis and hooks up various events, however some new events are hooked:

C#
m_UDP.OnListenStateChanged += new ListenHandler(m_UDP_OnListenStateChanged);
m_UDP.OnLoginRequested += new SendLoginHandler(m_UDP_OnLoginRequested);
m_UDP.OnAuthFeedback += new AuthenticatedHandler(m_UDP_OnAuthFeedback);
m_UDP.OnConnectionStateChanged += new 
  ConnectionStateChangeHandler(m_UDP_OnConnectionStateChanged);
m_UDP.OnCommandReceived += new IncomingCommandHandler(m_UDP_OnCommandReceived);
m_UDP.OnConnectionAuth += new ConnectionAuthHandler(m_UDP_OnConnectionAuth);
m_UDP.OnSocketError += new SocketErrorHandler(m_UDP_OnSocketError);
m_UDP.OnConnectionRequestTimedOut += new 
  RequestTimedOutHandler(m_UDP_OnConnectionRequestTimedOut);

OnLoginRequested is triggered when the server requests the login details from the client. The client must send a command packet back to the server with the opcode OPCODE_LOGINDETAILS and the appropriate data fields. In the chat sample, this packet contains the user's nickname and the server password:

C#
private void OnLoginRequested(object o, LoginSendEventArgs e)
{
    if(e.Connected)
    {
        e.ServerConnection.SendUnreliableCommand(0, 
               GenesisConsts.OPCODE_LOGINDETAILS, 
               new string[] {txtServerPW.Text, txtNickName.Text} );
        spState.Text = "Sending login details...";
    }
    else
    {
        spState.Text = "Connection rejected - " + e.Reason;
    }
}

The Connected property of the LoginSendEventArgs object is false if the server is unable to accept the connection, for example, if it has reached the maximum capacity. The reason for the rejection is also accessible if needed, in the Reason property.

OnAuthFeedback is triggered when the server has made a decision on whether or not to accept the connection. The eventargs contains a value that determines whether the auth succeeded, and the reason for the failure if otherwise.

OnConnectionAuth is fired when a remote host tries to connect to Genesis, remember this is used in the chat server for authenticating remote hosts. The chat client can accept no connections (as it is acting as a client), so a small piece of code is entered here to reject the connection and send a rejection reason back. If the event was not hooked at all by the client application, the connection would still be rejected, but no reason would be sent to the host attempting to connect.

C#
private void m_UDP_OnConnectionAuth(object o, ConnectionAuthEventArgs e)
{
    //Clients don't accept connections.
    e.AllowConnection = false;
    e.DisallowReason = "Can't connect directly to a chat client";
}

One thing of importance is how connections are established from the client. It starts in the chat client with the following code:

C#
/// <summary>
/// Connect to server button was clicked
/// </summary>
private void btnConnect_Click(object sender, System.EventArgs e)
{
    server_ip = txtServerIP.Text;
    m_UDP.RequestConnect(ref server_ip, 
       Convert.ToInt32(txtServerPort.Text), 
       out server_req_id);
    spState.Text = "Connecting...";
    btnConnect.Enabled = true;
}

The RequestConnect method handles initiating the connection. Notice that the server IP parameter is by reference; this is because it is possible to pass the method a host name (rather than an IP address), but the IP will be resolved and the actual string will be changed to the IP address. This allows the host application to take advantage of the hostname resolution, and is required when using other functions within Genesis (as many of the connection search functions will only accept the remote host IP address). The other parameter to note is the last parameter, which is an out parameter and returns the connection request ID. This ID is a number unique to all connection requests and allows a connection request to be cancelled via the CancelConnect method.

Note that establishing a connection is an asynchronous operation, and to catch whether a connection has succeeded requires the use of two events, OnConnectionRequestTimedOut and OnConnectionStateChanged. If the connection attempt times out, the former event is fired. If the connection attempt succeeds, then the latter is fired with arguments that show a connection has been established. Both of these events allow access to the remote host address and the connection request ID generated at the call to RequestConnect to allow differentiation between different connections and connection requests. This can be seen implemented in the GenesisChatClient project.

Sending Data to Remote Hosts

Genesis wraps this functionality in very easy to use method. Two of the methods reside in the IConnection object, these are shown below:

C#
int SendUnreliableCommand(byte flags, string opcode, string[] fields);
int SendReliableCommand(byte flags, string opcode, string[] fields);

These two methods allow sending a single command packet to the remote host associated with the IConnection object. The opcode and fields can be any data the host application recognizes, valid values for the flags are given below (from Constants.cs):

C#
//Command packet flags
public static byte FLAGS_NONE = 0;
public static byte FLAGS_CONNECTIONLESS = 1;
public static byte FLAGS_ENCRYPTED = 2;
public static byte FLAGS_COMPOUNDPIECE = 4;
public static byte FLAGS_COMPOUNDEND = 8;
public static byte FLAGS_RELIABLE = 16;
public static byte FLAGS_SEQUENCED = 32;

Of the flags available in the Constants.cs file, only one should ever be used manually in the method calls, and that is FLAGS_SEQUENCED which applies a sequencing to unreliable packets. The other flags are automatically set by Genesis and should not be changed by the host application.

It is possible to broadcast a command packet to multiple remote hosts using the following methods in the IGenesisUDP interface:

C#
int SendUnreliableCommandToAll(BroadcastFilter filter, 
                  byte flags, string opcode, string[] fields);
int SendReliableCommandToAll(BroadcastFilter filter, 
                  byte flags, string opcode, string[] fields);

These methods work exactly the same as the first two, except they take broadcast filter flags, which are defined in Constants.cs as:

C#
[Flags]
public enum BroadcastFilter : int
{
    None     = 0,  //Filter out everything
    Servers  = 1,  //Send to servers we are connected to.
    Clients  = 2,  //Send to clients connected to us.
    All      = Servers | Clients,
                  //Send to both servers
                  //and clients (every connection).
    AuthedOnly = 4,//Only send to authed clients or servers we are authed with.
}

This allows the broadcast to be limited to servers, clients, and optionally only those connections that have successfully been authenticated.

Points of Interest

It is important to note that the events in Genesis are thrown on a separate thread - not the UI thread. This means that the Invoke method is needed to pass the event to the UI thread, if changing UI elements is your goal. The chat server uses this technique to change the UI based on event data.

When the StopListen method is called, Genesis automatically sends out a disconnection packet to all of the remote hosts connected to it to inform them that it is shutting down.

The only method within Genesis that can take a hostname instead of an IP is RequestConnect. All other methods need an explicit IP address.

History

  • v1.00 - Initial revision.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United Kingdom United Kingdom
Born in England, I have been programming since a very early age when my dad gave me prewritten programs to type in and run on a Sinclair ZX81 machine (seeing my name printed out on a TV screen was enough to keep me entertained!). I later did work using basic and STOS basic on the Atari ST and after that got my first PC and used Microsoft's QBasic. Later when I was about 13 I was in an airport and saw a trial copy of Visual Basic on a magazine, which I bought and it got me hooked on the Microsoft development tools.

Currently I am studying a software engineering degree and have been working with .NET since 1.0. I have just moved over to Visual Studio 2005/.NET 2.0 and am loving it! During my degree I have worked for a year at DuPont, where I ended up changing a lot of their old existing software over to .NET and improving it in the process! Since then I have been back and done some consulting work involving maintaining some of their older C++/MFC software.

While most of my current interestes involve .NET I am also confident in working with C++ in Win32, VB, Java, and have even done some development work on the Linux platform (although most of this involved ensuring that software I wrote in C++ was platform independent).

I have a strong passion for software technology, both higher level and more recently, systems level stuff (the dissertation I am doing for my degree is to implement a small compiler and virtual machine in C# for a Pascal-style language).

Comments and Discussions

 
Questionhow does peer-to-peer work..??? Pin
davidlars9915-Jan-06 12:59
davidlars9915-Jan-06 12:59 

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.