Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Разрабатывем XMPP-клиент для Google Talk

Оригинал: Use XMPP to Create Your Own Google Talk Client
Автор: Sarath Lakshman
Дата публикации: 29 Июня 2012 г.
Перевод: А.Панин
Дата публикации перевода: 13 октября 2012 г.

Системы мгновенных сообщений являются одной из основных составляющих организации социальных сетей и Интернет. Популярный клиент Google Talk, использующий протокол XMPP (Extensible Messaging and Presence Protocol), вывел этот протокол обмена мгновенными сообщениями в лидеры среди открытых стандартов. Изучение протокола XMPP (известного ранее как Jabber) доставляет удовольствие - он прозрачен и использует простейшую архитектуру. После того, как вы поймете это, вам не составит труда разрабатывать собственные клиенты для XMPP/Google Talk без использования сторонних программных компонентов на понятном и мощном языке программирования Python.

XMPP - открытый протокол, основанный на XML. Он использует XML для реализации всего цикла обмена мгновенными сообщениями. В основе XMPP лежит децентрализованная клиент-серверная архитектура, в которой сервер выступает посредником при передаче сообщений, а также выполняет такие функции, как создание учетных записей пользователей, аутентификация пользователей, хранение списка контактов, и.т.д. Поскольку в данной статье рассматриваются клиенты, не будем углубляться в подробности реализации серверной части - просто будем считать сервер "черным ящиком", службой работающей на узле с определенным IP-адресом/именем узла на определенном порту и отвечающую определенным образом на наши XMPP-запросы.

Для соединения с сервером в нашей программе используется TCP-сокет. Описание сетевых технологий, TCP/IP, IP-адресов, портов и сокетов выходит за рамки данной статьи из-за большого объема информации, поэтому в том случае, если вам не знакомы данные понятия, следует обратиться к статье в Wikipedia.

Так как мы разрабатываем клиент для Google Talk, будем использовать имя узла gmail.com и порт 5222, являющийся стандартным портом для XMPP.

Последовательность взаимодействия между клиентом и сервером выглядит следующим образом:
  • Клиент соединяется с сервером и отправляет идентификационные данные: имя пользователя и пароль.
  • Сервер сравнивает предоставленные данные с информацией из базы данных и отправляет ответ клиенту.
  • После успешной аутентификации, клиент получает от сервера ответ, содержащий информацию о присутствующих в сети собеседниках. Этот ответ представляет собой данные о присутствии в сети людей из списка контактов. Более подробное пояснение будет приведено ниже.

Как говорилось ранее, передача XML-потока между клиентом и сервером начинается сразу после установления сединения.

Перед тем, как мы приступим к программированию, давайте определимся с рядом терминов и принципов, тесно связанных с XMPP. Для разработки XMPP-клиента совсем не обязательно понимать структуру XML-потоков, служащих для обмена данными между клиентом и сервером в том случае, когда используются библиотеки, скрывающие сложности протокола и предоставляющие нам API. В данной статье используется язык программирования Python, для которого существует библиотека python-xmpp, предоставляющая отточенный API. Тем не менее, для понимания основ процесса обмена сообщениями и взаимодействия клиента и сервера, мы рассмотрим некоторые примеры.

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

Ресурс (Resource)

XMPP-клиент может работать на большом спектре оборудования, начиная с мобильных телефонов и встраиваемых устройств и заканчивая лэптопами и настольными компьютерами. Тип устройства, на котором работает клиент, может быть отображен в атрибуте "from" XML-потока, отправленного клиентом, эта информация и называется ресурсом. Атрибут "from" записывается в следующем формате: from="имя_пользователя@домен/ресурс". Например, клиент, работающий на мобильном телефоне под управлением Android, будет отправлять что-то наподобие "userid@gmail.com/Android". Этот идентификатор может быть весьма полезен: инструменты для администрирования могут разделять подключенные клиенты на группы, руководствуясь этим критерием. Сервер может корректировать свои ответы в зависимости от используемого ресурса - например, в случае соединения с мобильного телефона, длина ответов сервера, размер изображений и другие параметры могут быть сведены к минимуму для уменьшения времени на передачу данных через медленное GPRS-соединение.

