C# + .NET: Minimalistic asynchronous UDP example

Recently I have been experimenting with networking in Unity3d.

As you may know, there are various systems to aid you with building networked applications in Unity, ranging in approaches and capabilities.

However, if you are more familiar with the regular socket APIs, or just need to port a bit of existing code, you would probably rather have a couple of simple methods to send and receive data.

So here's just that, using .NET's System.Net.Sockets.UdpClient to asynchronously receive and handle data as it arrives:

using System;
using System.Net.Sockets;
using System.Net;
 
namespace UdpTest {
	class Program {
		static void OnUdpData(IAsyncResult result) {
			// this is what had been passed into BeginReceive as the second parameter:
			UdpClient socket = result.AsyncState as UdpClient;
			// points towards whoever had sent the message:
			IPEndPoint source = new IPEndPoint(0, 0);
			// get the actual message and fill out the source:
			byte[] message = socket.EndReceive(result, ref source);
			// do what you'd like with `message` here:
			Console.WriteLine("Got " + message.Length + " bytes from " + source);
			// schedule the next receive operation once reading is done:
			socket.BeginReceive(new AsyncCallback(OnUdpData), socket);
		}
		static void Main(string[] args) {
			UdpClient socket = new UdpClient(5394); // `new UdpClient()` to auto-pick port
			// schedule the first receive operation:
			socket.BeginReceive(new AsyncCallback(OnUdpData), socket);
			// sending data (for the sake of simplicity, back to ourselves):
			IPEndPoint target = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5394);
			// send a couple of sample messages:
 			for (int num = 1; num <= 3; num++) {
				byte[] message = new byte[num];
				socket.Send(message, message.Length, target);
			}
			Console.ReadKey();
		}
	}
}

Using this approach I was able to create a networking system in Unity that works both for P2P connections between game' instances and connecting to external servers.

