Click here to Skip to main content
15,891,423 members
Articles / Programming Languages / C#

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 241.7K   8.2K   134  
An article that shows the implementation of a lightweight UDP server and client with optional reliable channel.
/*
 * Genesis Socket Server and Client
 * (C)Copyright 2005/2006 Robert Harwood <robharwood@runbox.com>
 * 
 * Please see included license.txt file for information on redistribution and usage.
 */
#region Using directives

using System;
using System.Collections;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Diagnostics;
using System.IO;

#endregion

namespace GenesisCore
{
    internal class GenesisUDP : IGenesisUDP
    {
        private Thread m_UDPThread;         //UDP receive thread
        private string m_CurrentIP;         //Current local IP
        private int m_CurrentPort;          //Current local port
        private IPEndPoint m_LocalEndPoint; //Current local endpoint (contians the IP and port)
        private Socket m_Socket;            //UDP Socket
        private int m_iState;               //Current state of the UDP server
		private bool m_bEncrypt;			//Should we encrypt connections?
        private ConnectionList m_Clients;   //Remote hosts connected to us
        private ConnectionList m_Servers;   //Remote hosts we are connected to
        private DateTime m_NextClearOldConnections;
        private DateTime m_NextPingConnections;
        private DateTime m_NextReliableResend;
        private Timer m_TickTimer;
		private ConnTimeout m_ConnTimeouts;
		private DateTime m_NextTimeoutCheck;
		private string m_Name;

        /// <summary>
        /// Called when a debug message comes from the UDP server
        /// </summary>
        public event DebugHandler OnDebugMessage;

        /// <summary>
        /// Called every time data is received, contains raw, unparsed packet data
        /// </summary>
        public event ReceivedHandler OnDataReceived;

        /// <summary>
        /// Called whenever the server starts or stops listening
        /// </summary>
        public event ListenHandler OnListenStateChanged;

        /// <summary>
        /// Called whenever the internal UDP socket throws an exception
        /// </summary>
        public event SocketErrorHandler OnSocketError;

        /// <summary>
        /// Called whenever a connectionless command is received.
        /// </summary>
        public event IncomingCommandHandler OnConnectionlessCommand;

        /// <summary>
        /// Called when a remote host has sent its login data.
        /// </summary>
        public event ConnectionAuthHandler OnConnectionAuth;

        /// <summary>
        /// Called when a host we are connecting to asks for our login data.
        /// </summary>
        public event SendLoginHandler OnLoginRequested;

        /// <summary>
        /// Called whenever a command is received.
        /// </summary>
        public event IncomingCommandHandler OnCommandReceived;

        /// <summary>
        /// Called whenever a connection to a remote host is established or lost
        /// </summary>
        public event ConnectionStateChangeHandler OnConnectionStateChanged;

        /// <summary>
        /// Called whenever login credentials sent to a remote host have been accepted or denied.
        /// </summary>
        public event AuthenticatedHandler OnAuthFeedback;

		/// <summary>
		/// Called whenever we try to conect to a remote host but the host
		/// doesnt respond fast enough.
		/// </summary>
		public event RequestTimedOutHandler OnConnectionRequestTimedOut;

        /// <summary>
        /// Constructor, initialises the UDP server
        /// </summary>
        public GenesisUDP(string Name)
        {
            m_iState = GenesisConsts.UDP_STATE_IDLE;
            m_Clients = new ConnectionList(this);
            m_Servers = new ConnectionList(this);
            m_TickTimer = new Timer(new TimerCallback(TimerTick), null, 500, 500);
            m_NextClearOldConnections = DateTime.Now;
            m_NextPingConnections = DateTime.Now;
            m_NextReliableResend = DateTime.Now;
			m_ConnTimeouts = new ConnTimeout(this);
			m_NextTimeoutCheck = DateTime.Now;
			m_Name = Name;
        }

        #region Public accessors
        /// <summary>
        /// Gets the state of the UDP server
        /// </summary>
        public int State
        {
            get { return m_iState; }
        }

		/// <summary>
		/// Gets or sets whether or not connections should be encrypted.
		/// </summary>
		public bool Encrypt
		{
			get{ return m_bEncrypt; }
			set{ m_bEncrypt = value; }
		}

		public ConnectionList Clients
		{
			get{ return m_Clients; }
		}

		public ConnectionList Servers
		{
			get{ return m_Servers; }
		}
        #endregion

        #region Server side start/stop listening methods
        /// <summary>
        /// Starts the UDP system listening for packets
        /// </summary>
        /// <returns>UDP_OK or error code</returns>
        public int StartListen(string IP, int Port)
        {
            DebugDump("Starting UDP server...");

            //Set the address to bind to
            m_CurrentIP = IP;
            DebugDump("Set server IP to " + IP);
            m_CurrentPort = Port;
            DebugDump("Set server port to " + Port.ToString( ));

            //Attempt to start the receive loop.
            try
            {
                m_UDPThread = new Thread(new ThreadStart(ReceiveLoop));
				m_UDPThread.Name = "RecieveThread";
                m_UDPThread.Start();
                DebugDump("Started the listen thread.");
            }
            catch (Exception e)
            {
                DebugDump("Exception: " + e.Message);
                return GenesisConsts.UDP_FAIL;
            }
            return GenesisConsts.UDP_OK;
        }

