Хэл Фултон - Программирование на языке Ruby
DRb.start_service("druby://:1234", myobj) # Порт 1234.
# ...
Если порт выбирается динамически, то для получения полного URI, включающего и номер порта, можно воспользоваться методом класса uri.
DRb.start_service(nil, myobj)
myURI = DRb.uri # "druby://hal9000:2001"
Поскольку drb —многопоточная программа, любое серверное приложение должно выполнять join в потоке сервера (чтобы не дать приложению завершиться преждевременно и тем самым уничтожить выполняющийся поток).
# Предотвратить преждевременный выход.
DRb.thread.join
На стороне клиента мы вызываем метод start_service без параметров и с помощью класса DRbObject создаем локальный объект, соответствующий удаленному. Обычно первым параметром методу DRbObject.new передается nil.
require "drb"
DRb.start_service
obj = DRbObject.new(nil, "druby://hal9000:2001")
# Сообщения, передаваемые obj, перенаправляются
# удаленному объекту на стороне сервера...
Следует подчеркнуть, что на стороне сервера привязка осуществляется к единственному объекту, который должен отвечать на все получаемые запросы. Если клиентов несколько, то объект должен быть безопасным относительно потоков, чтобы не оказаться в некорректном состоянии. (Для совсем простых или узкоспециализированных приложений это может быть и необязательно.)
Мы не можем вдаваться в технические детали. Но имейте в виду, что если клиент читает или изменяет внутреннее состояние удаленного объекта, то при наличии нескольких клиентов возможна интерференция. Во избежание таких неприятностей мы рекомендуем применять механизмы синхронизации, например класс Mutex. (Подробнее о потоках и синхронизации рассказывается в главе 13.)
Скажем хотя бы несколько слов о безопасности. Ведь не всегда желательно, чтобы с вашим сервером мог соединяться кто угодно. Помешать им пытаться вы не можете, зато можете сделать такие попытки безуспешными.
В программе drb есть понятие списка контроля доступа (ACL). Это не что иное, как списки клиентов (или категорий клиентов), которым явно разрешен (или запрещен) доступ.
Приведем пример. Для создания нового списка ACL мы воспользуемся классом ACL, которому передадим один или два параметра.
Второй (необязательный) параметр метода ACL.new служит для ответа на вопрос: «Мы запрещаем доступ всем клиентам, кроме некоторых, или, наоборот, разрешаем доступ всем клиентам, кроме некоторых?» По умолчанию принимается первый вариант, который обозначается константой DENY_ALLOW равной 0. Второй режим обозначается ALLOW_DENY равной 1.
Первый параметр ACL.new представляет собой обычный массив строк, которые идут парами. Первая строка в паре должна быть равна "deny" или "allow", вторая описывает одного клиента или группу клиентов (по имени или по адресу):
require "drb/acl"
acl = ACL.new( %w[ deny all
allow 192.168.0.*
allow 210.251.121.214
allow localhost] )
Первая пара в этом примере, строго говоря, излишня, но проясняет смысл всей конструкции.
А как используются ACL? Метод install_acl приводит ACL в действие. Его необходимо вызывать перед обращением к start_service, иначе он не возымеет эффекта.
# Продолжение примера...
DRb.install_acl(acl)
DRb.start_service(nil, some_object)
# ...
Теперь, после запуска сервиса любой неавторизованный запрос на соединение приведет к исключению RuntimeError.
Это, конечно, не все, что можно сказать о библиотеке drb. Но для обзора вполне достаточно. В следующем разделе мы рассмотрим простой drb-сервер и drb-клиент, близкие к реальным программам. А затем поговорим о программах Rinda и Ring.
20.2. Пример: эмуляция биржевой ленты
В этом примере сервер публикует в сети биржевые котировки акций. К серверу может обратиться любой клиент, желающий узнать, сколько сейчас стоит его пакет.
Но мы добавили одну тонкость. Не желая следить за малейшими колебаниями цен, мы реализовали модуль Observer, который позволяет подписаться на информационный канал. Клиент следит за поступающими сведениями и предупреждает нас, когда изменение цены превысит заданный порог.
Сначала рассмотрим модуль DrbObservable. Это прямолинейная реализация паттерна Observer (Наблюдатель), описанного в замечательной книге Э. Гаммы, Р. Хелма, Р. Джонсона и Дж. Влиссидеса «Паттерны проектирования» (см. сноску в разделе 12.3.1). Еще этот паттерн называют «Издатель-Подписчик».
В листинге 20.1 наблюдатель определен как объект, отвечающий на вызов метода update. Сервер добавляет наблюдателей по их просьбе и посылает им уведомления, обращаясь к методу notify_observers.
Листинг 20.1. Модуль DrbObservablemodule DRbObservable
def add_observer(observer)
@observer_peers ||= []
unless observer.respond_to? :update
raise NameError, "наблюдатель должен отвечать на вызов '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
Сервер (он же канал) в листинге 20.2 эмулирует биржевые котировки с помощью последовательности псевдослучайных чисел (простите мою иронию, но это очень точно соответствует характеру рынка). Символ, идентифицирующий компанию, — всего лишь косметическое украшение, никакого реального смысла в этой программе он не имеет. При каждом изменении цены посылается уведомление всем наблюдателям.
Листинг 20.2. Канал биржевых котировок (drb-сервер)require "drb"
require "drb_pbserver"
# Генерировать случайные котировки.
class MockPrice
MIN = 75
RANGE = 50
def initialize(symbol)
@price = RANGE / 2
end
def price
@price += (rand() - 0.5)*RANGE
if @price < 0
@price = [email protected]
elsif @price >= RANGE
@price = 2*RANGE - @price
end
MIN + @price
end
end
class Ticker # Периодически получать котировку акций.
include DRbObservable
def initialize(price_feed)
@feed = price_feed
Thread.new { run }
end
def run
lastPrice = nil
loop do
price = @feed.price
print "Текущая котировка: #{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 'Нажмите [return] для завершения.'
gets
На платформе Windows примененный способ завершения программы вызывает сложности. Функция gets в этом случае может блокировать главный поток. Если вы это видите, попробуйте вместо обращения к gets поставить DRb.thread.join (а завершайте программу нажатием Ctrl+C).
Неудивительно, что клиент (листинг 20.3) начинает с установления соединения с сервером. Он получает ссылку на объект показа котировок и устанавливает верхний и нижний пороги изменения цены. Затем клиент выводит сообщение пользователю всякий раз, как цена выходит за пределы указанного диапазона.
Листинг 20.3. Наблюдатель биржевых котировок (drb-клиент)require "drb"
class Warner
include DRbUndumped
def initialize(ticker, limit)
@limit = limit
ticker.add_observer(self) # Любой объект Warner
# является наблюдателем.
end
end
class WarnLow < Warner
def update(time, price) # Обратный вызов наблюдателя.
if price < @limit
print "--- #{time.to_s}: Цена ниже #@limit: #{price}n"
end
end
end
class WarnHigh < Warner
def update(time, price) # Обратный вызов наблюдателя.
if price > @limit
print "+++ #{time.to_s}: Цена выше #@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 'Нажмите [return] для завершения.'
gets
Модуль DRbUndumped (см. листинге 20.3) следует включать в любой объект, который не нужно подвергать маршалингу. Самого присутствия этого модуля в числе предков объекта достаточно, чтобы drb не пытался применять к нему маршалинг. Вот исходный текст этого модуля целиком: