http://www.developer.com/

Back to article

Dynamic Service Discovery with Java


February 19, 2008

Overview

Writing a network service application in Java is usually a straightforward matter: Open a ServerSocket on some specified port on the machine, listen for requests, and process them when they arrive. Likewise, the client side is fairly straightforward: Open a Socket connection to a specified host and port, then start communicating with the service on the other end. But sometimes, the need to specify a port number to run the service on, and the need to know a valid hostname (or IP address) and port on the client, adds a bit more "required knowledge" to the utility than you want end users to worry about (or that you want to support later). If you were to search for a way to eliminate the technical mumbo jumbo from the end user's point of view and also eliminate the need to fix the hostname (or IP address) and port number ahead of time, you would end up in the land of dynamic service discovery.

When all the mystery is stripped away, dynamic service discovery is basically a technique where a client application makes an open query on the entire local network, saying "I'm looking for service XYZ ... anyone here provide that? If so, where are you?" The service itself listens for such questions and responds with something like "I provide service XYZ, and I am running at 10.1.2.3 on port 9999." Now, if you have already done a little research in this area, you may be aware of Apple's work in this area. Their Bonjour stack basically provides a system-wide discovery service that, among many other things, allows two people both running the iTunes application to immediately see and play from each other's music shares just by firing up the application. Adding a simple version of this magic to your own Java-based services is really rather simple.

About this Article

I will first talk, perhaps at excessive length, about the conceptual underpinnings of a good service discovery layer. If you are sure you are completely familiar with how this works, you might skip the concept section and go straight to the details section. I assume the reader is comfortable enough with basic Java networking. As such, I will not call out each line of code or even explain the differences between a TCP packet compared to a UDP packet. I also tend to use lots of Java5-isms in my code (generics, enhanced-for, and so forth), so do be warned if you are still relegated to an older compiler.

A Real Life Example

I work in a small software shop. I once needed to provide fellow developers with a little service utility to make their (and my) life easier. The client side of involved a little Swing app that ran on their desktop and interacted with the a network service. However, given the rather dynamic nature of our working environment, I didn't want to have to distribute an update for the client Swing app just because I had to move the service to a different host machine. And, although the connections settings could be passed in on the command line, this needlessly burdens the end users to remember the argument list format when they already have enough to keep track of. I also knew we might eventually need to run multiple such services, and I did not want to personally manage a list of machines and port numbers just to keep everyone updated as to what the current connection options were. So, this was all handled via service discovery: When the end user started the Swing app client, he was given a list of all currently available options (with descriptive human words, not geeky connection detail lingo) to choose from. It ended up being so seamless that I doubt many of them even appreciated the effort. (And that was fine with me.)

Concepts

By way of a crude sequence diagram, service discovery works like this:

As I stated earlier, the gimmick is that the clients ask the machines on the local network to reply whether they provide some service the client is looking for. This generally takes the rather simple form of a multicast datagram packet. (A multicast datagram packet, rather than going to just one specific host address, instead goes to all machines on the local network that are listening on the same channel address. It is somewhat analogous to the concept of television or radio.) The exact "body" of that query packet is largely up to you to devise, but to be effective, it must include that it is (1) a request for (2) some named (or otherwise identifiable) service. This does introduce a condition that a "service name" needs to be introduced. This can take any practical form: webserver, echoservice, MySlickUtility, and so on. The person who writes the service side will pick something appropriate. The person who writes the client side (who may or may not the same person) just needs to know what service name to ask for in their query packet.

The server receives multicast datagram packets from some particular channel address, analyzes the body of those packets to see whether they should reply to it, and then replies back (if appropriate) with its specific connection information. The body of the reply packet is again ultimately up to you, but should include that it is (1) a reply for (2) the named service in question, and (3) include any connection details the client needs, such as hostname and port number. You also may want to include a friendly, human label for the service instance itself in the event the human user needs to be presented with a less technical label for the service.

Back on the client, after making the query on the network, it then waits a little while (a second or two is probably sufficient) to get any reply packets back. It should check the reply packets to make sure they are for the service name it asked for. Then, the connection details for the services that replied can be collected. In the case of multiple replies, you either can select the first one, select one at random, or select based on some information in the reply packets (such as a load balancing metric or priority number). Perhaps the best option is to give the user a list and let them decide which one to use.

