Sockets

Trabalhando com Sockets no ReactPHP

No primeiro artigo da série fizemos uma Introdução à programação assíncrona em PHP usando o ReactPHP, depois vimos sobre Promises no ReactPHP. Hoje falaremos sobre Sockets, um assunto muito relevante para a criação de aplicações cliente-servidor.

PHP Básico
Curso de PHP Básico
CONHEÇA O CURSO

Antes, entretanto, tem dois artigos que muito recomendo você ler antes de continuar neste, que são estes:

Criando o primeiro servidor

No seu projeto, instale o componente:

$ composer require react/socket:^1.3

Não vamos instalar manualmente o componente Event Loop pois ele já é uma dependência de react/socket.

Já estamos prontos para criar o nosso primeiro servidor, baseado no do artigo sobre Programação de Sockets em PHP:

<?php

require './vendor/autoload.php';

use React\Socket\Server;
use React\EventLoop\Factory;
use React\Socket\ConnectionInterface;
use React\Stream\WritableResourceStream;

$loop = Factory::create();
$socket = new Server('127.0.0.1:7181', $loop);
$stdout = new WritableResourceStream(\STDOUT, $loop);

$socket->on('connection', static function (ConnectionInterface $connection) {
    $connection->write("Client [{$connection->getRemoteAddress()}] connected \n");
});

$stdout->write("Listening on: {$socket->getAddress()}\n");

$loop->run();

No construtor de Server informamos o IP (local) e a porta que o nosso servidor rodará, não informando o protocolo antes do IP, ele considera por padrão como sendo tcp. Depois, passamos a ouvir o evento connection e informamos um handler que é executado sempre que uma nova conexão é estabelecida com o nosso servidor. Esse handler recebe como parâmetro um objeto do tipo ConnectionInterface que nos permite trabalhar com a conexão que foi realizada. No caso, sempre que um cliente se conectar ao nosso servidor, vamos imprimir no lado dele a mensagem que definimos no método write().

Para testar esse exemplo, basta que você inicie o servidor:

$ php index.php

E para se conectar ao servidor, se seu sistema operacional é baseado em Unix, em outra janela do terminal:

telnet 127.0.0.1 7181

Ao se conectar, no lado do servidor teremos:

$ ~/D/w/reactphp&gt; php index.php
Listening on: tcp://127.0.0.1:7181

No lado do cliente:

