June 2008 - Posts

Right, so in Part 1 we looked at the requirements for a Discovery Service and in Part 2 we looked at how we'd host the WCF service. Now it's time to look at the UDP itself. First off though, if you look in Part 2, at the declaration I've got for DiscoveryServer, you'll see I'm inheriting from a DiscoveryBase. This base class will handle most of the UDP stuff, since both DiscoveryClient and DiscoveryServer need a very similar set of operations.

Let's look at the most important method on DiscoveryBase:

private UdpClient _udpClient;
private long _connected;
private int _timeToLive = 2;
private IPAddress _address;
internal void Prepare(IPAddress address, int localPort)
{
    // Ensure we're not already discovering
    if (Interlocked.CompareExchange(ref _connected, 1, 0) == 1)
        throw new InvalidOperationException("Already in discovery mode.");

    // Create the client
    _udpClient = new UdpClient(localPort);

    // If it's broadcast we set the broadcast flag
    if (IsBroadcast(address))
        _udpClient.EnableBroadcast = true;

    // If it's multicast we join the multicast group
    if (IsMulticast(address))
        _udpClient.JoinMulticastGroup(address, _timeToLive);

    // Store the location to connect to
    _address = address;
}

As you can see, it's not terribly complicated. There's a little bit of concurrency logic to ensure we're not already discovering, we create a UdpClient object, and then either set it to broadcast or join a multicast group depending on the IP address. Finally we cache the address so that our child classes can access it via the Address property. _timeToLive is an interesting property, it defines how many router hops any multicast broadcasts will go. For now, I've just set this to 2, but you could easily make it configurable.

Now let's hop back to DiscoveryServer and have a look at how it kicks off it's discovery:

public void Publish(IPAddress address, int port)
{
    Prepare(address, port);

    // Set up the discovery service
    _host = new ServiceHost<IDiscoveryService>(this, _address);
    Binding binding = BindingAddressParser.CreateTransportBinding(_address);
    _host.AddServiceEndpoint(typeof(IDiscoveryService), binding, _address);
    _host.Open();

    // Now asynchronously listen for any incoming requests
    Client.BeginReceive(Receive, null);
}

So, as you can see, it calls Prepare on DiscoveryBase, in order to ready itself for UDP operations. It then begins hosting the IDiscoveryService on a transport and binding determined by an address which is passed into the DiscoveryServers constructor. Finally it kicks off an asynchronous receive operation on the UDP client. Let's have a closer look at Receive:

private void Receive(IAsyncResult ar)
{
    if (Connected) // This will be false if the UDP client has been closed
    {
        IPEndPoint remoteEP = null;
        byte[] result = Client.EndReceive(ar, ref remoteEP);

        // Now asynchronously listen for any incoming requests
        Client.BeginReceive(Receive, null);

        // Is there actual information to fetch?
        if ((result != null) && (remoteEP != null) && (result.Length > 0))
        {
            string message = Encoding.ASCII.GetString(result);

            if (message == @"get\services")
            {
                // Handle the get services request by sending the address where the service information is hosted
                message = BindingAddressParser.ResolveAddressHostName(_address);
                byte[] response = Encoding.ASCII.GetBytes(message);
                Client.Send(response, response.Length, remoteEP);
            }
        }
    }
}

So, what happens is we check if we're connected (the Connected property on DiscoveryBase returns whether the _connected field we saw in Prepare is equal to 1), we then kick off another asynchronous receive just in case there's more requests on the way. Then we decide whether it's a valid request. If it it, we send the address where we hosted the IDiscoveryService.

In Part 4 we'll start looking at the DiscoveryClient.

Right, so in Part 1 we looked at an overview of how this discovery system should work. Now let's start looking at specifics. Before we start diving into the UDP stuff, let's look at the information that the server will publish and how this will be accomplished. If you recall, the mechanism I suggested was that the server host a normal WCF service that held the information about the services, and that the UDP would be used to provide access to this service. So let's start with the WCF side.

First off we're going to require our ServiceInfo class. This is a relatively simple class, which contains an Address property and a set of Name/Value pairs. Needless to say, it needs to be decorated with the DataContract attribute. To provide access to this information we will have the IDiscoveryService interface:

namespace Sanity.Net
{
    [ServiceContract]
    internal interface IDiscoveryService
   
{

        [OperationContract]
        ServiceInfoCollection GetServices();

   
}
}

You'll note that I made this interface internal, since there's no real call for outside applications to access it without going through the Discovery system we're writing. Our next step is to create a DiscoveryServer class which will take the address it needs to publish as a parameter in its constructor. In order to keep things simple, I'm only going to allow the default binding for the transport mechanism. As I discussed in Part 1, discovery is not the place to get fancy with communication methods, it's where you find out about the fancy communication methods.

We'll create a Publish method on the DsicoveryServer, that will create the service host and open the service endpoint:

private string _address;
private ServiceHost<IDiscoveryService> _host;
public void Publish()
{
// Set up the discovery service
_host = new ServiceHost<IDiscoveryService>(this, _address);
Binding binding = BindingAddressParser.CreateTransportBinding(_address);
_host.AddServiceEndpoint(typeof(IDiscoveryService), binding, _address);
_host.Open();
}

Now, you'll notice my BindingAddressParser, that's just a little class that will infer the binding from the address, NetTcpBinding for "net.tcp" and so forth.

Our next step is to add our ServiceInfoCollection to the class so that services can be given to the discovery server:

private ServiceInfoCollection _localServices;
public ServiceInfo AddService(string address)
{
address = BindingAddressParser.ResolveAddressHostName(address);
return _localServices.Add(address);
}
Now an interesting item is that ResolveAddressName. If you think about it publishing an address like "net.tcp://localhost:21021/MyService" is not terribly useful, since we need to know where localhost actually is. So ResolveAddressHostName uses DNS to replace localhost with the machines actual name. It will also resolve an IP addresses to the machines name as well (e.g. 127.0.0.1).

Finally, we can implement the IDiscoveryService interface:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class DiscoveryServer : DiscoveryBase, IDiscoveryService
{

ServiceInfoCollection IDiscoveryService.GetServices()
{
return _localServices;
}
}

Okay, we now have a server-side that will take services with arbitrary information and make them available on TCP/IP. In Part 3 we will look at making this available on UDP as well.