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://: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://: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://: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://: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://: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://:9001', server)
DRb.thread.join
這是相應的客戶端
require 'drb'
require './foo'
DRb.start_service
obj = DRbObject.new(nil, 'druby://: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 使用的替代教程