Client-server autodiscovery in C# and UDP Sockets

This article brings ready to use source-code to C# Developers that explore a very important networking feature that is not usually used by applications and may increase speed of application deployment since it’s possible to take advantage of the source-code to enable any .NET application to have “Zero Configuration” feature. For this article I wrote two demo applications and a couple of classes to to support the proof-of-concept project.

This article details the use of a method known as “Auto Discovery”. The working example will mimic DHCP behavior by exchanging broadcast packets using C#, Sockets and Threads.

Complete source-code download link:
http://nullskull.com/FileUpload/-895676619_UDPAutoDiscovery1.0.zip  

This article brings to C# Developers the possibility of taking advantage of the source-code to enable any .NET application to have a “Zero Configuration” feature.

For this article I wrote two demo applications and a couple of classes to support the proof-of-concept.

The  code is clean and may serve as base code for any auto discovery service where client-server applications may communicate between them without knowing their exact IP address prior to first network hand-shake.

The classes and demo code are free for non-commercial use and any new or existing client-server may take advantage of this technique.  The only limitation is that will work for servers and workstations that share the same “Netmask” so corporate networks are the best environments for usage.  Even thru VPN with correct routing tables will work fine.

Some background for UDP Broadcasting:
When you connect your computer to a WiFi hotspot your computer will be asking for an IP Address that must be provided by a DHCP
server which is usually the same router you just connected. This works by broadcasting packets over the network so it is possible to transfer data.

The source-code that I bring here will do exactly the same as DHCP does and it was implemented using native .NET Sockets and Threading classes.

You may change the source-code to fill your needs when writing your own app and may be very useful and save you time because already implements multi-threading and Demo code are functional tested under .NET 3.5 and compiled using Visual Studio 2008. The code use only managed objects so probably will run under other Operating Systems.

The “Server” and the “Client” demo app have the following behavior when run together:
-
When started a BackgroundWorker is created act as multi-threaded server. So many clients may use the service at the same time;
- The Auto Discovery Server will bind the port 18500/UDP and wait for incoming packets;
- If incoming client packet reach the server sending a dummy 3 byte packet that is used as “Get Server Address” identifier the Server will reply a pre-configured IP Address and Port that could be used to disclose Web Service HTTP address or any other usage;
- The client after broadcasting the packet will try to connect on the Host/Port received. If it is unreachable will display a debug message on the console.

There are two classes implemented and may be ready to use on other project with little or no adaptations.

namespace Bruno.Ratnieks.AutoDiscovery
public class AutoDiscoveryServer
public class AutoDiscoveryClient

For client usage the coder may instantiate the following class:
    public class clientSide
    {
         public clientSide()
        {

            BackgroundWorker objWorkerServerDiscovery = new BackgroundWorker();
            objWorkerServerDiscovery.WorkerReportsProgress = true;
            objWorkerServerDiscovery.WorkerSupportsCancellation = true;

            AutoDiscoveryClient svcAutoDiscoveryClient = new AutoDiscoveryClient(ref objWorkerServerDiscovery);
            objWorkerServerDiscovery.DoWork += new DoWorkEventHandler(svcAutoDiscoveryClient.Start);
            objWorkerServerDiscovery.ProgressChanged += new ProgressChangedEventHandler(logWorkers_ProgressChanged);
             objWorkerServerDiscovery.RunWorkerAsync();
        }
         private void logWorkers_ProgressChanged(object sender, ProgressChangedEventArgs e)
         {
             // Report thread messages to Console
           Console.WriteLine(e.UserState.ToString());
        }
    }

By declaring the clientSide() on the application will be enough to start the working thread and start look for a server on the network
            clientSide cs = new clientSide();

Make sure the application will keep running because it work as backgroud thread.
This action may be simplified by the developer fairly easy. Just ignoring the backgroundWorker code, removing reportProgress calls and calling the AutoDiscoveryClient.Start() directly.

