Websocket em PHP? Sim! É possível!
Tags: php, swoole, websocket, aws
Published: 2020-03-15 23:28:24


Vou contar um pouco sobre o que é o PHP Swoole e como foi utiliza-lo para desenvolver um microserviço de websocket escalável e totalmente integrado com os serviços da Amazon Web Services (AWS).

#Por que Websocket em PHP?

Escutei este pergunta em diversos momentos, até mesmo pela própria equipe onde trabalho. Já haviamos desenvolvido um websocket uitilizando Ratchet, mas era um escopo muito menor e ficou claro alguns problemas que teríamos em um escopo maior. Neste novo desafio precisavamos de uma biblioteca muito mais robusta, escalável e com mais ferramentas a disposição.

Na fase de planejamento do projeto testei diversos websockets em diferentes linguagens, mas acabei escolhendo o PHP Swoole, pois ele fornecia coroutine, velocidade, escalabilidade e mais um pouco.

A confiança em utilizar o Websocket em PHP, algo que dificilmente a comunidade recomendaria, foram os testes de carga, o conhecimento da linguagem na empresa e a facilidade de outras pessoas assumirem ou fornecerem suporte ao projeto. 

#Benchmark

No final do artigo [Introdução ao swoole](https://www.treinaweb.com.br/blog/introducao-ao-swoole-framework-php-assincrono-baseado-em-corrotinas/), podemos ver um benchmark por números de requisições entre os seguintes cenários: PHP puro, NodeJs, Go e PHP Swoole.

 
**PHP Puro** (882 requisições por segundo)
```php
Running 10s test @ http://127.0.0.1:8101
  4 threads and 200 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     3.46ms    9.56ms 141.27ms   98.92%
    Req/Sec     1.41k     1.70k    9.47k    85.45%
  8871 requests in 10.05s, 1.47MB read
  Socket errors: connect 0, read 9275, write 0, timeout 0
Requests/sec:    882.46
Transfer/sec:    149.95KB
```

**NodeJS** (49.720 requisições por segundo)
```php
Running 10s test @ http://127.0.0.1:8101
  4 threads and 200 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     4.05ms    1.09ms  43.57ms   98.31%
    Req/Sec    12.49k     1.46k   13.49k    97.03%
  502227 requests in 10.10s, 53.16MB read
  Socket errors: connect 0, read 110, write 0, timeout 0
Requests/sec:  49720.54
Transfer/sec:      5.26MB
```

**GO** (187.280 requisições por segundo)
```php
Running 10s test @ http://127.0.0.1:8101
  4 threads and 200 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.05ms  624.03us  42.09ms   92.46%
    Req/Sec    47.05k     2.31k   50.59k    95.50%
  1873010 requests in 10.00s, 228.64MB read
  Socket errors: connect 0, read 48, write 0, timeout 0
Requests/sec: 187280.40
Transfer/sec:     22.86MB
```

**PHP Swoole** (193.149 requisições por segundo)

```php
Running 10s test @ http://127.0.0.1:8101
  4 threads and 200 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     0.87ms  660.42us  42.95ms   98.60%
    Req/Sec    48.55k     3.12k   53.67k    88.75%
  1933132 requests in 10.01s, 304.19MB read
  Socket errors: connect 0, read 41, write 0, timeout 0
Requests/sec: 193149.91
Transfer/sec:     30.39MB
```
 
#Mas o que é o PHP Swoole

O Swoole é um Event-Driven, assíncrono e baseado em corotina, com alto desempenho escrito em C e C++ para PHP.

Uma extensão PHP, que permite escrever serviços de alto desempenho, escaláveis e simultâneos, TCP, UDP, Unix socket, HTTP, WebSocket , sem muito conhecimento sobre non-blocking I/O e Linux Kernel de baixo nível.

 
#Como funciona o PHP Swoole?

O Swoole funciona diferente do modelo tradicional do PHP, ele é executado no modo CLI.

![PHP Swoole](https://camo.githubusercontent.com/5d7808facc40cb47a23056827d38afa639d7b3ec/68747470733a2f2f692e696d6775722e636f6d2f675150747377462e706e67)


* **Master**: É processo principal, ele forka o **Main Reactor** e o **Manager**, é o processo raiz de toda a aplicação.

* **Main Reactor**: Thread principal, gerencia e faz o balanceamento entre os reactors auxiliares.

* **Reactor**: Multi-thread e totalmente assíncrono, responsável por receber solicitações e entregar ao Manager.

* **Manager**: Processo gerenciador, forka e gerencia os workers.

* **Worker**: É aqui que você realmente deve se preocupar. Onde realmente as tarefas são executadas.

* **Task Worker**: São auxiliáres dos **Workers**, bastante utilizado para tarefas paralelas e não bloqueantes ao worker.


**Algumas diferenças entre o Swoole e o PHP-FPM são:**

* O Swoole forka um determinado número de workers baseado na quantidade de núcleos da CPU, para utilizar todos os núcleos da CPU.

* O Swoole suporta conexões de longa duração para servidor websocket ou servidor TCP / UDP.

* Swoole suporta várias requisições ao mesmo tempo (não bloqueantes).

* Swoole pode gerenciar e reutilizar o status na memória.


Para um entendimento mais avançado de como o PHP Swoole funciona, mesmo não tratando de Websocket, eu recomendo a leitura do artigo disponibilizado em Benchmark.


#Instalando PHP Swoole

**Instalação básica**

```php
pecl install swoole
```

**Instalação Recomendada**

```php
git clone https://github.com/swoole/swoole-src.git
cd swoole-src
git checkout v4.4.12
phpize
./configure
make && make install
 ```

#Criando Websocket Server

Exemplo básico da construção de um websocket server com 3 eventos (open, mensage, close)

```php
<?php
$server = new swoole_websocket_server("0.0.0.0", 9502, SWOOLE_PROCESS);
$server->set(array(
    'task_worker_num' => 10,
    'log_file' => '/var/log/supervisor/swoole.log',
    'open_tcp_keepalive'  =>  true
));


$server->on('open', function($server, $request)
{
    echo "connection open: {$req->fd}\n";
});


$server->on('message', function($server, $frame)
{
    echo "received message: {$frame->data}\n";
    foreach ($server->connections as $fd) {
        $server->push($fd, $frame->data);
    }
});


$server->on('close', function($server, $fd)
{
    echo "connection close: {$fd}\n";
});


$server->on('task', function($server, $fd)
{
    echo "Task";
});


$server->start();
 ```

A variável **$server** é usada para construir o próprio servidor websocket.

Nela podemos definir algumas configurações de inicialização com o método ->set().

No exemplo acima inicializamos o servidor com 10 workers para task, o caminho do log e habilitamos a conexão persistente do websocket.

Utilizamos o método ->on() para mapear os eventos de callback dentro do server, no exemplo acima configuramos evento de abertura de conexão, mensagem e desconexão.

 
#Como testar?

Você pode utilizar a extensão do Chrome chamada WS Client:
[https://chrome.google.com/webstore/detail/web-socket-client/lifhekgaodigcpmnakfhaaaboididbdn](https://chrome.google.com/webstore/detail/web-socket-client/lifhekgaodigcpmnakfhaaaboididbdn)

Basta colocar a URL como ws://localhost:9501 e enviar qualquer mensagem de texto. No código descrito acima será feito um broadcast, todos que estiverem conectados receberão uma cópia da mensagem.
![Websocket Client](https://dev-to-uploads.s3.amazonaws.com/i/assb8i4o69zwfdvecslx.png)

A mensagem pode ser de qualquer tipo, mas normalmente usamos padrão Json entre os sistemas.

 
#Memória

A memória entre os Workers não é compartilhada, já que cada worker recebe uma cópia da classe.

Mas para trabalhar com memória compartilhada entre workers existem 3 funcionalidades nativas no Swoole:

[swoole_buffer](https://www.swoole.co.uk/docs/modules/swoole-buffer), [swoole_channel](https://www.swoole.co.uk/docs/modules/swoole-channel), [swoole_table](https://www.swoole.co.uk/docs/modules/swoole-table).

Ou criar uma memória compartilhada via Unix Socket, um bom exemplo é o repositório abaixo:

[Swoole Shared Memory](https://github.com/Yurunsoft/swoole-shared-memory) 

No caso do nosso projeto, utilizamos a memória compartilhada via Unix Socket para as listas de FDs associando a uma hash e o Redis (Utilizando o Type Hash com Lua) para gerenciamento de salas.

Outra dica importante é utilizar Ec2 focada em memória, pela maneira que o Swoole funciona.

#Variáveis importantes do Callback

A variável **$frame** no escopo do callback possui 4 atributos importantes:

* **$frame->fd:** Descritor de arquivo, é o id único de cada conexão no sistema.

    * Cada aba dos clientes é um FD único associado a um hash.

    * Por padrão o swoole usa um inteiro incremental iniciando de 1. **Os FDs são resetados a cada vez que o server inicializa.**

* **$frame->data**: A mensagem do cliente que chegou no websocket, na imagem acima seria o “1234567”.

* **$frame->opcode:** Tipo da mensagem, default é TEXT.

    * Exemplos de opcode:

        * WEBSOCKET_OPCODE_TEXT = 0x1, utf8 text data;
        * WEBSOCKET_OPCODE_BINARY = 0x2, binary data;
        * WEBSOCKET_OPCODE_PING = 0x9, ping data;

* **$frame->finish:** Retorna um boolean se o frame está completo.

 

A variável **$server** no escopo do callback possui 4 métodos/atributos importantes:

* **$server->isEstablished($fd):** Retorna boolean se o **FD** está conectado.
* **$server->push($fd, $message):** Envia a mensagem para o **FD**.
* **$server->connection:** Retorna um array numérico com a lista de todos os **FD**s conectados.
* **$server->getClientInfo($fd):** Retorna um array com as informações do **FD** fornecido.

 

#AWS Elastic Load Balance (timeout)

> Por padrão, o Elastic Load Balancing define o valor do tempo limite de inatividade para 60 segundos. Portanto, se o destino não enviar dados pelo menos a cada 60 segundos enquanto a solicitação estiver em trânsito, o load balancer poderá fechar a conexão front-end. Para garantir que operações demoradas, como uploads de arquivo, tenham tempo para serem concluídas, envie pelo menos 1 byte de dados antes de decorrer cada período de tempo limite de inatividade e aumente a duração do período do tempo limite de inatividade conforme o necessário.

>[https://docs.aws.amazon.com/pt_br/elasticloadbalancing/latest/application/application-load-balancers.html#connection-idle-timeout](https://docs.aws.amazon.com/pt_br/elasticloadbalancing/latest/application/application-load-balancers.html#connection-idle-timeout)

Para resolver este problema, enviamos um pacote a cada 30 segundos utilizando o opcode de PING.

Este pacote não ativa o evento onMessage e mantém todas as conexões ativas.

Sem este keep_alive via ping, todos nossos clientes desconectariam e reconectariam a cada 60 segundos.

```php
$server->on('managerStart', function($server, $fd)
{
    $server->tick(30000, function () use ($server) {
        foreach ($server->connections as $id) {
            if ($server->isEstablished($id)) {
                $server->push($id, 'ping', WEBSOCKET_OPCODE_PING);
            }
        }
    });
}
```

Explicando o código acima, adicionamos no server um mapeamento para evento de callback **managerStart** que será executado somente 1 vez por inicialização do servidor.

Com isso adicionamos um timer de 30 segundos que vai correr a lista de todos FDs conectados no websocket, conferindo se o mesmo ainda está conectado e enviando um pacote de PING para manter a conexão aberta.

 

#Task

Utilizamos a task por exemplo para publicar no SNS (Simple Notification Service) da AWS uma cópia de cada mensagem que é trafegada no Websocket, mas você pode usa-la para qualquer tarefa não bloqueante dentro de todos os eventos.

```php
$server->on('managerStart', function( $server , $task_id , $from_id , $data )
{
    $this->awsService->publish($data['topic'], $data['subject'], $data['payload']);
}

// Disparando manualmente uma task paralela
$server->task([
    'topic' => 'message',
    'subject' => 'Message',
    'payload' => $frame->data
]);
```


#Configurar a máquina para suportar 2 milhões de conexões

[https://gist.github.com/mustafaturan/47268d8ad6d56cadda357e4c438f51ca](https://gist.github.com/mustafaturan/47268d8ad6d56cadda357e4c438f51ca)

 

#Lista de callbacks

[https://www.swoole.co.uk/docs/modules/swoole-server/callback-functions](https://www.swoole.co.uk/docs/modules/swoole-server/callback-functions)

 

#Conclusão

Podemos concluir que realmente é possível desenvolver um servidor Websocket em PHP Swoole, escalável e sem muita complexidade, batendo de frente com as linguagens mais rápidas do mercado.

O único cuidado é ao utilizar a memória compartilhada entre workers, não esqueça de remover o que não estiver sendo usado, pois o Swoole vai manter tudo em memória.

Bom, é isso. Acabei resumindo muita coisa, mas caso precisem, eu posso explicar como funciona todo o sistema de handshake e gerenciamento entre workers em outro post, ou até mesmo desenvolver um chat de exemplo para a comunidade.
 

#Fontes

* [https://www.swoole.co.uk](https://www.swoole.co.uk)
* [https://wiki.swoole.com/](https://wiki.swoole.com/)
* [https://www.treinaweb.com.br/blog/introducao-ao-swoole-framework-php-assincrono-baseado-em-corrotinas/](https://www.treinaweb.com.br/blog/introducao-ao-swoole-framework-php-assincrono-baseado-em-corrotinas/)
* [https://gist.github.com/mustafaturan/47268d8ad6d56cadda357e4c438f51ca](https://gist.github.com/mustafaturan/47268d8ad6d56cadda357e4c438f51ca)