$ telnet 127.0.0.1 7181
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Client [tcp://127.0.0.1:50353] connected

O objeto $connection também implementa a interface EventEmitterInterface que nos permite ouvir alguns eventos relacionados à conexão, por exemplo, através do evento data conseguimos receber os dados enviados pelo cliente.

Para testarmos de forma efetiva o evento data, desenvolveremos o exemplo “Echo Server” do artigo Programação de Sockets em PHP:

<?php

require './vendor/autoload.php';

use React\Socket\Server;
use React\EventLoop\Factory;
use React\Socket\ConnectionInterface;
use React\Stream\WritableResourceStream;

$loop = Factory::create();
$socket = new Server('127.0.0.1:7181', $loop);
$stdout = new WritableResourceStream(\STDOUT, $loop);

$socket->on('connection', static function (ConnectionInterface $connection) {
    $connection->write("Client [{$connection->getRemoteAddress()}] connected \n");

    $connection->on('data', static function ($data) use ($connection) {
        $connection->write("Server says: {$data}");
    });
});

$stdout->write("Listening on: {$socket->getAddress()}\n");

$loop->run();

Se você comparar essa implementação com a que implementamos usando apenas o “PHP puro” (no artigo Programação de Sockets em PHP), verá que essa é bem mais simples. O RectPHP abstrai toda a parte complicada de lidar com streams/buffers, oferecendo uma API de bem alto nível.

No terminal do cliente ao interagir com o servidor (submeter algumas entradas):

$ telnet 127.0.0.1 7181
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Client [tcp://127.0.0.1:50397] connected
Hello
Server says: Hello
World
Server says: World

Diferentemente da nossa implementação pura, os servidores criados usando o ReactPHP aceitam múltiplos clientes, sem que façamos nada de especial a respeito (a não ser que tenhamos algum caso de uso mais específico, que é o caso do nosso próximo exemplo).

PHP Intermediário
Curso de PHP Intermediário
CONHEÇA O CURSO

Desenvolvendo um chat

Avançando um pouquinho mais, vamos criar um simples chat que funcionará pelo terminal. Crie um novo projeto, no meu caso, vou chamá-lo de “reactchat”. Nele, instale a dependência do reactphp/socket:

$ composer require react/socket:^1.3

A diferença desse exemplo para o que criamos no artigo Programação de Sockets em PHP será:

  • Usará o componente de socket do ReactPHP;
  • Um membro poderá citar outro (usando @) para enviar uma mensagem privada;
  • O ReactPHP abstrai em muitos aspectos a programação de sockets, portanto, bem mais simples de desenvolver e manter do que usando as funções nativas da extensão Streams do PHP.

Usaremos como referência o exemplo desenvolvido pelo Sergey Zhuk no artigo: Build A Simple Chat With ReactPHP Socket: Server.

Publiquei o exemplo no Github, você pode obtê-lo de lá: KennedyTedesco/reactchat

O index.php inicia o servidor:

<?php

// https://github.com/KennedyTedesco/reactchat

require './vendor/autoload.php';

use ReactChat\Chat;
use ReactChat\Member;
use React\Socket\Server;
use React\EventLoop\Factory;
use React\Socket\ConnectionInterface;
use React\Stream\WritableResourceStream;

$loop = Factory::create();
$socket = new Server('127.0.0.1:7181', $loop);
$stdout = new WritableResourceStream(\STDOUT, $loop);

$chat = new Chat();

$socket->on('connection', static function (ConnectionInterface $connection) use ($chat) {
    $member = new Member($connection);
    $member->write('Informe o seu nome: ');

    $connection->on('data', static function ($data) use ($member, $chat) {
        if ($data !== '' && $member->getName() === null) {
            // Define o nome do membro
            $member->setName(\str_replace(["\r", "\n"], '', $data));
            // Adiciona o membro ao chat
            $chat->addMember($member);
        }
    });
});

$stdout->write("Listening on: {$socket->getAddress()}\n");

$loop->run();

Não obstante, quando uma conexão é estabelecida, a primeira coisa que ele faz é pedir o nome, considerando que é alguém que está querendo entrar no chat. Essa pessoa só é adicionada ao chat depois que definir o nome.

A classe Member encapsula uma conexão e oferece alguns métodos úteis para se trabalhar com essa conexão:

<?php

// https://github.com/KennedyTedesco/reactchat

declare(strict_types=1);

namespace ReactChat;

use Closure;
use React\Socket\ConnectionInterface;

final class Member
{
    private $name;
    private $connection;

    public function __construct(ConnectionInterface $connection)
    {
        $this->connection = $connection;
    }

    public function getName() : ?string
    {
        return $this->name;
    }

    public function setName(string $name) : void
    {
        $this->name = $name;
    }

    public function write(string $data) : void
    {
        $data = \str_replace(["\r", "\n"], '', $data);

        $this->connection->write($data . \PHP_EOL);
    }

    public function onData(Closure $handler) : void
    {
        $this->connection->on('data', $handler);
    }

    public function onClose(Closure $handler) : void
    {
        $this->connection->on('close', $handler);
    }
}

E é no Chat que adicionamos novos membros e definimos os handlers para os eventos de data e close das conexões dos membros.

<?php

// https://github.com/KennedyTedesco/reactchat

declare(strict_types=1);

namespace ReactChat;

use SplObjectStorage;

final class Chat
{
    private $members;

    public function __construct()
    {
        $this->members = new SplObjectStorage();
    }

    public function addMember(Member $member) : void
    {
        $this->members->attach($member);

        $this->newMessageTo("Bem-vindo(a), {$member->getName()}", $member);
        $this->newMessageToAll("{$member->getName()} entrou na sala;", $member);

        $member->onData(function ($data) use ($member) {
            $this->newMessage("{$member->getName()} diz: {$data}", $member);
        });

        $member->onClose(function () use ($member) {
            $this->members->detach($member);
            $this->newMessage("{$member->getName()} saiu da sala.", $member);
        });
    }

    private function newMessage(string $message, Member $exceptMember) : void
    {
        // Se um membro foi citado usando @, envia a mensagem apenas para ele (mensagem privada).
        // Caso contrário, envia a mensagem para todos os membros
        $mentionedMember = $this->getMentionedMember($message);

        if ($mentionedMember instanceof Member) {
            $this->newMessageTo($message, $mentionedMember);
        } else {
            $this->newMessageToAll($message, $exceptMember);
        }
    }

    private function newMessageTo(string $message, Member $to) : void
    {
        // Envia para um membro específico
        foreach ($this->members as $member) {
            if ($member === $to) {
                $member->write($message);

                break;
            }
        }
    }

    private function newMessageToAll(string $message, Member $exceptMember) : void
    {
        // Envia para todos, exceto para o $exceptMember (quem está enviando a mensagem)
        foreach ($this->members as $member) {
            if ($member !== $exceptMember) {
                $member->write($message);
            }
        }
    }

    private function getMentionedMember(string $message) : ?Member
    {
        \preg_match('/\B@(\w+)/i', $message, $matches);

        $nameMentioned = $matches[1] ?? null;

        if ($nameMentioned !== null) {
            /** @var Member $member */
            foreach ($this->members as $member) {
                if ($member->getName() === $nameMentioned) {
                    return $member;
                }
            }
        }

        return null;
    }
}

Se você acompanhou os outros artigos da série, não terá dificuldade para entender o que está acontecendo nesse exemplo. O ReactPHP torna as coisas bem simples do nosso lado.

O método addMember() é o mais importante da classe, pois ele adiciona um membro na pool de conexões, de tal forma que ele passará a receber as mensagens do chat e também poderá interagir com os outros membros.

O método getMentionedMember() avalia por uma regex se algum pattern @nome foi destacado na mensagem que será enviada, se sim, ele procura e retorna o membro que possui esse nome. Ele é o núcleo do funcionamento do envio das mensagens privadas lá no método newMessage().

Para testar, tudo o que você precisa é iniciar o servidor:

$ php index.php

Depois, abra umas três abas no seu terminal para inicializar alguns clientes do chat (dando nomes diferentes para eles):

telnet 127.0.0.1 7181

Você pode, inclusive, testar o envio de mensagens privadas usando @nomeDoMembro.

PHP Avançado
Curso de PHP Avançado
CONHEÇA O CURSO

Concluindo

Ao invés de utilizarmos o terminal para a conexão dos membros do chat, poderíamos usar uma interface web e, para se conectar ao servidor, utilizaríamos WebSockets que já é uma realidade estável em todos os principais navegadores. Inclusive, esse deverá ser o assunto do próximo artigo da série. =D

Até a próxima!

Programação de Sockets em PHP

Neste artigo veremos uma introdução à programação de sockets com o PHP. Antes, recomendo que primeiro você leia o artigo Uma introdução a TCP, UDP e Sockets que discorre sobre a base teórica fundamental.

O PHP dispõe de uma extensão chamada Sockets, que foi introduzida ainda no PHP 4 e ela tem uma boa fidelidade em relação à forma como sockets são programados em C, além dela expor a possibilidade de se trabalhar com algumas características do sistema operacional, o que pode ser útil dependendo do que se precisa desenvolver. No entanto, essa extensão nos impõe alguns problemas e dificuldades, o primeiro é que ela não vem instalada por padrão, sendo necessário na compilação do PHP adicionar a flag -enable-sockets para ativá-la. Ela também não possui uma boa usabilidade para o desenvolvedor, é uma extensão difícil de usar, de bem mais baixo nível. Além disso, os resources abertos por ela só podem ser acessados pelas funções da mesma família, ou seja, as que iniciam pelo nome socket_ e isso é um fator que limita o desenvolvimento, principalmente porque o PHP dispõe de várias funções para trabalhar com streams como stream_get_contents(), fwrite(), fread() etc, que não podem ser utilizadas junto com as funções da extensão Sockets.

PHP Básico
Curso de PHP Básico
CONHEÇA O CURSO

Mas a solução para os nossos problemas está na extensão Stream, nativa do PHP e que já vem habilitada por padrão. Como o próprio nome sugere, essa extensão lida com toda a abstração de I/O (input e output) do PHP e o detalhe mais importante pra nós é que desde o PHP 5 essa extensão nos permite trabalhar com TCP/IP Sockets e também com Unix Sockets. Os sockets criados usando as funções da extensão Stream (as que iniciam por stream_socket_) nos permitem usar as clássicas funções para lidar com arquivos, o que torna o desenvolvimento bem mais intuitivo.

Leitura recomendada antes de continuar: Streams no PHP.

Em resumo, temos duas formas de trabalhar com Sockets no PHP:

  • Usando as funções da extensão Sockets, que são funções de mais baixo nível, que nos expõem uma variedade maior de possibilidades ao lidar com algumas características de sockets a nível do sistema operacional, mas que prejudica o desenvolvimento por impor um grau maior de dificuldade;
  • Usando as funções da extensão Stream, que é uma extensão habilitada por padrão e utilizada por tudo que envolve streams no PHP. Essa extensão expõem funções de mais alto nível, mas ela pode ser um fator limitante se for preciso trabalhar com características de mais baixo nível, mas isso não costuma ser um problema para a grande maioria das aplicações que implementam o uso de sockets.

Echo Server

O primeiro e mais acessível exemplo que podemos construir para entender como um servidor de socket opera é o de um “Echo Server”, ele basicamente envia de volta tudo o que recebe do cliente. Crie um novo diretório onde normalmente você guarda os seus projetos e então crie um arquivo index.php com a seguinte implementação:

<?php

declare(strict_types=1);

// Queremos que o PHP reporte apenas erros graves, estamos explicitamente ignorando warnings aqui.
// Warnings que, por sinal, acontecem bastante ao se trabalhar com sockets.
error_reporting(E_ERROR | E_PARSE);

// Inicia o servidor na porta 7181
$server = stream_socket_server('tcp://127.0.0.1:7181', $errno, $errstr);

// Em caso de falha, para por aqui.
if ($server === false) {
    fwrite(STDERR, "Error: $errno: $errstr");

    exit(1);
}

// Sucesso, servidor iniciado.
fwrite(STDERR, sprintf("Listening on: %s\n", stream_socket_get_name($server, false)));

// Looping infinito para "escutar" novas conexões
while (true) {
    // Aceita uma conexão ao nosso socket da porta 7181
    // O valor -1 seta um timeout infinito para a função receber novas conexões (socket accept timeout) e isso significa que a execução ficará bloqueada aqui até que uma conexão seja aceita;
    $connection = stream_socket_accept($server, -1, $clientAddress);

    // Se a conexão foi devidamente estabelecida, vamos interagir com ela.
    if ($connection) {
        fwrite(STDERR, "Client [{$clientAddress}] connected \n");

        // Lê 2048 bytes por vez (leitura por "chunks") enquanto o cliente enviar.
        // Quando os dados não forem mais enviados, fread() retorna false e isso é o que interrompe o loop.
        // fread() também retornará false quando o cliente interromper a conexão.
        while ($buffer = fread($connection, 2048)) {
            if ($buffer !== '') {
                // Escreve na conexão do cliente
                fwrite($connection, "Server says: $buffer");
            }
        }

        // Fecha a conexão com o cliente
        fclose($connection);
    }
}

Na raiz do projeto, utilizando o terminal, execute o servidor:

$ php index.php

O resultado será algo como:

kennedytedesco@kennedy-pc ~/D/w/sockets&gt; php index.php
Listening on: 127.0.0.1:7181

Como o próprio output explicita, agora temos um servidor rodando na porta 7181 do host local e ele está pronto para receber uma conexão de um cliente.

Para conectar ao servidor aberto, vou utilizar o cliente Telnet disponível no meu sistema operacional, bastando executar:

$ telnet 127.0.0.1 7181

Tendo sucesso na conexão:

kennedytedesco@kennedy-pc ~&gt; telnet 127.0.0.1 7181
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

E o servidor imediatamente imprime qual cliente se conectou:

kennedytedesco@kennedy-pc ~/D/w/sockets&gt; php index.php
Listening on: 127.0.0.1:7181
Client [127.0.0.1:52357] connected

A porta do cliente é conhecida pelo servidor no momento que esse se conecta a ele. A cada nova conexão uma porta aleatória é associada ao cliente.

A partir desse momento, o cliente pode interagir com o servidor, enviando mensagens, por exemplo:

kennedytedesco@kennedy-pc ~&gt; telnet 127.0.0.1 7181
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Hello World!
Server says: Hello World!

O cliente enviou “Hello World” e recebeu como resposta “Server says: Hello World!“, essa é a dinâmica do nosso servidor, ele envia de volta tudo o que recebe. Apertando as teclas de atalho CTRL + C no terminal onde o servidor está rodando, você consegue pará-lo e, consequentemente, isso também desconecta o cliente.

Essa é a dinâmica da interação entre o cliente e o servidor:

O nosso servidor opera da seguinte maneira:

  • 1) Ele abre um socket, que na prática diz ao sistema operacional que todos os dados recebidos por esse socket devem ser enviados para o nosso programa;
  • 2) Ele passa a “ouvir” por conexões de clientes;
  • 3) Ele aceita uma conexão quando ela é estabelecida. Nesse momento, a comunicação entre cliente-servidor é possível;
  • 4) Ele troca dados com o cliente;
  • 5) Ele fecha a conexão ou deixa que o cliente a finalize;
  • 6) Quando uma conexão é finalizada, ele volta para o passo 2) e todo esse ciclo é executado enquanto novas conexões existirem e/ou enquanto o servidor estiver em execução.