This code will make sure you console application will not exist during execution:
            while (true)
            {
                 // Do nothing and wait for any key to exit console application
               Console.ReadLine();
                 return;
            }

When the client receive data or need to output messages to the console it will use the ReportProgress method provided by the BackgroundWorker class which raises a new event and you may parse the string received on the method clientSide.logWorkers_ProgressChanged().

The incoming “UserState” will contain the message sent by the background thread.

Below you can see the server loading which share the same structure of the client:
    public class serverSide
    {
        public serverSide(string serverIP, int serverPort)
        {
            BackgroundWorker workerAutoDiscovery = new BackgroundWorker();

            workerAutoDiscovery.WorkerReportsProgress = true;
            workerAutoDiscovery.WorkerSupportsCancellation = true;

            AutoDiscoveryServer svcAutoDiscovery = new AutoDiscoveryServer(ref workerAutoDiscovery, serverIP, serverPort);          
            
            workerAutoDiscovery.DoWork += new DoWorkEventHandler(svcAutoDiscovery.Start);
            workerAutoDiscovery.ProgressChanged += new ProgressChangedEventHandler(logWorkers_ProgressChanged);
             workerAutoDiscovery.RunWorkerAsync();
        }
         private void logWorkers_ProgressChanged(object sender, ProgressChangedEventArgs e)
         {
             // Report thread messages to Console
           Console.WriteLine(e.UserState.ToString());
        }
    }

By creating a new instance of serverSide class the server will be automatically running just like the client.

The difference is that you set on the constructor the IP Address and Port that will be sent to client when a packet is received.

You may use the UDP channel to reply any kind of data to client. but usually TCP is used for extensive data exchange for better error checking and packet delivery safety. That’s why the server demo app reply an IP Address and Port.

So the client will know were to connect to continue the communication using the benefits of TCP/IP.

The “UDP Broadcast” communication method will work in local networks as mentioned before and will reach all computers so it’s not a good idea to use this kind of network channel to send and receive sensitive data.
    
Declaring the serverSide code is pretty simple. Just a constructor that receive two parameters.

// That info will be sent to client as our TCP SERVER -- Not implemented.
            // Change here the IP and port.
            serverSide ss = new serverSide("10.1.1.1", 80);

As explained previusly clients will receive the dummy IP 10.1.1.1 and port 80 and try to connect using TCP protocol.
80 is the default HTTP port so Web Services servers may be auto discovered by using the code I’m publishing here.

The core of the demo is located on the following namespace and you may download it here with all other source code for immediate compile and run.

namespace Bruno.Ratnieks.AutoDiscovery
public class AutoDiscoveryServer
public class AutoDiscoveryClient

Below you see a print screen for client and server running at the same time:




The other classes used on the code are here:

// AutoDiscoveryClient Class
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.ComponentModel;
using System.Threading;
using netService;

namespace Bruno.Ratnieks.AutoDiscovery
{
    public class AutoDiscoveryClient
    {

         public bool disposing = false;

        // Fixed Port for broadcast.
        // You may change it but CLIENT and SERVER must be configured with the same port.
        private int AutoDiscoveryPort = 18500;

         // Specify timeout since UDP is a state-less protocol
        // 5000ms - 5 seconds.
        private int ServerSyncTimeout = 5000;
        
        private int AutoDiscoveryTimeout = 10000;

         // Sample byte sequency that Identify a Server Address Request. You may change on the client-side also.
        // Implementing other byte sequences for other actions are also valid. You as developer may know that ;)
        byte[] packetBytes = new byte[] { 0x1, 0x2, 0x3 };

         // Do not change:
        // We will set this variable when Auto Discovery server reply with ACK;
        // Do not change. This will be set if Server is found and check for TCP Connections is OK
        public string ServerAddress = String.Empty;
        public int ServerPort = 0;

        private BackgroundWorker worker;