        /// <summary>
        /// Stops the UDP server listening.
        /// </summary>
        /// <returns>UDP_OK or error code</returns>
        public int StopListen()
        {
            //Fail and return if the socket is not listening.
            if (m_iState != GenesisConsts.UDP_STATE_LISTENING)
                return GenesisConsts.UDP_FAIL;

			//Update application of my status change
			if (OnListenStateChanged != null)
				OnListenStateChanged(null, new ListenEventArgs(false));

            DebugDump("Clearing client connection records.");
            m_Clients.RemoveAllConnections("Server shutting down.");

            DebugDump("Clearing server connection records.");
            m_Servers.RemoveAllConnections("Client shutting down.");

			DebugDump("Clearing timeout list.");
			m_ConnTimeouts.RemoveAllEntries( );

            DebugDump("Stopping the UDP server...");
            m_iState = GenesisConsts.UDP_STATE_CLOSING;

            DebugDump("Closing the socket...");
			m_Socket.Shutdown(SocketShutdown.Both);
            m_Socket.Close( );
            DebugDump("Closed OK.");

            m_UDPThread = null;

            return GenesisConsts.UDP_OK;
        }
        #endregion

        #region Receive Loop
        /// <summary>
        /// Receive loop continually receives data.
        /// </summary>
        private void ReceiveLoop()
        {
            int iExCount = 0;

			if(m_CurrentIP == "")
				m_LocalEndPoint = new IPEndPoint(IPAddress.Any, m_CurrentPort);
			else
				m_LocalEndPoint = new IPEndPoint(IPAddress.Parse(m_CurrentIP), m_CurrentPort);

            DebugDump("Created the local IP endpoint at " + m_LocalEndPoint.ToString( ));
            try
            {
                m_Socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
                DebugDump("Created the socket.");

                m_Socket.Bind(m_LocalEndPoint);
                DebugDump("Bound the socket to the local end point, now entering receive loop...");

                m_iState = GenesisConsts.UDP_STATE_LISTENING;

                if (OnListenStateChanged != null)
                    OnListenStateChanged(null, new ListenEventArgs(true));

                while (true)
                {
                    try
                    {
                        //Make space to store the data from the socket
                        Byte[] received = new Byte[GenesisConsts.MAX_COMMAND_LEN];
                        
                        //Create an end point, just give it temporary values
                        EndPoint remoteEP = new IPEndPoint(m_LocalEndPoint.Address, m_LocalEndPoint.Port);

                        //Read bytes from the socket
                        int bytesReceived = m_Socket.ReceiveFrom(received, ref remoteEP);
                        IPEndPoint remoteIPEP = (IPEndPoint)remoteEP;

                        //string str_received = Encoding.UTF8.GetString(received);
                        string str_received = Util.BytesToString(received);
                        str_received = str_received.Substring(0, bytesReceived);

                        //Fire the received event if it is being used (allowing raw data to be caught)
                        if(OnDataReceived != null)
                            OnDataReceived(null, new ReceivedEventArgs((IPEndPoint)remoteEP, str_received));

                        //Handle connectionless packets
                        try
                        {
                            int pcmd = 0;

                            //Allocate a temporary command object
                            Command new_cmd = new Command();

                            //Parse the start of the command
                            new_cmd.OPCode = str_received.Substring(pcmd, 2);
                            pcmd += 2;
                            new_cmd.SequenceNum = Util.BytesToUint(str_received.Substring(pcmd, 4));
                            pcmd += 4;
                            new_cmd.Flags = (byte)str_received[pcmd];
                            pcmd++;

                            //If the command packet is connectionless then parse it now.
                            if ((new_cmd.Flags & GenesisConsts.FLAGS_CONNECTIONLESS) > 0)
                            {
                                new_cmd.NumFields = Util.BytesToShort(str_received.Substring(pcmd, 2));
                                pcmd += 2;
                                new_cmd.FieldSizes = new short[new_cmd.NumFields];
                                for (short i = 0; i < new_cmd.NumFields; i++)
                                {
                                    new_cmd.FieldSizes[i] = Util.BytesToShort(str_received.Substring(pcmd, 2));
                                    pcmd += 2;
                                }
                                pcmd += 2;
                                new_cmd.AllFields = str_received.Substring(pcmd);
                                new_cmd.Initialize();

                                //Process the connectionless command
                                ProcessConnectionlessComand(remoteIPEP, new_cmd);
                            }
                            else
                            {
                                //Locate connection this command belongs to and process it there.
                                Connection conn;

                                //Check both the client and server lists
                                m_Clients.ConnectionByRemoteEndpoint(remoteIPEP, out conn);
                                if(conn == null)
                                    m_Servers.ConnectionByRemoteEndpoint(remoteIPEP, out conn);

                                if (conn == null)
                                    DebugDump("Connection-related packet with no matching connection arrived - ignored.");
                                else
                                    conn.ProcessCommandPacket(str_received);
                            }
                        }
                        catch
                        {
                            DebugDump("Exception whilst parsing input from " + remoteIPEP.ToString() + " as command, probably not a command. Ignoring.");
                        }

                        //Reset the exception count to 0
                        iExCount = 0;
                    }
                    catch (SocketException se)
                    {
                        if ((se.ErrorCode != 10061) && (se.ErrorCode != 10054))
                        {
                            //Fire the socket error event.
                            if (se.ErrorCode != 10004)
                            {
                                if (OnSocketError != null)
                                    OnSocketError(null, new SocketEventArgs(se.ErrorCode, se.Message));
                            }

                            DebugDump("Socket Exception in receive loop (" + se.ErrorCode.ToString() + "): " + se.Message);
                            if (m_iState != GenesisConsts.UDP_STATE_LISTENING)
                                break;

                            iExCount++;
                        }
                    }
                    catch (Exception e)
                    {
                        DebugDump("Exception in receive loop: " + e.Message);
                        if (m_iState != GenesisConsts.UDP_STATE_LISTENING)
                            break;

                        iExCount++;
                    }

                    if (iExCount == GenesisConsts.MAX_EXCEPTIONS)
                    {
                        DebugDump("Got too many consecutive exceptions in the receive loop, terminating.");
                        break;
                    }
                }
            }
            catch (SocketException se)
            {
                //Fire the socket error event.
                if (OnSocketError != null)
                    OnSocketError(null, new SocketEventArgs(se.ErrorCode, se.Message));

                DebugDump("Socket Exception (" + se.ErrorCode.ToString() + "): " + se.Message);
            }
            catch (Exception e)
            {
                DebugDump("Exception: " + e.Message);
            }

            //We are out of the loop but the server thinks we are not? make sure it does.
            if((m_iState != GenesisConsts.UDP_STATE_IDLE) && (m_iState != GenesisConsts.UDP_STATE_CLOSING))
            {
				StopListen();
            }

            DebugDump("Set the system state to idle.");

            //Set the state to idle.
            m_iState = GenesisConsts.UDP_STATE_IDLE;

            DebugDump("Exited the receive loop.");
        }
        #endregion

