Migração bem-sucedida para uma solução XMPP personalizada
Publicados: 2019-04-08Vou falar sobre os desafios que enfrentamos ao migrar de um bate-papo de terceiros para uma solução de mensagens personalizada baseada em XMPP para nosso cliente, Forward Health, uma solução de mensagens de saúde baseada no Reino Unido. Este artigo abordará os motivos da migração, nossas expectativas versus as realidades da implementação e o desafio de criar funcionalidades adicionais.
Onde começamos
A Forward Health, nosso cliente, queria criar um aplicativo de comunicação móvel para profissionais de saúde no Reino Unido, incluindo a funcionalidade de bate-papo. Como uma startup, eles queriam mostrar seu produto em funcionamento rapidamente. Ao mesmo tempo, as mensagens precisavam ser confiáveis, robustas e capazes de enviar dados confidenciais do paciente com segurança. Para conseguir isso, decidimos usar uma das soluções de terceiros disponíveis para a funcionalidade de bate-papo .
A funcionalidade de bate-papo não é algo trivial, especialmente quando se destina a dar suporte ao setor de saúde. À medida que o aplicativo crescia, encontramos mais casos extremos e alguns bugs no lado da biblioteca que o terceiro não estava disposto a trabalhar. Além disso, a Forward Health queria adicionar novos recursos que não eram suportados pela biblioteca de terceiros. Mudar para uma solução personalizada foi o próximo passo.
Foi quando começamos a trabalhar com o MongooseIM . O MIM é uma solução de código aberto baseada no protocolo XMPP bem estabelecido. Trabalhamos com uma empresa externa Erlang Solutions Limited para configurar nosso back-end e fornecer suporte na implementação de soluções personalizadas.
No início, tudo sobre as mensagens parecia diferente. Anteriormente, tínhamos todas as nossas necessidades atendidas pelo SDK e sua API REST. Agora, usando o MongooseIM, tivemos que levar algum tempo para entender a natureza do XMPP e implementar nosso próprio SDK . Descobriu-se que o servidor XMPP “bare bones” só passava estrofes (mensagens XML) entre clientes em tempo real. As estrofes podem ser de diferentes tipos, ou seja, mensagens normais de bate-papo, presença, solicitações e respostas. Uma grande variedade de módulos pode ser adicionada ao servidor para, por exemplo, armazenar mensagens e permitir que os clientes as consultem.
No lado do cliente (Android, iOS) havia alguns SDKs de baixo nível. Infelizmente, eles estavam agindo apenas como uma camada que permitia a comunicação com o MongooseIM e alguns de seus módulos conectáveis chamados XEPs (XMPP Extension Protocol responsável, entre outras coisas, pelo envio de notificações push para cada mensagem). Toda a arquitetura de manipulação de mensagens, armazenamento e consulta de mensagens, teve que ser implementada por nossa equipe.
O que veio em nosso socorro foi a biblioteca de terceiros que usamos anteriormente. Ele tinha uma API muito bem pensada, então fizemos nossa solução funcionar de maneira semelhante. Separamos o código específico do XMPP em nosso SDK interno com a interface correspondente a uma da solução anterior. Isso resultou em apenas algumas alterações no código do nosso aplicativo após a migração.
Durante a implementação do MongooseIM, fomos surpreendidos várias vezes por elementos que pensávamos que seriam padrão, mas não estavam disponíveis para nós, nem mesmo pelo XEP.
Implementando os principais recursos do bate-papo baseado em XMPP
Carimbos de data e hora
Você pode pensar, como fizemos, que os carimbos de data/hora seriam tão simples quanto “Recebo uma mensagem, exponho isso na interface do usuário com um carimbo de data/hora”. Não, não é tão fácil. Por padrão, as estrofes de mensagem não têm um campo de carimbo de data/hora. Felizmente para nossa equipe, o XMPP é um protocolo facilmente extensível. No back-end, implementamos um recurso personalizado, adicionando um carimbo de data/hora a cada mensagem que passava pelo servidor MongooseIM. Em seguida, o destinatário teria o carimbo de data/hora anexado à mensagem.
Por que um remetente não pode adicionar um carimbo de data/hora? Bem, não sabemos se eles têm a hora correta definida em seu telefone.
Por que não há nenhum XEP para isso? Talvez porque o XMPP seja um protocolo em tempo real, então teoricamente toda mensagem enviada é recebida imediatamente.
EDIT: Como Florian Schmaus apontou: “Na verdade, existe um, embora possa ser facilmente esquecido por causa de seu nome confuso: XEP-0203: Delayed Delivery”. Ele adiciona um carimbo de data/hora a uma mensagem somente se sua entrega estiver atrasada. Caso contrário, a mensagem foi enviada agora.
Mensagens off-line
Quando ambos os usuários estão logados no aplicativo, eles podem enviar mensagens um para o outro em tempo real. Mas e se um deles estiver offline? A resposta rápida é: as mensagens precisam ser armazenadas em buffer no backend . O recurso de mensagens offline lida com esse trabalho e envia todas as estrofes armazenadas em buffer para o usuário assim que ele faz login novamente.
Mas então surgem várias perguntas:
- Por quanto tempo essas mensagens devem ser armazenadas em buffer?
- Quantos deles?
- Eles devem ser reenviados logo após o login novamente? Mas vai inundar o cliente com as mensagens, não vai?
- E se um usuário apenas fizer login, mas não entrar no chat com as novas mensagens. Será que todos eles vão embora?
- E se um usuário estiver conectado em vários dispositivos?
Tornou-se evidente que o recurso de mensagem offline só conseguia enviar mensagens para o primeiro dispositivo que voltasse a ficar online, e essas mensagens seriam perdidas para todos os outros dispositivos. Decidimos descartar esse recurso e armazenar as mensagens no backend XMPP de uma maneira diferente e persistente.