Данная информация, предоставляемая клиентам из списка контактов, может также использоваться для отправки сообщений в зависимости от ресурсов. Например, я могу отправить сообщение "Hello Android guys!" всем людям в списке контактов, подключившимся с устройств под управлением Android, в то же время отправив "Hello, netbook guys!" всем подключившимся с нетбуков.

Станс (Stanza)

Станс является неделимой командой XMPP и одной из фундаментальных структур уровня протокола. Вы можете отправлять неограниченное число станс посредством установленного соединения между сервером и клиентом.

Как только аутентификация успешно пройдена и налажен обмен данными в формате XML между сервером и клиентом, клиент и сервер могут обмениваться (предоставляя возможность передачи мгновенных сообщений) тремя основными типами XML-станс - <message/>, <pesence/> и <iq/>. Рассмотрим их.

Уведомление о присутствии (Presence)

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

Когда сервер получает эту информацию, он отправляет копии станса всем клиентам из списка контактов пользователя. Если вы используете клиент Pidgin для работы с Gtalk, наведите указатель мыши на пользователя из списка контактов для того, чтобы увидеть часть этой информации.

Пример станса уведомления о состоянии:
<presence from="slynux@slynux.com/Android"> 
	<show>xa</show> 
	<status>Writing for LFY</status> 
</presence>

Сообщение (Message)

Данный станс используется для обмена сообщениями между пользователями. Он выглядит так, как показано ниже:
<message from="slynux@gmail.com/Home" 
		 to="slynuxguy@gmail.com" 
		 type="chat"> 
	<body>Hey, Whats up ?</body> 
	<subject>Query</subject> 
</message>

Запрос (IQ)

Станс IQ (Info/Ouery) является аналогом запросов GET и POST в протоколе HTTP. Мы используем IQ-запросы для получения информации от сервера и храним ответы для последующего использования. Если запрос некорректен или не может быть исполнен, сервер возвращает станс с сообщением об ошибке. Протокол XMPP устанавливает правило, по которому каждый iq-станс должен иметь атрибут "id", значение которого генерируется клиентом (или используемой библиотекой для работы с XMPP).

Этот атрибут устанавливается с таким же значением и в ответе сервера, что позволяет сопоставить полученные ответы с соответствующими iq-запросами (это удобно, в том случае, когда клиент отправляет несколько запросов друг за другом). Поскольку генерация уникальных значений атрибута "id" осуществляется на уровне библиотеки (в нашем случае python-xmpp), мы не будем заниматься этим.

Например, запрос содержимого ростера (о нем сказано ниже) выглядит так:
<iq from="slynuxguy@gmail.com" 
		id="7" 
		to="slynuxguy@gmail.com/Pidgin" 
		type="get"> 
	<query xmlns="jabber:iq:roster"/> 
</iq>
Пример ответа на такой запрос:
<iq to="slynuxguy@gmail.com/Pidgin" 
	id="7" 
	type="result"> 
	<query xmlns="jabber:iq:roster"> 
		<item jid="user3@gmail.com/Home"/> 
		<item jid="user4@gmail.com/Adium"/> 
		<item jid="user4@gmail.com/Android"/> 
	</query> 
</iq>

Вы можете заметить несколько записей для пользователя user4 - "user4@gmail.com/Adium" и "user4@gmail.com/Android". Adium и Android - названия ресурсов, как обсуждалось ранее. Это говорит о том, что user4 подключен к серверу с двух клиентов одновременно.

Ростер (Roster)

Под ростером в протоколе XMPP понимается список контактов, содержащий атрибут присутствия для каждого контакта. Ваш ростер содержит список идентификаторов пользователей Jabber (называемых JID) и состояние каждого из пользователей, разрешивших вам получать их статус. Когда вы подключаетесь, ваш клиент оповещает сервер о вашем присутствии в сети, а сервер в свою очередь делает все остальное - оповещает людей из вашего списка контактов о вашем присутствии и получает их статусы для обновления данных в списке контактов вашего клиента. Ваш ростер обновляется тогда, когда вы отправляете уведомление о присутствии.