		#region Generic command sending methods
        /// <summary>
        /// Sends a string of data to a remote host
        /// </summary>
        /// <param name="IP">IP address of remote host</param>
        /// <param name="Port">Port of remote host</param>
        /// <param name="Data">Data to send</param>
        /// <returns>UDP_OK or error code</returns>
        public int SendData(string IP, int Port, string Data)
        {
            if (m_iState != GenesisConsts.UDP_STATE_LISTENING)
                return GenesisConsts.UDP_FAIL;

            try
            {
                //byte[] databytes = Encoding.UTF8.GetBytes(Data);
                byte[] databytes = Util.StringToBytes(Data);

                EndPoint remoteEP = new IPEndPoint(IPAddress.Parse(IP), Port);
                m_Socket.SendTo(databytes, remoteEP);
            }
            catch (SocketException se)
            {
                DebugDump("Socket Exception (" + se.ErrorCode.ToString() + "): " + se.Message);
                return GenesisConsts.UDP_FAIL;
            }
            catch (Exception e)
            {
                DebugDump("Exception: " + e.Message);
                return GenesisConsts.UDP_FAIL;
            }
            return GenesisConsts.UDP_OK;
        }

        /// <summary>
        /// Sends a connectionless command packet to a remote host
        /// </summary>
        /// <param name="IP">IP address of remote host</param>
        /// <param name="Port">Port of remote host</param>
        /// <param name="opcode">Packet OPCode</param>
        /// <param name="fields">Packet fields</param>
        /// <returns>UDP_OK or error code</returns>
        public int SendConnectionlessCommand(string IP, int Port, string opcode, string[] fields)
        {
            return SendCommand(IP, Port, "", 0, GenesisConsts.FLAGS_CONNECTIONLESS, opcode, fields);
        }

		/// <summary>
		/// Sends an unreliable command to all open connections (optionally filtered).
		/// </summary>
		public int SendUnreliableCommandToAll(BroadcastFilter filter, byte flags, string opcode, string[] fields)
		{
			int i;
			Connection[] cons;

			//Don't send anything.
			if(filter == BroadcastFilter.None)
				return GenesisConsts.UDP_OK;

			//We are sending to servers?
			if((filter & BroadcastFilter.Servers) > BroadcastFilter.None)
			{
				this.GetConnections(true, out cons);

				if(cons != null)
				{
					for(i = 0; i < cons.Length; i++)
					{
						//Does the connection need to be authed?
						if((filter & BroadcastFilter.AuthedOnly) > BroadcastFilter.None)
							if(!cons[i].Authed)
								continue;

						cons[i].SendUnreliableCommand(flags, opcode, fields);
					}
				}
			}

			//We are sending to clients?
			if((filter & BroadcastFilter.Clients) > BroadcastFilter.None)
			{
				this.GetConnections(false, out cons);

				if(cons != null)
				{
					for(i = 0; i < cons.Length; i++)
					{
						if((filter & BroadcastFilter.AuthedOnly) > BroadcastFilter.None)
							if(!cons[i].Authed)
								continue;

						cons[i].SendUnreliableCommand(flags, opcode, fields);
					}
				}
			}

			return GenesisConsts.UDP_OK;
		}

		/// <summary>
		/// Sends a reliable command to all open connections (optionally filtered).
		/// </summary>
		public int SendReliableCommandToAll(BroadcastFilter filter, byte flags, string opcode, string[] fields)
		{
			int i;
			Connection[] cons;

			//Don't send anything.
			if(filter == BroadcastFilter.None)
				return GenesisConsts.UDP_OK;

			//We are sending to servers?
			if((filter & BroadcastFilter.Servers) > BroadcastFilter.None)
			{
				this.GetConnections(true, out cons);

				if(cons != null)
				{
					for(i = 0; i < cons.Length; i++)
					{
						//Does the connection need to be authed?
						if((filter & BroadcastFilter.AuthedOnly) > BroadcastFilter.None)
							if(!cons[i].Authed)
								continue;

						cons[i].SendReliableCommand(flags, opcode, fields);
					}
				}
			}

			//We are sending to clients?
			if((filter & BroadcastFilter.Clients) > BroadcastFilter.None)
			{
				this.GetConnections(false, out cons);

				if(cons != null)
				{
					for(i = 0; i < cons.Length; i++)
					{
						if((filter & BroadcastFilter.AuthedOnly) > BroadcastFilter.None)
							if(!cons[i].Authed)
								continue;

						cons[i].SendReliableCommand(flags, opcode, fields);
					}
				}
			}

			return GenesisConsts.UDP_OK;
		}

        /// <summary>
        /// Sends a formatted command packet (or packets) to the specified remote host
        /// </summary>
        /// <param name="IP">IP address of remote host</param>
        /// <param name="Port">Port of remote host</param>
        /// <param name="key">Encryption key</param>
        /// <param name="seq_num">Starting sequence number of packets</param>
        /// <param name="flags">Packet flags</param>
        /// <param name="opcode">Packet OPCode</param>
        /// <param name="fields">Packet fields</param>
        /// <returns>UDP_OK or error code</returns>
        public int SendCommand(string IP, int Port, string key, uint seq_num, byte flags, string opcode, string[] fields)
        {
            try
            {
                ArrayList commands = new ArrayList();
                string curpacket = "";
                bool send_now = true;
				int retval;

                //Attempt to get a connection object that corresponds to where
                //we are ending this command
                Connection destination = null;
                ConnectionByIPPort(IP, Port, out destination);

                //Can't send reliable packets to remote hosts without a connection entry.
                if(destination == null)
                {
                    if ((flags & GenesisConsts.FLAGS_RELIABLE) > 0)
                        return GenesisConsts.UDP_RELIABLENODESTINATION;
                }

                //Make sure fields isn't null, set it to a zero length string array
				if (fields == null)
					fields = new string[0];
				else
				{
					//Clone the fields array so we only work with a copy.
					fields = (string[])fields.Clone( );
				}

                //Encrypt if we should (if there is a key)
                if (key != "")
                {
                    flags |= GenesisConsts.FLAGS_ENCRYPTED;
                    for (int i = 0; i < fields.Length; i++)
                        fields[i] = Util.XORCrypt(fields[i], key);
                }

                //Limit field sizes
                for (int i = 0; i < fields.Length; i++)
                    if (fields[i].Length > GenesisConsts.MAX_FIELD_LEN)
                        fields[i] = fields[i].Substring(0, GenesisConsts.MAX_FIELD_LEN);

                //Limit field number
                if(fields.Length > GenesisConsts.MAX_FIELDS)
                {
                    string[] new_fields = new string[GenesisConsts.MAX_FIELDS];
                    for (int i = 0; i < GenesisConsts.MAX_FIELDS; i++)
                        new_fields[i] = fields[i];

                    fields = null;
                    fields = new_fields;
                }

                //Create the ideal packet
                curpacket = Util.CreatePacketHeader(seq_num, flags, opcode, key, fields);
                for (int i = 0; i < fields.Length; i++)
                    curpacket += fields[i];

                //Is the ideal packet too big to fit?
                if (curpacket.Length > GenesisConsts.MAX_COMMAND_LEN)
                {
                    //Unreliable command packet is too long - cannot split unreliable commands
                    if ((flags & GenesisConsts.FLAGS_RELIABLE) == 0)
                        return GenesisConsts.UDP_UNRELIABLETOOLONG;

                    //Set the compound flag on
                    flags |= GenesisConsts.FLAGS_COMPOUNDPIECE;

                    //Recreate the ideal packet with the new flag setting
                    curpacket = Util.CreatePacketHeader(seq_num, flags, opcode, key, fields);
                    for (int i = 0; i < fields.Length; i++)
                        curpacket += fields[i];

                    //Add the first command to the commands array
                    commands.Add(curpacket.Substring(0, GenesisConsts.MAX_COMMAND_LEN));

                    //Make some variables for the command loop
                    uint seq_offset = 1;
                    string remainder_header = Util.CreatePacketHeader((seq_num + seq_offset), flags, GenesisConsts.OPCODE_COMPOUNDPIECE, key, null);
                    string remaining_field_data = curpacket.Substring(GenesisConsts.MAX_COMMAND_LEN, (curpacket.Length - GenesisConsts.MAX_COMMAND_LEN));

                    //While the remainder data is to large, kepe breaking it up
                    while ((remaining_field_data.Length + remainder_header.Length) > GenesisConsts.MAX_COMMAND_LEN)
                    {
                        //Add the additional command packet
                        commands.Add(remainder_header + remaining_field_data.Substring(0, (GenesisConsts.MAX_COMMAND_LEN - remainder_header.Length)));

                        //Increment the sequence offset
                        seq_offset++;

                        //Get the remaining data we have not yet sent
                        remainder_header = Util.CreatePacketHeader((seq_num + seq_offset), flags, GenesisConsts.OPCODE_COMPOUNDPIECE, key,  null);
                        remaining_field_data = remaining_field_data.Substring((GenesisConsts.MAX_COMMAND_LEN - remainder_header.Length), (remaining_field_data.Length - (GenesisConsts.MAX_COMMAND_LEN - remainder_header.Length)));
                    }

                    //Switch the flags - set to compound end (last piece)
                    flags &= (byte)(~GenesisConsts.FLAGS_COMPOUNDPIECE);
                    flags |= GenesisConsts.FLAGS_COMPOUNDEND;
                    commands.Add(Util.CreatePacketHeader((seq_num + seq_offset), flags, GenesisConsts.OPCODE_COMPOUNDPIECE, key, null) + remaining_field_data);

                    //Now send all the command packets
                    for (int i = 0; i < commands.Count; i++)
                    {
                        //If there is no destination connection we can't track sequence numbers
                        //this should only be the case for connectionless commands.
                        if (destination != null)
                        {
                            if ((flags & GenesisConsts.FLAGS_RELIABLE) > 0)
                            {
                                send_now = (!destination.RQueue.CommandsWaiting());

                                //If it is reliable then add the packet to the reliable queue if we can
                                retval = destination.CacheReliablePacket((string)commands[i]);
								if(retval != GenesisConsts.UDP_OK)
									return retval;							

                                destination.LastSentPacketR++;
                            }
                            else
                                destination.LastSentPacket++;
                        }

                        //Send the data down the wire
                        if(send_now)
                            SendData(IP, Port, (string)commands[i]);
                    }
                }
                else
                {
                    //Only a single command, just send the ideal packet
                    if (destination != null)
                    {
                        if ((flags & GenesisConsts.FLAGS_RELIABLE) > 0)
                        {
                            send_now = (!destination.RQueue.CommandsWaiting());

							//Attempt to add the reliable packet to the queue.
                            retval = destination.CacheReliablePacket(curpacket);
							if(retval != GenesisConsts.UDP_OK)
								return retval;

                            destination.LastSentPacketR++;
                        }
                        else
                            destination.LastSentPacket++;
                    }

                    if(send_now)
                       SendData(IP, Port, curpacket);
                }

                //Clear the commands array
                commands.Clear();

                return GenesisConsts.UDP_OK;
            }
            catch (Exception e)
            {
                DebugDump("Error occured during command send (" + e.Message + ")");
                return GenesisConsts.UDP_FAIL;
            }
        }
		#endregion

