Ruby 编程/标准库/DRb
分布式 Ruby (DRb) 通过实现 远程过程调用 允许 Ruby 程序之间进行进程间通信。
分布式 Ruby 为 Ruby 启用了远程方法调用。它是标准库的一部分,因此您可以预期它在大多数使用 MRI Ruby 的系统上安装。由于底层对象序列化依赖于 Marshal,它是在 C 中实现的,因此可以预期到很好的速度。
让我们从一个简单的例子开始,这样这个模块的使用就变得清晰了。
这是 server.rb
,我们在这里创建一个对象的单个实例(在本例中为 Hash)并在 TCP 端口 9000 上共享它。
# Load the DRb library
require 'drb'
# Create the front object
myhash = { counter: 0 }
def myhash.inc(elem)
self[elem] += 1
end
# Start the service
DRb.start_service('druby://127.0.0.1:9000', myhash)
# Make the main thread wait for the DRb thread,
# otherwise the script execution would already end here.
DRb.thread.join
这是 client.rb
require 'drb'
# We create a DRbObject instance that is connected to our server.
# All methods executed on this object will be executed to the remote one.
obj = DRbObject.new(nil, 'druby://127.0.0.1:9000')
puts obj[:counter]
obj.inc(:counter)
puts obj[:counter]
puts "Last access time = #{obj[:lastaccess]}"
obj[:lastaccess] = Time.now
在一个 shell 会话中启动服务器(或在后台),并在另一个会话中运行客户端几次
$ ruby client.rb 0 1 Last access time = $ ruby client.rb 1 2 Last access time = Fri Oct 22 22:23:59 BST 2004
服务器和客户端不需要在同一台机器上运行。如果您希望服务器监听所有接口(因此也监听远程连接),则需要在 server.rb
中将 'localhost' 更改为 '0.0.0.0'。然后,需要通过在 client.rb
中用服务器的 IP(或主机名)替换 'localhost' 来配置客户端以连接到远程服务器。
即使只是这个简单的例子也极其强大。上面的对象可以用作 Web 服务器上会话数据的共享数据存储。每个网页请求都可以查找和存储此共享对象中的信息。它适用于 Web 页面是通过独立的 CGI 脚本、Webrick 线程、Apache mod_ruby 还是 fcgi/mod_fastcgi 提供服务的。它甚至适用于您拥有 Web 服务器集群的情况。此外,如果重新启动 Apache,会话数据不会丢失。
DRb 的设计实际上相当复杂而优雅,但基本原理非常简单
DRb 将方法调用打包成一个包含方法名和参数的数组,使用 Marshal 库将其转换为字节流,并将其发送到服务器。然后,服务器在前端对象上执行调用以确定结果。收到的返回值和最终异常被放入另一个数组中,转换为字节流并返回给客户端。
由于 DRb 是用 Ruby 编写的,您可以查看代码,其中包含大量注释和示例。您可以在系统上的 /usr/local/lib/ruby/2.1/drb/drb.rb
等位置找到它,或者您可以在 此处 找到文档和示例的解析版本。
如果您使用 DRb 对象来存储会话数据,请确保只有 Web 服务器可以联系您的 DRb 对象,并且它不能从外部直接访问,否则不受欢迎的访客可以直接操纵其内容。如果您所有客户端都在同一台机器上,则可以将其绑定到 localhost (127.0.0.1);否则,您可以将其放在单独的专用网络上,使用防火墙规则或 DRb ACL 来阻止来自不受欢迎客户端的访问。在调用 DRb.start_service
之前执行此操作很重要。
ACL
的示例用法
require 'drb'
require 'drb/acl'
acl = ACL.new(%w{deny all
allow localhost
allow 192.168.1.*})
DRb.install_acl(acl)
DRb.start_service('druby://127.0.0.1:9000', obj)
请注意,每个对象都包含方法,如果被恶意方调用,这些方法可能非常危险。其中一些是私有的(例如 exec、system),DRb 阻止调用这些方法,但还有其他一些公共方法同样危险(例如 send、instance_eval、instance_variable_set)。例如,考虑 obj.instance_eval("`rm -rf /*`")
。
因此,与整个互联网共享对象是一件危险的事情。如果您要这样做,那么您应该至少使用 $SAFE=1
运行,并且您应该从一个空白状态启动您的对象,而不包含这些危险的方法。您可以像这样实现
class BlankSlate
safe_methods = [:__send__, :__id__, :object_id, :inspect, :respond_to?, :to_s]
(instance_methods - safe_methods).each do |method|
undef_method method
end
end
class MyService < BlankSlate
def increase_count
@count ||= 0
@count += 1
end
end
DRb.start_service('druby://127.0.0.1:9000', MyService.new)
请注意,此示例不使用 initialize()
来将 @count 设置为 0。如果它这样做了,客户端将能够通过调用 initialize
方法来重置 @count。
以下是来自 Evil-Ruby 的另一种实现。
# You can derivate your own Classes from this Class
# if you want them to have no preset methods.
#
# klass = Class.new(KernellessObject) { def inspect; end }
# klass.new.methods # raises NoMethodError
#
# Classes that are derived from KernellessObject
# won't call #initialize from .new by default.
#
# It is a good idea to define #inspect for subclasses,
# because Ruby will go into an endless loop when trying
# to create an exception message if it is not there.
class KernellessObject
class << self
def to_internal_type; ::Object.to_internal_type; end
def allocate
obj = ::Object.allocate
obj.class = self
return obj
end
alias :new :allocate
end
self.superclass = nil
end
此外,您可能希望构建一个包装器对象并共享它,而不是共享您的原始对象。包装器对象可以有一组有限的方法(仅您真正想共享的方法),验证传入数据的参数,并在数据被清理后委派给另一个对象。
通过 DRb 共享的对象收到的每个传入方法调用都在一个新线程中执行。如果您考虑一下,这是非常必要的;可能有许多客户端,服务器无法控制客户端何时决定向其发送方法调用。DRb 不序列化请求,因此一个客户端无法阻止其他客户端。
但是,这意味着您必须对 DRb 对象进行与在任何其他线程应用程序中一样多的谨慎。例如,考虑一下如果两个客户端同时决定运行会发生什么
obj[:counter] = obj[:counter] + 1
在同一时间。可能发生两个客户端都会检索 obj[:counter] 并看到相同的值(例如 100),然后独立地加 1,然后都写回 101。如果您希望 :counter 生成唯一的序列号,这可能不是您想要的。
即使页面顶部显示的 myhash.inc
方法也会遇到同样的问题,因为两个客户端可能会决定在同一时间调用 inc(:counter)
,导致服务器上的两个线程遇到相同的竞争条件。解决方法是用 Mutex
保护递增操作
require 'drb'
require 'thread'
class MyStore
def initialize
@hash = { :counter=>0 }
@mutex = Mutex.new
end
def inc(elem)
# The mutex makes sure that in case there being another thread running the
# block given to the synchronize method, the current thread will wait until the
# other thread finishes execution of this part, before it runs the block itself.
@mutex.synchronize do
self[elem] = self[elem].succ
end
end
def [](elem)
@hash[elem]
end
def []=(elem,value)
@hash[elem] = value
end
end
mystore = MyStore.new
DRb.start_service('druby://127.0.0.1:9000', mystore)
DRb.thread.join
为什么客户端运行 DRb.start_service
?
一个很好的问题,它引导我们了解 DRb 的另一个有趣方面。
在正常操作中,DRb 将使用 Marshal 发送方法调用的参数;当它们在服务器端被取消封送时,它将拥有这些对象的副本。对于从方法返回的结果也是如此;它将被封送、发回,客户端将拥有该对象的副本。
在许多简单情况下,对象的复制不是问题,但有几种情况下可能会出现问题
- 如果服务器对它接收到的本地副本进行更改,那么客户端将不会看到该更改。
- 参数或响应对象可能非常大,您可能不想来回发送它们(例如,一个对象,它包含对其他对象的引用,形成一棵树)
- 某些类型的对象根本无法封送:它们包括文件、套接字、proc/块、具有单例类的对象,以及任何间接包含这些对象的(例如,在实例变量中)对象。
在这些情况下,DRb 可以改为发送包含联系信息的“代理对象”,以允许通过 DRb 调用原始对象:即,可以找到原始对象的主机名和端口。这对于任何无法封送的对象都是自动完成的,或者您可以通过在您的对象中包含 DRbUndumped 来强制执行。
我们如何证明这一点?好吧,考虑在以下文件 foo.rb
中定义的类
class Foo
def initialize(x)
@x = x
end
def inc
@x = @x.succ
end
end
现在,让我们创建一个接受对象的服务器并调用它的 'inc' 方法
require 'drb'
require './foo'
class Server
def update(obj)
obj.inc
end
end
server = Server.new
DRb.start_service('druby://127.0.0.1:9001', server)
DRb.thread.join
这是相应的客户端
require 'drb'
require './foo'
DRb.start_service
obj = DRbObject.new(nil, 'druby://127.0.0.1:9001')
a = Foo.new(10)
b = Foo.new(20)
puts a
puts b
obj.update(a)
obj.update(b)
puts a
puts b
现在,如果我们运行它,会发生什么
$ ruby client2.rb #<Foo:0x817e760 @x=10> #<Foo:0x817e74c @x=20> #<Foo:0x817e760 @x=10> #<Foo:0x817e74c @x=20>
糟糕。我们传递了我们的对象 'a' 和 'b',但由于它们被复制到服务器上,只有本地副本被 'inc' 更新。客户端上的对象不受影响。
现在尝试像这样修改 Foo 的定义
class Foo
include DRbUndumped
# ... same as before
或者,您可以像这样修改客户端程序
a = Foo.new(10)
b = Foo.new(20)
a.extend DRbUndumped
b.extend DRbUndumped
# ... same as before
现在结果是我们希望看到的
$ ruby client2.rb #<Foo:0x817e648 @x=10> #<Foo:0x817e634 @x=20> #<Foo:0x817e648 @x=11> #<Foo:0x817e634 @x=21>
所以发生的事情是,我们没有封送 Foo 的实例,而是封送了构建代理对象所需的信息:它包含客户端的主机名、端口和对象 ID,可用于与原始对象进行通信。当我们将 'a' 的代理对象传递给服务器时,它调用 obj.inc,'inc' 方法调用通过 DRb 传回到客户端机器,原始对象 'a' 实际上位于那里。您实际上已经创建了对该对象的远程“引用”,它可以像普通对象引用一样传递,只不过它可以从一台机器传递到另一台机器。通过此引用的方法调用会影响同一个对象。
现在,这就是为什么客户端程序需要运行 DRb.start_service
的原因 - 即使从我们的角度来看它是“客户端”,也可能存在生成这些 DRb 代理“引用”的方法调用参数,此时客户端也会成为这些对象的服务器。
我们在这里没有指定主机或端口,因此 DRb 在系统上选择任何空闲的 TCP 端口,主机是系统根据 'gethostname' 调用确定的主机名 - 例如,如果机器名为 server.example.com,那么 DRb 可能会选择 druby://server.example.com:45123
但是,当两台机器之间存在防火墙时,这些双向方法调用可能会成为问题。您可以在 DRb.start_service 中选择客户端的固定端口,而不是动态选择端口;这样可以让您在防火墙中为 DRb 打开一个洞。但是,如果您在 NAT 防火墙后面,它几乎肯定无法正常工作。
通过 SSH 运行 DRb
[edit | edit source]解决通过防火墙进行双向方法调用的问题的一种方法是通过 SSH 运行 DRb。您不仅可以通过防火墙进行单个出站 TCP 连接获得双向操作;您的方法调用还被安全加密!
以下是设置方法。
- 为客户端端选择一个端口(例如 9000)和一个端口(例如 9001)
- 建立一对隧道 ssh 连接:客户端的端口 9001 重定向到服务器端的端口 9001,而服务器端的端口 9000 重定向到客户端端的端口 9000。
$ ssh -L9001:127.0.0.1:9001 -R9000:127.0.0.1:9000 server.example.com
-L 标志请求将本地(客户端)端端口 9001 的连接重定向到 ssh 隧道,然后重新连接到服务器端的 127.0.0.1:9001。-R 标志请求将远程(服务器)端端口 9000 的连接重定向回 ssh 隧道,然后连接到客户端端的 127.0.0.1:9000。 - 在服务器端,像往常一样执行 DRb.start_service('druby://127.0.0.1:9001', a)
- 在客户端端,执行 DRb.start_service('druby://127.0.0.1:9000'),而不是只执行 DRb.start_service。这为我们提供了固定端口号以供使用。
- 在客户端端,连接到远程对象作为
obj = DRbObject.new(nil, 'druby://127.0.0.1:9001')
瞧,您已启动并运行。您可以尝试上面提到的 DRbUndumped 示例,客户端位于 NAT 防火墙后面。另外请注意,ssh -L 和 -R 选项默认绑定到 127.0.0.1,因此其他机器上的用户无法连接到隧道端点(当然,同一台机器上的其他用户可以这样做)。
除了从命令行建立 SSH 连接外,还可以使用 Net::SSH,这是一个纯 Ruby 实现的 SSH。如果您还没有安装 Net::SSH,请使用 gem install net-ssh
安装它。要创建连接,请在使用 DRb 之前执行以下操作
require 'net/ssh'
require 'thread'
channel_ready = Queue.new
Thread.new do
Net::SSH.start('ssh.example.com','username',:port=>22) do |session|
session.forward.local( 9001, '127.0.0.1', 9001)
session.forward.remote( 9000, '127.0.0.1', 9000 )
session.open_channel do |channel|
end
channel_ready << true
session.loop
end
end
channel_ready.pop
之后,您可以在主线程中执行 DRb 代码,就像在之前的 SSH 示例中一样。channel_ready
Queue 只会强制主线程等待通道打开。
注意:在使用 SSH 和 DRb 时,请不要将 'localhost' 替换为 '127.0.0.1',这会导致连接被拒绝。
通过 SSL 运行 DRb
[edit | edit source]SSL 是另一种安全加密连接的方法(注意:SSL 和 SSH *不是*同一件事!)
在线教程:HTTP://segment7.net/projects/ruby/drb/DRbSSL/
通过防火墙运行 DRuby - 仅 Ruby 解决方案(HTTP://www.ruby-talk.org/cgi-bin/scat.rb/ruby/ruby-talk/89976)通常客户端安装了防火墙,因此标准 DRb 将无法进行回调,从而使 block/io/DRbUndumped?参数变得无用。为了确保 DRb 按正常运行,可以使用HTTP://rubyforge.org/projects/drbfire 和 HTTP://drbfire.rubyforge.org/classes/DRbFire.html
来自文档
- 从 require 'drb/drbfire' 开始。
- 在指定服务器 URL 时使用 drbfire:// 而不是 druby://。
- 在客户端上调用 DRb.start_service 时,将服务器的 uri 指定为 uri(与正常用法不同,正常用法是 *不* 指定 uri)。
- 在调用 DRb.start_service 时指定正确的配置,特别是使用哪个角色。服务器:DRbFire::ROLE => DRbFire::SERVER 和客户端:DRbFire::ROLE => DRbFire::CLIENT
简单服务器
require 'drb/drbfire'
front = ['a', 'b', 'c']
DRb.start_service('drbfire://some.server.com:5555', front, DRbFire::ROLE => DRbFire::SERVER)
DRb.thread.join
以及一个简单的客户端
require 'drb/drbfire'
DRb.start_service('drbfire://some.server.com:5555', nil, DRbFire::ROLE => DRbFire::CLIENT)
DRbObject?.new(nil, 'drbfire://some.server.com:5555').each do |e|
puts e
end
外部链接
[edit | edit source]关于 DRb 使用的替代教程