         public AutoDiscoveryClient(ref BackgroundWorker worker)
         {
             this.worker = worker;
            worker.ReportProgress(1, "AutoDiscovery::Started at " + AutoDiscoveryPort + "/UDP");
        }

         public void Start(object sender, DoWorkEventArgs e)
         {
             try
            {
                 while (this.disposing == false)
                 {
                     // Must look for server.. Repeat until configured.
                     if (ServerAddress == String.Empty)
                     {
                          this.worker.ReportProgress(2, "AutoDiscovery::Looking for server..");
                         // Broadcast the query
                         sendBroadcastSearchPacket();
                     }
                     Thread.Sleep(3000);
                }
            }
            catch (Exception ex)
            {
               Console.WriteLine(ex.Message);
             }
        }

        public void Stop()
        {
             this.disposing = true;
        }

         // Here is to check if the server replied by Auto Discovery is Alive.
        private bool serverIsReachable(string host, int port)
         {
             try
            {
                TcpClient handle = TCP_Client.Connect(new IPEndPoint(IPAddress.Parse(host), port), ServerSyncTimeout);
                  if (handle.Connected == true)
                 {
                     handle.Close();
                     return (true);
                 }
                 else
                 {
                     return (false);
                }
            }
            catch (Exception e)
            {
                worker.ReportProgress(1, "AutoDiscovery::Connect Error:" + e.Message);
                 return (false);
            }
        }

         private bool sendBroadcastSearchPacket()
         {
            bool returnVal = false;
            UdpClient udp = new UdpClient();
            udp.EnableBroadcast = true;
            udp.Client.ReceiveTimeout = AutoDiscoveryTimeout;
            IPEndPoint groupEP = new IPEndPoint(IPAddress.Parse("255.255.255.255"), AutoDiscoveryPort);

            try
            {
                 udp.Send(packetBytes, packetBytes.Length, groupEP);
                byte[] receiveBytes = udp.Receive(ref groupEP);

                string returnData = Encoding.ASCII.GetString(receiveBytes, 0, receiveBytes.Length);
                 if (returnData.Substring(0, 3) == "ACK")
                {
                    string[] splitRcvd = returnData.Split(' ');
                     this.worker.ReportProgress(3, "AutoDiscovery::Response Server is " + splitRcvd[1] + ":" + splitRcvd[2]);

                     // Check if the server is reachable! Try to connect it using TCP.
                     if (serverIsReachable(splitRcvd[1], Convert.ToInt16(splitRcvd[2])))
                    {
                        ServerAddress = splitRcvd[1];
                        ServerPort = Convert.ToInt16(splitRcvd[2]);
                        returnVal = true;
                     }
                      else
                     {
                          this.worker.ReportProgress(3, "AutoDiscovery::WARNING Server found but is unreachable. Retrying..");
                         return (false);
                     }


                  }
                 else if (returnData.Substring(0, 3) == "NAK")
                 {
                     this.worker.ReportProgress(3, "AutoDiscovery::INVALID REQUEST");
                 }
                 else
                 {
                     this.worker.ReportProgress(3, "AutoDiscovery::Garbage Received?");
                }

               Console.WriteLine("Sleeping. No work to do.");
                 Thread.Sleep(3000);
            }
            catch (SocketException e)
             {
                 this.worker.ReportProgress(1, "AutoDiscovery::Timeout. Retrying "+e.Message);
            }
            udp.Close();
            return (returnVal);
        }
    }
}

// AutoDiscoveryServer Class
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Net.NetworkInformation;
using System.ComponentModel;


namespace Bruno.Ratnieks.AutoDiscovery
{
    public class AutoDiscoveryServer
    {

         private System.ComponentModel.BackgroundWorker workerUDP;

         // Port the UDP server will listen to broadcast packets from UDP Clients.
        private int AutoDiscoveryPort = 18500;