Ниже приведен пример XML-потока, комбинирующего три типа станс в сессии обмена данными с сервером. Обратите внимание, что это не вывод отладочной информации - в случае вывода отладочной информации сообщения бы были намного подробнее. Это просто структурный пример внутреннего представления XML-потока, предназначенный для демонстрации обмена данными между клиентом и сервером, взятый из документации XMPP. В этом примере клиент (обозначенный "C:" в примере) прошел аутентификацию на сервере gmail.com как user5@gmail.com.

C:<stream:stream> 
C:	<presence/> 
C:	<iq type="get" id="1"> 
		<query xmlns="jabber:iq:roster"/> 
	</iq> 
S:	<iq type="result" id="1" > 
		<query xmlns="jabber:iq:roster"> 
			<item jid="user1@gmail.com"/> 
			<item jid="user2@gmail.com"/> 
			<item jid="user3@gmail.com"/> 
		</query> 
	</iq> 
C:	<message from="user5@gmail.com" 
		 to="user2@gmail.com"> 
		<body>Hello world!</body> 
	</message> 
S:	<message from="user3@gmail.com" 
		 to="user5@gmail.com"> 
		<body>Kudos to you</body> 
	</message> 
C:	<presence type="unavailable"/> 
C:</stream:stream>

Можно привести много примеров запросов, таких как: добавление контакта, удаление контакта, открытие группового чата, и.т.д. Все они осуществляются при помощи XML-потока. Протокол XMMP включает в себя метод для защиты потока данных от вмешательства и прослушивания. Этот метод шифрования данных называется TLS (Transport Layer Security) с расширением "STARTTLS", которое разработано на основе расширений для протоколов IMAP, POP3 и ACAP.

Займемся кодом клиента

Теперь подумаем, как мы можем разработать свой собственный XMPP-клиент для Google Talk с нуля, используя простой и мощный язык программирования Python с удивительными возможностями стандартной библиотеки, сформированной по принципу "все включено" ("batteries included"). В дополнение к этому существует множество отдельных устанавливаемых расширений и библиотек для Python - практически для всего того, что можно сделать с использованием других языков.

Для разработки клиента будем использовать модуль поддержки XMPP для Python, который, скорее всего, придется установить. Если вы используете дистрибутив, основанный на Debian, такой как Ubuntu, используйте следующую команду:
$ sudo apt-get install python-xmpp
Для других дистрибутивов, в репозиториях которых отсутствует данный пакет, скачайте архив и установите модуль при помощи следующих команд:
$ wget http://downloads.sourceforge.net/project/xmpppy/xmpppy/0.5.0-rc1/xmpppy-0.5.0rc1.tar.gz?use_mirror=nchc
$ tar -xzvvf  xmpppy-0.5.0rc1.tar.gz
$ cd  xmpppy-0.5.0rc1
$ sudo python setup.py install
Теперь давайте напишем основу кода клиента. Данный код позволяет соединиться с сервером и пройти аутентификацию. Основа клиента состоит всего из 12 строк - верите ли вы этому?
#!/usr/bin/env python 

import xmpp 

user="username@gmail.com" 
password="password" 
server="gmail.com" 

jid = xmpp.JID(user) 
connection = xmpp.Client(server,debug=[]) 
connection.connect() 
result = connection.auth(jid.getNode(), password,"LFY-client") 

connection.sendInitPresence() 

while connection.Process(1): 
	pass
Сохраните файл как base.py и запустите:
$ chmod a+x base.py
$./base.py

