Brincando com APIs de XMPP
10 min
Primeiramente, antes de ler este artigo, se quiser saber o básico sobre XMPP, leia meu artigo anterior: Breve introdução ao XMPP.
Hoje em dia, todo indivíduo envolvido com Web já consumiu alguma API HTTP, isso já é parte do dia a dia de qualquer desenvolvimento de aplicação web. Porém, nos últimos meses, começaram a aparecer alguns websites oferecendo APIs em outros protocolos, entre eles, o XMPP. Consumir uma API com esse protocolo é um pouco mais complicado que uma versão HTTP, mas o escopo e os benefícios obtidos são bem diferentes, como demonstraremos neste artigo.
XMPP em aplicações Web
Enquanto não temos suporte completo aos websockets do HTML5, que seria uma maneira de criar conexões TCP a partir do browser, a alternativa é usar a extensão BOSH do XMPP, que especifica como um cliente deve usar o XMPP via uma conexão HTTP. Vamos olhar com mais detalhe nos componentes envolvidos nesse tipo de conexão no diagrama abaixo, roubado diretamente da especificação da extensão:
Servidor XMPP
|
| [unwrapped data streams]
|
BOSH Connection Manager
|
| [HTTP + <body/> wrapper]
|
Cliente (Browser)
- Entre o browser e o BOSH Connection Manager: por meio de long polling ou de qualquer outra técnica Comet, o browser envia requisições HTTP que respeitam a especificação BOSH (o HTTP +
<body>
wrapper no diagrama acima) para um gerenciador de conexões (BOSH connection manager), cuja responsabilidade é identificar a sessão XMPP e manter as informações dessa sessão. - Entre o BOSH connection manager e o servidor XMPP: o connection manager deve criar e manter uma conexão TCP para cada sessão aberta com o servidor XMPP e é dessa forma que a tradução HTTP para TCP é feita.
Esse BOSH connection manager pode ser tanto um componente da sua infra quanto implementado dentro do próprio servidor XMPP. Você pode encontrar mais informações sobre esses conectores em http://xmpp.org/tech/bosh.shtml.
Strophejs
O Strophejs é uma biblioteca que implementa um cliente de XMPP em javascript, logo, pode ser utilizado em aplicações web. Entre as funcionalidades que essa biblioteca implementa, podemos citar:
- está em conformidade com a especificacão BOSH e o XMPP;
- é cross-browser;
- possui builders e usa jQuery, o que facilita o manuseio de XML;
- suporta vários métodos de autenticação;
- faz long polling e permite requisições HTTP cross-domain por meio de um componente flash;
Mashup de comparação em tempo real
O objetivo dessa aplicação é permitir que o usuário possa comparar dois assuntos (palavras-chave) de sua escolha em tempo real. Por exemplo, podemos disparar uma busca para comparar as linguagens Java e Ruby em tempo real.
Para que isto seja possível, iremos utilizar a API XMPP do Collecta, um site de busca em tempo real. A API do Collecta utiliza o modelo Publish-Subscribe para publicar os resultados de busca em tempo real para os usuários da API. Então vamos rever o cenário:
- Logo ao abrir o site a aplicação web connecta anonimamente no servidor XMPP por meio do Strophe;
- O usuário entra com uma busca;
- A aplicação web se inscreve em um nó pubsub (no caso, a busca);
- Ao confirmar a inscricão, a aplicação web está pronta para receber os eventos publicados;
- Ao receber um evento, a aplicação web faz um parse do XML e apresenta o resultado em HTML;
- O usuário pode agora comparar os resultados;
Todo o código deste exemplo está no Github caso queira acompanhar e executar o exemplo. Antes de detalharmos o que acontece em cada fluxo, as dependências dessa aplicação são as seguintes:
- jQuery: auxilia bastante no parsing de XML;
- flXHR.js: componente flash que permite requisições cross-domain, necessário para a aplicação se conectar num servidor XMPP em outro domínio;
- strophejs: sem ele não conseguimos usar o XMPP;
- dependências do strophejs: códigos para gerar md5, base64 e usar o componente flash;
- API key do Collecta: para usar a API do Collecta você precisa de uma chave.
Mesmo sendo todo implementado em HTML e Javascript, é importante servir essa página em um servidor web, para que o componente flash funcione.
Então vamos apresentar como se implementa cada fase do fluxo dessa aplicação (o foco será apenas no código javascript):
Ao abrir o site
A inicialização da conexão ao servidor XMPP já é feita logo que o usuário abre o site.
// config.js
var Config = {
API_KEY: 'YOUR_COLLECTA_API_KEY',
BOSH_SERVICE: 'http://collecta.com/xmpp-httpbind',
HOST: 'guest.collecta.com',
};
No arquivo de configuração acima, temos 3 itens importantes: a chave da API, o BOSH connection manager ao qual iremos conectar (tente abrir essa URL no seu browser) e o host (servidor XMPP) que queremos conectar. Temos um problema nesse ponto, a chave da API vai ser exposta aos usuários. Para o caso da API do Collecta, o único problema seria outro usuário consumir o rate limit, mas mesmo assim o time da Collecta deve procurar alternativas ainda, uma vez que a API é nova.
// real-time-comparison.js
// initiating a BOSH connection to create an anonymous connection to the Collecta XMPP server
connection = new Strophe.Connection(Config.BOSH_SERVICE);
connection.connect(Config.HOST, null, onConnect);
Graças ao strophejs, basta criar uma conexão como é feito acima, passando o serviço BOSH que deseja usar e depois chamar a função connect desse objeto. Os parâmetros passados são o host desejado, o usuário (que é null porque desejamos conectar anonimamente) e a referência à função que deve ser chamada (callback) ao receber algum status do processo de conexão.
// real-time-comparison.js
function onConnect(status) {
if (status == Strophe.Status.CONNECTING) {
// ... outros status
} else if (status == Strophe.Status.CONNECTED) {
// adding one handler for each type of XMPP stanza
connection.addHandler(onPresence, null, 'presence', null, null, null);
connection.addHandler(onIq, null, 'iq', null, null, null);
connection.addHandler(onMessage, null, 'message', null, null, null);
console.log('Sending presence...');
// app is available to use only after sending and receiving presence from the server
connection.send($pres().tree());
}
}
A função onConnect, ao receber o status informando que está conectado, irá criar os 3 handlers necessários para tratar o recebimento de cada um dos 3 stanzas existentes no XMPP. O primeiro parâmetro é o callback de referência e depois o tipo do stanza (leia a documentação do strophejs para saber quais são os outros parâmetros). Após a criação dos handlers, em toda conexão XMPP que é feita, o cliente deve enviar um stanza de presença para o servidor. Como estamos conectando anonimamente, neste caso, após o envio desse stanza, o servidor deve retornar um stanza de presença informando o Jabber ID do usuário anônimo.
// real-time-comparison.js
function onPresence(prs) {
// in this case, presence got from the XMPP server means to activate UI and allow user to enter the 2 terms to compare
console.log('Got presence!');
anonymous_jid = $(prs).attr('to');
// ... outras coisas
return true;
}
Sendo assim, quando o servidor retorna a presença, nós simplesmente parseamos o xml com jQuery e pegamos o atributo “to”, que indica pra quem o servidor está mandando essa presença, ou seja, o usuário da aplicação. É importante retornar true no fim desse callback, pois sem ele o strophejs irá apagar esse handler. Com o return true, o handler não será apagado e deverá ser reutilizado no próximo stanza de presença que esse cliente receber.
O usuário entra com uma busca
Neste momento o usuário pode entrar com a busca no campo, ao clicar no botão “Go”, a seguinte função é chamada:
// real-time-comparison.js
function startComparison() {
// creates 2 search subscriptions and send the request to Collecta XMPP API via Strophejs
console.log('Subscribing to nodes: ' + terms[0] + ' and ' + terms[1]);
connection.send(Collecta.subscribeSearchStanza(terms[0]).tree());
connection.send(Collecta.subscribeSearchStanza(terms[1]).tree());
}
Inscrição em um nó Pubsub de busca
Para evitar repetição de código, criamos uma pequena biblioteca chamada Collecta que monta os stanzas necessários para inscrever e desinscrever em um nó Pubsub, que representam as buscas. Nesta biblioteca também se encontra uma função para converter os resultados de busca de XML para JSON a fim de ser usado na hora de construir o HTML para o usuário.
// real-time-comparison.js
var Collecta = {
subscribeSearchStanza: function(searchName) {
// generate XML for a search subscription on Collecta XMPP API
return $iq({type: 'set', from: anonymous_jid, to: 'search.collecta.com', id: searchName })
.c('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'})
.c('subscribe', {node: 'search', jid: anonymous_jid})
.up().c('options')
.c('x', {xmlns: 'jabber:x:data', type: 'submit'})
.c('field', {"var": 'FORM_TYPE', type: 'hidden'})
.c('value').t('http://jabber.org/protocol/pubsub#subscribe_options')
.up().up().c('field', {"var": 'x-collecta#apikey'})
.c('value').t(Config.API_KEY)
.up().up().c('field', {"var": 'x-collecta#query'})
.c('value').t(searchName);
},
// ...
O trecho de código acima apresenta como devemos criar um stanza utilizando o XML Builder implementado no strophejs. No caso dessa função, estamos criando um stanza do tipo iq para realizar a inscrição do nosso usuário (observador) em um nó PubSub. A construção desse stanza respeita a especificação PubSub do XMPP. Dê uma olhada na documentação da API do Collecta para ver como deve ser esse stanza que é enviado ao servidor.
Confirmação da inscrição
// real-time-comparison.js
function onIq(xml) {
// in this case, the IQ stanzas handled are just subscription results
xmlDom = $(xml);
var iqId = xmlDom.attr('id');
if (iqId == terms[0] || iqId == terms[1]) {
if (xmlDom.find('subscription:first').attr('subscription') == 'subscribed') {
console.log('Subscribed to ' + iqId);
}
}
return true;
}
Após enviar o stanza de inscrição, devemos esperar uma resposta do servidor. Quem processa essa resposta é o handler que tínhamos criado anteriormente. O procedimento é simples, com jQuery podemos navegar no DOM da resposta, se o id da resposta for o mesmo da busca realizada e o status da inscrição é “subscribed”, então o usuário está inscrito e agora só precisa aguardar os itens publicados.
Recebendo um evento publicado
// real-time-comparison.js
function onMessage(msg) {
// messages stanzas are converted to JSON and then prepended to the correspondent panel in the UI
var result = Collecta.processItem($(msg));
for (var i = 0; i < result.length; i++) {
$('#term' + result[i].term + 'panel').prepend(
'<p>[' +
result[i].category +
'] - <a href="' +
result[i].url +
'" target="_blank" title=" Access ' +
terms[result[i].term] +
' information">' +
result[i].title +
'</a><br />...' +
result[i].description +
'...<br />Published in ' +
result[i].published +
'</p>'
);
}
return true;
}
Os eventos publicados vêm em um stanza “message” e extendidos com a especificação PubSub. O único passo necessário e realizar o parse dessa resposta e convertê-lo para JSON, assim poderemos construir todo o HTML necessário para repassar esses resultados de busca para o usuário de forma mais fácil.
Após todo esse processo, o usuário já pode visualizar os resultados na página assim que eles forem sendo publicados no nó PubSub. Ufa!
Quando há a necessidade de se obter resultados em tempo real na web, o antigo modelo de puxar a informação (pull) por meio de uma requisição HTTP já não é tão eficiente, pois perde-se tanto no desempenho do servidor, que terá uma alta carga devido às inúmeras requisições desnecessárias feitas, quanto na velocidade da informação, pois entre uma requisição e outra pode-se perder o efeito tempo real desejado.
Está crescendo cada vez mais o push como modelo para aplicações Web, na qual o servidor deve fornecer a informação para os clientes web logo que ela for publicada. Juntamente com o modelo de push, o modelo Publish-Subscribe também está se provando efetivo, uma vez que não é só implementado em XMPP, mas também em Webhooks, Pubsubhubbub, entre outros.
O diferencial do XMPP neste caso é a especificação da extensão Publish-Subscribe, que é bastante detalhada e cobre vários fluxos que podem existir no modelo PubSub, além é claro dos vários servidores que já implementam este módulo. Vale a pena experimentar.