Less is more. Robert Browning, "Andrea del Sarto" There are a plethora of technologies today that enable distributed computing. These include various flavors of RPC, as well as such things as COM, CORBA, DCE, and Java's RMI. These all vary in complexity, but they all do essentially the same thing. They provide relatively transparent communication between objects in a networking context so that remote objects can be used as though they were local. Why should we want to do something like this in the first place? There might be many reasons. One excellent reason is to share the burden of a computing problem between many processors at once. An example would be the SETI@home program, which uses your PC to process small data sets in the search for extraterrestrial intelligence. (SETI@home is not a project of the SETI Institute, by the way.) Another example would be the grassroots effort to decode the RSA129 encryption challenge (which succeeded a few years ago). There are countless other areas where it is possible to split a problem into individual parts for a distributed solution. It's also conceivable that you might want to expose an interface to a service without making the code itself available. This is frequently done via a Web application, but the inherently stateless nature of the Web makes this a little unwieldy (in addition to other disadvantages). A distributed programming mechanism makes this kind of thing possible in a more direct way. Ruby's answer to this challenge is drb, or distributed Ruby by Masatoshi Seki. (The name is also written DRb.) It doesn't have such advanced facilities as CORBA's naming service, but it is a simple and usable library with all the most basic functionality you would need. An Overview: Using drb A drb application has two basic componentsa server and a client. A rough breakdown of their responsibilities is given here. The server: Starts a TCPServer and listens on a port. Binds an object to the drb server instance. Accepts connections from clients and responds to their messages. May optionally provide access control (security). The client: Establishes a connection to the server process. Binds a local object to the remote server object. Sends messages to the server object and gets responses. The class method start_service takes care of starting a TCP server that listens on a specified port; it takes two parameters. The first is a URI (Universal Resource Identifier) specifying a port. (If it is nil, a port will be chosen dynamically.) The second is an object to which we want to bind. This object will be remotely accessible by the client, invoking its methods as though it were local.
require "drb" myobj = MyServer.new DRb.start_service("druby://:1234", myobj) # Port 1234 # ... If the port is chosen dynamically, the class method uri can be used to retrieve the full URI, including the port number.
DRb.start_service(nil, myobj) myURI = DRb.uri # "druby://hal9000:2001" Because drb is threaded, any server application will need to do a join on the server thread (to prevent the application from exiting prematurely and killing the thread).
# Prevent premature exit DRb.thread.join On the client side, we can invoke start_service with no parameters, and use DRbObject to create a local object that corresponds to the remote one. We typically use nil as the first parameter in creating a new DRbObject.
require "drb" DRb.start_service obj = DRbObject.new(nil, "druby://hal9000:2001") # Messages passed to obj will get forwarded to the # remote object on the server side... We should point out that on the server side, when we bind to an object, we really are binding to a single object which will answer all requests that it receives. If there is more than one client, we will have to make our code thread-safe to avoid that object somehow getting into an inconsistent state. (For really simple or specialized applications, this might not be necessary.) We can't go into great detail here. Just be aware that if a client both reads and writes the internal state of the remote object, two or more clients have the potential to interfere with each other. To avoid this, we recommend a straightforward solution using some kind of synchronization mechanism like a Mutex. (Refer to Chapter 7 for more on threads and synchronization issues.) We will say at least a few words about security. After all, you may not want just any old client to connect to your server. You can't prevent them from trying, but you can prevent their succeeding. Distributed Ruby has the concept of an access control list, or ACL (often pronounced to rhyme with "crackle"). These are simply lists of clients (or categories of clients) that are specifically allowed (or not allowed) to connect. Here is a little example. We use the ACL class to create a new ACL, passing in one or two parameters. The second (optional) parameter to ACL.new answers the question, "Do we deny all clients except certain ones, or allow all clients except certain ones?" The default is DENY_ALLOW, represented by a 0; ALLOW_DENY is represented by a 1. The first parameter for ACL.new is simply an array of strings; these strings are taken in pairs, where the first in the pair is deny or allow, and the second represents a client or category of clients (by name or address). Here is an example:
acl = ACL.new( %w[ deny all allow 192.168.0.* allow 210.251.121.214 allow localhost] ) The first entry deny all is somewhat redundant, but it does make the meaning more explicit. Now how do we use an ACL? The install_acl method will put an ACL into effect for us. Note that this has to be done before the call to the start_service method, or it will have no effect.
# Continuing the above example... DRb.install_acl(acl) DRb.start_service(nil, some_object) # ... When the service then starts, any unauthorized client connection will result in a RuntimeError being thrown on the server side (with the message "Forbidden"). There is somewhat more to drb than we cover here. But this is enough for an overview. We'll also mention that drb comes with a module (rinda.rb) that does much the same as Sun's Javaspaces, the basis of Jini. (The name is a pun based on Linda, the technology underlying Javaspaces.) For those who are more interested in CORBA, there are efforts by another Japanese developer to produce a complete Ruby-CORBA mapping. At present, there is already an interface definition, an IDL compiler called Ridl, and Ruby-ORBit, which provides a wrapper for ORBit. We are far out of our depth here; but if this appeals to you, you can refer to the Rinn project in the Ruby Application Archive. Case Study: A Stock Ticker Simulation This example is taken from the Pragmatic Programmers' Ruby course (used by permission). Here we're assuming that we have a server application that is making stock prices available to the network. Any client wanting to check the value of his thousand shares of Gizmonic Institute can contact this server. There is a small twist to this, however. We don't just want to watch every little fluctuation in the stock price. We've implemented an Observer module that will let us subscribe to the stock feed; the client then watches the feed and warns us only when the price goes above or below a certain value. First let's look at the DrbObservable module. This is a straightforward implementation of the Observer pattern, another design pattern from the "Gang of Four's" Design Patterns. This is also known as the Publish-Subscribe pattern. This module is actually an adaptation of the standard observer.rb library. It has been changed so that it does not abort on an error. Listing 9.13 defines an observer as an object that responds to the update method call. Observers are added (by the server) at their own request, and are sent information via the notify_observers call. Listing 9.13 The drb Observer Module module DRbObservable def add_observer(observer) @observer_peers ||= [] unless observer.respond_to? :update raise NameError, "observer needs to respond to `update'" end @observer_peers.push observer end def delete_observer(observer) @observer_peers.delete observer if defined? @observer_peers end def notify_observers(*arg) return unless defined? @observer_peers for i in @observer_peers.dup begin i.update(*arg) rescue delete_observer(i) end end end end The server (or feed) in Listing 9.14 simulates the stock price by a sequence of pseudorandom numbers. (This is as good a simulation of the market as we have ever seen, if you will pardon the irony.) The stock symbol identifying the company is only used for cosmetics in the simulation, and has no actual purpose in the code. Every time the price changes, the observers are all notified. Listing 9.14 The drb Stock Price Feed (Server) require "drb" require "drb_observer" # Generate random prices class MockPrice MIN = 75 RANGE = 50 def initialize(symbol) @price = RANGE / 2 end def price @price += (rand() - 0.5)*RANGE if @price < 0 @price = -@price elsif @price >= RANGE @price = 2*RANGE - @price end MIN + @price end end class Ticker # Periodically fetch a stock price include DRbObservable def initialize(price_feed) @feed = price_feed Thread.new { run } end def run lastPrice = nil loop do price = @feed.price print "Current price: #{ price} \n" if price != lastPrice lastPrice = price notify_observers(Time.now, price) end sleep 1 end end end ticker = Ticker.new(MockPrice.new("MSFT")) DRb.start_service('druby://localhost:9001', ticker) puts 'Press [return] to exit.' gets Not surprisingly, the client (in Listing 9.15) begins by contacting the server. It gets a reference to the stock ticker object and sets its own desired values for the high and low marks. Then the client will print a message for the user every time the stock price goes above the high end or below the low end. We should mention the concept behind the DrbUndumped module. Very little code is actually associated with this module; basically it defines a _dump method that raises an exception when it is called. In other words, it prevents an object from being dumped. Any object obj of a class that includes this module will thus return true for an expression like obj.kind_of? DRbUndumped. Refer to Chapter 1, "Ruby in Review," (or Chapter 5, "OOP and Dynamicity in Ruby") if this is unclear. For those familiar with Java, this is like the opposite of the Serializable interface. In short, drb by default passes objects by value (that is, via marshalling). But by including DRbUndumped, we can pass objects by referencewhen we want to expose an interface to an object we are managing (or when an object simply cannot be marshalled for whatever reason). Listing 9.15 The drb Stock Price Watcher (Client) require "drb" class Warner include DRbUndumped def initialize(ticker, limit) @limit = limit ticker.add_observer(self) # all warners are observers end end class WarnLow < Warner def update(time, price) # callback for observer if price < @limit print "- #{ time.to_s} : Price below #@limit: #{ price} \n" end end end class WarnHigh < Warner def update(time, price) # callback for observer if price > @limit print "+++ #{ time.to_s} : Price above #@limit: #{ price} \n" end end end DRb.start_service ticker = DRbObject.new(nil, "druby://localhost:9001") WarnLow.new(ticker, 90) WarnHigh.new(ticker, 110) puts "Press [return] to exit." gets There are other ways to approach this problem. But we feel that this is a good solution that well demonstrates the simplicity and elegance of distributed Ruby. |