Теперь запустим Pidgin и этот клиент одновременно. Используйте две учетные записи Gtalk для одновременной работы Pidgin и данного клиента. После запуска base.py, наведите курсор мыши на контакт, используемый нашим клиентом (учетная запись пользователя, под которой произошла аутентификация). Проверьте статус - "LFY-client" - статус, установленный нашим клиентом. Попробуйте отправить сообщение нашему клиенту - конечно же, он не ответит, так как он пока еще не достаточно проработан и мы не задали действие на случай получения сообщения.

Теперь давайте попробуем установить, как выглядят XML-потоки. Включите отладку: замените строку connection = xmpp.Client(server,debug=[]) на connection = xmpp.Client(server), удалив параметр debug=[]. Теперь при запуске клиента можно видеть XML-поток, как показано на рисунке 1.

Отладочная информация (XML-поток) в окне терминала
Рисунок 1: Отладочная информация (XML-поток) в окне терминала

Разработка бота для GTalk

Вы наверняка использовали или сталкивались с ботами GTalk, отвечающими автоматически на приходящие сообщения - примером могут служить боты для транслитерации Google. Если мы добавим бот транслитерации в список контактов и отправим ему фонетическое слово, он отправит в ответ транслитерированное слово в кодировке Unicode на языке Хинди. Несложно разработать подобный бот в том случае, если есть отдельная программа для транслитерации.

Теперь изменим файл base.py, добавив обработчик принятых сообщений. Когда новое сообщение принято, этот обработчик отвечает сообщением "Welcome to my first GTalk Bot :)".
#!/usr/bin/env python 

import xmpp 

user="username@gmail.com" 
password="password" 
server="gmail.com" 

def message_handler(connect_object, message_node): 
	message = "Welcome to my first Gtalk Bot :)" 
	connect_object.send( xmpp.Message( message_node.getFrom() ,message)) 

jid = xmpp.JID(user) 
connection = xmpp.Client(server) 
connection.connect() 
result = connection.auth(jid.getNode(), password, "LFY-client") 
connection.RegisterHandler('message', message_handler) 

connection.sendInitPresence() 

while connection.Process(1): 
	pass

Здесь функция connection.RegisterHandler('message',message_handler) используется для указания, что функция message_handler() должна быть вызвана при приеме станса сообщения. Два аргумента, передаваемых функции - это объект соединения и станс сообщения. Используя функцию getFrom(), мы получаем JID пользователя, отправившего сообщение и отправляем сообщение этому пользователю. Для получения текста присланного сообщения, используется функция message_node.getBody().

Бот для удаленного выполнения команд

