Comunicação cliente-servidor em tempo real com Socket.IO

Guia com exemplos reais e casos de uso

João Gabriel
Accenture Digital Product Dev
13 min readMar 5, 2020

--

Você já precisou criar uma aplicação que precisasse de comunicação cliente-servidor em tempo real? Neste post, vamos te ajudar a fazer isso usando o framework Socket.IO, incluindo exemplos reais de casos de uso. Vamos lá?

O que já estamos acostumados a usar

Provavelmente todo desenvolvedor iOS já utilizou algum tipo de requisição para consultar uma informação na internet acessando alguma API. Uma forma de implementar esse tipo de requisição é usando uma URLSession. Nesse tipo de comunicação, o cliente faz uma requisição e sempre espera uma resposta do servidor, como no esquema abaixo:

Exemplo do fluxo de uma requisição e resposta para um servidor.

Este tipo de comunicação app-servidor serve muito bem para a maioria das aplicações atualmente, nas quais é necessário requisitar algumas informações específicas. Mas você já se deparou com alguma situação em que seja necessário que o servidor requisite o cliente a qualquer momento? Seja para atualizar uma informação na tela ou mudar remotamente alguma propriedade do app? Essas situações precisam de uma comunicação bidirecional, e o Socket pode te ajudar com isso.

Motivações

As comunicações bidirecionais, nas quais o cliente pode requisitar o servidor e vice-versa, são possíveis com comunicação Socket. Como mostra o seguinte fluxo:

Exemplo do fluxo de uma requisição e resposta em uma comunicação socket.

Este tipo de comunicação pode ser muito importante em determinadas aplicações e, dependendo do projeto, pode ser um recurso primordial. Alguns exemplos de funcionalidades que se beneficiam do uso de Socket são:

  • Compartilhamento de coordenada/localização em tempo real (disponível neste tutorial);
  • Chat (disponível neste tutorial);
  • Login / Validação via QR Code (disponível nesse tutorial);
  • Jogos;
  • Gráficos de mercado financeiro;
  • Qualquer outro tipo de app que necessite de atualização em tempo real.

Diferente das requisições que estamos acostumados, uma conexão Socket permite que ambas as partes conectadas se comuniquem entre si a qualquer momento. Uma vez iniciada a conexão, tanto o cliente como o servidor podem escutar e emitir mensagens.

O que é o Socket.IO?

Socket.IO é um framework que facilita a implementação de Socket e está disponível para iOS, Android, e linguagens de programação utilizadas em back-end e front-end. Os exemplos deste post vão usar Swift (iOS) e Javascript (Web) para implementar o cliente e NodeJS para implementar o funcionamento do back-end.

Comunicação do servidor com seus clientes

Um servidor que implementa socket possui uma lista de clientes sockets conectados e cada cliente possui um Socket ID. Assim, o servidor pode selecionar quais clientes socket vão receber uma determinada mensagem. É possível que o servidor selecione apenas um ID de cliente socket para enviar a mensagem ou um grupo específico de IDs. Além disso, também pode mandar uma mensagem para todos os clientes sockets conectados naquele momento.

Funcionamento

O Socket.IO funciona basicamente com dois métodos: o de emitir e o de escutar. Ambos sempre recebem dois parâmetros, o evento e os dados. No exemplo abaixo temos um socket emitindo (método emmit) um evento “mensagem” e o dado enviado é um texto com o conteúdo da mensagem:

Trecho de código em Swift exemplificando um evento sendo emitido.

No caso, o nome do evento é “mensagem”, e o conteúdo da mensagem é o texto “conteúdo da mensagem”. O exemplo abaixo escuta o evento acima utilizando o método on:

Trecho de código em Swift exemplificando um evento sendo escutado.

Neste caso de escuta, sempre quando o evento “mensagem for escutado, a closure será executada com o parâmetro data, que representa os dados que foram enviados na mensagem, no caso a String “conteúdo da mensagem”.

Eventos padrão

O framework possui alguns eventos padrão que sempre serão chamados em qualquer conexão socket e podem ser utilizados para dar algum tipo de feedback na aplicação cliente ou servidora. Os principais eventos padrão que podem ser bem úteis são:

Lista de eventos padrão Socket.

Utilizando Socket.IO

Vamos agora a um passo a passo para poder criar um primeiro app com Socket! 😃

Instalando Socket.IO na aplicação iOS

É possível instalar Socket.IO com CocoaPods, Carthage e Swift Package Manager. No exemplo deste post vamos utilizar Swift Package Manager. Siga os passos a seguir para instalar na sua aplicação:

  1. Crie um projeto no Xcode em File -> New -> Project.
  2. Selecione iOS e Single View App e clique em Next.
  3. Digite um nome para o projeto e clique em Next.
  4. Vá em File -> Swift Packages -> Add Package Dependency
  5. Depois cole o link https://github.com/socketio/socket.io-client-swift.git, e clique em Next.
  6. Selecione Branch: master e clique em Next.
  7. Para adicionar o framework, clique em Finish.

Exemplos de utilização

Os exemplos a seguir são algumas aplicações que se beneficiam do uso de Socket:

  1. Compartilhamento de coordenada/localização em tempo real

A implementação que utiliza coordenadas (dois valores respectivos para x e y, por exemplo) pode ser usada em contextos diferentes. Aqui vou apresentar dois exemplos que se beneficiam do compartilhamento em tempo real de uma coordenada. Um exemplo de alteração da posição de elementos na tela:

Exemplo de aplicação que recebe uma coordenada e movimenta um elemento na tela.

E abaixo podemos ver um exemplo de atualização de um pin em um mapa representando a geolocalização de um dispositivo em movimento:

Exemplo de aplicação que recebe uma coordenada e atualiza um pin em um mapa.

No caso, o simulador à direita representa o dispositivo em movimento, e é possível simular isso em Simulator -> Debug -> Location -> Freeway Drive. A cada mudança de localização, o simulador da direita envia a informação da coordenada atual para o servidor com socket, que repassa essa informação para os outros clientes socket conectados. O simulador da esquerda recebe o evento contendo a coordenada e atualiza a posição do pin na tela.

A única diferença entre esses dois exemplos é a implementação da UI. Ambos utilizam a mesma implementação de socket. Abaixo, mostro os detalhes da implementação. Antes de falar do cliente socket de fato, vamos dar uma olhada no trecho de código abaixo:

Código do SocketParser em Swift, uma classe que auxilia a conversão da estrutura recebida para um objeto específico.

A classe SocketParser será utilizada para converter os dados recebidos dos observadores de eventos socket. Os observadores retornam Arrays do tipo Any. No caso o SocketParser converte as informações para o tipo genérico.

A struct SocketPosition representa a estrutura de uma coordenada 2D, contendo o valor x e y:

Struct representando a coordenada 2D

Nessa implementação criaremos um protocolo para que a controller do módulo que estamos desenvolvendo consiga receber as informações da classe que gerencia o socket:

Protocolo para ser implementado pela controller, permitindo que o SocketManager se comunique com a controller.

Dessa forma a controller pode ser acionada quando o socket se conecta e quando uma nova posição é recebida.

A classe SocketTutorialManager é um exemplo de uma estrutura que pode ser utilizada para implementar um cliente de Socket.IO em Swift.

Implementação do SocketPositionManager, que gerencia o acesso ao Socket.

A classe SocketTutorialManager possui a variável manager, um SocketManager que recebe algumas propriedades de configuração como a URL do servidor que implementa socket. A propriedade de configuração log, quando recebe true, o SocketManager passa a mostrar no console os logs para facilitar processos de debug.

Na inicialização do SocketTutorialManager temos a chamada do setupSocket(), que é responsável por inicializar a variável socket, que é do tipo SocketIOClient, e será utilizada para emitir e ouvir eventos. Ainda na inicialização temos a chamada do método setupSocketEvents() que configura os observadores de eventos socket. E para finalizar a inicialização temos a chamada do método connect() do SocketIOClient, que faz com que o socket se conecte à URL e inicie o trabalho de ouvir e emitir eventos.

A função setupSocketEvents() configura os observadores de eventos. O primeiro observador adicionado recebe um clientEvent do tipo .connect que é um evento padrão, como já foi mencionado nesse tutorial no tópico de Eventos Padrão. Isso faz com que essa escuta seja executada sempre que o socket se conectar ou se reconectar com sucesso. O segundo observador adicionado é do evento customizado chamado “drawing”, assim como todo evento. Dentro da closure do evento “drawing”, temos a utilização do SocketParser para converter o array de Any recebido para SocketPosition.

A função socketChange(position: SocketPosition) converte o SocketPosition para um dicionário String:Any com o index x e y para representar a coordenada e emite a mensagem com o método emmit, enviando um evento customizado “drawing” contendo o dicionário.

Para finalizar, o método stop() deve ser chamado quando o módulo for encerrado. Isso chama o método removeAllHandlers() do SocketIOClient para que seja removido da memória os observadores do framework.

A implementação da tela

Para implementar a tela na qual se quer movimentar um elemento de UI, implemente o protocolo SocketPositionManagerDelegate na sua UIViewController, ou na respectiva controller que estiver utilizando. Assim para fazer um elemento se mover na tela utilize a seguinte implementação:

Método implementado pela Controller, que manipula a posição de um elemento “element” na tela.

Obs.: neste caso a variável element representa uma UIView previamente declarada e inserida na tela, que vai ter sua localização alterada. Essa UIView está recebendo um CGPoint que utiliza os valores do SocketPosition para ser inicializado.

Para implementar uma tela na qual se quer realizar a mudança de localização de um pin em um mapa é necessário utilizar o SocketPosition recebido. Os valores de x e y do SocketPosition podem ser utilizados para inicializar um CLLocationCoordinate2D, podendo, assim, atualizar um pin ou dar um zoom em alguma região específica.

Método implementado pela Controller, que solicita que a tela altere a posição de um pin, utilizando um CLLocationCoordinate2D.

Obs.: neste caso a variável screen representa uma UIView previamente declarada e associada como a view da UIViewController. Essa UIView já possui um MKMapView devidamente inserido na tela com um pin dentro, que vai ter sua localização alterada. A screen, então, recebe um CLLocationCoordinate2D que foi inicializado com os valores do SocketPosition.

Essa mesma implementação pode ser usada para qualquer tipo de aplicação com coordenadas. Isso pode ser útil em jogos em que objetos podem se mover devido à interação de outro usuário.

Aplicação servidora

No caso deste exemplo, é possível utilizar uma aplicação disponibilizada no site do Socket.IO, com este link. A intenção inicial deste projeto é ser uma lousa na qual todos os usuários podem desenhar em tempo real com todos os usuários conectados, usando pincéis de cores diferentes. Além disso, este projeto possui uma implementação bem pertinente para o nosso exemplo, pois apenas repassa tudo o que recebe no evento “drawing”. Caso queira implementar o servidor, o exemplo abaixo seria o suficiente para rodar essa aplicação de exemplo em NodeJS.

Servidor codificado em NodeJS.

A constante io no caso representa o nosso acesso ao módulo Socket.IO. Na linha 13 temos um observador sendo criado (utilizando o método on()), que recebe a função onConnection que por sua vez é executada assim que receber uma mensagem chamada “connection”. No caso de Node.JS, o evento “connection” equivale ao evento padrão connect quando uma conexão é estabelecida.

A função onConnection é responsável por realizar a configuração inicial dos observadores que vão ficar ativos para aquele cliente socket. Nesta função, o observador “drawing” está sendo criado e ele recebe um data, que representa os dados recebidos no evento, e este mesmo data é enviado logo em seguida para broadcast com o mesmo nome de evento “drawing”. Essa chamada do método emit usando broadcast significa que o servidor vai emitir o evento para todos os clientes sockets conectados, exceto o próprio socket que enviou a mensagem. Sendo assim, podemos utilizar este servidor com este evento para realizar diversos testes, pois ele simplesmente repassa os dados enviados para broadcast, não se importando com a estrutura desse data.

Para mais detalhes sobre a implementação do Whiteboard acesse o repositório.

2. Chat

Este é um exemplo clássico que pode ser implementado com Socket. Nele, será necessário atualizar uma tabela de mensagens de uma conversa, assim que a mensagem é enviada por um outro cliente.

Exemplo de aplicação de chat que recebe e emite mensagens de texto.

O código abaixo representa uma estrutura para implementação em iOS:

Implementação do SocketChatManager, que gerencia o acesso ao Socket para um app com chat.

Este exemplo é muito semelhante ao anterior. As únicas diferenças são os novos métodos register(user), que emite um evento do tipo “add user” com o nome do usuário atual que quer entrar no chat, e o método send(message), que emite um evento do tipo “new message” com o conteúdo da mensagem que se quer enviar. Por último, existe uma grande diferença na implementação do método setupSocketEvents() que agora configura diversos novos eventos, e são eles:

  • user joined” = recebe o username do usuário conectado e o novo total de usuários conectados.
  • user left” = recebe o username do usuário que se desconectou e o novo total de usuários ainda conectados.
  • new message” = recebe o username do usuário que enviou a mensagem e a mensagem enviada.
  • typing” = recebe o username do usuário que está digitando uma mensagem.
  • stop typing” = recebe o username do usuário que parou de digitar.

Aplicação servidora

A implementação do servidor de chat é bem parecida também com o exemplo anterior, mas existem alguns eventos a mais para repassar as informações sobre os usuários e suas mensagens.

Servidor para aplicação chat codificado em nodeJS.

Uma grande diferença em relação ao outro exemplo é a variável numUsers, que representa o total de usuários no chat. Essa variável é atualizada pelos eventos “add user” e “disconnect”. O método acionado no “add user” incrementa o número inteiro da variável numUsers, além de também salvar na respectiva conexão socket o username do usuário que se conectou. O método “add user” também emite o evento “user joined” para broadcast, sinalizando o username e o novo número de usuários que estão no chat e emite para o próprio socket conectado o evento “login” contendo apenas o numero de sockets no chat.

Observação importante: aqui temos uma situação na qual existem duas chamadas de eventos diferentes, sendo que uma utiliza o emmit direto do socket e outra usa broadcast. É importante notar a diferença entre elas. No caso, quando usamos broadcast a mensagem é enviada a todos os sockets conectados, menos o socket atual utilizado. E, no caso de chamar diretamente a emissão do evento a partir do socket, o evento será enviado apenas para o socket atual no qual o método emit foi chamado.

3. Login / Validação via QR Code

A leitura de QR Code pode ser utilizada para diversas aplicações, podendo ser usada como segundo fator de autenticação para fazer login ou validar alguma transação ou operação.

Exemplo de aplicação para validação utilizando a leitura de QRCode.

Ao lado esquerdo está o cliente web exibindo o QR Code, e à direita temos uma aplicação iOS que escaneia o QRCode e envia a mensagem para o servidor. Logo em seguida a mensagem é exibida no cliente Web.

Obs.: neste exemplo, a implementação da parte Socket fica apenas na parte do cliente web (browser exibindo o site).

O funcionamento de uma validação por QRCode é bem mais simples do que parece ser. A sequência de passos abaixo mostra como é feito o processo.

Fluxo de funcionamento da validação usando QRCode.

Uma sequência de passos acontece para este tipo de validação, iniciando pelo cliente web:

  1. Se conecta ao servidor via socket;
  2. Assim que se conecta, obtém seu SocketID;
  3. Página web mostra na tela o SocketID (só que codificado com o QRCode).

Logo em seguida, o cliente mobile (iOS) passa pelas seguintes etapas:

  1. Realiza leitura do QRCode;
  2. Converte o QRCode para texto (esse texto é o SocketID do Cliente Web);
  3. Envia request para servidor com o SocketID e a mensagem de texto.

Por sua vez, o servidor também segue as seguintes etapas:

  1. Recebe requisição com SocketID e mensagem;
  2. Converte os valores recebidos para String;
  3. Envia apenas para o SocketID especificado(a).

E ao final, o cliente web recebe a mensagem do servidor e a exibe na tela.

Aplicação Cliente: iOS

Como especificado acima, a aplicação mobile neste exemplo não precisa ter uma implementação socket. Aqui, a aplicação apenas faz a leitura do QRCode e o envia junto com a mensagem desejada, no body de uma requisição POST.

Aplicação Cliente: Web

A aplicação web, assim que é carregada no cliente, precisa se conectar ao servidor com socket e obter seu socket ID, para exibir o ID em forma de QRCode. Neste exemplo estamos utilizando jQuery para o processo. Abaixo segue um exemplo de como fica a página .html preparada para exibir o QRCode e a mensagem:

Exemplo de implementação da página .html que exibe o QRCode.

Nesta página .html existe uma div e dois paragraphs (<p>). A imagem do QRCode será inserida na div com id “qrCodeImage“, o socket ID será inserido no texto do span com id “qrCodeSocket“, e a mensagem recebida do cliente mobile será inserida no texto do span com id “qrCodeMessage”. Temos também nesta página um span com o id “socketStatus” apenas para mostrar o status da conexão socket.

Além disso, esta página também carrega o script “ClienteWeb.js”. Ele configura o socket para se comunicar com o servidor. O código deste script que roda no cliente é o seguinte:

Exemplo de implementação do script jQuery que roda no cliente para interagir com socket.

A variável socket é inicializada com a url do servidor atual (window.location.host), que espera ter a implementação ouvindo eventos socket.

Em jQuery, a função passada no método ready do $(document) é executada toda vez que a página é carregada e pode ser manipulada de forma segura. Dentro dessa função são declarados os observadores do socket. O primeiro observador socket adicionado é o “connect”, o evento padrão de conexão bem sucedida. Este evento executa algumas configurações iniciais, alterando o texto do status para “connect”, adicionando a imagem do qrcode com o método .qrCode que recebe como conteúdo o socket ID do cliente (socket.io.engine.id) e adicionando o mesmo socket ID no spanqrCodeSocket”.

Os observadores padrão de tentativa de reconexão (“reconnecting“) e de desconexão (“disconnect”) apenas removem a imagem do qrCode e o socket ID. O observador do evento “qrCodeMessage” recebe uma data com o texto da mensagem enviada pelo cliente mobile e a exibe no campo qrCodeMessage, executando uma animação com um gif antes de exibir novamente o QRCode.

Aplicação servidora

A implementação do servidor neste caso precisa de uma rota na API para receber a requisição POST e depois selecionar o socket especificado com um ID para enviar a mensagem. Isso pode ser implementado da seguinte forma:

Exemplo de implementação em NodeJS de uma rota para receber os dados e enviar ao socket ID a mensagem.

O ponto chave da implementação é o momento em que, na linha 11, o evento “qrCodeMessage“ é enviado com o conteúdo recebido message, para apenas o socket ID do cliente especificado utilizando o método .to(qrCode), no qual no caso o qrCode é de fato o socketID do cliente web.

Um único detalhe para que seja possível utilizar a variável io em qualquer parte do código: é necessário inserir a mesma dentro da application, e é possível fazer isso da seguinte forma:

Exemplo em nodeJS de como deixar a referência ao socket acessível de qualquer parte da aplicação.

E está feito!! 🙌🏻😃

Conclusão

Com este tutorial, é possível ter uma visão geral do funcionamento da framework e suas possíveis aplicações práticas. Qualquer dúvida ou sugestão, é só deixar um comentário ou me acionar por meio dos contatos abaixo. 😜

Quer trocar este tipo de conhecimento pessoalmente aqui na Concrete com dezenas de desenvolvedores iOS? Saiba mais neste link e se candidate a uma de nossas vagas. Vamos aprender juntos. ;)

--

--