Меньше — это больше.
В настоящее время имеется немало технологий, поддерживающих распределенную обработку: различные варианты RPC, а также COM, CORBA, DCE и Java RMI.
Одни проще, другие сложнее, но в принципе все делают одно и то же - предоставляют относительно прозрачный способ связи между находящимися в сети объектами так, чтобы с удаленными объектами можно было работать, как с локальными.
Зачем это вообще может понадобиться? Причин много. Например, чтобы распределить некоторую вычислительную задачу между многими процессорами. Примером может послужить программа SETI@home, которая использует ваш ПК для обработки небольших объемов данных в поисках внеземного разума (кстати, эта программа не является проектом института SETI). Другой пример — привлечение широких масс к взлому шифра RSA129 (эта попытка увенчалась успехом несколько лет назад). Существует очень много задач, которые можно разбить на небольшие части, пригодные для распределенного решения.
Можно также представить себе, что вы хотите предоставить интерфейс к некоему сервису, не раскрывая исходных текстов. Часто это делается с помощью Web-приложений, но из-за отсутствия состояния в протоколе HTTP это не всегда удобно (есть и другие недостатки). Механизм распределенного программирования позволяет решать подобные задачи более естественно.
В мире Ruby ответом на этот вызов стала программа drb, написанная Масатоси Секи (Masatoshi Seki); еще ее название записывают так: DRb. Существуют и другие способы распределенной обработки на Ruby, но drb, пожалуй, самый легкий. Здесь нет сложных служб разрешения имен, как в CORBA. Это всего лишь простая и удобная библиотека, предоставляющая всю необходимую функциональность. В данной главе мы рассмотрим основы работы как с ней самой, так и с надстроенной над ней системой Rinda.
20.1. Обзор: библиотека drb
Библиотека drb
состоит из двух основных частей: серверной и клиентской. Грубую границу между ними можно провести следующим образом:
Сервер:
• запускает TCPServer и начинает прослушивать порт;
• привязывает объект к экземпляру сервера drb
;
• принимает запросы на соединение от клиентов и отвечает на их сообщения;
• дополнительно может предоставлять контроль доступа (безопасность).
Клиент:
• устанавливает соединение с сервером;
• привязывает локальный объект к удаленному экземпляру сервера;
• посылает сообщения серверу и получает ответы.
Метод класса start_service
отвечает за запуск TCP-сервера, прослушивающего указанный порт. Он принимает два параметра: URI (универсальный идентификатор ресурса), задающий порт (если он равен nil
, то порт выбирается динамически), и объект, к которому мы хотим привязаться. Этот объект будет доступен удаленному клиенту, который сможет вызывать его методы, как если бы объект был локальным.
require 'drb'
myobj = MyServer.new
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] )