A further refinement of the preceding discussion attempts to address the "unreliable" part of UDP. During the window in which a client is waiting and collecting replies for its query, it might issue the query a few more times. Although this can be a good idea on congested networks (from the standpoint of your application, albeit not from the standpoint of the network itself) it is also a good idea to add "known answer suppression" into the mix: If a client already has received a reply from one server, but is sending a followup query, it includes a list of servers it has already processed. This allows the server to silently ignore the followup request, which would otherwise just add more (and needless, in this case) traffic. The code I am providing with this article does not implement a known answer suppression technique. However, it is not a technically difficult thing to add in, should the reader wish to do so.

To be clear, there are two separate labels (not counting the optional, friendly, human description). First is the "service name" being something fairly generic, such as "iTunes". Second is the "service instance name" being something absolutely unique to the local network, such as "Rob's Music." Only the background code needs to work with the service name when it makes a request or sends back a reply. The service name is like a generic identifier for what kind or category of service is provided or searched for. But, if the user needs to be informed or involved in any way, the service instance name (or, as mentioned, a possibly better description) should be used. Likewise, if you log anything, you'll want to use the service instance name to identify which exact instance of that service is being used. It is a subtle point, but understanding the distinction is vital.

Something for Nothing?

It is rightly said you cannot get something for nothing. Those of you who have worked with multicast network services know that there isn't just one magic channel that all the multicast traffic goes out on—you still have to co-ordinate the server and client to use the same multicast address and port. Because this still must be kept in sync, it is indeed worth asking whether you have gained anything or merely moved the mountain (slightly). Here's where the idea pays off: Even though different service applications will be running on different ports, the service discovery code only needs to run on one agreed-upon multicast channel. In other words, Although you have shifted the mountain slightly, you only really need to climb up it one time. (Furthermore, the same service cannot run on the same port inside the same machine... however, any number of programs can join the same multicast channel. This lets each service instance run on any port available, again without needing to coordinate what those port numbers actually are.)

