🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

A Decent String Slinger.. No, not THAT GUY!.. its just a c# UDP server.

posted in Septopus for project Unsettled World
Published October 12, 2018
Advertisement

Okay, with some help over on the Network & MultiPlayer Forum I've reigned in some of the code smell in my UDP Socket Class.

This is the wheel that turns each of my game servers, nearly everything else is game specific logic.

I'm sharing it in it's entirety here for your use however you like.  I'll keep this post updated as I make improvements.  If I make any major changes I'll at least provide a link to them here if possible.

This class is a multi-purpose UDP Server/Client class that expects and produces strings.  Now, I know that this isn't the BEST or most optimized way to deal with data, however I find it to be extremely flexible for rapid development of server logic code.  When I finalize the server logic I may rewrite the class serialization/deserialization routines to output byte[]'s directly and at that point I'll simply remove the string variable handling from this class so they only interact with byte[]'s.

I mean, strings aren't all bad right?

spideyOK.jpg.2a4e79ad2b58fd019c07e6d94bd3626f.jpg

At least somebody agrees... ;)

Anyhow, here it is.  Have fun. 


using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace StringSlinginUDPServer
{
    public class UDPSocket : IDisposable
    {
        //Constant for configuring the prevention of ICMP connection resets
        private const int SIO_UDP_CONNRESET = -1744830452;

        //UDP socket
        private Socket _socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

        //Async Callback
        private AsyncCallback recv = null;

        //Buffer Size Constant
        private const int bufSize = 8 * 1024;

        //Raw string data from client packets
        private Dictionary<EndPoint, Queue<string>> messageDictionary;

        //Queue for holding raw string data from server packets when in client mode.
        private Queue<string> cQ;

        //Referece to the data store used by the server(for access to the current game time clock)
        private TimeCodeDataProvider dataStore;

        //Time code storage for last sent/received messages
        private double lastSentMessage = 0;
        private double lastReceivedMessage = 0;

        //Boolean to determine which mode we're in so received messages get put in the right place.
        private bool clientMode = false;

        //Max string lenght allowed by the servers.
        private int maxMessageLength = 1450;

        // Generic Object Pool Class
        public class ObjectPool<T>
        {
            // ConcurrentBag used to store objects.
            private ConcurrentBag<T> _objects;
            private Func<T> _objectGenerator;

            // Object pool contructor used to get a delegate for implementing instance initialization
            // or retrieval process
            public ObjectPool(Func<T> objectGenerator)
            {
                if (objectGenerator == null) throw new ArgumentNullException("objectGenerator");
                _objects = new ConcurrentBag<T>();
                _objectGenerator = objectGenerator;
            }

            // GetObject retrieves the object from the object pool (if already exists) or else
            // creates an instance of object and returns
            public T GetObject()
            {
                T item;
                if (_objects.TryTake(out item)) return item;
                return _objectGenerator();
            }

            // PutObject stores the object back to pool.
            public void PutObject(T item)
            {
                _objects.Add(item);
            }
        }

        ObjectPool<State> statePool = new ObjectPool<State>(() => new State());

        //State class for async receive.
        class State
        {
            public string smessage;
            public int bytes = 0;
            public byte[] buffer = new byte[bufSize];
            public EndPoint epFrom = new IPEndPoint(IPAddress.Any, 0);
        }

        //IDisposable stuff
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                // free managed resources
                if (_socket != null)
                {
                    _socket.Dispose();
                    _socket = null;
                }
            }
        }

        //Server "Mode" 
        public UDPSocket(Dictionary<EndPoint, Queue<string>> msgsDict, TimeCodeDataProvider DATASTORE)
        {
            clientMode = false;
            messageDictionary = msgsDict;
            dataStore = DATASTORE;
            lastSentMessage = dataStore.TimeCodeDoubleValue;
        }

        //Client "Mode"
        public UDPSocket(Queue<string> mq, TimeCodeDataProvider DATASTORE)
        {
            clientMode = true;
            cQ = mq;
            dataStore = DATASTORE;
            lastSentMessage = dataStore.TimeCodeDoubleValue;
        }

        public void CloseSocket()
        {
            _socket.Close();
        }

        //Internal connection status checking
        public int SendHowStale()
        {
            //Console.WriteLine("lmc: " + ((int)dataStore.UWServerSeconds - (int)lastSentMessage).ToString());
            if (lastSentMessage == 0)
                lastSentMessage = dataStore.TimeCodeDoubleValue;
            return (int)(dataStore.UWServerSeconds - lastSentMessage);
        }

        //Internal connection status checking
        public int ReceiveHowStale()
        {
            //Console.WriteLine("lmc: " + ((int)dataStore.UWServerSeconds - (int)lastSentMessage).ToString());
            if (lastReceivedMessage == 0)
                lastReceivedMessage = dataStore.TimeCodeDoubleValue;
            return (int)(dataStore.UWServerSeconds - lastReceivedMessage);
        }

        //Start/Bind a Server socket.
        public void Server(string address, int port)
        {
            //In case restarting uncleanly, dunno if this actually does anything..  
            _socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.ReuseAddress, true);
            //Ensure all async packets contain endpoint info and etc.
            _socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true);
            //Ignore ICMP port unreachable exceptions.
            _socket.IOControl((IOControlCode)SIO_UDP_CONNRESET, new byte[] { 0, 0, 0, 0 }, null);
            //Bind to port.
            if (address == "all")
            {
                _socket.Bind(new IPEndPoint(IPAddress.Any, port));
            }
            else
            {
                _socket.Bind(new IPEndPoint(IPAddress.Parse(address), port));
            }
            //Start receive callback process.
            Receive();
        }

        //Setup a Client to Server socket.
        public void Client(string address, int port)
        {
            //Dunno if these two options do anything for client sockets, but they don't seem to break anything.
            _socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true);
            _socket.IOControl((IOControlCode)SIO_UDP_CONNRESET, new byte[] { 0, 0, 0, 0 }, null);
            _socket.Connect(IPAddress.Parse(address), port);
            //Start receive callback process.
            Receive();
        }

        //ServerSend sends to any EndPoint from THIS server.
        public void ServerSend(string text, EndPoint ep)
        {
            try
            {
                byte[] data = Encoding.ASCII.GetBytes(text);

                _socket.SendTo(data, ep);
                lastSentMessage = dataStore.TimeCodeDoubleValue;
                //Console.WriteLine("TO NET: " + text);
            }
            catch (Exception ex)
            {
                Console.WriteLine("ServerSend Exception: " + ex.Message);
            }
        }

        //Client Send only sends to the connected Server.
        public void cSend(string text)
        {
            try
            {
                byte[] data = Encoding.ASCII.GetBytes(text);
                _socket.Send(data);
                lastSentMessage = dataStore.TimeCodeDoubleValue;
                //Console.WriteLine("TO NET: " + text);
            }
            catch (Exception ex)
            {
                Console.WriteLine("cSend Exception: " + ex.Message);
            }
        }

        //Setup Async Callback
        private void Receive()
        {
            State so = statePool.GetObject();
            try
            {
                _socket.BeginReceiveFrom(so.buffer, 0, bufSize, SocketFlags.None, ref so.epFrom, recv = (_Receive), so);
            }
            catch (Exception)
            {
                statePool.PutObject(so);
            }
        }

        //Receive Callback
        private void _Receive(IAsyncResult ar)
        {
            State so = (State)ar.AsyncState;
            try
            {
                so.bytes = _socket.EndReceiveFrom(ar, ref so.epFrom);
                lastReceivedMessage = dataStore.TimeCodeDoubleValue;
                so.smessage = Encoding.ASCII.GetString(so.buffer, 0, so.bytes);
                //Console.WriteLine("Msg frm ThreadID: " + Thread.CurrentThread.ManagedThreadId.ToString());
                //Console.WriteLine("FROM NET: " + text);
                if (so.smessage.Length < maxMessageLength)
                {
                    if (clientMode)
                    {
                        cQ.Enqueue(so.smessage);
                    }
                    else
                    {
                        if (!messageDictionary.TryGetValue(so.epFrom, out Queue<string> queue))
                        {
                            messageDictionary.Add(so.epFrom, queue = new Queue<string>());
                        }
                        queue.Enqueue(so.smessage);
                    }
                }
                _socket.BeginReceiveFrom(so.buffer, 0, bufSize, SocketFlags.None, ref so.epFrom, recv, so);
            }
            catch (Exception)
            {
                statePool.PutObject(so);
            }
        }
    }
}