Вы наблюдали процесс написания бота при помощи нескольких строк кода. Теперь давайте попробуем реализовать другую идею: удаленно управлять компьютером при помощи бота по протоколу XMPP. Обычно мы используем SSH (Secure Shell) для удаленного администрирования - это позволяет нам использовать командную оболочку, поэтому мы можем исполнять команды на удаленном компьютере. Сможем ли мы сделать что-то похожее на основе XMPP-бота? Да! Давайте изменим код для работы в режиме бота с целью удаленного выполнения команд. Замените простую функцию message_handler() на приведенную ниже.
def message_handler(connect_object,message_node): 
	command = str(message_node.getBody()) 

	process = subprocess.Popen(command,shell=True,stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
	message = process.stdout.read() 
	if message=="": 
		message=process.stderr.read() 

	connect_object.send( xmpp.Message( message_node.getFrom() ,message))
Примечание: Необходимо добавить строку import subprocess в начало файла программы, поскольку этот модуль сейчас используется в функции message_handler().

В этом обработчике сообщений мы получаем текст сообщения, принятого от отправителя, и выполняем как команду, используя модуль Python под названием "subprocess". Мы проверяем дескрипторы файлов process.stdout (стандартный вывод дочернего процесса) на наличие данных, затем process.stderror (стандартный вывод сообщений об ошибках дочернего процесса). После этого данные, возвращенные дочерним процессом отсылаются отправителю сообщения.

Как и раньше, попробуйте с помощью Pidgin (работающей с учетной записью другого пользователя) отправить боту команды, такие как ls, cat /proc/cpuinfo, и.т.д. Вы должны увидеть ожидаемые результаты исполнения этих команд в виде ответных сообщений в окне Pidgin.

Этот бот не предусматривает контроля доступа - любой пользователь из списка контактов может отправить команды и они будут выполнены. Давайте добавим простую проверку идентификатора отправителя сообщения, ограничив таким образом возможность исполнения команд для всех, кроме одного лица из списка контактов. Все другие пользователи, отправляющие команды должны получать сообщение об ошибке с текстом "Access denied". Замените функцию message_handler() на представленную ниже:
def message_handler(connect_object,message_node): 

	admin = "admin_user@gmail.com" 
	from_user = message_node.getFrom().getStripped() 

	if admin == from_user:  # allow to execute command only if admin requested 
		command = str(message_node.getBody()) 

		process = subprocess.Popen(command,shell=True,stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
		message = process.stdout.read() 
		if message=="": 
			message=process.stderr.read() 
	else: 
		message="Access denied!\nContact system admin" 

	connect_object.send( xmpp.Message( message_node.getFrom() ,message))

Поиск невидимых пользователей

Большинство протоколов мгновенных сообщений поддерживают невидимость пользователей, поэтому каждый желающий может находиться в сети без уведомления лиц из списка контактов о статусе. В этом отношении GTalk не является исключением. Когда я начал разбираться в протоколе XMPP, я отметил интересную вещь в его реализации в рамках GTalk: невидимость реализована только на стороне клиента, а не на стороне сервера. Мы можем легко обнаружить таких невидимых, но подключенных пользователей в нашем списке контактов, просто исследуя сообщения о присутствии. (Как было объяснено ранее, когда клиент подключается к серверу/сети, отправляется уведомление о присутствии.)

Когда пользователь проходит процесс аутентификации и становится невидимым, клиент отправляет уведомление о присутствии со статусом "unavailable" всем лицам из списка контактов. Таким образом, уведомления о присутствии со статусом "unavailable" говорят о том, что пользователь установил режим невидимости. Все клиенты GTalk игнорируют эти уведомления; поэтому эти пользователи не появляются в списке контактов. Мы же можем написать программу для получения списка невидимых пользователей:
#!/usr/bin/python -W ignore::DeprecationWarning 

import xmpp 

user="user@gmail.com" 
password="password" 
server="gmail.com" 

def presenceHandler(conn, presence): 
	if presence: 
		if presence.getType() == "unavailable": 
			print presence.getFrom().getStripped() 

print "Invisible users:"

jid = xmpp.JID(user) 
connection = xmpp.Client(server,debug=[]) 
connection.connect() 
result = connection.auth(jid.getNode(), password,"Client Name") 

connection.RegisterHandler('presence',presenceHandler) 
connection.sendInitPresence() 

while connection.Process(1): 
	pass

Разработка графического интерфейса для клиента

До этого момента наш простейший клиент GTalk, разработанный на языке Python с применением модуля поддержки XMPP был программой с интерфейсом командной строки, которую необходимо запускать в терминале. Вы можете разработать графический интерфейс с целью придания вашему клиенту привлекательности для пользователя и улучшения пользовательских качеств. Две наиболее популярных библиотеки для создания графических пользовательских интерфейсов на Python - это Qt и GTK. Во время разработки графического интерфейса нужно помнить о том, что необходимо работать с XMPP и графическим интерфейсом в отдельных потоках. Функция connection.Process(1) должна вызываться в бесконечном цикле.

В приведенных выше программах мы использовали для этой цели цикл while. В случае работы с Qt или GTK работа с окнами осуществляется в цикле приема событий (event loop) - поэтому использование еще одного бесконечного цикла внутри цикла приема событий приведет к потере отзывчивости интерфейса или к тому, что окна перестанут реагировать на события. Напротив, использование модуля для поддержки потоков в Python позволит выполнять работу с протоколом XMPP в отдельном потоке.

Надеюсь, вам понравилось работать с Google Talk и XMPP. Удачи в разработках и до встречи.