Back-End

21 ago, 2015

Construindo um servidor TCP com PHP e Ratchet

Publicidade

Eu normalmente jogo muito com os servidores TCP, clientes e com coisas assim no meu trabalho diário. Eu gosto de usar o daemon xinet.d do Linux para lidar com as portas TCP.

Essa abordagem funciona bem. Você não precisa abrir qualquer porta. xinet.d abre para você e invoca os scripts PHP. O problema aparece quando chamamos intensamente o nosso servidor xinet.d. Ele cria uma instância de PHP por pedido. Não é um problema com um pedido, por exemplo, de 3 segundos, mas se temos de lidar com 10 pedidos por segundo, nossa carga do servidor vai crescer. A solução: um servidor dedicado.

Com o PHP, podemos criar servidores dedicados usando, por exemplo, o Ratchet. Eu quero criar uma biblioteca usando Ratchet para abrir as portas TCP e registrar callbacks para essas portas para manipular os pedidos (padrão Reactor). Você conhece Silex? Claro que sim. Essa biblioteca pega emprestada a ideia do Silex (registrar callbacks para rotas) para o mundo do TCP.

Deixe-me mostrar exemplos:

Exemplo 1:

use React\EventLoop\Factory as LoopFactory;
use G\Pxi\Pxinetd;
 
$loop = LoopFactory::create();
$service = new Pxinetd('0.0.0.0');
 
$service->on(8080, function ($data) {
    echo $data;
});
 
$service->register($loop);
$loop->run();

Esse é o exemplo mais simples. Um servidor de eco TCP. Nós abrimos a porta 8080 para todas as interfaces (0.0.0.0) e retornamos um simples eco de entrada.

Podemos começar portas diferentes também:

use G\Pxi\Pxinetd;
use React\EventLoop\Factory as LoopFactory;
 
$loop = LoopFactory::create();
$service = new Pxinetd('0.0.0.0');
 
$service->on(8080, function ($data) {
    echo $data;
});
 
$service->on(8888, function ($data) {
    echo $data;
});
 
$service->register($loop);
$loop->run();

Exemplo 2:

Nós também podemos trabalhar com a conexão

use G\Pxi\Pxinetd;
use G\Pxi\Connection;
use React\EventLoop\Factory as LoopFactory;
 
$loop = LoopFactory::create();
$service = new Pxinetd('0.0.0.0');
 
$service->on(8080, function ($data) {
    echo $data;
});
 
$service->on(8088, function ($data, Connection $conn) {
    var_dump($conn->getRemoteAddress());
    echo $data;
    $conn->send("....");
    $conn->close();
});
 
$service->register($loop);
$loop->run();

Exemplo 3:

Eu sou um grande fã das configurações YAML, para que possamos carregar as configurações de um arquivo YAML, claro:

// Services/Reader1.php
use G\Pxi\Connection;
use G\Pxi\MessageIface;
 
class Reader1 implements MessageIface
{
    public function onMessage($data, Connection $conn)
    {
        echo $data . $conn->getRemoteAddress();
    }
}
	
use G\Pxi\Pxinetd;
use G\Pxi\YamlFileLoader;
use Symfony\Component\Config\FileLocator;
use React\EventLoop\Factory as LoopFactory;
 
$loop = LoopFactory::create();
$service = new Pxinetd('0.0.0.0');
 
$loader = new YamlFileLoader($service, new FileLocator(__DIR__ ));
$loader->load('conf3.yml');
 
$service->on(8080, function ($data) {
    echo "$data";
});
 
$service->register($loop);
$loop->run();

Exemplo 4:

Estamos usando symfony/config e os componentes symfony/yaml para que a gente possa utilizar de hierarquia dentro de nossos arquivos YAML:

use G\Pxi\Pxinetd;
use G\Pxi\YamlFileLoader;
use Symfony\Component\Config\FileLocator;
use React\EventLoop\Factory as LoopFactory;
 
$loop = LoopFactory::create();
$service = new Pxinetd('0.0.0.0');
 
$loader = new YamlFileLoader($service, new FileLocator(__DIR__));
$loader->load('conf4.yml');
 
$service->on(8080, function ($data) {
    echo "$data";
});
 
$service->register($loop);
$loop->run();

config4.yml:

imports:
  - { resource: conf4_2.yml }
ports:
  9999:
    class: Services\Reader1

config4_2.yml

ports:
  7777:
    class: Services\Reader1

Exemplo 5:

E, finalmente, um bônus. Este script é de thread única. Isso significa que se um processo demorar muito tempo, ele vai bloquear para o resto dos processos. Podemos implemente threads, mas eu tento evitá-las ao máximo. Prefiro criar um aplicativo Silex (atrás de um servidor http) e executar as solicitações HTTP para “emular” threads de uma forma simples.

use G\Pxi\Pxinetd;
use G\Pxi\YamlFileLoader;
use Symfony\Component\Config\FileLocator;
use React\EventLoop\Factory as LoopFactory;
 
$loop = LoopFactory::create();
$service = new Pxinetd('0.0.0.0');
 
$loader = new YamlFileLoader($service, new FileLocator(__DIR__ ));
$loader->load('conf5.yml');
 
$service->on(8080, function ($data) {
    echo "$data";
});
 
$service->register($loop);
$loop->run();

conf5.yml

ports:
  9999:
    class: Services\Reader1
  9991:
    url: http://localhost:8899/onMessage/{data}
  9992:
    url: http://localhost:8899/simulateError/{data}

E agora o nosso servidor Silex em execução na porta 8899:

include __DIR__ . "/../../vendor/autoload.php";
 
use Silex\Application;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
$app = new Application();
 
$app->get('/onMessage/{data}', function ($data) {
    return "OK" . "'{$data}'";
});
 
$app->get('/simulateError/{data}', function ($data) {
    throw new NotFoundHttpException();
});
 
$app->run();

E isso é tudo. O que você acha? Você pode ver toda a biblioteca na minha conta no GitHub.

***

Gonzalo Ayuso faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://gonzalo123.com/2015/04/13/building-tcp-server-daemon-with-php-and-rachet/