         // Sample byte sequency that Identify a Server Address Request. You may change on the client-side also.
        // Implementing other byte sequences for other actions are also valid. You as developer may know that ;)
        byte[] packetBytes = new byte[] { 0x1, 0x2, 0x3 };

         // In the following example code we reply to incoming client an IP Address that
        // Client must use as server for any purpose. (TCP Server not implemented)
        public IPAddress addrDaemonListenIP;
        
        // Which port we will broadcast as TCP Server (not implemented).
        public int BroadCastDaemonPort = 0;

         private bool disposing = false;

        public AutoDiscoveryServer(ref BackgroundWorker workerUDP, string serverIP, int serverPort)
        {
             this.workerUDP = workerUDP;
             this.BroadCastDaemonPort = serverPort;
             this.addrDaemonListenIP = IPAddress.Parse(serverIP);
         }

        public void Stop()
        {
             workerUDP.CancelAsync();
            workerUDP.Dispose();
            this.disposing = true;
        }

         /// <summary>
        /// Start the listener.
        /// </summary>
        public void Start(object sender, DoWorkEventArgs e)
         {
             try
            {
                 this.workerUDP.ReportProgress(30, "AutoDiscovery::Service Listening " + this.AutoDiscoveryPort + "/UDP");
                byte[] ReceivedData = new byte[1024];

                 // Local End-Point
                IPEndPoint LocalEP = new IPEndPoint(IPAddress.Any, AutoDiscoveryPort);
                IPEndPoint RemoteEP = new IPEndPoint(IPAddress.Any, 0);
                UdpClient newsock = new UdpClient(LocalEP);

                ReceivedData = newsock.Receive(ref RemoteEP);
                IPEndPoint IncomingIP = (IPEndPoint)RemoteEP;

                 while (!disposing)
                 {
                      if (ReceivedData.SequenceEqual(packetBytes))
                      {
                         // Use ReportProgress from BackgroundWorker as communication channel between main app and the worker thread.
                         this.workerUDP.ReportProgress(1, "Discovery from " + IncomingIP + "/UDP");

                         // Here we reply the Service IP and Port (TCP)..
                         // You must point to your server and service port. For example a webserver: sending the correct IP and port 80.

                        byte[] packetBytesAck = Encoding.ASCII.GetBytes("ACK " + addrDaemonListenIP + " " + BroadCastDaemonPort); // Acknowledged

                         newsock.Send(packetBytesAck, packetBytesAck.Length, RemoteEP);

                          this.workerUDP.ReportProgress(1, "Answering(ACK) " + packetBytes.Length + " bytes to " + IncomingIP);
                     }
                      else
                     {
                          // Unknown packet type.
                         this.workerUDP.ReportProgress(1, "Answering(NAK) " + packetBytes.Length + " bytes to " + IncomingIP);
                        byte[] packetBytesNak = Encoding.ASCII.GetBytes("NAK"); // Not Acknowledged

                         newsock.Send(packetBytesNak, packetBytesNak.Length, RemoteEP);
                    }
                    ReceivedData = newsock.Receive(ref RemoteEP);
                }
            }
            catch (Exception ex)
            {
               Console.WriteLine(ex.Message);
            }
        }
    }
}

There is one more class netService.TCP_Client that you may need to compile the client that is inside the zip file.

Complete source-code download link:
http://nullskull.com/FileUpload/-895676619_UDPAutoDiscovery1.0.zip  

Note: I''ve used VS2008 C# with .NET 3.5 but 2.0 and greater will do the job. Older versions of Visual Studio were tested and no problems at all to compile the code.

Best wishes,
Bruno Ratnieks
Sniffer Networks do Brasil Ltda.
Bruno@sniffer.net

By Bruno Ratnieks   Popularity  (7507 Views)
Biography - Bruno Ratnieks
Bruno Lacerda Ratnieks Sniffer Networks CTO Programming Languages? I've met logic!