		#region Client side connection methods (request/cancel)
		/// <summary>
		/// 
		/// </summary>
		/// <param name="RemoteIP"></param>
		/// <param name="RemotePort"></param>
		/// <param name="request_id"></param>
		/// <returns></returns>
        public int RequestConnect(ref string RemoteIP, int RemotePort, out string request_id)
        {
			request_id = "";

            DebugDump("Requesting a connection to " + RemoteIP + ":" + RemotePort.ToString());

			try
			{
				IPAddress.Parse(RemoteIP);
			}
			catch
			{
				//The string was not an IP - possibly a hostname? Try to resolve it.
				string oldname = RemoteIP;
				IPAddress[] ips;
				
				try
				{
					//Attempt to resolve the hostname
					ips = System.Net.Dns.GetHostEntry(RemoteIP).AddressList;
				}
				catch
				{
					DebugDump("Unable to resolve hostname '" + oldname + ".");
					return GenesisConsts.UDP_UNABLETORESOLVE;
				}

				RemoteIP = ips[0].ToString();
				DebugDump("Resolved hostname '" + oldname + "' to IP address '" + RemoteIP + "'.");
			}

            Connection temp;
            m_Servers.ConnectionByIPPort(RemoteIP, RemotePort, out temp);
            if (temp != null)
            {
                DebugDump("Unable to try to connect, connection exists in server list.");
                return GenesisConsts.UDP_ALREADYCONNECTED;
            }

            m_Clients.ConnectionByIPPort(RemoteIP, RemotePort, out temp);
            if (temp != null)
            {
                DebugDump("Unable to try to connect, connection exists in client list.");
                return GenesisConsts.UDP_ALREADYCONNECTED;
            }

            //Generate request ID
			string req_id = m_ConnTimeouts.GetNewRequestID( );

			//Send the request ID back to the caller.
			request_id = req_id;

			//Send the connection request packet
			SendConnectionlessCommand(RemoteIP, RemotePort, GenesisConsts.OPCODE_CONNECTIONREQUEST, new string[] {req_id});
            DebugDump("Sent connection request (" + req_id + ") to remote host.");

			//Add it to the timeout list.
			m_ConnTimeouts.AddConnectionEntry(RemoteIP, RemotePort, GenesisConsts.CONNECTION_TIMEOUT_TIME, req_id);

            return GenesisConsts.UDP_OK;
        }