My Usage Looks Like(c# console app):


namespace StringSlinginUDPServer
{
    class MyServer
    {
	public static bool running = true;
	public static Dictionary<EndPoint, Queue<string>> MessageDictionary = new Dictionary<EndPoint, Queue<string>>();
	public static Thread udpServerThread;

	static void UDPServer()
	{
		using (s = new UDPSocket(MessageDictionary, TimeCodeDataProvider))
		{
			s.Server("IP.ADD.RE.SS" or "all", PortNumber);
			while(running)
			{
				//Servery Stuff Goes Here.
    			//Like reiteratively dequeuing the Message Dictionary Queues and processing/replying to all commands/etc...
			}
		}
	}

	static void Main(string[] args)
	{
		udpServerThread = new Thread(new ThreadStart(UDPServer));
		udpServerThread.Start();
		while (running)
		{
			//Logic to check the connection status and start/restart the thread if it's dead/halted..
			//Trap key sequences to close server/etc..
		}
	}
    }
}

And here's a rough example of how I use the Client functionality, this routine checks the server via a simple UDP transaction.


static bool UDPCheckOK()
        {
            bool res = false;
            Queue<string> udpResponseQ = new Queue<string>();
            using (c = new UDPSocket(udpResponseQ, TimeCodeDataProvider))
            {
                c.Client("127.0.0.1", PortNumber);
                c.cSend("SOCKETCHECKSTRING");
                int wc = 0;
                while (udpResponseQ.Count < 1)
                {
                    wc++;
                    if (wc >= 20)
                    {
                        goto timeout;
                    }
                    Thread.Sleep(50);
                }
                timeout:;
                if (udpResponseQ.Count > 0)
                {
                    while (udpResponseQ.Count > 0)
                    {
                        switch (udpResponseQ.Dequeue())
                        {
                            case "SOCKETCHECKOKSTRING":
                                res = true;
                                break;
                        }
                    }
                }
            }

            return res;
        }

Happy Coding!

0 likes 0 comments

Comments

Septopus

Code Revision Added:

IS NOW:


if (!messageDictionary.TryGetValue(so.epFrom, out Queue<string> queue))
{
	messageDictionary.Add(so.epFrom, queue = new Queue<string>());
}
queue.Enqueue(so.smessage);

WAS:


if (!messageDictionary.ContainsKey(so.epFrom))
{
	messageDictionary.Add(so.epFrom, new Queue<string> ());
}
messageDictionary[so.epFrom].Enqueue(so.smessage);

The "WAS" version actually results in a double lookup into the Dictionary to find the Queue in question and add the new item to it.

Thanks again to the folks over in the Networking & Multiplayer forums for the optimization tips!

October 13, 2018 04:29 AM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement