Pull to refresh

Применение D-Bus в веб-системах

Reading time 7 min
Views 16K
В процессе разработки нескольких Интернет-сервисов мы заметили, что значительная часть их функционала является общей, и руководствуясь принципом DRY (Don't Repeat Yourself — не повторяйся), приняли решение вынести общий функционал в отдельный модуль.

К модулю были предъявлены следующие требования:
  • независимость от использующих его сервисов;
  • простота «клиентского» кода;
  • многопоточность и высокая скорость работы.

Оговорюсь, что наши сервисы написаны на PHP и работают на сервере Apache под управлением Linux. Основные вопросы были: «на чём писать модуль?» и «как обращаться к модулю из PHP-скриптов?». Анализ средств реализации модуля, был проведён с учётом используемого программного обеспечения, а так же наших личных предпочтений, знаний и опыта.
Было предложено 3 основных варианта:

  1. Реализовать модуль в виде ещё одного веб-сервиса на PHP и делать вызовы из клиентских сервисов используя CUrl или с помощью SOAP. Недостатки этого подхода — медленная работа интерпитируемого языка, затраты на сетевые запросы (ненужные на текущий момент, так как предпологается, что модуль и сервисы будут запущены на одном сервере), сложность реализации разделяемых объектов при паралельных запросах.
  2. Реализовать модуль в виде FastCGI-приложения с использованием многопоточности. К положительным сторонам этого варианта следует отнести: возможность написания модуля на C/C++, что увеличило бы быстродействие и позволило реализовать разделяемые объеты в многопоточной среде. Обращения к модулю из PHP-скриптов, можно было бы осуществить с помощью доменных сокетов Unix, что позволило бы избежать затрат на осуществление ненужных обращений к сети (при расположении сервисов и модуля на одном сервере).
  3. Помимио описанных подходов, наше внимание привлекла система межпроцессного взаимодействия (IPC) D-Bus, широко используемая в Linux в настоящее время. Возможности и характеристики D-Bus, такие как быстродействие, надёжность, настраиваемость, наличие высокоуровневых библиотек-обёрток, показались нам привлекательными и удовлетворяющими наши требования. По сути, использование второго варианта привело бы нас к написанию собственного аналога D-Bus. Далее, встал вопрос о вызовах модуля из клиентского PHP-кода. В Интернете нам встретилось две библиотеки реализующих D-Bus на PHP: от Pecl и японская от GREE Labs. Но, так как мы уже успели написать работающий тестовый пример на Pecl D-Bus, японскую реализацию мы не удостоили должного внимания. Со стороны C++ мы решили использовать QtDBus, по причине знакомства программистов с библиотекой Qt.

«Клиентский» код


Итак, перейдём к реализации. Начнём с «клиентского» PHP-кода. Допустим, что имеется некоторое приложение (наш модуль написанный на Qt), которое зарегистрировало D-Bus-сервис с уникальным именем «test.service». Сервис представляет собой иерархическую структуру зарегистрированных в нём объектов. Если нет необходимости в иерархии можно использовать путь к объекту "/" (подобно корневой директории в Linux). В нашем случае в сервисе имеется объект "/test". Объекты предоставляют интерфейсы, содержащие наборы методов. Объект "/test" имеет интерфейс «test.iface» с методом «sum».

client.php:
// Создание объекта, соединяющегося с системной шиной D-Bus.
$dbus = new DBus(DBus::BUS_SYSTEM);

// Создание объекта, для осуществления межпроцессных вызовов.
$proxy = $dbus->createProxy("test.service", "/test", "test.iface");

try {
	// Осуществляем вызов метода
	$result = $proxy->sum(42, 13);
	var_dump($result);
}
catch (Exception $e) {
	print $e->getMessage()."\n";
}


В коде происходит вызов метода «sum» из интерфейса «test.iface» у объекта, расположенного по пути "/test", на сервисе «test.service» через системную шину D-Bus. Метод вызывается с двумя целочисленными аргументами. В результате выполнения данного скрипта на сервисе «test.service» должно быть выполнено сложение 42-х и 13-ти, а результат выведен с помощью функции var_dump.

Реализация D-Bus-модуля


При проектировании архитектуры модуля, мы решили использовать в своих целях терминологию ZendFramework (что может показаться странным для программы написааной на C++). Это было обусловленно тем, что такие термины как «сервис», «интерфейс», «объект» уже использовались нами применительно к D-Bus. И, чтобы избежать путаницы, мы взяли понятия «действия» (Action) и «контроллера» (Controller) из ZendFramework.
Под термином «действие» мы решили понимать унаследованный от QThread класс, представляющий собой нить, в которой будет реализован любой необходимый функционал.
А «контроллером» назвали класс, инкапсулирующий вызовы действий в своих методах. При этом, контроллер нужно унаследовать от QObject и QDBusContext.

Головная функция

Приведём код головной функции модуля (файл main.cpp). Здесь происходит регистрация контроллера на системной шине D-Bus.

#include <QCoreApplication>
#include <QDebug>
#include <QDBusConnection>
#include "TestController.h"

#define SERVICE_NAME "test.service"
#define OBJECT_PATH "/test"

int main(int argc, char *argv[]) {
	QCoreApplication app(argc, argv);
	// Создаём соединение с системной шиной D-Bus
	QDBusConnection conn = QDBusConnection::systemBus();

	// Регистрируем сервис
	if (! conn.registerService(SERVICE_NAME)) {
		qDebug() << "Error:" <<  conn.lastError().message();
		exit(EXIT_FAILURE);
	}

	TestController controller;
	// Регистрируем контроллер
	conn.registerObject(OBJECT_PATH, &controller, QDBusConnection::ExportAllContents);

	return app.exec();
}


«Контроллер»

Следует обратить внимание на то, что методы контроллера, открытые для межпроцессорных вызовов по D-Bus, работают последовательно. То есть, если первый клиент осуществляет вызов метода sum, второй должен ждать пока выполнение метода не окончится. Поэтому, мы решили сократить код методов до минимума, чтобы избежать длительного ожидания. Таким образом, при каждом клиентском вызове происходит запуск рабочей нити (действия) и выход из метода.

Рассмотрим класс контроллера (файл TestController.h). Реализацию метода для краткости напишем в заголовочном файле.

#ifndef TEST_CONTROLLER_H
#define TEST_CONTROLLER_H

#include <QObject>
#include <QDBusContext>
#include <QDBusConnection>
#include <QDebug>
#include "SumAction.h"

class TestController: public QObject, protected QDBusContext {
	Q_OBJECT
	Q_CLASSINFO("D-Bus Interface", "test.iface") // Имя интерфейса

public:
	Q_INVOKABLE int sum(int a, int b) {
		// Сообщаем D-Bus, что ответ прийдёт позже.
		setDelayedReply(true);
		// Запускаем нить
		(new SumAction(a, b, this))->start();
		// Формальный возврат значения для компилятора. Реальный результат будет возвращён из нити.
		return 0;
	};
};

#endif // TEST_CONTROLLER_H


«Действия»

В действиях мы будем размещать функционал модуля. Каждому методу контроллера будет соответствовать класс действия. Поэтому целесообразно написать класс Action, базовый для всех действий.
Action.h
#ifndef ACTION_H
#define ACTION_H

#include <QThread>
#include <QDBusMessage>
#include <QDBusArgument>
#include <QTime>
#include <QDBusReply>

class QDBusContext;

class Action: public QThread {
	Q_OBJECT

public:
	Action(const QDBusContext* context);
	virtual ~Action();

	// Получение результата указаного в шаблоне типа
	template<typename X>
	QDBusReply<X> reply() const { return _reply; }

protected:
	// Неконстантная ссылка для записи результата в классе наследнике.
	inline QDBusMessage& reply() { return _reply; }
	// Полученный запрос
	inline const QDBusMessage& request() { return _request; }

private slots:
	void onFinished();

private:
	QDBusConnection* _connection;
	QDBusMessage _request;
	QDBusMessage _reply;
};

#endif // ACTION_H

Action.cpp
#include "Action.h"

#include <QDBusConnection>
#include <QDBusContext>
#include <QDebug>
#include <QDir>

Action::Action(const QDBusContext* context) {
	if (context != 0) {
		// Создаём копию соединения
		_connection = new QDBusConnection(context->connection());
		_request = context->message();
	}
	else {
		_connection = 0;
		_request = QDBusMessage();
	}

	// Создаём ответ на запрос
	_reply = _request.createReply();

	// Присоединяем обработчик завершения нити
	if (! connect(this, SIGNAL(finished()), this, SLOT(onFinished())))
		qFatal("SIGNAL/SLOT connection error");
}

Action::~Action() {
	if (_connection != 0)
		delete _connection;
}

void Action::onFinished() {
	if (_connection != 0) {
		// Отсылка результата по D-Bus
		_connection->send(_reply);
	}

	/*
	 * Удаление объекта произойдёт только в случае если нить была запущена из
	 * нити, находящейся в цикле обработки событий (event loop).
	 */
	deleteLater();
}


Унаследовав этот класс, мы можем сосредоточится на реализации необходимого функционала не заботясь о деталях взаимодействия с D-Bus. Всё, что нужно сделать, это сохранить параметры в свойствах класса, сложить a и b, и записать результат по ссылке reply().

SumAction.h:
#ifndef SUMACTION_H
#define SUMACTION_H

#include "Action.h"

class SumAction: public Action {
	Q_OBJECT
public:
	SumAction(int a, int b, const QDBusContext* context):
		Action(context),
		_a(a),
		_b(b)
	{}
	virtual ~SumAction() {};
protected:
	void run() {
		reply() << _a + _b;
	}
private:
	int _a;
	int _b;
};

#endif // SUMACTION_H

Конфигурация D-Bus


Откомпилировав описанный выше модуль, мы получим приложение регистрирующее D-Bus-сервис «test.service». Попробуем запустить его. Скорее всего, результат будет следующим:

$ ./dbus-test
Error: "Connection ":1.66" is not allowed to own the service "test.service" due to security policies in the configuration file"

Для решения данной проблемы необходимо внести изменения в конфигурацию D-Bus. D-Bus предоставляет возможность гибкой настройки безопастности и функционала. Для работы нашего примера достаточно создать в файл: /etc/dbus-1/system.d/dbus-test.conf следующего содержания:

<!DOCTYPE busconfig PUBLIC
 "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
 "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig> 
  <policy context="default">
    <allow own="test.service"/>
    <allow send_destination="test.service"/>
    <allow receive_sender="test.service"/>
  </policy>
</busconfig>

Перезапускать демон D-Bus — нет необходимости. Изменения вступят в силу после сохранения файла.
Повторим запуск модуля и, если он благополучно запустился, попробуем обратится к нему из PHP-скрипта.

$ php client.php
int(55)

Вот и ожидаемый результат: 42 + 13 = 55. Исходники можно взять здесь.

Заключение


Описанный выше способ межпроцессного взаимодействия, позволил нам наладить взаимодействие модуля, написанного на C++, с несколькими веб-сервисами, нуждающимися в его функционале. Таким образом, мы получили высокую производительность и гибкость при построении сложной информационной системы, которые нам предоставляет С++ (и Qt в частности), и удобство разработки и поддержки веб-сервисов на PHP.
Tags:
Hubs:
+16
Comments 19
Comments Comments 19

Articles