DRb allows Ruby programs to communicate with each other on the same machine or over a network. DRb uses remote method invocation (RMI) to pass commands and data between processes.
We'll start with a simple example DRb service:
Download simple_service.rb
#!/usr/bin/env ruby -w
# simple_service.rb
# A simple DRb service
# load DRb
require 'drb'
# start up the DRb service
DRb.start_service nil, []
# We need the uri of the service to connect a client
puts DRb.uri
# wait for the DRb service to finish before exiting
DRb.thread.join
Now lets go through this line by line. First we load DRb, this should be familiar to everyone. Next we tell DRb to start up. The first nil argument can be a DRb URI, which looks like "druby://localhost:2250". If it is nil, DRb uses the first open port. The second argument is an optional service to provide, in this case an Array. After starting the service, we print the URI to the console. Finally we join the DRb thread. The DRb thread won't exit until an interrupt is sent (such as a ^C).
So what does this really do? Well, it opens up a socket on your machine, and starts listening for requests. If it gets a request, it'll hand back a reference to that Array up there. At that point, the client can do whatever it wants with the Array.
So lets write a client:
#!/usr/bin/env ruby -w
# simple_client.rb
# A simple DRb client
require 'drb'
DRb.start_service
# attach to the DRb server via a URI given on the command line
remote_array = DRbObject.new nil, ARGV.shift
puts remote_array.size
remote_array
puts remote_array.size
If you run simple_server.rb then take the URI it gives you and pass it into simple_client.rb, you'll get the following output:
0
1
How does this work? DRbObject.new nil, ARGV.shift
connects to the URI given on the pathname and connects to the Array there, and acts as a proxy to it. Any calls you make to the DRbObject get passed across to the Array you attached to on the server side.
Now what happens if you run simple_client.rb again with the same URI?
1
2
That's right, its a single instance of the array that is shared among all clients. You can attach any number of clients to the DRb service and have them all work on that one object. (Before you get all up in arms about concurrency issues, we'll get to an excellent way to handle concurrency called a TupleSpace in a future article.)
Now you might be wondering why we had to put DRb.start_service in the client. The truth is that every DRb client is also a service. Lets look at another example that calculates the distance between to points:
#!/usr/bin/env ruby -w
# server
require 'drb'
class DistCalc
def find_distance(p1, p2)
Math.sqrt((p1.x - p2.x)**2 + (p1.y - p2.y)**2)
end
end
DRb.start_service nil, DistCalc.new
puts DRb.uri
DRb.thread.join
#!/usr/bin/env ruby -w
# client
require 'drb'
Point = Struct.new 'Point', :x, :y
class Point
include DRbUndumped
def to_s
"(#{x}, #{y})"
end
end
DRb.start_service
dist_calc = DRbObject.new nil, ARGV.shift
p1 = Point.new 0, 0
p2 = Point.new 1, 0
puts "The distance between #{p1} and #{p2} is #{dist_calc.find_distance p1, p2}"
Here the client provides two points to the server, and uses DRbUnumped to present a DRbObject to the server, rather than sending the point across the wire, creating a copy on each side. When the server asks for each point's x and y value, it connects back to the client to retrieve them.
This way the an object is always stored in only one place. If the server were to save the points, then recalulate them again later, the distance would still be accurate, since there is only one instance of each point in the system.
DRbUndumped can also be used to prevent large structures from being transfered. As an example, here is a jukebox server, and a tabletop jukebox console. The tabletop consoles will allow people to select songs to play from their tables.
The server has a list of songs, and the tabletop consoles are able to read those songs and select one to play. It doesn't make sense for the tabletop consoles to pull the entire song across the connection, so we'll only define it on the server.
#!/usr/bin/env ruby -w
# Jukebox Server
require 'drb'
Song = Struct.new 'Song', :title, :artist
class Song
def to_s
"#{title} by #{artist}"
end
end
class Jukebox
attr :songs
def initialize(songs)
@songs = songs
end
def play(index)
puts "playing #{@songs[index]}"
end
end
songs = [
Song.new("Amish Paradise", "Weird Al"),
Song.new("Eat it", "Weird Al")
]
DRb.start_service nil, Jukebox.new(songs)
puts DRb.uri
DRb.thread.join
#!/usr/bin/env ruby -w
# Tabletop client
require 'drb'
DRb.start_service
jukebox = DRbObject.new nil, ARGV.shift
loop do
puts "Select a song:"
jukebox.songs.each_with_index do |s, i|
puts "#{i + 1}) #{s}"
end
print "> "
STDOUT.flush
index = gets.to_i
begin
jukebox.play(index)
puts "Playing: #{jukebox.songs[index]}"
rescue
puts "Invalid selection"
retry
end
end
This looks good, but if you run the server then the client, you'll get an error message like this:
$ ruby client.rb druby://localhost:49812
Select a song:
/Users/drbrain/lib/ruby/drb/drb.rb:124:in `load': undefined class/module Struct::Song (ArgumentError)
from /Users/drbrain/lib/ruby/drb/drb.rb:124:in `load'
from /Users/drbrain/lib/ruby/drb/drb.rb:165:in `recv_reply'
from /Users/drbrain/lib/ruby/drb/drb.rb:299:in `send_message'
from /Users/drbrain/lib/ruby/drb/drb.rb:226:in `method_missing'
from /Users/drbrain/lib/ruby/drb/drb.rb:225:in `open'
from /Users/drbrain/lib/ruby/drb/drb.rb:225:in `method_missing'
from client.rb:11
from client.rb:8:in `loop'
from client.rb:27
When the client retrieves a song and tries to load it, the Song class isn't defined on the client. Since DRb uses Marshall to load and dump objects when sending them between client and server, the class must be defined on both sides of the connection.
There are two ways to resolve this, the obvous one is to define the Song class on both the client side, and the server side. The other way is to use include DRbUndumped on one side of the connection, and hand the other end a DRbObject.
If we add DRbUndumped, our extension to class Song will look like this:
class Song
include DRbUndumped
def to_s
"#{title} by #{artist}"
end
end
The client first attaches a DRbObject to the Jukebox instance on the server. Next the client asks for the songs on the Jukebox, and instead of getting back a Song instance, it instead gets back a DRbObject, since the Song class is now undumpable.