xmpp.pasis.pp.ua

XMPP blog

View project on GitHub
Language: English | Русский

libstrophe: Первая XMPP программа

При изучении языка программирования первую программу принято писать “Hello world”. Для XMPP есть похожая традиция - echo bot. Давайте напишем такую программу на языке Си с использованием библиотеки libstrophe. И попутно разберем основы использования libstrophe.

Примеры сборки представлены для Unix подобных систем или Cygwin. Хотя код полноценно работает под Windows и Visual Studio, примеры такой сборки здесь не рассматриваются.

Исходные тексты libstrophe распространяются с простыми примерами, включая basic.c и bot.c:

Пример Описание
basic.c Пример установки XMPP соединения
bot.c Пример echo бота

То есть, у нас уже есть готовый код бота. Здесь же мы повторим тот же путь небольшими шагами.

Подготовка

Установите библиотеку libstrophe:

  • Обратитесь к пакетному менеджеру вашей системы
  • Для бинарных дистрибутивов дополнительно установите пакет для разработки (libstrophe-dev или libstrophe-devel)
  • Альтернативно, libstrophe можно собирать и установить из исходных текстов (https://github.com/strophe/libstrophe)

Для простоты сборки нашего бота создадим Makefile:

all:
	gcc `pkg-config --cflags --libs libstrophe` -o bot bot.c

Как вы уже поняли, код мы будем сохранять в файл bot.c. Для сборки программы достаточно выполнить make.

Соединение с XMPP сервером

Перед использованием libstrophe ее необходимо инициализировать для настройки глобальных объектов. После окончания использования библиотеки, хорошим тоном будет вызвать финализацию глобальных объектов. Обычно это делается при старте и перед завершением программы соответственно:

include <strophe.h>

int main()
{
	xmpp_initialize();

	/* Код, использующий libstrophe API. */

	xmpp_shutdown();
	return 0;
}

Первым шагом работы с XMPP является установка XMPP соединения с сервером:

void bot_run()
{
	/* Контекст libstrophe. */
	xmpp_ctx_t *ctx;
	/* Объект XMPP соединения. */
	xmpp_conn_t *conn;
	/* Код возврата для libstrophe API. */
	int rc;

	/* Создание нового контекста. */
	ctx = xmpp_ctx_new(NULL, NULL);
	/* Создание нового "пустого" объекта XMPP соединения. */
	conn = xmpp_conn_new(ctx);
	/* Установка JID для соединения. */
	xmpp_conn_set_jid(conn, "user@xmpp.org");
	/* Установка пароля для соединения. */
	xmpp_conn_set_pass(conn, "password");
	/* Объявление о намерении установить XMPP соединение. */
	rc = xmpp_connect_client(conn, NULL, 0, conn_handler, NULL);
	if (rc == XMPP_EOK) {
		/* Запуск блокирующего цикла событий. */
		xmpp_run(ctx);
	}

	/* Освобождение ранее созданных объектов. */
	xmpp_conn_release(conn);
	xmpp_ctx_free(ctx);
}

Данный блок кода имеет недостающую деталь: conn_handler. Мы разберем его дальше. А для начала рассмотрим тот же блок более детально.

libstrophe оперирует небольшим количеством фундаментальных объектов. Среди которых контекст и соединение, имеющие тип xmpp_ctx_t и xmpp_conn_t соответственно. Такие объекты создаются с помощью интерфейса ..._new() и освобождаются интерфейсами ..._free() или ..._release(). При этом интерфейс _release() указывает на то, что объект работает по принципу подсчета ссылок (англ. reference counting) и объект уничтожается физически только при освобождении последней ссылки. Обычно пользователи не используют подсчет ссылок для соединений, поэтому xmpp_conn_release() сразу уничтожает соответствующий объект в таком случае.

Контекст - это домен изоляции. Для каждого контекста необходимо запускать свой независимый цикл событий, который обслуживает не пересекающиеся множества соединений и обработчиков событий. Для большинства нужд достаточно создавать один контекст для XMPP приложения. Множество контекстов могут понадобится продвинутым пользователям в целях оптимизации многопоточных приложений с множеством разных соединений.

Объект соединения - это объект, которые представляет отдельное XMPP соединение. Изначально, такой объект создается без конфигурации и впоследствии настраивается различными атрибутами, такими как JID, пароль и другие. Объект соединения имеет внутреннее состояние, которое может быть DISCONNECTED, CONNECTING или CONNECTED. Новое соединение находится в состоянии DISCONNECTED до тех пор, пока пользователь не вызовет xmpp_connect_client(). Важно отметить, что xmpp_connect_client() выполняет только начальный этап установки соединения, в частности, преобразование доменного имени и начало установки TCP соединения. Последующая установка XMPP сессии производится асинхронно в цикле событий.

Замените строки “user@xmpp.org” и “password” на свою конфигурацию.

Как было упомянуто выше, libstrophe обрабатывает бóльшую часть функциональности в цикле событий. Поэтому важно вовремя его выполнять и не блокировать приложение надолго. Существует два способа запустить цикл событий: xmpp_run() и xmpp_run_once(). Первый вариант является блокирующим бесконечным циклом, который прерывается при вызове xmpp_stop(). Второй вариант выполняет одну итерацию цикла событий и завершается. Второй подход полезен, если приложение имеет свой цикл событий или пользуется другим сторонним циклом, например GTK.

Цикл событий

libstrophe написана в событийно-ориентированной парадигме. При наступлении события, libstrophe вызывает соответствующий пользовательский обработчик, чтобы приложение отреагировало на событие. Пользователь регистрирует обработчики на следующие категории событий: события с XMPP соединением, входящие XMPP строфы, таймеры. Событие, для которого не зарегистрирован обработчик, теряется.

Обработчик событий соединения является обязательным и регистрируется во время вызова xmpp_connect_client(). В коде выше такой обработчик - conn_handler(). Основные события такого обработчика: XMPP_CONN_CONNECT и XMPP_CONN_DISCONNECT. Первое событие генерируется после успешной установки XMPP соединения, а второе - после завершения или разрыва соединения. Обработчик принимает на вход объект соединения, таким образом можно использовать одну функцию обработчика для разных соединений. Также все обработчики событий принимают на вход пользовательские данные, которые могут быть уникальными для каждого обработчика. В примере выше, в xmpp_connect_client() мы передаем значение NULL как пользовательские данные, так как нет необходимости их использовать.

Приведем пример обработчика conn_handler():

void conn_handler(xmpp_conn_t *conn,
		  xmpp_conn_event_t status,
		  int error,
		  xmpp_stream_error_t *stream_error,
		  void *userdata)
{
	xmpp_ctx_t *ctx = xmpp_conn_get_context(conn);
	xmpp_stanza_t *pres;

	/* Мы не обрабатываем эти аргументы в примере. */
	(void)error;
	(void)stream_error;

	if (status == XMPP_CONN_CONNECT) {
		/* Регистрируем обработчик сообщений. */
		xmpp_handler_add(conn, message_handler, NULL,
				 "message", NULL, NULL);

		/* Отправляем начальный <presence/>, чтобы
		   отображаться как "в сети" у контактов. */
		pres = xmpp_presence_new(ctx);
		xmpp_send(conn, pres);
		xmpp_stanza_release(pres);
	} else {
		/* Предполагаем, что это событие
		   XMPP_CONN_DISCONNECT. */
		xmpp_stop(ctx);
	}
}

Обработчик событий соединения всегда принимает на вход следующие аргументы - это API.

Аргумент Описание
conn Соединение, на котором произошло событие
status Событие (XMPP_CONN_CONNECT, XMPP_CONN_DISCONNECT)
error Значение ошибки при разрыве соединения
stream_error Значение ошибки потока XMPP, если такая была
userdata Пользовательские данные, переданные в xmpp_connect_client()

Рассмотрим важные моменты обработчика:

  • При успешной установке XMPP соединения мы регистрируем обработчик строф с тегом “message”. После чего функция message_handler() будет вызвана для каждого входящего сообщения.
  • Отправляем строфу <presence/>, чтобы изменить свой статус на “online”.
  • После завершения соединения останавливаем цикл событий с помощью xmpp_stop(), после чего xmpp_run() завершит выполнение.

Регистрируя обработчик сообщений внутри обработчика выше мы не потеряем сообщения, даже если они придут на TCP сокет до выполнения xmpp_handler_add(). Это гарантируется принципом работы libstrophe: обработчики событий блокируют выполнение цикла событий до момента возврата. А XMPP строфы обрабатываются внутри цикла событий последовательно.

Отправка строфы с помощью xmpp_send() на самом деле лишь добавляет ее в очередь отправки. Строфа будет отправлена фактически внутри цикла событий, когда сетевая подсистема позволит.

Пользователь должен освободить созданную строфу, когда она больше не нужна. В нашем случае это происходит после отправки строфы. Как было упомянуто выше, интерфейс ..._release() отпускает ссылку на объект, но не обязательно уничтожает его. Функции libstrophe берут дополнительную ссылку на строфу при надобности. Как следствие, можно не беспокоиться о жизненном цикле строфы внутри цикла событий. В большинстве случаев, ссылку на строфу можно освобождать после вызова xmpp_send(). Взяв это за правило, вы уменьшите вероятность утечек памяти.

Работа со строфами

Последним шагом для нашего бота будет обработка входящих сообщений:

int message_handler(xmpp_conn_t *conn, xmpp_stanza_t *stanza, void *userdata)
{
        xmpp_ctx_t *ctx = xmpp_conn_get_context(conn);
	xmpp_stanza_t *reply;
	char *text;

	text = xmpp_message_get_body(stanza);
	if (text == NULL) {
		/* Игнорируем пустые сообщения. */
		return 1;
	}

	if (strcmp(text, "quit") == 0) {
		/* Мы получили команду к выходу. */
		xmpp_disconnect(conn);
		xmpp_free(ctx, text);
		return 0;
	}

	/* Создаем новое сообщение в ответ на оригинальную строфу. */
	reply = xmpp_message_new(ctx, "chat", xmpp_stanza_get_from(stanza),
				 generate_id());
	/* Устанавливаем текст внутри элемента <body/>. */
	xmpp_message_set_body(reply, text);
	xmpp_free(ctx, text);

	xmpp_send(conn, reply);
	xmpp_stanza_release(reply);

	return 1;
}

Как и в случае с обработчиком состояний соединения, обработчик строф имеет свой интерфейс:

Аргумент Описание
conn Соединение, куда пришла строфа
stanza Строфа, представленная объектом xmpp_stanza_t
userdata Пользовательские данные, переданные в xmpp_handler_add()

Обработчик возвращает значение 0 или 1. Эти значения можно рассматривать как логическое значение, которое обозначает оставлять ли обработчик активным или удалить его. Для обработчика входящих сообщений мы обычно оставляем его активным. Но иногда мы ожидаем одну конкретную строфу, после обработки которой нам больше не нужен обработчик.

Логика данной функции следующая:

  • Игнорируем сообщения, которые не содержат элемент <body/>
  • При входящем сообщении “quit” инициируем завершение XMPP соединения
  • Создаем новый объект строфы, которая содержит копию текста из оригинального сообщения и предназначена отправителю оригинального сообщения
  • Добавляем новую строфу в очередь отправки
  • Завершаем обработчик и передаем управление обратно циклу событий

xmpp_message_get_body() возвращает текст внутри элемента <body/> в виде новой выделенной строки. Пользователь должен освободить эту строку, когда она больше не нужна, чтобы избежать утечек памяти. Важно следить, какой интерфейс возвращает выделенную строку, а какой - указатель на внутреннюю строку, не требующую освобождения. Как правило, выделенные строки возвращаются с типом char*, а строки const char* освобождать не стоит. Также необходимо следить за тем, что строки const char* действительны до уничтожения соответствующего объекта.

xmpp_disconnect(), как и xmpp_send() выполняет основной функционал асинхронно внутри цикла событий. Эта функция фактически добавляет </stream:stream> в очередь отправки и при получении входящего элемента </stream:stream> генерирует событие XMPP_CONN_DISCONNECT.

В примере выше мы использовали функцию generate_id(). Её цель - генерировать уникальный идентификатор для атрибута id. Существует множество подходов к генерации идентификатора, например, монотонно увеличивающееся число, случайная строка, UUID и так далее. Рассмотрим простую реализацию:

const char *generate_id()
{
	static char str[9];
	static uint32_t counter = 0;

	snprintf(str, sizeof(str), "%x", counter++);
	return str;
}

Фундаментальным интерфейсом для работы со строфами является следующий:

  • xmpp_stanza_new()
  • xmpp_stanza_set_name()
  • xmpp_stanza_set_text()
  • xmpp_stanza_set_attribute()
  • xmpp_stanza_add_child()
  • xmpp_stanza_release()

Но для часто используемых типов строф libstrophe предоставляем удобные обвертки поверх упомянутого API. Например, xmpp_message_set_body() можно реализовать базовыми функциями следующим образом:

void xmpp_message_set_body(xmpp_stanza_t *msg, const char *text)
{
	xmpp_ctx_t *ctx = xmpp_stanza_get_context(msg);
	xmpp_stanza_t *body;
	xmpp_stanza_t *stanza_text;

	body = xmpp_stanza_new(ctx);
	stanza_text = xmpp_stanza_new(ctx);

	xmpp_stanza_set_name(body, "body");
	xmpp_stanza_set_text(stanza_text, text);

	xmpp_stanza_add_child(body, stanza_text);
	xmpp_stanza_release(stanza_text);
	xmpp_stanza_add_child(msg, body);
	xmpp_stanza_release(body);
}

Как видно, обвертки позволяют писать код более компактно, но их использование не является обязательным.

Финальный код бота

В заключение, соберем все части воедино. Код также доступен по сслыке: bot.c.

#include <strophe.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

const char *generate_id()
{
	static char str[9];
	static uint32_t counter = 0;

	snprintf(str, sizeof(str), "%x", counter++);
	return str;
}

int message_handler(xmpp_conn_t *conn, xmpp_stanza_t *stanza, void *userdata)
{
        xmpp_ctx_t *ctx = xmpp_conn_get_context(conn);
	xmpp_stanza_t *reply;
	char *text;

	text = xmpp_message_get_body(stanza);
	if (text == NULL) {
		/* Игнорируем пустые сообщения. */
		return 1;
	}

	if (strcmp(text, "quit") == 0) {
		/* Мы получили команду к выходу. */
		xmpp_disconnect(conn);
		xmpp_free(ctx, text);
		return 0;
	}

	/* Создаем новое сообщение в ответ на оригинальную строфу. */
	reply = xmpp_message_new(ctx, "chat", xmpp_stanza_get_from(stanza),
				 generate_id());
	/* Устанавливаем текст внутри элемента <body/>. */
	xmpp_message_set_body(reply, text);
	xmpp_free(ctx, text);

	xmpp_send(conn, reply);
	xmpp_stanza_release(reply);

	return 1;
}

void conn_handler(xmpp_conn_t *conn,
		  xmpp_conn_event_t status,
		  int error,
		  xmpp_stream_error_t *stream_error,
		  void *userdata)
{
	xmpp_ctx_t *ctx = xmpp_conn_get_context(conn);
	xmpp_stanza_t *pres;

	/* Мы не обрабатываем эти аргументы в примере. */
	(void)error;
	(void)stream_error;

	if (status == XMPP_CONN_CONNECT) {
		/* Регистрируем обработчик сообщений. */
		xmpp_handler_add(conn, message_handler, NULL,
				 "message", NULL, NULL);

		/* Отправляем начальный <presence/>, чтобы
		   отображаться как "в сети" у контактов. */
		pres = xmpp_presence_new(ctx);
		xmpp_send(conn, pres);
		xmpp_stanza_release(pres);
	} else {
		/* Предполагаем, что это событие
		   XMPP_CONN_DISCONNECT. */
		xmpp_stop(ctx);
	}
}

void bot_run()
{
	xmpp_ctx_t *ctx;
	xmpp_conn_t *conn;
	int rc;

	ctx = xmpp_ctx_new(NULL, NULL);
	conn = xmpp_conn_new(ctx);
	xmpp_conn_set_jid(conn, "user@xmpp.org");
	xmpp_conn_set_pass(conn, "password");
	rc = xmpp_connect_client(conn, NULL, 0, conn_handler, NULL);
	if (rc == XMPP_EOK) {
		xmpp_run(ctx);
	}

	xmpp_conn_release(conn);
	xmpp_ctx_free(ctx);
}

int main()
{
	xmpp_initialize();

	bot_run();

	xmpp_shutdown();
	return 0;
}

Отладка неполадок

В случае неполадок или при желании увидеть детали работы XMPP, отладочные логи придут на помощь. libstrophe предоставляет удобный механизм работы с логами. Самый простой способ включения - это воспользоваться встроенным обработчиком логов, который выводит логи в stderr. Для этого достаточно модифицировать вызов xmpp_ctx_new():

void bot_run()
{
	/* ... */
	ctx = xmpp_ctx_new(NULL, xmpp_get_default_logger(XMPP_LEVEL_DEBUG));
	/* ... */
}