Como criar um chat com PHP e Swoole: um guia passo a passo

Como criar um chat com PHP e Swoole: um guia passo a passo

Quando um desenvolvedor recebe a tarefa de implementar um chat dificilmente o PHP será a linguagem escolhida. Isso ocorre por alguns motivos, como por exemplo, dificilmente vermos o uso de sockets devido o seu uso não ser simples como em outras linguagens.

Além disso ele possui a característica de ser stateless, já que aplicações em PHP não estão em execução a todo instante. A cada requisição HTTP, o PHP-FPM inicializa o código, processa a requisição, retorna a resposta e termina o processo. Isso faz com que nada relacionado ao código fique em memória.

Como facilitar o processo com PHP?

Felizmente hoje já existem diversas alternativas para os problemas apresentados. Programação assíncrona, I/O não bloqueante, promessas e muito mais.

Existem diferentes formas de utilizar esses recursos. Algumas mais simples como simplesmente adicionar uma biblioteca via composer, que fornecem uma API simples que abstrai dificuldades dos recursos nativos. Outras instalando uma extensão e habilitando no PHP, que convenhamos não é nada difícil, geralmente bastando um único comando e você já tem o que precisa.

Dentre essas alternativas temos nomes como ReactPHP, Amp, Workerman e Swoole. Sendo que todas elas estão a pelo menos 5 anos disponíveis para uso, algumas delas tendo ainda mais tempo.

Hoje vamos focar no uso do Swoole, mostrando como é fácil a criação de um chat com ele.

Um pouco sobre o Swoole

Swoole é uma extensão para o PHP escrita em C/C++ com alto desempenho baseado em uma arquitetura de eventos, assíncrona, com corrotinas de E/S não bloqueantes.

Com ele é possível criar serviços escaláveis TCP, UDP, Unix Socket, HTTP, WebSocket, FastCGI e mais, utilizando sintaxe já conhecida do PHP.

Não é necessário ter conhecimento avançado sobre programação E/S não bloqueante ou como funcionam as chamadas do Kernel Linux.

Requisitos para o desenvolvimento

Para desenvolver esse chat vou utilizar a versão mais recente do PHP, a 8.1. Além disso, obviamente precisamos realizar a instalação do Swoole.

Há diferentes formas de realizar a instalação: Docker, apt, yum, brew, pecl  e realizando a compilação manualmente. O único requisito do Swoole é o PHP na versão 7.3 ou superior.

Para ter a inicialização mais simples irei utilizar a imagem docker oficial pois assim já tenho o PHP e Swoole na ultima versão de ambos. Irei inicialmente apenas baixar a imagem que irei utilizar:

docker pull openswoole/swoole:latest-dev

Se quiser conferir as demais formas de instalação, pode consultar diretamente a documentação.

Mãos na massa

Vamos começar pelo frontend. Por padrão o JavaScript já fornece um cliente WebSocket que possui as funcionalidades que precisamos. Com isso não será necessário o uso de nenhuma biblioteca.

Para nosso chat precisamos de um espaço onde será exibido as mensagens. Além disso um campo para digitar uma mensagem, e um botão para enviar.

Para realizar a conexão com o Socket é bem simples, bastando uma linha:

const ws = new WebSocket('ws://localhost:8081');

Com isso a conexão já está aberta. A partir dai temos 4 eventos que podem ocorrer:

  • open – Quando a conexão é estabelecida.
  • close – Quando a conexão é encerrada. Podendo ser causada por falha do servidor, erro de comunicação ou pela chamada do método de de conexão.
  • error – Quando ocorre algum erro na conexão.
  • message – Ao receber alguma mensagem pela conexão. Todas as informações transmitidas chegarão por esse evento.

Diferentemente do que algumas pessoas imaginam, o protocolo Websocket não possui funcionalidade de salas, grupos ou eventos customizáveis. Ele basicamente define esses 4 eventos e a forma como a comunicação deve ser iniciada e ocorrer. Tudo além disso deve ser desenvolvido conforme necessidade.

Bibliotecas famosas, como o Socket.io, fornecem essas funcionalidades, porém tudo isso roda em cima do mesmo protocolo. Apenas definiram um padrão de mensagens e fornecem bibliotecas para o backend e frontend que falam o mesmo padrão.

Frontend

Baseado no que precisamos, teremos uma página simples com os recursos necessários.

Temos uma div onde será exibido as mensagens trocadas e um formulário com um campo para digitar a mensagem e um botão para enviar.

Inicializando a parte de Javascript, precisamos realizar a conexão com o WebSocket. Além disso, inicializar variáveis com a referencia para os elementos que iremos manipular:

Para facilitar a exibição de mensagens, já que teremos dois momentos onde novas serão adicionadas (Quando receber uma mensagem do servidor e ao enviar uma nova), vamos definir uma função que centralize essa lógica:

Agora precisamos enviar mensagens. Para isso é necessário escutar o evento de envio do formulário. Ao receber o evento, primeiramente é necessário impedir que ele recarregue a página.

Em seguida resgatar o valor digitado. Se estiver em branco, simplesmente ignorar. Caso tenha, é necessário enviar a mensagem para o servidor, limpar o campo e exibir a mensagem na tela.

Para transmitir mensagens o WebSocket suporta valores de texto e binário. Nesse caso estarei transmitindo JSON formatado como string em ambos sentidos e realizando o parse.

Agora resta escutar o evento de transferência de dados e exibir as mensagens recebidas.

Juntando tudo isso, teremos no final:



<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Swoole Socket Client</title>
    <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cstyle%3E%0A%20%20%20%20%20%20%20%20%23messages%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20display%3A%20flex%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20flex-direction%3A%20column%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20margin%3A%2024px%2012px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20border%3A%202px%20solid%20black%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20padding%3A%208px%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20.right%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20margin-left%3A%20auto%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%3C%2Fstyle%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="<style>" title="<style>" />
</head>
<body>
    <h1>Chat</h1>
    <div id="messages"></div>
    <form id="form">
        <label for="message">Mensagem:</label>
        <input type="text" id="message" name="message">
        <button>Enviar</button>
    </form>

    <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%3E%0A%20%20%20%20%20%20%20%20const%20ws%20%3D%20new%20WebSocket('ws%3A%2F%2Flocalhost%3A8081')%3B%0A%20%20%20%20%20%20%20%20const%20form%20%3D%20document.querySelector('%23form')%3B%0A%20%20%20%20%20%20%20%20const%20message%20%3D%20document.querySelector('%23message')%3B%0A%20%20%20%20%20%20%20%20const%20messages%20%3D%20document.querySelector('%23messages')%3B%0A%0A%20%20%20%20%20%20%20%20const%20addMessage%20%3D%20(value%2C%20other%20%3D%20false)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20messages.innerHTML%20%2B%3D%20%60%3Cwp-p%20class%3D%22%24%7Bother%20%3F%20'right'%20%3A%20'left'%7D%22%3E%3Cstrong%3E%24%7Bother%20%3F%20'Outro'%20%3A%20'Eu'%7D%3A%20%3C%2Fstrong%3E%20%24%7Bvalue%7D%3C%2Fwp-p%3E%60%3B%0A%20%20%20%20%20%20%20%20%7D%3B%0A%0A%20%20%20%20%20%20%20%20form.addEventListener('submit'%2C%20(event)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20event.preventDefault()%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20value%20%3D%20message.value%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(!value)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20message.value%20%3D%20''%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20ws.send(JSON.stringify(%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20message%3A%20value%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D))%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20addMessage(value)%3B%0A%20%20%20%20%20%20%20%20%7D)%3B%0A%0A%20%20%20%20%20%20%20%20ws.onmessage%20%3D%20(event)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20data%20%3D%20JSON.parse(event.data)%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20addMessage(data.message%2C%20true)%3B%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="<script>" title="<script>" />
</body>
</html>


Backend

Já para o backend é um pouco mais simples. O Swoole fornece uma classe para inicializar um servidor de WebSocket apenas informando o host e porta onde irá rodar.

Essa classe herda da classe do servidor HTTP, pois é possível simultaneamente atender a solicitações de ambos protocolos. Justamente por isso, você pode escutar a eventos de ambos, porém vamos nos atentar apenas aos específicos de WebSocket:

  • start – Ocorre quando o servidor iniciar. Nele dever ser inicializado tudo que é necessário no uso do servidor, porém não é possível utilizar operações bloqueantes.
  • handshake – O seu uso é opcional. Ele ocorre quando um cliente tenta se conectar ao servidor. Por padrão ele implementa a versão 13 do protocolo WebSocket. Se for utilizado se faz necessário implementar manualmente todo o processo de handshake.
  • open – Ocorre quando o cliente completa a conexão com sucesso. É chamada após o handshake.
  • close – Ocorre quando a conexão é encerrada, seja pelo cliente ou pelo servidor.
  • message – Ocorre quando o cliente envia alguma informação pela conexão.

Todos os eventos recebem em seu callback pelo menos uma instância do objeto servidor onde é possível acessar os clientes ativos.

Os eventos que ocorrem quando o cliente já está conectado (open, close, message) recebem pelo menos o identificador numérico da conexão do cliente.

Para facilitar o desenvolvimento podemos instalar um pacote utilitário para que as IDEs possam completar o código com as classes do Swoole:

composer require --dev swoole/ide-helper:@dev

Para começar nosso servidor precisamos inicializar a instância do servidor e começar a escutar requisições.

Além disso podemos definir os eventos start, open e close e realizar log das ações.

Com isso já temos um servidor funcional e que aceita conexões. Agora precisamos pegar as mensagens recebidas e transmitir para todos que estão conectados.

Ao receber uma mensagem o evento irá fornecer uma instância do servidor e uma instância de um frame. No frame irá conter os dados recebidos, além se os dados são do tipo texto ou binário.

Com isso, podemos percorrer a lista de conexões do servidor e enviar a mensagem para todos, ignorando apenas o ID de quem enviou a mensagem.

 

Com isso já temos o servidor funcional. No final temos no backend:



<?php

declare(strict_types=1);

use Swoole\Http\Request;
use Swoole\WebSocket\Server;
use Swoole\WebSocket\Frame;

$server = new Server('0.0.0.0', 8081);

$server->on('start', function (Server $server) {
echo "WebSocket Server is listen on {$server->host}:{$server->port}", PHP_EOL;
});

$server->on('open', function (Server $server, Request $request) {
echo "Client connected: {$request->fd}", PHP_EOL;
});

$server->on('message', function (Server $server, Frame $frame) {
echo "Client {$frame->fd} message: {$frame->data}", PHP_EOL;

foreach ($server->connections as $connection) {
if ($connection === $frame->fd) {
continue;
}

$server->push($connection, $frame->data);
}
});

$server->on('close', function (Server $server, int $fd) {
echo "Client disconnected: {$fd}", PHP_EOL;
});

$server->start();


Como estou utilizando o docker, vou aproveitar das características da imagem oficial.

Por padrão ela está com o supervidord configurado para iniciar um arquivo nomeado server.php que está em /var/www.

Sabendo disso, já coloquei meu código em um arquivo com esse nome. E para inicializar basta executar o comando na pasta onde está o arquivo:

docker run --rm -it -p 8081:8081 -v $(pwd):/var/www openswoole/swoole:latest-dev

Assim já tenho um servidor de desenvolvimento rodando na porta 8081.

Testando o chat

Para acessar o frontend você pode disponibiliza-lo utilizando um servidor de desenvolvimento do PHP, ou simplesmente abrindo o arquivo no navegador diretamente.

Após acessa-lo pode começar a enviar a mensagens. Para facilitar abra uma segunda abra em uma guia anonima e acesse o chat. Assim será mais fácil ver ele funcionando.

Lembrando que o chat é bem simples, então não exibe histórico de conversas ao recarregar a página e simplesmente separa as mensagens entre as que você enviou e todo o resto.

Conclusão

Tivemos uma breve introdução sobre WebSocket com Swoole e criamos um chat simples. Agora você já tem um ponto de partida para criar o seu próprio chat e buscar mais informações sobre o Swoole.

Essa extensão poderosa e eficiente possui muitos outros recursos úteis para serem utilizadas em projetos em produção. Vale a pena entender mais sobre ele e começar a praticar o seu uso.

Em breve podemos trazer mais possibilidades de uso do Swoole e informações sobre como ele funciona.

Líder em Treinamento e serviços de Consultoria, Suporte e Implantação para o mundo open source. Conheça nossas soluções:

CURSOSCONSULTORIA

Anterior Curso gratuito 4Linux com emprego garantido para desenvolvedores
Próxima Descubra como o novo curso de Soft Skills da 4Linux pode impulsionar sua carreira em TI

About author

Caique Portela
Caique Portela 8 posts

Caique Portela é formado em Análise e Desenvolvimento de Sistemas na FATEC e técnico em Redes de Computadores pelo CEAP. Possui mais de 7 anos de experiência com TI, sendo pelo menos 3 anos atuando diretamente como desenvolvedor. Já trabalhou como suporte técnico e consultor de infraestrutura Linux, hoje atua como desenvolvimento e treinamento na 4Linux.

View all posts by this author →

Você pode gostar também

Desenvolvimento

Como automatizar seu ambiente de desenvolvimento com VSCode Remote Container

Você já considerou utilizar a extensão VSCode Remote Container para automatizar a criação do ambiente de desenvolvimento da sua equipe? Neste artigo quero falar brevemente sobre esta extensão, os pré-requisitos

Desenvolvimento

Descubra os benefícios e vantagens do Zend Framework 2 para desenvolvimento PHP

Se você está lendo este artigo provavelmente você ja desenvolveu ou pretende desenvolver algum projeto de software, talvez você nunca tenha usado um framework antes por isso acho necessário uma

Desenvolvimento

Integrando Telegram ao Rocket.Chat

Recentemente já mostramos como realizar a integração manual de um sistema de chat com o Rocket utilizando o Omnichannel. Nesse post, inclusive, citamos os recursos que já foram desenvolvidos pela