20.1. An Overview: Using drb
A drb application has two basic componentsa server and a client. A rough breakdown of their responsibilities is as follows:
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 (a 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"
Since 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 may 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, then two or more clients have the potential to interfere with each other. To avoid this, we recommend a straightforward solution using an asynchronization mechanism such as a Mutex. (Refer to Chapter 13, "Threads in Ruby," for more on threads and synchronization issues.)
Let's look at security at least a little. After all, you may not want just any old client to connect to your server. You can't prevent clients 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 an 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). The following is an example:
require "drb/acl" acl = ACL.new( %w[ deny all allow 192.168.0.* allow 126.96.36.199 allow localhost] )
The first entry deny all is arguably 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 by the server.
There is somewhat more to drb than we cover here. But this is enough for a good overview. In the next section, we'll look at a simple drb server and client that are almost real-world code. We'll also look at Rinda and Ring before we close the chapter.