		/// <summary>
		/// Cancels a connect request (just removes the timeout entry)
		/// </summary>
		/// <param name="RemoteIP">IP of the connection request to cancel</param>
		/// <param name="RemotePort">Port of the connection request to cancel</param>
		/// <returns>UDP_OK or error code</returns>
		public int CancelConnect(string RemoteIP, int RemotePort)
		{
			m_ConnTimeouts.RemoveConnectionEntry(RemoteIP, RemotePort);
			return GenesisConsts.UDP_OK;
		}

		/// <summary>
		/// Cancels a connect request (just removes the timeout entry)
		/// </summary>
		/// <param name="RequestID">The request ID of the connection attempt to be aborted.</param>
		/// <returns>UDP_OK or error code</returns>
		public int CancelConnect(string RequestID)
		{
			m_ConnTimeouts.RemoveConnectionEntry(RequestID);
			return GenesisConsts.UDP_OK;
		}
        #endregion

        #region Connectionless packet handling
        /// <summary>
        /// Processes a connectionless command.
        /// </summary>
        /// <param name="c">Command to process</param>
        /// <returns>UDP_OK or error code.</returns>
        private int ProcessConnectionlessComand(IPEndPoint remote_ep, Command c)
        {
            int retval = GenesisConsts.UDP_OK;

            try
            {
                //========================================================================
                //Connection request from a remote host
                if (c.OPCode == GenesisConsts.OPCODE_CONNECTIONREQUEST)
                {
                    Connection temp;
                    bool cant_connect = false;

                    m_Servers.ConnectionByIPPort(remote_ep.Address.ToString(), remote_ep.Port, out temp);
                    if (temp != null)
                    {
                        DebugDump("Unable to accept connection, connection exists in server list.");
                        retval = GenesisConsts.UDP_ALREADYCONNECTED;
                        cant_connect = true;
                    }

                    m_Clients.ConnectionByIPPort(remote_ep.Address.ToString(), remote_ep.Port, out temp);
                    if (temp != null)
                    {
                        DebugDump("Unable to accept connection, connection exists in client list.");
                        retval = GenesisConsts.UDP_ALREADYCONNECTED;
                        cant_connect = true;
                    }

                    //Can't accept this connection as we already have it in the list
                    //Client will just have to wait until it times out.
                    if (cant_connect)
                    {
                        SendConnectionlessCommand(remote_ep.Address.ToString(), remote_ep.Port, GenesisConsts.OPCODE_CONNECTIONACK, new string[] { c.Fields[0], "FAIL", "Connection from this client already exists." });
                        return retval;
                    }

                    string encryption_key = "";
					
					//Is encryption on? If so generate an encryption key.
					if(m_bEncrypt)
						encryption_key = Util.GenerateEncryptionKey();

					//Create a new connection object and add it to the clients list...
                    Connection new_conn = new Connection(this);
                    new_conn.EncryptionKey = encryption_key;
                    new_conn.RemoteEP = remote_ep;

                    DebugDump("Remote host at " + remote_ep.ToString() + " requested a connection.");
                    DebugDump("Sending acknowledgement packet.");
                    SendConnectionlessCommand(remote_ep.Address.ToString(), remote_ep.Port, GenesisConsts.OPCODE_CONNECTIONACK, new string[] { c.Fields[0], "OK", encryption_key });

                    DebugDump("Adding remote host to clients list.");
                    m_Clients.NewConnection(new_conn);

                    return GenesisConsts.UDP_OK;
                }

                //========================================================================
                //Connection acknowledge from a remote host
                if (c.OPCode == GenesisConsts.OPCODE_CONNECTIONACK)
                {
                    DebugDump("Got connection acknowledgement from " + remote_ep.ToString() + ".");

					if(!m_ConnTimeouts.EntryExists(c.Fields[0]))
					{
						//Make sure we actually _asked_ for this connection ack.
						DebugDump("Connection accept packet sent by " + remote_ep.ToString() + " but was unrequested - ignored.");
					}
                    else if (c.Fields[1] == "OK")
                    {
						//Server accepted our connection.

                        DebugDump("Connection " + c.Fields[0] + " accepted by " + remote_ep.ToString());

						//Create and store a connection object for the server that accepted us.
                        Connection new_con = new Connection(this);
                        new_con.EncryptionKey = c.Fields[2];
                        new_con.RemoteEP = remote_ep;
                        new_con.Server = true; //Is a connection to a server
						new_con.RequestID = c.Fields[0];
                        m_Servers.NewConnection(new_con);

						//Remove from the timeouts list
						m_ConnTimeouts.RemoveConnectionEntry(c.Fields[0]);

                        //Sending login data is to be handled in the actual application
                        //Basically it needs to send an command packet to the host containing the login data.
                        DebugDump("Server is requesting login information, sending.");
                        if (OnLoginRequested != null)
                            OnLoginRequested(null, new LoginSendEventArgs(new_con, true, ""));
                        else
                            //Application did nothing, so send a blank login packet now.
                            SendCommand(remote_ep.Address.ToString(), remote_ep.Port, new_con.EncryptionKey, 0, 0, GenesisConsts.OPCODE_LOGINDETAILS, null);
                    }
                    else
                    {
						//Server did not accept our connection.

                        //Make a temporary connection object
                        Connection new_con = new Connection(this);
                        new_con.EncryptionKey = c.Fields[1];
                        new_con.RemoteEP = remote_ep;
                        new_con.Server = true; //Is a connection to a server

                        DebugDump("Connection not accepted - " + c.Fields[2] + ".");
                        if (OnLoginRequested != null)
                            OnLoginRequested(null, new LoginSendEventArgs(new_con, false, c.Fields[1]));
                    }

                    return GenesisConsts.UDP_OK;
                }
            }
            catch (Exception e)
            {
                DebugDump("Exception whilst processing connectionless command (" + e.Message + ")");
                return GenesisConsts.UDP_FAIL;
            }

            //========================================================================
            //Call the event (so it can be handled in the application)
            if (OnConnectionlessCommand != null)
                OnConnectionlessCommand(null, new CommandEventArgs(c, null, remote_ep));

            return GenesisConsts.UDP_OK;
        }
        #endregion

