Server Push: Long Polling usando PHP

Server Push: Long Polling usando PHP

Há situações em que precisamos obter uma resposta de um servidor a cada X intervalo de tempo. Alguns programadores criam rotinas que ficam perguntando para o servidor toda hora. O problema dessa abordagem é que ela sobrecarrega o servidor com muitas requisições, aumentando tráfego de rede e podendo até derrubar o servidor.

A técnica do Server Push consiste em manter uma conexão aberta entre cliente e servidor. Quando houver conteúdo para o servidor enviar ao cliente, ele o envia. Assim, o cliente não precisa ficar “perguntando” para o servidor se ele tem novo conteúdo.

Para que já programou usando sockets TCP/IP, por exemplo, parece bem simples. O problema de fazer isso via HTTP é que este protocolo não permite conexões persistentes, ou seja, o servidor recebe uma requisição, responde-a e fecha a conexão. Para contornar isso, usaremos o que é chamado de Long Polling, descrito com mais detalhes no link abaixo:

http://en.wikipedia.org/wiki/Push_technology#Long_polling

Em suma, a técnica consiste no seguinte: o cliente faz uma requisição ao servidor. Se o servidor tiver conteúdo para mandar, manda-o e fecha a conexão. Caso ele não tenha nada para enviar, aguarda mais um tempo e verifica de novo. Ou seja, o servidor fica em loop infinito, enquanto não tiver dados para enviar. O cliente, ao receber a informação, processa-a e reabre a conexão com o servidor.

Vamos a um exemplo de implementação.

Servidor (server.php):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
// arquivo cujo conteúdo será enviado ao cliente
$dataFileName = 'data.txt'; 
 
while ( true )
{
    $requestedTimestamp = isset( $_GET['timestamp'] ) ? (int)$_GET['timestamp'] : null;      
 
    // o PHP faz cache de operações "stat" do filesystem. Por isso, devemos limpar esse cache   
    clearstatcache();  
 
    $modifiedAt = filemtime( $dataFileName );       
 
    if ( $requestedTimestamp == null || $modifiedAt > $requestedTimestamp )
    {
        $data = file_get_contents( $dataFileName );
 
        $arrData = array(
            'content' => $data,
            'timestamp' => $modifiedAt
        );
 
        $json = json_encode( $arrData );
 
        echo $json;
 
        break;
    }
    else
    {
        sleep( 2 );
        continue;
    }
}

Cliente (client.js):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function getContent( timestamp )
{
    var queryString = { 'timestamp' : timestamp };
 
    $.get ( 'server.php' , queryString , function ( data )
    {
        var obj = jQuery.parseJSON( data );
        $( '#response' ).html( obj.content );
 
        // reconecta ao receber uma resposta do servidor
        getContent( obj.timestamp );
    });
}
 
$( document ).ready ( function ()
{
    getContent();
});

index.php

1
2
<script src="http://code.jquery.com/jquery.min.js" type="text/javascript"></script>
<script src="client.js" type="text/javascript"></script>

Conteúdo

Crie um arquivo data.txt e modifique o conteúdo dele enquanto está com o cliente aberto (lembre-se de salvar o conteúdo do data.txt a cada modificação). A cada modificação, o novo conteúdo é exibido no cliente.

É importante notar que esse é um exemplo bem cru. É necessário implementar um timeout no servidor. Veja que, se o cliente desconectar antes de o servidor enviar uma resposta, este ficará com um processo em loop infinito, podendo se estender indefinidamente, caso nunca mais haja conteúdo a ser enviado como resposta para aquela requisição.

O código completo está no meu GitHub, neste link:
https://github.com/beraldo/Server-Push

De brinde, nesse link também há um cliente feito para iOS, que fiz para testes e resolvi colocar junto no repositório. :)

Exemplo Usando Banco de Dados

Muitos me perguntam como usar Long Polling com banco de dados em vez de fazer com arquivo de texto. Por isso vou incluir um exemplo usando um simples banco de dados, em SQLite. Vou usar a classe PDO para comunicação com o banco. Assim o exemplo é adaptável a qualquer outro SGBD. Caso não saiba como trabalhar com PDO, recomendo ler este post.

Primeiramente, vou criar um banco de dados SQLite, com o nome de comments.db, onde criarei uma tabela que armazenará comentários. A tabela possui os campos id, author, comment e timestamp. É por este último campo que faremos a busca, filtrando por timestamps superiores ao passado por parâmetro para o script. Usarei este comando para criar a base de dados:

$ sqlite3 comments.db "CREATE TABLE comments(id INTEGER PRIMARY KEY, author VARCHAR, comment TEXT, timestamp INTEGER);"

Tendo o banco criado, podemos alterar o arquivo server.php, para utilizá-lo em vez de ler o arquivo data.txt. O script ficará assim:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$dbFile = 'comments.db';
 
$PDO = new PDO( "sqlite:" . $dbFile );
 
$requestedTimestamp = isset ( $_GET [ 'timestamp' ] ) ? (int)$_GET [ 'timestamp' ] : time();
 
while ( true )
{
    $stmt = $PDO->prepare( "SELECT author, comment, timestamp FROM comments WHERE timestamp > :requestedTimestamp" );
    $stmt->bindParam( ':requestedTimestamp', $requestedTimestamp );
    $stmt->execute();
    $rows = $stmt->fetchAll( PDO::FETCH_ASSOC );
 
    if ( count( $rows ) > 0 )
    {
        $json = json_encode( $rows );
        echo $json;
        break;
    }
    else
    {
        sleep( 2 );
        continue;
    }
}

NOTA: para usar MysQL em vez do SQLite, basta trocar estas duas linhas:

$dbFile = 'comments.db';
$PDO = new PDO( "sqlite:" . $dbFile );

Por esta:

$PDO = new PDO( 'mysql:host=servidor_mysql;dbname=nome_do_banco', 'usuario', 'senha' );

Para mais detalhes sobre como usar PDO com MySQL, veja este post.

Vamos precisar mudar um pouco nosso cliente Javascript, deixando desta forma:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function getContent( timestamp )
{
    var queryString = { 'timestamp' : timestamp };
    $.get ( 'server.php' , queryString , function ( data )
    {
        var obj = jQuery.parseJSON( data );
 
        for (var k in obj)
        {
            var comment = "<p>" + obj[k].comment + "</p>";
            var timestamp = obj[k].timestamp;
            $( '#response' ).append( comment );
        }
 
        // reconecta ao receber uma resposta do servidor
        getContent( timestamp );
    });
}
 
$( document ).ready( function ()
{
    getContent();
});

Enquanto o script server.php estiver em execução, você deve fazer um INSERT na tabela comments, inserindo um registro com o timestamp superior ao atual. Por exemplo:

INSERT INTO comments(author, comment, TIMESTAMP) VALUES('Beraldo', 'Oi, sou um comentário', 1421002330);

ATENÇÃO: Lembre-se de alterar o timestamp do INSERT acima para um que seja superior ao timestamp atual.

Pronto. Agora você tem um exemplo de Long Polling usando banco de dados. :)

 

The following two tabs change content below.
Graduado em Ciência da Computação, pela Universidade Federal do Paraná (UFPR), é desenvolvedor de software desde 2008, com foco em Desenvolvimento Web com PHP.