To be fair, maximum pay-off only comes if there is a system-wide service running in the machine that all applications (server- and client-side) can interact with. Each server-side application talks to this service and registers itself when it starts up, handing over its instance name and connection parameters (and unregistering when it stops running). The requests that come in over the network are actually responded to via this system-global service. (This is what Bonjour and various work-alikes are.) Your own project might not be of sufficient importance to install and run your own operating system service. (Then again, maybe it is, but my own project wasn't.) However, if you trade down a little bit, each service instance can easily run its own query responder on an agreed-upon multicast channel. This still allows for the actual service itself to run on whichever dynamic port is available (or whichever one suits the whims of whomever installs and runs it) while allowing for clients to find the service without any advanced knowledge. You really do seem to get something for nothing out of the deal.

As is usually the case when dealing with connectionless datagram packets in general, the devil is in the (implementation) details. If a service runs its own responder, that will definitely need to exist in its own thread. Likewise, the clients need a little co-ordination to "wait a second or so" when gathering replies (if any) and possibly add a little prompt to involve the user if multiple replies do come back. I will not be discussing how to handle the user interface side of the client. (However, you might want to refer to another article of mine that discusses coordination of tasks in a Swing application.)

Details

By way of a reminder, this "automagic" service discovery technique employs the use of a multicast address. The address range 224.0.0.0 through 239.255.255.255 (or alternately, 224.0.0.0/4) is a special range of IP addresses set aside for multicast use. They cannot be assigned to a specific host. (This being said, these addresses still have ports.) Furthermore, a handful of addresses in the 224.0.0.x address space are already reserved for specific use. You can get some good information and links by reading this wikipedia entry. You should also refer to the Java API documentation for the MulticastSocket class. (The Socket and ServerSocket classes are probably more familiar, but those are not used for multicast traffic.) Also, you might want to refer to the Java API documentation for the DatagramPacket class, which forms the transmission container for UDP data.

Just to give a brief example, multicast networking code looks as follows:

...
MulticastSocket socket = new MulticastSocket(9999);
InetAddress address = InetAddress.getByName(230.0.0.1);
socket.joinGroup(address);
...
DatagramPacket inboundPacket, outboundPacket;
...
socket.receive(inboundPacket);
...
socket.send(outboundPacket);
...

The sample code I will be working with uses the 230.0.0.1 address and the 4321 port. This is a rather arbitrary choice, and can easily be changed as needed. Also, instead of giving a step-by-step explanation of my particular implementation for the server-side responder code and client-side browser code, I will rather be showing a simple "time server" application and client, and then the steps needed to connect both sides to the provided implementation. It is left as an exercise to the reader to poke around in source code, but the biggest pieces involve rather vanilla networking and threading code.

Time Server Example: Starting Point

Just to keep the demo simple, consider you want to write a TCP-based network service that, when connected to, immediately emits the machine's current time to the client and then disconnects. This is rather elementary networking code on both sides. First, the server:

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;

public class TimeServer {
  public static void main(String[] args) {

    ServerSocket serverSocket = null;

    /* section one */
    try {
      serverSocket = new ServerSocket();
      serverSocket.bind(
            new InetSocketAddress(
                  InetAddress.getLocalHost(),
                  9999));
    }
    catch (IOException ioe) {
      System.err.println(
            "Could not bind a server socket to port 9999: "+ioe);
      System.exit(1);
    }

    /* section two */
    System.out.println("Server is now taking connections...");
    while (true) {
      try {
        Socket socket = serverSocket.accept();
        System.out.println("Connection from: "+
                           socket.getInetAddress());
        OutputStreamWriter writer = new OutputStreamWriter(
              socket.getOutputStream());
        writer.write(new Date().toString()+"\r\n");
        writer.flush();
        socket.close();
      }
      catch (IOException ie) {
        System.err.println("Exception: "+ie);
      }
    }

  }

}

And now the client:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.Socket;

public class TimeClient {
  public static void main(String[] args) {
    try {
      Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
      InputStreamReader reader =
          new InputStreamReader(socket.getInputStream());
      BufferedReader bufferedReader = new BufferedReader(reader);
      String line = bufferedReader.readLine();
      System.out.println(line);
      socket.close();
    }
    catch (IOException ie) {
      System.err.println("Exception: "+ie);
      System.exit(1);
    }

    System.out.println("\nThat's all, folks.");
    System.exit(0);
  }
}

This is a completely straightforward, no-frills TCP network server and client application. The important point to note is the hard-coded port, 9999, in both the server and the client, and the hard-coded server address (that for the localhost, in this case) in the client. Although these values can be put into property or configuration files, they are still effectively static, fixed values. If the server, running on one machine, changes port numbers, but the client isn't aware of this, it will just look like the server is unavailable.

But, with just a little addition using a few classes (that you can download and experiment with on your own), making things be completely dynamic is fairly simple. First is the new server code:

  public static void main(String[] args) {

    ServerSocket serverSocket = null;
    String SERVICE_NAME  = "timeDemo";
    String INSTANCE_NAME = "Time_Server_1";
    
    try {
      serverSocket = new ServerSocket();
      serverSocket.bind(
          new InetSocketAddress(InetAddress.getLocalHost(),
          0));    /*bind to any available port number*/
    }
    catch (IOException ioe) {
      System.err.println(
          "Could not bind a server socket to a free port: "+ioe);
      System.exit(1);
    }
    
    /* Create a descriptor for the service you are providing. */
    ServiceDescription descriptor = new ServiceDescription();
    descriptor.setAddress(serverSocket.getInetAddress());
    descriptor.setPort(serverSocket.getLocalPort());
    descriptor.setInstanceName(INSTANCE_NAME);
    System.out.println("Service details: "+descriptor);

    /* Read special note for code you should add here */

    /*
     * Set up a responder and give it the descriptor (above)
     * we want to publish. Start the responder, which
     * works in its own thread.
     */
    ServiceResponder responder =
        new ServiceResponder(SERVICE_NAME);
    responder.setDescriptor(descriptor);
    responder.startResponder();

    /* Back to the usual routine of servicing requests */
    System.out.println(
        "Responder listening. Now taking connections...");
    while (true) {
      try {
        Socket socket = serverSocket.accept();
        System.out.println(
            "Connection from: "+socket.getInetAddress());
        OutputStreamWriter writer =
            new OutputStreamWriter(socket.getOutputStream());
        writer.write(new Date().toString()+"\r\n");
        writer.flush();
        socket.close();
      }
      catch (IOException ie) {
        System.err.println("Exception: "+ie);
      }
    }

The important aspects of this change are that, once the service knows the address and port it is running on, a "descriptor" object is created. (This object is just a plain data container.) Then, a "responder" is created and passed the descriptor object for the service. This responder forms the server-side half of the service discovery code. Running in its own thread, it binds to an internally configured multicast channel, listens for lookup queries, and sends back a reply containing the information in this descriptor. That's about it. The rest of the Time Server runs exactly as it did before: listening for connections on the port it is running on, and pushing across the current time.

The comment in the modified server code mentions a special note. At this location, you should really arrange for the server to make its own lookup query (you'll see how in a moment) to check that the instance name it wants to use has not yet been claimed. (If it has, the instance name should be modified, and the check repeated as needed.) I've omitted this partly for clarity, and partly to not duplicate what you will see in the client code.

Now, you have the code modifications for the client.

public class TestClient implements ServiceBrowserListener {

  public static final String SERVICE_NAME = "timeDemo";

  public static void main(String[] args) {
    new TestClient();
  }

  ServiceBrowser browser;
  Vector<ServiceDescription> descriptors;
  
  TestClient() {
    descriptors = new Vector<ServiceDescription>();

    /* first browse for any 'timeDemo' instance */
    browser = new ServiceBrowser();
    browser.addServiceBrowserListener(this);
    browser.setServiceName(SERVICE_NAME);
    browser.startListener();
    browser.startLookup();
    System.out.println(
        "Browser started. Will search for 2 secs.");
    try {
      Thread.sleep(2000);
    }
    catch (InterruptedException ie) {
      // ignore
    }
    browser.stopLookup();
    browser.stopListener();
    
    /* now if the browser found any matches, we'll
     * print out the complete list, but only connect
     * to the first one.
     */
    if (descriptors.size()>0) {

      System.out.println("\n---TIME SERVERS---");
      for (ServiceDescription descriptor : descriptors) {
        System.out.println(descriptor.toString());
      }

      System.out.println("\n---FIRST SERVER'S TIME IS---");
      ServiceDescription descriptor = descriptors.get(0);
      try {
        Socket socket = new Socket(descriptor.getAddress(),
                                   descriptor.getPort());
        InputStreamReader reader =
           new InputStreamReader(socket.getInputStream());
        BufferedReader bufferedReader =
           new BufferedReader(reader);
        String line = bufferedReader.readLine();
        System.out.println(line);
        socket.close();
      }
      catch (IOException ie) {
        System.err.println("Exception: "+ie);
        System.exit(1);
      }
    }
    else {
      System.out.println("\n---NO TIME SERVERS FOUND---");
    }
    
    System.out.println("\nThat's all folks.");
    System.exit(0);
  }

  /* This is an event call-back method the browser uses
   * when it receives a response. This will only be
   * called during the two-second execution window.
   */
  public void serviceReply(ServiceDescription descriptor) {
    int pos = descriptors.indexOf(descriptor);
    if (pos>-1) {
      descriptors.removeElementAt(pos);
    }
    descriptors.add(descriptor);
  }

}

The structure is a little bit different this time just to satisfy needing an instance method to implement the ServiceBrowserListener interface. (However, you could write an inner/anonymous implementation and keep everything entirely in the static 'main' method, if you want.) Before connecting, the client first needs to run the discovery browser to poll the network for available services. After letting the browser run for a little while, the browser is stopped, and the vector of responses is displayed. (The vector stores the same descriptor objects that the server-side code uses.) The first response in the vector is used to make a direct connection, just as before.

And, there you have it. You can run as many Time Server instances on the network as you want (just use different instance names for each). You can run them on any port you want, and you can bring the servers up and down as desired. Whenever this client runs, it will look for any instance that responds to the browse query.

Conclusion

Adding dynamic service discovery to an application actually can be a fairly simple matter. The browser/responder code only needs to be written once, and then it can be packaged along with your client and server application components time after time. (And I've already given you a good, basic starting point for this code.)

Other Directions

If you skipped the Concept section, you should note there are a few extra things that the browser/responder can incorporate to add a little more robustness and efficiency. One of these is "known answer suppression." This requires a little variation in the packets the browser sends out to include a list of those instances it already knows about, because the browse period is likely (and, for practical purposes, should be) long enough to send out a few browse requests. It also requires a corresponding variation in the responder code to look for you "known answer" tokens in the browse requests so it can ignore them. Another nice touch is to have the server send out an "available/not-available" announcement when it starts up or when it shuts down. (Catching a control-C signal can be arranged using the Runtime.addShutdownHook method.) Clients that watch for these extra announcements can maintain an even more up-to-date list of available servers. (In a chat application, you would see someone immediately join or leave, for example.) Finally, if you start making a lot of use of discovery code, and you have a lot of server and client nodes, you might consider a known-answer caching mechanism (with some extra intelligence to allow these caches to gradually expire, and the like). You even can have client browsers running in a passive mode to cache responses that were triggered by other browse requests in more active clients. I'll leave implementation of these ideas to the reader.

Download the Code

You can download the code that accompanies this article here.

About the Author

Rob Lybarger is a software guy (and a degreed aerospace engineer) in the Houston, TX area who has been using Java for nearly ten years. He has used various versions of Windows, various distributions of Linux, but loves Mac OS X. He likes exploring new techniques and new approaches to organizing and wiring applications, and loves to let the computer do as much work for him as possible.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date