PHP Intermediário
Curso de PHP Intermediário
CONHEÇA O CURSO

Voltando ao nosso exemplo, definimos um timeout de -1 na stream_socket_accept e, conforme explicado no código, isso fará o fluxo da execução ficar bloqueado nessa parte até que uma nova conexão seja aceita/estabelecida. No entanto, podemos alterar o exemplo e definir um timeout de, por exemplo, 1 segundo. É que o vamos fazer. Os comentários definidos anteriormente foram suprimidos, pra melhorar a legibilidade:

<?php

declare(strict_types=1);

error_reporting(E_ERROR | E_PARSE);

$server = stream_socket_server('tcp://127.0.0.1:7181', $errno, $errstr);

if ($server === false) {
    fwrite(STDERR, "Error: $errno: $errstr");

    exit(1);
}

fwrite(STDERR, sprintf("Listening on: %s\n", stream_socket_get_name($server, false)));

while (true) {
    $connection = stream_socket_accept($server, 1, $clientAddress);

    if ($connection) {
        fwrite(STDERR, "Client [{$clientAddress}] connected \n");

        while ($buffer = fread($connection, 2048)) {
            if ($buffer !== '') {
                fwrite($connection, "Server says: $buffer");
            }
        }

        fclose($connection);
    } else {
        fwrite(STDERR, "Aguardando ... \n");
    }
}

Nessa alteração, definimos um timeout de 1 segundo e adicionamos esse else na verificação se a conexão foi aceita:

} else {
  fwrite(STDERR, "Aguardando ... \n");
}

Dessa forma, ao iniciar o servidor, a cada 1 segundo ele vai imprimir “Aguardando …” até que uma conexão seja aceita:

kennedytedesco@kennedy-pc ~/D/w/sockets&gt; php index.php
Listening on: 127.0.0.1:7181
Aguardando ...
Aguardando ...
Aguardando ...
Aguardando ...
Aguardando ...
Aguardando ...
Client [127.0.0.1:54215] connected

A ideia por trás de definir um timeout não negativo para a stream_socket_accept() é a de liberar o fluxo da execução do programa ali dentro do while, para que ele possa desempenhar outras tarefas que julgar necessárias.

Aceitando múltiplos clientes

Você deve ter percebido que o nosso servidor só aceita um cliente por vez. Se você tentar abrir um terceiro terminal e se conectar a ele, verá que a conexão não será devidamente estabelecida se outra estiver aberta. Podemos resolver isso de duas formas:

  • 1) Abrir múltiplos processos “ouvindo” o mesmo socket (mas isso só vai funcionar bem em sistemas baseados em Unix, além disso, nem sempre é possível sair forkando processos, sem contar o custo disso pro sistema operacional que é alto (uso de memória e trocas de contexto), sendo mais efetivo trabalhar com threads ou coroutines);
  • 2) Manter um único processo (single-thread) mas receber todas as conexões e monitorar a mudança de estado delas para então decidir se vamos ler e/ou escrever;

Vamos implementar a segunda opção, o “segredo” dela está no uso da função stream_select() que executa uma chamada de sistema select que monitora um ou mais recursos (resources) por mudanças, dessa forma, conseguimos alternar entre diferentes recursos de forma não bloqueante. Esse que é um conceito intimamente relacionado à programação assíncrona. Recomendo, aproveitando o gatilho, a leitura do artigo Introdução à programação assíncrona em PHP usando o ReactPHP.

A implementação do nosso Echo Server que aceita múltiplas conexões de forma não bloqueante:

<?php

declare(strict_types=1);

error_reporting(E_ERROR | E_PARSE);

final class EchoSocketServer
{
    private $except;
    private $server;

    private $buffers = [];
    private $writable = [];
    private $readable = [];
    private $connections = [];

    public function __construct(string $uri)
    {
        $this->server = stream_socket_server($uri);
        stream_set_blocking($this->server, false);

        if ($this->server === false) {
            exit(1);
        }
    }

    public function run() : void
    {
        while (true) {
            $this->except = null;
            $this->writable = $this->connections;
            $this->readable = $this->connections;

            // Adiciona a stream do servidor no array de streams de somente leitura,
            // para que consigamos aceitar novas conexões quando disponíveis;
            $this->readable[] = $this->server;

            // Em um looping infinito, a stream_select() retornará quantas streams foram modificadas,
            // a partir disso iteramos sobre elas (tanto as de escrita quanto de leitura), lendo ou escrevendo.
            // A stream_select() recebe os arrays por referência e ela os zera (remove seus itens) até que uma stream muda de estado,
            // quando isso acontece, a stream_select() volta com essa stream para o array, é nesse momento que conseguimos iterar escrevendo ou lendo.
            if (stream_select($this->readable, $this->writable, $this->except, 0, 0) > 0) {
                $this->readFromStreams();
                $this->writeToStreams();
                $this->release();
            }
        }
    }

    private function readFromStreams() : void
    {
        foreach ($this->readable as $stream) {
            // Se essa $stream é a do servidor, então uma nova conexão precisa ser aceita;
            if ($stream === $this->server) {
                $this->acceptConnection($stream);

                continue;
            }

            // Uma stream é um resource, tipo especial do PHP,
            // quando aplicamos um casting de inteiro nela, obtemos o id desse resource;
            $key = (int) $stream;

            // Armazena no nosso array de buffer os dados recebidos;
            if (isset($this->buffers[$key])) {
                $this->buffers[$key] .= fread($stream, 4096);
            } else {
                $this->buffers[$key] = '';
            }
        }
    }

    private function writeToStreams() : void
    {
        foreach ($this->writable as $stream) {
            $key = (int) $stream;
            $buffer = $this->buffers[$key] ?? null;

            if ($buffer && $buffer !== '') {
                // Escreve no cliente o que foi recebido;
                $bytesWritten = fwrite($stream, "Server says: {$this->buffers[$key]}", 2048);

                // Imediatamente remove do buffer a parte que foi escrita;
                $this->buffers[$key] = substr($this->buffers[$key], $bytesWritten);
            }
        }
    }

    private function release() : void
    {
        foreach ($this->connections as $key => $connection) {
            // Quando uma conexão é fechada, ela entra no modo EOF (end-of-file),
            // usamos a feof() pra verificar esse estado e então devidamente executar fclose().
            if (feof($connection)) {
                fwrite(STDERR, sprintf("Client [%s] closed the connection; \n", stream_socket_get_name($connection, true)));

                fclose($connection);
                unset($this->connections[$key]);
            }
        }
    }

    private function acceptConnection($stream) : void
    {
        $connection = stream_socket_accept($stream, 0, $clientAddress);

        if ($connection) {
            stream_set_blocking($connection, false);
            $this->connections[(int) $connection] = $connection;

            fwrite(STDERR, sprintf("Client [%s] connected; \n", $clientAddress));
        }
    }
}

$server = new EchoSocketServer('tcp://127.0.0.1:7181');
$server->run();

Você pode testar agora abrir várias conexões ao servidor que ele responderá corretamente para cada uma delas. A implementação em sua essência não difere tanto da primeira que fizemos, com a diferença que agora estamos armazenamento as streams em arrays, controlando os buffers e monitorando as mudanças das streams pela stream_select().

Recomendação de leitura

Se você quer aprender programação assíncrona no PHP usando o ReactPHP, recomendo a leitura:

PHP Avançado
Curso de PHP Avançado
CONHEÇA O CURSO

Concluindo

A última implementação que fizemos não foi muito trivial e ela também nem é a melhor opção para resolver o nosso problema. Uma opção melhor e que prefiro é usar programação assíncrona com o padrão Reactor (o mesmo usado por NodeJS e ReactPHP), torna o trabalho com sockets bem mais organizado. Para isso, temos bons recursos no ecossistema PHP, como:

Além disso, se você precisa trabalhar com sockets usando a extensão ext-socket (aquela de mais baixo nível que precisa estar previamente compilada), a library php-socket-raw fornece uma interface orientada a objetos que abstrai grande parte da dificuldade.

Até a próxima!

Uma introdução a TCP, UDP e Sockets

A ideia desse artigo é introduzir o essencial para desenvolvedores que desejam compreender sockets. O foco estará na parte teórica, que servirá de base para outros artigos que lidarão com a parte prática da programação com sockets.

A rede de computadores

Uma rede de computadores caracteriza-se por dois ou mais computadores interligados, não importando por qual meio (cabos, ondas de rádio etc), desde que sejam capazes de trocar informações entre si e/ou compartilhar seus recursos de hardware.

Em uma rede de computadores também podemos ter clientes e servidores. Servidor é uma máquina que fornece um serviço qualquer na rede e Cliente é uma máquina que consome o serviço fornecido pelo Servidor. Também dizemos que algumas aplicações são clientes e outras servidoras, quando fornecem ou consomem serviços na rede. Por exemplo, o Mozilla Thunderbird é um cliente de e-mail, já o Postfix é um servidor de e-mail. Um navegador de internet (Chrome, Firefox etc) é uma aplicação cliente que requisita dados de um servidor.

Uma rede funciona sob protocolos (que são regras que definem o funcionamento dela) e a família de protocolos mais conhecida e utilizada é a TCP/IP, que engloba os protocolos IP (da camada de rede), TCP e UDP (da camada de transporte), HTTP (da camada de aplicação) entre outros.

A referência de como as redes funcionam e são construídas vem do modelo OSI, que foi definido pela ISO em meados dos anos 80 para servir de referência para o projeto de hardware e software das redes, pois o que acontecia é que cada fornecedor implementava o seu próprio padrão e isso prejudicava a interoperabilidade. Apesar do modelo OSI ainda ser a referência teórica (o modelo) para as redes, a arquitetura TCP/IP é que tem a aplicabilidade e o protagonismo nas intranets e na internet.

O modelo OSI possui sete camadas enquanto o TCP/IP é dividido em cinco. O diagrama abaixo mostra onde as camadas do modelo TCP/IP se “encaixam” no OSI:

O protocolo IP (Internet Protocol) é o mais importante da família TCP/IP (ele se localiza na camada de rede) e ele deve ser associado com outros protocolos e, no contexto desse artigo, os mais importantes e mais utilizados são o TCP e o UDP.

TCP vs UDP

Ambos são protocolos da camada de transporte e, quando precisamos de confiabilidade no transporte do dado, usamos o protocolo IP associado ao TCP (que garante a entrega das informações). Quando priorizamos mais velocidade e menos controle, associamos o protocolo IP ao UDP (tráfego de voz e vídeo são bons exemplos onde o UDP teria boa aplicabilidade, ademais, perdendo um ou outro pacote, não interfere totalmente no todo, permanecendo inteligível).

Observe que a comunicação no TCP se dá nas duas pontas:

Algumas das principais características do TCP (Transmission Control Protocol):

  • Orientado à conexão (só transmite dados se uma conexão for estabelecida depois de um Three-way Handshake);
  • É Full-duplex, ou seja, permite que as duas máquinas envolvidas transmitam e recebam ao mesmo tempo;
  • Garante a entrega, sequência (os dados são entregues de forma ordenada), não duplicação e não corrompimento;
  • Automaticamente divide as informações em pequenos pacotes;
  • Garante equilíbrio no envio dos dados (para não causar “sobrecarga” na comunicação);

No UDP, a comunicação se dá em uma ponta e, se algum segmento falha, isso é ignorado e o fluxo continua:

Algumas das principais características do UDP (User Datagram Protocol):

  • Diferente do TCP ele não é orientado à conexão;
  • Não é confiável como o TCP, ele não garante a entrega completa dos dados;
  • É preciso que dividamos manualmente os dados em datagramas (entidades de dados);
  • Não garante a sequência da entrega, portanto, os dados podem chegar em uma ordem aleatória;

Por essas características menos restritivas (menos controles) ele é muito mais rápido que o TCP. Como contrapartida, ele exige um pouco mais do desenvolvedor na hora de implementá-lo.

Por fim, é importante pontuar que tanto o UDP quanto o TCP trabalham com portas, que são elementos lógicos que interligam clientes e servidores de aplicações em redes TCP/IP. O cliente precisa saber qual porta ele se conectará no servidor. Por exemplo, servidores web por padrão usam a porta 80 para servir as páginas e quando acessamos uma página web usando o protocolo http (que é um protocolo de aplicação da TCP/IP) uma conexão TCP à porta 80 do servidor é feita.

Administrador de redes Júnior
Formação: Administrador de redes Júnior
Nesta formação, você verificará os princípios essenciais para um administrador de redes: os fundamentos de uma rede de computadores (LANs, PANs, WANs, sistema binário e o modelo OSI), o funcionamento completo de ponta-a-ponta do protocolo TCP/IP e as estruturas essenciais para operações de routing e switching (os protocolos CDP, LLDP e LACP, além de tabelas de roteamento, ,protocolos OSPF e RIP e o algoritmo Spanning Tree).
CONHEÇA A FORMAÇÃO

O que é um Socket?

Socket provê a comunicação entre duas pontas (fonte e destino) – também conhecido como two-way communication – entre dois processos que estejam na mesma máquina (Unix Socket) ou na rede (TCP/IP Sockets). Na rede, a representação de um socket se dá por ip:porta, por exemplo: 127.0.0.1:4477 (IPv4). Um socket que usa rede é um Socket TCP/IP.

Muito do que fazemos no dia a dia faz uso de sockets. O nosso navegador utiliza sockets para requisitar as páginas; quando acessamos o nosso servidor pelo protocolo de aplicação SSH também estamos abrindo e utilizando um socket.

Sabendo que TCP/IP é base da nossa comunicação na internet, considerando o modelo de rede OSI, os sockets estão entre a camada de aplicação e a de transporte:

Para os processos envolvidos a sensação é que a comunicação está acontecendo diretamente entre eles, no entanto, ela está passando pelas camadas da rede. Essa abstração provida pelos Sockets é o que chamamos de comunicação lógica. Outra forma de entender os Sockets é que eles são a “interface” de comunicação interprocessos.

Todo cliente deve conhecer o socket do servidor (conjunto ip e porta) para se comunicar, mas o servidor só vai conhecer o socket do cliente quando este realizar uma conexão com ele, ou seja, a conexão no modelo cliente-servidor é sempre iniciada pelo cliente.

O diagrama abaixo mostra que a porta do servidor precisa ser previamente conhecida pelo cliente, enquanto que pro servidor não importa qual é a porta do cliente, ele vai conhecê-la quando a conexão dele com o cliente for estabelecida.

Sistemas operacionais baseados no Unix proveem uma interface padrão para operações I/O (input e output) que se passa por descritores de arquivo. Um descritor de arquivo é representado por um número inteiro que se associa a um arquivo aberto e, nesses sistemas, há uma generalização de que “tudo é um arquivo”, então, nesse contexto, um arquivo pode ser uma conexão de rede (um socket é um tipo especial de arquivo), um arquivo de texto, um arquivo de áudio, até mesmo uma pasta é um tipo especial de arquivo.

Como um socket se comporta como um arquivo, chamadas de sistema de leitura e escrita são aplicáveis, da mesma forma como funcionam em um arquivo ordinário, e é aqui que entra a programação de sockets com a API POSIX sockets. Linguagens como o Java, PHP etc, abstraem isso fornecendo ao desenvolvedor uma API de ainda mais alto nível.

Unix Socket

Em sistemas Unix e agora recentemente também no Windows 10, temos um mecanismo para a comunicação entre processos que estão no mesmo host (ao invés da rede), chamado de Unix Socket. A diferença entre um Unix Socket (IPC Socket) de um TCP/IP Socket é que o primeiro permite a comunicação entre processos que estão na mesma máquina. Já o segundo, além disso, permite a comunicação entre processos através da rede.

No entanto, um TCP/IP Socket também pode ser usado para a comunicação de processos que estão na mesma máquina através do loopback que é uma interface virtual de rede que permite que um cliente e um servidor no mesmo host se comuniquem (em IPv4 através do IP 127.0.0.0).

A particularidade é que Unix Sockets estão sujeitos às permissões do sistema e costumam ser um pouco mais performáticos, pois não precisam realizar algumas checagens e operações, por exemplo, de roteamento, algo que acontece com os TCP/IP Sockets. Ou seja, se os processos estão na mesma máquina, Unix Sockets podem ser a melhor opção, mas se estiverem distribuídos na rede, os TCP/IP Sockets são a escolha certa.

Se você tem acesso a algum servidor baseado em Unix, execute netstat -a -p --unix que ele listará todos os Unix Sockets abertos no sistema operacional (bem como mostrará outras informações como o tipo do Socket, caminho etc). Se você quiser visualizar tanto os TCP/IP Sockets quanto os Unix Sockets, você pode executar netstat -na, ele exibirá duas tabelas listando todos os sockets abertos:

Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 127.0.0.1:9070          0.0.0.0:*               LISTEN
tcp        0      0 127.0.0.1:9072          0.0.0.0:*               LISTEN

...

Active UNIX domain sockets (servers and established)
Proto RefCnt Flags       Type       State         I-Node   Path
unix  2      [ ]         DGRAM                    26007836 /run/user/1000/systemd/notify
unix  2      [ ACC ]     SEQPACKET  LISTENING     1976     /run/udev/control

...

Existem quatro tipos de sockets: Stream Sockets, Datagram Sockets, Raw Sockets e Sequenced Packet Sockets sendo que os dois primeiros são os mais comuns e utilizados.

  • Stream Sockets (SOCK_STREAM): Esse tipo usa TCP, portanto, todas as características enumeradas anteriormente se aplicam a ele: garantia de entrega e ordem, orientado à conexão etc;
  • Datagram Sockets (SOCK_DGRAM): Esse tipo usa UDP, portanto, não é orientado à conexão, não garante entrega completa dos dados e exige um controle mais especial do desenvolvedor.

Concluindo

Sockets estão presentes em quase tudo o que fazemos na internet, naquele jogo multiplayer que você joga, naquele chat que você iniciou online e muito mais. As linguagens de programação (ou extensões delas) abstraem grande parte da programação com sockets. Espero que esse artigo te incentive a trabalhar/estudar um pouco mais sobre eles usando a sua linguagem favorita.

Recomendação de leitura

Se você programa ou tem interesse por PHP: Programação de Sockets em PHP.