Home > Uncategorized > Discovering services using Bonjour, DNSSD, and Ruby

Discovering services using Bonjour, DNSSD, and Ruby

June 3rd, 2009

This is the third part in a series of articles I’ve been writing on using Zeroconf (aka Bonjour) in Ruby.  The first post covered Zeroconf and how to install DNSSD, the most popular Ruby Zeroconf libary. The second post went through creating and registering a web service using Zeroconf so other machines on your network can find it.

In this post we’ll digress a bit and write a command-line tool to monitor and explore the information Zeroconf broadcasts on your network.  This will give us some background for the next post when we modify our web server to list other servers of a similar kind running on your network.  If you’re already familiar with how to browse and retrieve information about Zeroconf services using DNSSD then you may want to skip ahead.

Before I get too carried away, there are just a few notes about the require statements I’ll use in my examples.  As with all Ruby Zeroconf clients I will by requiring the DNSSD library.  Older versions of DNSSD seem to have compatibility issues when running on Linux so I always require DNSSD 0.7.1 (the latest at the time of this writing.)

We’re now ready to start browsing the network for Zeroconf services.  DNSSD supports browsing the network using either a synchronous or asynchronous API.  The synchronous API is new in version 0.7.1 and has the disadvantage that using it locks up your main application thread while it searches the network.  I haven’t used it in my applications and will only be covering the asynchronous API in this tutorial.  See the DNSSD 0.7.1 rdoc for an example of using the synchronous browsing API.

DNSSD.browse() is the core function to understand when browsing the network using DNSSD.  Given a Zeroconf service type  DNSSD.browse() will provide a DNSSD::Reply to a block of code which will be called each time a service is identified on the network.  It also returns a reference to a DNSSD::Service object which can be used to control the and stop the browsing.

For example let’s look at the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
require 'rubygems'
gem 'dnssd', '0.7.1'
require 'dnssd'
 
# Browse the network for web servers (_http._tcp services)
puts "Scanning network for web servers"
service = DNSSD.browse('_http._tcp.') do |reply|
 puts "received: #{reply.inspect}"
end
 
# Catch the interrupt signal
interrupted = false
Signal.trap("INT") do
  interrupted = true
end
 
# Run until the application is interrupted
loop do
  if interrupted
    puts "Halting network scan"
    service.stop
    exit
  end
end

On line 7, we call DNSSD.browse() and get back a reference to the DNSSD::Service object we can use to control the browsing.  We pass into DNSSD.browse() the kind of Zeroconf service we’re looking for (_http._tcp) along with a block of code which will print out the details of each service we find.  Inside that block of code we can access the service details using the reply object which is an instance of DNSSD::Reply.

The rest of the code in this example just runs the application until the user presses CTRL+C which sends it an interrupt signal which will terminate the application.  The application clean-up is handled on lines 20-22.  Notice the call to service.stop on line 21.  This tells the DNSSD::Service we got back on line 7 to stop running as we’re done with it.

Save and run this code, then try running your strawberry ice cream web service from the previous post at the same time in another window. You should see a message like:

received: #<DNSSD::Reply Strawberry\032Ice\032Cream\032\0431._http._tcp.local>

If you stop the web service you’ll see the message:

received: #<DNSSD::Reply Strawberry\032Ice\032Cream\032\0431._http._tcp.local>

Huh? That’s not what we expected! Why do we see the web server again when it’s shutting down?

It turns out that Zeroconf services broadcast when registering and then again when shutting down.  We can use the DNSSD::Reply’s flags attribute to determine whether the service is being added or should be removed.  The flags attribute is an instance of DNSSD::Flags and provides details about the state of the Zeroconf service.  Let’s modify our code to print out the flags it receives with each broadcast:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
require 'rubygems'
gem 'dnssd', '0.7.1'
require 'dnssd'
 
# Browse the network for web servers (_http._tcp services)
puts "Scanning network for web servers"
service = DNSSD.browse('_http._tcp.') do |reply|
 puts "received: #{reply.inspect}"
 puts "\tflags: #{reply.flags.inspect}"
end
 
# Catch the interrupt signal
interrupted = false
Signal.trap("INT") do
  interrupted = true
end
 
# Run until the application is interrupted
loop do
  if interrupted
    puts "Halting network scan"
    service.stop
    exit
  end
end

Run this new code in one window and try starting and stopping your web server in another.  You should now see a message which looks like this when the web server starts:

received: #<DNSSD::Reply Strawberry\032Ice\032Cream\032\0431._http._tcp.local>
    flags: #<DNSSD::Flags add>

and like this when the web server stops:

received: #<DNSSD::Reply Strawberry\032Ice\032Cream\032\0431._http._tcp.local>
    flags: #<DNSSD::Flags>

As you can see new services have the DNSSD::Flags::Add constant set.  Services that are being shutdown don’t have any flags.

With this in mind let’s modify our code one more time to show when services are added and removed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
require 'rubygems'
gem 'dnssd', '0.7.1'
require 'dnssd'
 
# Browse the network for web servers (_http._tcp services)
puts "Scanning network for web servers"
service = DNSSD.browse('_http._tcp.') do |reply|
  if (reply.flags == DNSSD::Flags::Add)
      puts "adding: #{reply.inspect}"
  else
      puts "removing: #{reply.inspect}"
  end
end
 
# Catch the interrupt signal
interrupted = false
Signal.trap("INT") do
  interrupted = true
end
 
# Run until the application is interrupted
loop do
  if interrupted
    puts "Halting network scan"
    service.stop
    exit
  end
end

Run this code again and try starting and stopping your web server.  Now instead of some cryptic flags you should see a friendly message indicating when services are being added and removed!

The last thing I’ll talk about in this post is how to resolve the the address of the web server from the DNSSD::Reply.  If you’ve read the DNSSD rdoc already you may have noticed that DNSSD::Reply has attributes we haven’t used yet.  The reason we haven’t used them is because most of them aren’t initialised when you get a reply back from the browse() method.  Instead the reply we get back has just enough information to tell us if the service is new, and for us to use the DNSSD.resolve() method to get all it’s details.

Like DNSSD.browse() the DNSSD.resolve() method is asynchronous and passes a DNSSD::Reply into a block when it’s done executing.  The DNSSD::Reply we get back from resolve() is fully-populated and gives us access to things like the target (host name) where the service is running.

Let’s look at a final example which will show us a complete example of how to browse for web services on your network using Ruby and Zeroconf. The changes happen on line’s 11-25. We’re now taking the reply from the DNSSD.browse() method and using it’s name, type, and domain to resolve the details of the service in a new thread. Since Thread#kill and timeout.rb are broken we’ll let the browsing thread sleep for a few seconds and then use the resolver service handle to stop the resolver whether it’s found any details or not.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
require 'rubygems'
gem 'dnssd', '0.7.1'
require 'dnssd'
 
# Browse the network for web servers (_http._tcp services)
puts "Scanning network for web servers"
service = DNSSD.browse('_http._tcp.') do |reply|
  if (reply.flags == DNSSD::Flags::Add)
    puts "adding: #{reply.inspect}"
 
    # Let's lookup the details
    resolver_service = DNSSD.resolve(reply.name, reply.type, reply.domain) do |resolved|
      puts "\tdomain = #{resolved.domain}"
      puts "\tflags = #{resolved.flags.inspect}"
      puts "\tfullname = #{resolved.fullname}"
      puts "\tinterface = #{resolved.interface}"
      puts "\tname = #{resolved.name}"
      puts "\tport = #{resolved.port}"
      puts "\tservice = #{resolved.service}"
      puts "\ttarget = #{resolved.target}"
      puts "\ttext_record = #{resolved.text_record.inspect}"
      puts "\ttype = #{resolved.type}"
    end
    sleep(2)
    resolver_service.stop
  else
      puts "removing: #{reply.inspect}"
  end
end
 
# Catch the interrupt signal
interrupted = false
Signal.trap("INT") do
  interrupted = true
end
 
# Run until the application is interrupted
loop do
  if interrupted
    puts "Halting network scan"
    service.stop
    exit
  end
end

And that’s it!  You’re using ruby and Zeroconf like a pro.  Let’s hope you remember all this because we’ll use it in the next post to find out what kinds of ice cream the other people on your network enjoy!

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Live
  • Ping.fm
  • Reddit
  • StumbleUpon
  • TwitThis

Mark Uncategorized , , , , , , , ,

Comments are closed.