Additional notes:

  • Sending in this example is synchronous, but has little to no effect (writing to a socket doesn't take long, moreso given the UDP datagram size limits).
  • AsyncResult.AsyncState can be any kind of object.
    In this example I'm using it to easily retrieve the socket that has received the data.
    For more complex systems, it can be a good idea to make a "context" class, which would contain the socket (UdpClient) and any other values that you may need.
  • Due to the current threading restrictions in Unity, you may not be able to access engine-related (unityengine.*) functionality from threads. To workaround this, push received messages into a list somewhere, and then handle them in the Update method of a MonoBehaviour.

Have fun!

Related posts:

4 thoughts on “C# + .NET: Minimalistic asynchronous UDP example

  1. Hello, i used your code but encountered a problem. It is the following:After executing the jobs, all of the threads started successfully and then there was a problem in initializing for the 2nd sensor, displaying “Error TemperatureMesurement-Job initialization 4_cam2_temperature:Normally, each socket address (protocol, network address or connection) may only be used once”(refering to initializejob on the top )and then a problem in requestdata() for sensor 2 stating “”Error TemperatureMesurement-Job requestData 4_cam2_temperature:The object reference was not set to an object instance”. The exception class used is Class System.exception. It is only requesting the temperature from a particular sensor and not from others as seen in the ASCII code –
    Only one reading can be taken from a sensor at a time. I used the concept of asynchronous sockets, that is, it is only reading the processor temperature of a single sensor at a time. When you add more, it shows that there is an error. Any ideas would be appreciated.

    CODE:
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    using System.Threading;
    using System.IO.Ports;

    namespace SmartLynx
    {
    public class RepeatableTemperatureMeasurementJob : RepeatableMeasurementJob
    {
    //general
    public temperatureGauge TempDevice;

    //udp
    UdpClient socketCmd;
    UdpClient socketCmdText;
    private const int cmdTextPort = 6666;
    private const int cmdPort = 6676;
    private double tempSensTop = 0;
    private double tempSensBottom = 0;
    private double tempProcessor = 0;
    private String firmware = “”;
    private String streamType = “”;
    private String camIP = “”;

    //GMH3700
    //private CommunicatorSerial ComPort;
    private SerialPort PortGMH3700 = new SerialPort();
    public string ComPortbyUser { get; set; }
    public int DecimalAddress { get; set; }

    public RepeatableTemperatureMeasurementJob(bool _jobActive, string _jobName, int _neededSubJob) :base(_jobActive, _jobName, _neededSubJob)
    {
    }

    public override void initializeJob()
    {
    try
    {
    if (TempDevice == temperatureGauge.udp)
    {
    camIP = parameters[0];
    socketCmdText = new UdpClient(cmdTextPort);
    socketCmdText.BeginReceive(new AsyncCallback(OnUdpDataCmdText), socketCmdText);
    socketCmd = new UdpClient(cmdPort);
    socketCmd.BeginReceive(new AsyncCallback(OnUdpDataCmd), socketCmd);
    }
    if (TempDevice == temperatureGauge.GMH3700)
    {
    PortGMH3700.PortName = ComPortbyUser;
    PortGMH3700.BaudRate = 4800; /* Baudrate ‘4800’ oder ‘38400’ */
    PortGMH3700.Parity = Parity.None;
    PortGMH3700.DataBits = 8;
    PortGMH3700.StopBits = StopBits.One;
    PortGMH3700.DtrEnable = true;
    PortGMH3700.RtsEnable = false;
    PortGMH3700.Open();
    }
    }
    catch (Exception e)
    {
    Console.WriteLine(“Error TemperatureMesurement-Job initialization ” + jobName + ” : ” + e.Message);
    }
    }

    public override void deinitializeJob()
    {
    try
    {
    if (TempDevice == temperatureGauge.udp)
    {
    //socketCmd.Close();
    //socketCmdText.Close();
    }
    if (TempDevice == temperatureGauge.GMH3700)
    {
    PortGMH3700.Close();
    }
    }
    catch (Exception e)
    {
    Console.WriteLine(“Error TemperatureMesurement-Job deinitialization ” + jobName + ” : ” + e.Message);
    }
    }

    protected override void doMeasurement()
    {
    if (TempDevice == temperatureGauge.udp)
    {
    tempSensTop = 0;
    tempSensBottom = 0;
    tempProcessor = 0;
    firmware = “”;
    streamType = “”;
    requestData();
    }

    if (TempDevice == temperatureGauge.GMH3700)
    {
    if (PortGMH3700.IsOpen)
    {
    byte AdressByte = Convert.ToByte(0xFF – Convert.ToByte(DecimalAddress));
    byte Aufrufcode = 0x00;
    byte bytestosendwithCRC = CRC(AdressByte, Aufrufcode);
    byte[] MessageToSend = { AdressByte, Aufrufcode, bytestosendwithCRC };

    PortGMH3700.Write(MessageToSend, 0, MessageToSend.Length);

    Thread.Sleep(200); // Auf Nachricht warten

    if (PortGMH3700.IsOpen && PortGMH3700.BytesToRead > 0)
    {
    int length = PortGMH3700.BytesToRead;
    byte[] ComBuf = new byte[length];
    PortGMH3700.Read(ComBuf, 0, length);

    double outdblFloatWert;
    Int16 outi16DezimalPunktPosition;
    byte[] MessageByte = ComBuf;

    try
    {
    Messwert32Decodieren(MessageByte[3], MessageByte[4], MessageByte[6], MessageByte[7], out outdblFloatWert, out outi16DezimalPunktPosition);
    measuredValue = outdblFloatWert;
    storeData();
    }
    catch (Exception ex)
    {
    Console.WriteLine(“Error GMH3700: Received wrong message ” + “(” + jobName + “)”); //
    }
    }
    }
    }
    }

    private void requestData()
    {
    try
    {
    IPAddress _camip = IPAddress.Parse(camIP);
    IPEndPoint _ep = new IPEndPoint(_camip, cmdTextPort);
    byte[] _sendbuf = Encoding.ASCII.GetBytes(“Cmd_ISL_GetCurrentSensorTemperature”);
    socketCmdText.Send(_sendbuf, _sendbuf.Length, _ep);

    byte[] _sendbuf2 = Encoding.ASCII.GetBytes(“Get_Temp”);
    socketCmdText.Send(_sendbuf2, _sendbuf2.Length, _ep);
    /*
    byte[] _sendbuf3 = Encoding.ASCII.GetBytes(“APP_Version”);
    socketCmdText.Send(_sendbuf3, _sendbuf3.Length, _ep);

    IPEndPoint _ep2 = new IPEndPoint(_camip, cmdPort);
    Byte[] _sendbuf4 = { 0x02, 0x00, 0x00, 0x10, 0x01, 0x00, 0x00, 0x00, 0xED };
    socketCmd.Send(_sendbuf4, _sendbuf4.Length, _ep2);
    */
    }
    catch (Exception e)
    {
    Console.WriteLine(“Error TemperatureMesurement-Job requestData ” + jobName + ” : ” + e.Message);
    }
    }

    void OnUdpDataCmdText(IAsyncResult result)
    {
    try
    {
    // this is what had been passed into BeginReceive as the second parameter:
    UdpClient socket = result.AsyncState as UdpClient;
    // points towards whoever had sent the message:
    IPEndPoint source = new IPEndPoint(IPAddress.Any, cmdTextPort);
    // get the actual message and fill out the source:
    byte[] message = socket.EndReceive(result, ref source);
    // do what you’d like with `message` here:
    String s = Encoding.ASCII.GetString(message, 0, message.Length);
    String searchStringSensor = “Sensor Temp Top “;
    String searchStringProzessor = “Temp = “;
    String searchStringVersion = “Version : “;
    int startIndexSensor = s.IndexOf(searchStringSensor);
    int startIndexProzessor = s.IndexOf(searchStringProzessor);
    int startIndexVersion = s.IndexOf(searchStringVersion);
    if (camIP == source.Address.ToString())
    {
    if (startIndexSensor == 0)
    {
    String[] substrings = s.Split(‘,’);
    String substringTop = substrings[0].Substring(16, substrings[0].Length – (4 + 16));
    String substringBottom = substrings[1].Substring(8, substrings[1].Length – (9 + 4));
    tempSensTop = Double.Parse(substringTop) / 100;
    tempSensBottom = Double.Parse(substringBottom) / 100;
    //measuredValue = tempSensTop;
    //storeData();
    }
    else if (startIndexProzessor == 0)
    {
    String substringProzessor = s.Substring(7, s.Length – 12);
    tempProcessor = Double.Parse(substringProzessor);
    measuredValue = tempProcessor;
    storeData();
    }
    else if (startIndexVersion > 0)
    {
    String substringVersion = s.Substring(18, s.Length – 18);
    firmware = substringVersion;
    }
    }
    // schedule the next receive operation once reading is done:
    socket.BeginReceive(new AsyncCallback(OnUdpDataCmdText), socket);
    }
    catch (Exception e)
    {
    Console.WriteLine(“Error TemperatureMesurement-Job OnUdpDataCmdTxt ” + jobName + ” : ” + e.Message);
    }
    }

    void OnUdpDataCmd(IAsyncResult result)
    {
    try
    {
    // this is what had been passed into BeginReceive as the second parameter:
    UdpClient socket = result.AsyncState as UdpClient;
    // points towards whoever had sent the message:
    IPEndPoint source = new IPEndPoint(IPAddress.Any, cmdPort);
    // get the actual message and fill out the source:
    byte[] message = socket.EndReceive(result, ref source);
    // do what you’d like with `message` here:
    string[] receiveString = BitConverter.ToString(message).Split(‘-‘);

    if (camIP == source.Address.ToString())
    {
    if (string.Concat(receiveString).Substring(0, 16) == “0201001001000002”)
    {
    streamType = receiveString[8] + receiveString[9];
    }
    }
    // schedule the next receive operation once reading is done:
    socket.BeginReceive(new AsyncCallback(OnUdpDataCmd), socket);
    }
    catch (Exception e)
    {
    Console.WriteLine(“Error TemperatureMesurement-Job OnUdpDataCmd ” + jobName + ” : ” + e.Message);
    }
    }
    private byte CRC(byte inbyByte0, byte inbyByte1) // for GMH3700
    {
    byte byCRCByte = 0;
    UInt16 ui16Integer = (UInt16)((inbyByte0 << 8) | inbyByte1);
    for (UInt16 ui16Zaehler = 0; ui16Zaehler < 16; ui16Zaehler++)
    {
    if ((ui16Integer & 0x8000) == 0x8000)
    {
    ui16Integer = (UInt16)((ui16Integer << 1) ^ 0x0700);
    }
    else
    {
    ui16Integer = (UInt16)(ui16Integer <> 8));
    return byCRCByte;
    }

    /*2 Übertragungs-Bytes zu unsigned 16 Bit Integer zusammenfassen */
    private UInt16 UInt16Decodieren(byte inbyByteA, byte inbyByteB) // for GMH3700
    {
    return (UInt16)((UInt16)((255 – inbyByteA) << 8) | inbyByteB);
    }

    /*2 unsigned 16 Bit Integer zu unsigned32 Bit Integer zusammenfassen */
    private UInt32 UInt32Decodieren(UInt16 inui16Wert1, UInt16 inui16Wert2) // for GMH3700
    {
    return (UInt32)(inui16Wert1 <> 14);
    ui16Integer = (UInt16)(ui16Integer & 0x3FFF);
    if ((ui16Integer >= 0x3FE0) && (ui16Integer = 3)
    {
    if (inByteArray[2] != CRC(inByteArray[0], inByteArray[1]))
    {
    return false;
    }
    }
    if (inLaenge >= 6)
    {
    if (inByteArray[5] != CRC(inByteArray[3], inByteArray[4]))
    {
    return false;
    }
    }
    if (inLaenge >= 9)
    {
    if (inByteArray[8] != CRC(inByteArray[6], inByteArray[7]))
    {
    return false;
    }
    }
    if (inLaenge >= 12)
    {
    if (inByteArray[11] != CRC(inByteArray[9], inByteArray[10]))
    {
    return false;
    }
    }
    return true;
    }

    /* 4 Bytes aus Übertragung in Messwert oder Fehlercode umrechnen */
    Int16 Messwert32Decodieren(byte inbyByte3, byte inbyByte4, byte inbyByte6, byte inbyByte7, out double outdblFloatWert, out Int16 outi16DezimalPunktPosition) // for GMH3700
    {
    outdblFloatWert = 0;
    outi16DezimalPunktPosition = 0;
    UInt16 ui16Integer1, ui16Integer2;
    ui16Integer1 = UInt16Decodieren(inbyByte3, inbyByte4);
    ui16Integer2 = UInt16Decodieren(inbyByte6, inbyByte7);
    UInt32 ui32Integer = UInt32Decodieren(ui16Integer1, ui16Integer2);
    /* Bytes zusammenfassen, mit Beispieldaten: 0x8DFFFFFC */
    outi16DezimalPunktPosition = (Int16)(((0xFF – inbyByte3) >> 3) – 15);
    /* Dezimalpunkt dekodieren, mit Beispieldaten: 0x0002*/
    ui32Integer = ui32Integer & 0x07FFFFFF;
    /* Rohwert dekodieren, mit Beispieldaten: 0x05FFFFFC */
    if ((100000000 + 0x2000000) > ui32Integer)
    {
    /* Daten sind gültige Werte */
    if (0x04000000 == (ui32Integer & 0x04000000))
    {
    ui32Integer = (ui32Integer | 0xF8000000);
    /* Mit Beispieldaten: 0xFDFFFFFC */
    }
    ui32Integer = (UInt32)((UInt64)ui32Integer + 0x02000000);
    /* Mit Beispieldaten: 0xFFFFFFFC */
    }
    else
    {
    /* Daten sind Fehlercodes, Fehlercode auscodieren */
    outdblFloatWert = (double)(ui32Integer – 0x02000000 – 16352.0);
    outi16DezimalPunktPosition = 0; return -36;
    /* Rückgabewert ist Fehlercode */
    }
    /* Umwandlung in Fliesspunkt Zahl, mit Beispieldaten: -4f */
    outdblFloatWert = (double)(Int32)ui32Integer;
    outdblFloatWert = outdblFloatWert / (Math.Pow(10.0f, (double)outi16DezimalPunktPosition));
    return 0; /* Rückgabewert ist OK, mit Beispieldaten: -0,04 */
    }

    public static byte[] combineByteArray(byte[] first, byte[] second) // for GMH3700
    {
    byte[] ret = new byte[first.Length + second.Length];
    Buffer.BlockCopy(first, 0, ret, 0, first.Length);
    Buffer.BlockCopy(second, 0, ret, first.Length, second.Length);
    return ret;
    }

    public byte[] addByteToByteArray(byte[] bArray, byte newByte) // for GMH3700
    {
    byte[] newArray = new byte[bArray.Length + 1];
    bArray.CopyTo(newArray, 0);
    newArray[newArray.Length – 1] = newByte;
    return newArray;
    }
    }
    }

  2. Hi ! Thanks for the code sample.
    About handling received data into a queue and dequeue it in some Update function, do you think that the best way is to treat a packet per frame or to process all the new packets at the next frame ?

    • Hello, you would usually process all available packets at the start of game frame to minimize latency. Occasionally games allow queuing up and distributing events (rather than packets themselves) to avoid everything happening at once after a lag spike, but this is not something you do with UDP, as the packets do not generally “buffer” like on TCP.

  3. You know, for the last few hours I felt like I’ve been banging my head against the wall trying to get my client and server talking via UDP. Your solution is so much easier than what I was working with!

    I had a Python client which was polling my game for the latest data, so the game was actually acting as the server in this case. Using your solution has worked a treat, so many thanks for sharing :)

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.