Dynamic Service Discovery with Java
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.)
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.