        #region Event Firing
        /// <summary>
        /// Called by a connection object when a command comes in
        /// </summary>
        /// <param name="cn">Connection that sent the command</param>
        /// <param name="cmd">Command received</param>
        /// <returns>UDP_OK or error code</returns>
        internal int CommandReceived(Connection cn, Command cmd)
        {
            if (OnCommandReceived != null)
                OnCommandReceived(null, new CommandEventArgs(cmd, cn, null));

            return GenesisConsts.UDP_OK;
        }

        /// <summary>
        /// Called when a connection is attempting to authenticate
        /// </summary>
        /// <param name="cn">Connection</param>
        /// <param name="cmd">Command</param>
        /// <returns>UDP_OK or error code</returns>
        internal int ConnectionAuthing(Connection cn, Command cmd)
        {
			//Is the connection already authed?
			if(cn.Authed)
			{
				DebugDump("Connection " + cn.RemoteEP.ToString() + " sent an auth packet but is already authed? Command ignored.");
				return GenesisConsts.UDP_OK;
			}

            ConnectionAuthEventArgs ea = new ConnectionAuthEventArgs(cn, cmd);

            DebugDump("Connection " + cn.RemoteEP.ToString() + " sent login data...");

            //Have the third party client app process the login data
            if (OnConnectionAuth != null)
                OnConnectionAuth(null, ea);

            //Clamp the disallow reason to 200 characters
            if (ea.DisallowReason.Length > 200)
                ea.DisallowReason = ea.DisallowReason.Substring(0, 200);

            if (!ea.AllowConnection)
            {
                DebugDump("Login data is bad, rejecting connection.");
                cn.SendUnreliableCommand(0, GenesisConsts.OPCODE_LOGINACK, new string[] { "FAIL", ea.DisallowReason });
                m_Clients.RemoveConnection(cn, true, ea.DisallowReason);
            }
            else
            {
                DebugDump("Login data is ok, connection authed.");
                cn.SendUnreliableCommand(0, GenesisConsts.OPCODE_LOGINACK, new string[] { "OK" });
                cn.Authed = true;
            }
            return GenesisConsts.UDP_OK;
        }

        /// <summary>
        /// Called internally when a connection is established or lost
        /// </summary>
        internal int ConnectionStateChanged(Connection cn, bool active, string reason)
        {
            if (OnConnectionStateChanged != null)
                OnConnectionStateChanged(null, new ConnectionStateChangeEventArgs(cn, active, reason));

            return GenesisConsts.UDP_OK;
        }

        /// <summary>
        /// Called internally when login has been accepted by a remote host
        /// </summary>
        /// <param name="cn">Connection of the host that authed us</param>
        /// <returns>UDP_OK or error code</returns>
        internal int AuthenticatedWithConnection(Connection cn, bool accepted, string reason)
        {
            if (OnAuthFeedback != null)
                OnAuthFeedback(null, new AuthenticatedEventArgs(cn, accepted, reason));

            return GenesisConsts.UDP_OK;
        }

		/// <summary>
		/// Called internally when a connection request times out.
		/// </summary>
		internal int ConnectionRequestTimedOut(string IP, int Port, string ReqID)
		{
			if(OnConnectionRequestTimedOut != null)
                OnConnectionRequestTimedOut(null, new RequestTimedOutEventArgs(IP, Port, ReqID));

			return GenesisConsts.UDP_OK;
		}
        #endregion

        #region Timed methods
        /// <summary>
        /// Called when timer has ticked
        /// </summary>
        private void TimerTick(object o)
        {
            if (m_iState == GenesisConsts.UDP_STATE_IDLE)
                return;

            //Clean old client connections
            if (m_NextClearOldConnections < DateTime.Now)
            {
                m_Clients.RemoveOldConnections();
                m_Servers.RemoveOldConnections();
                m_NextClearOldConnections = DateTime.Now.AddSeconds(GenesisConsts.CONNECTION_CLEAN_TIME);
            }

            //Ping all connections to servers
            if (m_NextPingConnections < DateTime.Now)
            {
                m_Servers.PingConnections();
                m_NextPingConnections = DateTime.Now.AddSeconds(GenesisConsts.CONNECTION_PING_DELAY);
            }

			//Retry sending reliable packets
            if (m_NextReliableResend < DateTime.Now)
            {
                m_Servers.ReliableRetry();
                m_Clients.ReliableRetry();
                m_NextReliableResend = DateTime.Now.AddSeconds(GenesisConsts.CONNECTION_RELIABLE_RETRY);
            }

			//Check the connection timeouts
			if (m_NextTimeoutCheck < DateTime.Now)
			{
				m_ConnTimeouts.CheckTimeouts( );
				m_NextTimeoutCheck = DateTime.Now.AddSeconds(GenesisConsts.CONNECTION_TIMEOUT_CHECK);
			}
        }
        #endregion

        #region Connection Methods
        /// <summary>
        /// Gets a connection by IP and port
        /// </summary>
        /// <param name="IP">IP to search for</param>
        /// <param name="Port">Port to search for</param>
        /// <param name="conn">Connection found (if any)</param>
        /// <returns>UDP_OK or UDP_NOTFOUND</returns>
        private int ConnectionByIPPort(string IP, int Port, out Connection conn)
        {
            int retval = GenesisConsts.UDP_OK;
            conn = null;

            if(m_Clients.ConnectionByIPPort(IP, Port, out conn) != GenesisConsts.UDP_OK)
                if(m_Servers.ConnectionByIPPort(IP, Port, out conn) != GenesisConsts.UDP_OK)
                    retval = GenesisConsts.UDP_NOTFOUND;

            return retval;
        }

		/// <summary>
		/// Gets a connection interface by IP and port
		/// </summary>
		/// <param name="IP">IP to search for</param>
		/// <param name="Port">Port to search for</param>
		/// <param name="conn">Connection found (if any)</param>
		/// <returns>UDP_OK or UDP_NOTFOUND</returns>
		public int ConnectionByIPPort(string IP, int Port, out IConnection conn)
		{
			return ConnectionByIPPort(IP, Port, out conn);
		}

        /// <summary>
        /// Removes a connection
        /// </summary>
        /// <param name="conn">Connection to remove</param>
        /// <param name="send_disconnect_packet">If true, will send a disconnection packet</param>
        /// <param name="reason">Reason to send (only matters if sending a disconnection packet</param>
        /// <returns>UDP_OK or error code</returns>
        private int RemoveConnection(Connection conn, bool send_disconnect_packet, string reason)
        {
            int retval = GenesisConsts.UDP_OK;

            retval = m_Servers.RemoveConnection(conn, send_disconnect_packet, reason);
            if (retval != GenesisConsts.UDP_OK)
                retval = m_Clients.RemoveConnection(conn, send_disconnect_packet, reason);

            return retval;
        }

		/// <summary>
		/// Removes a connection given tis interface.
		/// </summary>
		/// <param name="conn">Connection to remove</param>
		/// <param name="send_disconnect_packet">If true, will send a disconnection packet</param>
		/// <param name="reason">Reason to send (only matters if sending a disconnection packet</param>
		/// <returns>UDP_OK or error code</returns>
		public int RemoveConnection(IConnection conn, bool send_disconnect_packet, string reason)
		{
			return RemoveConnection((Connection)conn, send_disconnect_packet, reason);
		}

        /// <summary>
        /// Gets all the connections in the specified connection list
        /// </summary>
        /// <param name="servers">If true returns the servers list, else returns the clients list</param>
        /// <param name="found">The output array of found connections</param>
        /// <returns>UDP_OK or error code</returns>
        private int GetConnections(bool servers, out Connection[] found)
        {
            if (!servers)
                return m_Clients.GetConnections(out found);
            else
                return m_Servers.GetConnections(out found);
        }

		/// <summary>
		/// Gets all the connections in the specified connection list
		/// </summary>
		/// <param name="servers">If true returns the servers list, else returns the clients list</param>
		/// <param name="found">The output array of found connections</param>
		/// <returns>UDP_OK or error code</returns>
		public int GetConnections(bool servers, out IConnection[] found)
		{
			Connection[] cons;
			int retval = GetConnections(servers, out cons);

			found = (IConnection[])cons;
			return retval;
		}
        #endregion

		/// <summary>
		/// Returns a string array containing all local IP addresses
		/// </summary>
		public string[] GetLocalAddresses( )
		{
			return Util.GetLocalAddresses( );
		}

        /// <summary>
        /// Dumps a debug message
        /// </summary>
        /// <param name="Message">Debug message to dump</param>
        [Conditional("DEBUG")]
        public void DebugDump(string Message)
        {
            Message = m_Name + ": [" + Util.GetMethod(2) + "]: " + Message + "\r\n";

			Debug.Write(Message);

            //Fire the debug message event
            if (OnDebugMessage != null)
                OnDebugMessage(null, new DebugEventArgs(Message));
        }
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

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