Gerenciamento de arquivo de mensagens (MAM)
MAM é o armazenamento no servidor para mensagens. Quando um cliente está conectado, ele pode consultar o servidor em busca de mensagens. Você pode consultar por páginas, você pode consultar por datas. É flexível — você pode até consultar uma página antes ou depois de uma mensagem com um ID específico, adicionando filtros para mensagens da conversa exata.
Aqui está a pegadinha. As mensagens de bate-papo normais são armazenadas dentro das mensagens do MAM, que têm seus próprios IDs exclusivos. Quando um usuário recebe uma mensagem de bate-papo em um fluxo, ela não contém o ID do MAM. Eles têm que consultar o MAM para obtê-lo.
A recuperação do MAM é uma solicitação de rede, o que significa que pode levar um tempo relativamente longo. Quando um usuário entra em um bate-papo, ele deseja ver as mensagens imediatamente. Portanto, também precisamos de um banco de dados local .
Quando um usuário recebe uma mensagem em um stream (uma mensagem online), nós a salvamos no banco de dados local e a mostramos ao usuário. Dessa forma, exibimos mensagens que chegam em tempo real rapidamente ao usuário.
Além disso, toda vez que eles entram na tela de bate-papo, baixamos todas as mensagens de agora para a mensagem MAM mais recente armazenada no banco de dados local para essa conversa e as colocamos em um banco de dados, ignorando duplicatas.
É assim que lidamos com o armazenamento de mensagens antigas. Além disso, temos certeza de que no banco de dados há um conjunto completo de mensagens para uma conversa específica entre a primeira e a última mensagem do MAM.
Para acompanhar as mensagens baixadas do MAM, adicionamos duas propriedades às entidades de conversação:
- ID MAM da mensagem MAM mais recente no banco de dados
- ID MAM da mensagem MAM mais antiga no banco de dados
Manipular conjuntos quebrados de mensagens MAM em um banco de dados local seria muito problemático.
Além disso, ter essas duas propriedades para cada conversa nos permite armazenar mensagens de bate-papo normais no banco de dados enquanto ignoramos o wrapper — mensagem MAM. E quando o usuário entra no chat, podemos mostrar as últimas mensagens do banco de dados e em segundo plano buscar as mensagens faltantes do MAM.
Caixa de entrada
Todo aplicativo baseado em bate-papo precisa de uma tela com uma lista de bate-papos - um local onde você pode ver nomes, últimas mensagens e uma contagem de mensagens não lidas. Deve haver uma solução para isso!
Na verdade, não há… Há algo chamado Roster – ele pode conter uma lista de usuários marcados como “amigos”. Infelizmente, não há última mensagem nem contagem de mensagens não lidas anexadas a elas. Claro, você pode obter as informações necessárias do back-end em partes. No começo, queríamos fazer dessa maneira, mas funcionaria lentamente e seria complicado de fazer. Foi quando começamos a trabalhar com a Erlang Solutions no recurso Caixa de entrada, que também está se tornando de código aberto.
Quando um usuário se conecta ao back-end XMPP, o aplicativo busca sua caixa de entrada, que contém todas as conversas desse usuário — tanto chats individuais quanto em equipe. Cada um deles tem a última mensagem anexada e uma contagem de mensagens não lidas. O aplicativo salva toda a caixa de entrada no banco de dados local. Quando um usuário está no aplicativo e uma nova mensagem chega, atualizamos o estado da caixa de entrada localmente. Dessa forma, o aplicativo não precisa buscar a caixa de entrada para cada nova mensagem.
Resumo
Algumas soluções de bate-papo de terceiros fornecem um alto nível de abstração. Tudo bem se você quiser criar um aplicativo de bate-papo simples. Ao implementar nossa própria solução baseada em XMPP no aplicativo Forward, conseguimos obter acesso de baixo nível muito melhor, o que facilitou muito a resolução de problemas. Claro, levou algum tempo, mas agora sabemos que podemos fornecer qualquer recurso personalizado para ajudar os médicos no Reino Unido a se comunicarem de maneira segura e fácil, aprovado pelo NHS.
As mensagens têm tudo a ver com comunicação de alto desempenho e em tempo real. Ao mudar para o MIM, conseguimos otimizar cada parte da solução para melhorar a velocidade, a confiabilidade e, finalmente, a confiança. Atualmente, temos todo o código, por isso é fácil rastreá-los. Além disso, estamos após a fase de estabilização e vários relatórios relacionados a mensagens diminuíram drasticamente. Os usuários estão felizes em poder confiar na plataforma.
Projetar e escrever nosso próprio SDK foi uma tarefa desafiadora e gostamos. Era algo diferente de aplicativos simples onde você precisa buscar dados de um servidor e mostrá-los na tela. Durante a implementação, entendemos muitas opções de design da API de biblioteca de terceiros que usamos anteriormente. Por quê? Porque encontramos os mesmos problemas.