Chat simples utilizando Websocket, Vue.JS e PHP

O Websocket  é um protocolo que nos permite abrir uma comunicação bidirecional com o servidor utilizando um único soquete TCP (Transmission Control Protocol) através do navegador. Sendo assim, tanto o cliente quanto o servidor podem enviar informações entre si a qualquer momento. Neste artigo, veremos como implementar um simples aplicativo de chat utilizando este protocolo.

No lado cliente utilizaremos o Vue.JS para facilitar a construção da interface do chat, além de manter um código mais organizado.

No lado servidor utilizaremos o Ratchet que é uma biblioteca PHP que nos permite construir um servidor de Websocket de um jeito descomplicado.

Confira o exemplo em funcionamento, clicando aqui.

O lado servidor

Primeiramente, precisamos importar a biblioteca do Ratchet para nosso projeto. Para isso iremos utilizar o Composer. Portanto, vamos criar nosso composer.json.

{
    "require": {
        "cboden/ratchet": "^0.3.6"
    }
}

Basta agora rodar composer install no terminal e podemos começar. Vamos então criar a classe que será responsável por gerenciar as conexões recebidas.

<?php

use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

class SimpleChat implements MessageComponentInterface
{
    /** @var SplObjectStorage  */
    protected $clients;

    /**
     * SimpleChat constructor.
     */
    public function __construct()
    {
        // Iniciamos a coleção que irá armazenar os clientes conectados
        $this->clients = new \SplObjectStorage;
    }

    /**
     * Evento que será chamado quando um cliente se conectar ao websocket
     *
     * @param ConnectionInterface $conn
     */
    public function onOpen(ConnectionInterface $conn)
    {
        // Adicionando o cliente na coleção
        $this->clients->attach($conn);
        echo "Cliente conectado ({$conn->resourceId})" . PHP_EOL;
    }

    /**
     * Evento que será chamado quando um cliente enviar dados ao websocket
     *
     * @param ConnectionInterface $from
     * @param string $data
     */
    public function onMessage(ConnectionInterface $from, $data)
    {
        // Convertendo os dados recebidos para vetor e adicionando a data
        $data = json_decode($data);
        $data->date = date('d/m/Y H:i:s');

        // Passando pelos clientes conectados e enviando a mensagem
        // para cada um deles
        foreach ($this->clients as $client) {
            $client->send(json_encode($data));
        }

        echo "Cliente {$from->resourceId} enviou uma mensagem" . PHP_EOL;
    }

    /**
     * Evento que será chamado quando o cliente desconectar do websocket
     *
     * @param ConnectionInterface $conn
     */
    public function onClose(ConnectionInterface $conn)
    {
        // Retirando o cliente da coleção
        $this->clients->detach($conn);
        echo "Cliente {$conn->resourceId} desconectou" . PHP_EOL;
    }

    /**
     * Evento que será chamado caso ocorra algum erro no websocket
     *
     * @param ConnectionInterface $conn
     * @param Exception $e
     */
    public function onError(ConnectionInterface $conn, \Exception $e)
    {
        // Fechando conexão do cliente
        $conn->close();

        echo "Ocorreu um erro: {$e->getMessage()}" . PHP_EOL;
    }
}

Criamos a propriedade $clients que será responsável por armazenar as conexões recebidas. Cada conexão é um objeto do tipo ConnectionInterface disponibilizado pela biblioteca, portanto utilizamos um SplObjectStorage , pois possui algumas vantagens além de um desempenho melhor para armazenamento de objetos. Através do método send() do objeto de conexão, conseguimos enviar as informações para o lado cliente.

Pronto, agora que já temos a classe, basta criar o arquivo que irá iniciar o servidor para aguardar as conexões.

<?php

use Ratchet\Http\HttpServer;
use Ratchet\Server\IoServer;
use Ratchet\WebSocket\WsServer;

// Incluindo biblioteca e classe do chat
require 'vendor/autoload.php';
require 'class/SimpleChat.php';

// Iniciando conexão
$server = IoServer::factory(
    new HttpServer(
        new WsServer(
            new SimpleChat()
        )
    ),
    8080
);

$server->run();

Basicamente, inicia o servidor de Websocket, que passará a receber conexões na porta 8080.

Agora precisamos executar este arquivo para iniciar nosso servidor de Websocket. Para isso você precisa ir até o terminal, navegar até a pasta dele e executar o comando abaixo.

php server.php

Pronto, agora nosso servidor de Websocket foi iniciado e está apenas aguardando as conexões, precisamos agora então criar o lado cliente.

O lado cliente

Javascript

Vamos iniciar o aplicativo do Vue que será responsável por gerenciar nossa página. Caso você nunca tenha utilizado o Vue, recomendo a leitura do guia oficial do framework.

var app = new Vue({

    // Elemento que o aplicativo será iniciado
    el: "#app",

    // Propriedades do aplicativo
    data: {
        user: 'Anônimo',
        text: null,
        messages: [],
        ws: null,
    },

    // Quando iniciado o aplicativo
    created: function() {
        // Inicia a conexão com o websocket
        this.connect();
    },

    // Métodos do aplicatvo
    methods: {

        // Método responsável por iniciar conexão com o websocket
        connect: function(onOpen) {

            var self = this;

            // Conectando
            self.ws = new WebSocket('ws://localhost:8080');

            // Evento que será chamado ao abrir conexão
            self.ws.onopen = function() {
                self.addSuccessNotification('Conectado');
                // Se houver método de retorno
                if (onOpen) {
                    onOpen();
                }
            };

            // Evento que será chamado quando houver erro na conexão
            self.ws.onerror = function() {
                self.addErrorNotification('Não foi possível conectar-se ao servidor');
            };

            // Evento que será chamado quando recebido dados do servidor
            self.ws.onmessage = function(e) {
                self.addMessage(JSON.parse(e.data));
            };

        },

        // Método responsável por adicionar uma mensagem de usuário
        addMessage: function(data) {
            this.messages.push(data);
            this.scrollDown();
        },

        // Método responsável por adicionar uma notificação de sucesso
        addSuccessNotification: function(text) {
            this.addMessage({color: 'green', text: text});
        },

        // Método responsável por adicionar uma notificação de erro
        addErrorNotification: function(text) {
            this.addMessage({color: 'red', text: text});
        },

        // Método responsável por enviar uma mensagem
        sendMessage: function() {

            var self = this;

            // Se não houver o texto da mensagem ou o nome de usuário
            if (!self.text || !self.user) {
                // Saindo do método
                return;
            }

            // Se a conexão não estiver aberta
            if (self.ws.readyState !== self.ws.OPEN) {

                // Exibindo notificação de erro
                self.addErrorNotification('Problemas na conexão. Tentando reconectar...');

                // Tentando conectar novamente e caso tenha sucesso
                // envia a mensagem novamente
                self.connect(function() {
                    self.sendMessage();
                });

                // Saindo do método
                return;
            }

            // Envia os dados para o servidor através do websocket
            self.ws.send(JSON.stringify({
                user: self.user,
                text: self.text,
            }));

            // Limpando texto da mensagem
            self.text = null;

        },

        // Método responsável por "rolar" a scroll do chat para baixo
        scrollDown: function() {
            var container = this.$el.querySelector('#messages');
            container.scrollTop = container.scrollHeight;
        },

    }

});
  • Linha 4: Definimos que o escopo do nosso aplicativo será o elemento que tenha o id igual à app. Dentro deste elemento poderemos acessar as propriedades e métodos que veremos a seguir.
  • Linha 7 até 12: Definimos as propriedades do nosso aplicativo.
    • user: nome de usuário que está conectado;
    • text: mensagem atual do usuário;
    • messages: lista com todas as mensagens recebidas;
    • ws: objeto que irá armazenar a conexão com o Websocket;
  • Linha 29: Criamos um novo objeto de conexão com base nas informações de nosso servidor.
  • Linha 47: Os dados recebidos são apenas texto, porém para conseguirmos interpretar as informações, vamos utilizar o formato JSON, portanto convertemos os dados recebidos utilizando JSON.parse() e passamos para o método addMessage() que criaremos adiante.
  • Linha 53 até 56: Adiciona a mensagem na propriedade messages e o Vue vai se encarregar de atualizar a página para nós. Além disso chamamos o método scrollDown() para rolar o scroll das mensagens para baixo para que o usuário possa acompanhá-las.
  • Linha 80: Através da propriedade readyState do objeto do Websocket, conseguimos saber o status da conexão que pode ser: CONNECTING, OPEN, CLOSING e CLOSED.
  • Linha 87 até 89: Como a conexão não está aberta, chamamos o método de conexão novamente, porém passamos como parâmetro uma função (callback) que será chamada após a conexão ser realizada, enviando a mensagem novamente para que o usuário não tenha que pressionar enter de novo.
  • Linha 96 até 99: Os dados enviados chegarão no servidor no formato texto, portanto convertemos o JSON para texto utilizando JSON.stringify() e enviamos para o servidor através do método send() do Websocket.
  • Linha 107 até 110: Buscamos o elemento que contém as mensagens através do querySelector() e definimos que a posição do scroll é o tamanho dele. Na prática, rola o scroll até o fim.

HTML

<div id="app" class="container">

    <h3 class="text-center">Chat simples utilizando Websocket, Vue.JS e PHP</h3>

    <div id="messages">
        <div class="col s12">
            <ul class="list-unstyled" v-cloak>
                <li v-for="message in messages">
                    <span class="date" v-if="message.date">[{{ message.date }}]</span>
                    <span class="name" v-if="message.user">{{ message.user }}:</span>
                    <span class="text" :style="{ color: message.color }">
                        {{ message.text }}
                    </span>
                </li>
            </ul>
        </div>
    </div>

    <div class="row">

        <div class="col-12 col-sm-3">
            <input type="text" class="form-control" placeholder="Usuário" v-model="user">
        </div>

        <div class="col-12 col-sm-9">
            <input type="text" class="form-control" placeholder="Mensagem" v-model="text"
                   @keyup.enter="sendMessage">
        </div>

    </div>

</div>
  • Linha 7: v-cloak nos permite esconder o elemento até o framework ter sido completamente carregado, pois caso contrário você verá a página do jeito que está por alguns segundos até o carregamento completo do framework.
  • Linha 8: v-for nos permite repetir um elemento de acordo com uma collection. No caso, iremos repetir o elemento li (item da lista) para cada item da propriedade messages.
  • Linha 9 e 10:  O v-if nos permite definir se um elemento será criado ou não através de uma condição. Neste caso, se a propriedade não existir no objeto message, não exibiremos o elemento. Este teste é necessário pois as notificações de sucesso e erro não possuem data e usuário. Por fim, através das {{ }} exibimos o valor da propriedade.
  • Linha 11: O :style (ou v-bind:style) nos permite definir uma propriedade CSS dinamicamente. No caso, vamos definir a cor do texto de acordo com a propriedade color do objeto message, pois definimos no aplicativo que a notificação de sucesso será green e de erro red.
  • Linha 26 e 27: O v-model nos permite vincular um campo de formulário com uma propriedade, sendo assim sempre que o campo for atualizado, a propriedade será atualizada e vice-versa. O @keyup (ou v-on:keyup) nos permite chamar um método quando uma tecla for pressionada, no caso definimos que no enter chamaremos o método sendMessage() para enviar a mensagem.

Hospedagem

Se você seguiu os passos acima, provavelmente o aplicativo deve estar funcionando localmente na sua máquina. Porém, como você faria para ele funcionar em um serviço de hospedagem?

Primeiramente, se você utiliza algum serviço de hospedagem compartilhada, dificilmente você vai conseguir subir o aplicativo lá, pois para que de certo você terá que ter acesso ao servidor via SSH (Secure Shell).

Supondo que você tenha acesso via SSH, basta enviar os arquivos e então executar o comando para iniciar o servidor, da mesma forma que você fez localmente.

php server.php

Lembre-se que a porta 8080 deve estar aberta para acesso externo, portanto, se necessário, você deve liberá-la no firewall e roteador.

No lado cliente, você terá que trocar o localhost pelo endereço do seu servidor, por exemplo, ws://rafaelcouto.com.br:8080. Feito isso, tudo deveria funcionar normalmente. Porém, quando você fechar a conexão SSH, o servidor irá parar a execução; para evitar isso, você precisa criar um serviço. Você pode fazer isso da seguinte maneira:

  • 1) Navegar até /etc/init: cd /etc/init
  • 2) Criar o arquivo chatsocket.conf: sudo nano chatsocket.conf
  • 3) Colocar o conteúdo abaixo no arquivo, trocando “/path/to/server.php” pelo caminho real do arquivo server.php no seu servidor e então salvar.
# Info
description "Runs the Chat Web Socket"  
author      "Your Name Here"

# Events
start on startup  
stop on shutdown

# Automatically respawn
respawn  
respawn limit 20 5

# Run the script!
# Note, in this example, if your PHP script (the socket) returns
# the string "ERROR", the daemon will stop itself.
script  
    [ $(exec /usr/bin/php -f /path/to/server.php) = 'ERROR' ] && ( stop; exit 1; )
end script

Pronto, agora você pode iniciar o serviço através do comando service chatsocket start ,  fechar a conexão SSH e o servidor de Websocket continuará sendo executado.

Conclusão

Neste artigo vimos como implementar um simples chat utilizando Websocket com PHP. O Ratchet facilita muito a implementação do servidor bastando apenas criar uma classe e implementar os eventos disponibilizados pela biblioteca.

Existem diversas técnicas para simular uma aplicação em tempo real, como o Long Polling e Server-sent Event, porém nenhuma delas proporciona uma comunicação bidirecional real e de baixa latência como o Websocket.

Vimos a possibilidade de criar um aplicativo de chat, porém o Websocket não se limita apenas a isso. Você pode utilizar o mesmo conceito para qualquer aplicativo que precise receber e/ou enviar informações em tempo real.

Enfim, espero que este artigo tenha sido útil de alguma forma para você e que você possa começar a utilizar o Websocket para criação de aplicativos em tempo real.

Código fonte disponível em: https://github.com/rafaelcouto/chat-simples-utilizando-websocket-vue-js-e-php/

Referências