Como faço para implementar a "Pesquisa longa" básica?

776

Posso encontrar muitas informações sobre como o Long Polling funciona (por exemplo, isso e isso ), mas não há exemplos simples de como implementar isso no código.

Tudo o que posso encontrar é o cometa , que se baseia na estrutura Dojo JS e em um sistema de servidor bastante complexo.

Basicamente, como eu usaria o Apache para atender as solicitações e como escreveria um script simples (digamos, em PHP) que "pesquisaria longamente" o servidor em busca de novas mensagens?

O exemplo não precisa ser escalonável, seguro ou completo, ele só precisa funcionar!

dbr
fonte

Respostas:

512

É mais simples do que eu pensava inicialmente. Basicamente, você tem uma página que não faz nada, até que os dados que você deseja enviar estejam disponíveis (por exemplo, uma nova mensagem chega).

Aqui está um exemplo realmente básico, que envia uma sequência simples após 2-10 segundos. 1 em 3 chances de retornar um erro 404 (para mostrar o tratamento de erros no próximo exemplo de Javascript)

msgsrv.php

<?php
if(rand(1,3) == 1){
    /* Fake an error */
    header("HTTP/1.0 404 Not Found");
    die();
}

/* Send a string after a random number of seconds (2-10) */
sleep(rand(2,10));
echo("Hi! Have a random number: " . rand(1,10));
?>

Nota: Em um site real, a execução em um servidor Web comum como o Apache rapidamente amarra todos os "threads de trabalho" e deixa-o incapaz de responder a outras solicitações. Existem maneiras de contornar isso, mas é recomendável escrever um "servidor de pesquisa longa" em algo como o twisted do Python , que não depende de um thread por solicitação. O cometD é popular (disponível em vários idiomas), e o Tornado é uma nova estrutura criada especificamente para essas tarefas (foi criada para o longo código de pesquisa do FriendFeed) ... mas como um exemplo simples, o Apache é mais do que adequado ! Esse script pode ser facilmente escrito em qualquer idioma (eu escolhi o Apache / PHP, pois eles são muito comuns, e eu os estava executando localmente)

Em Javascript, você solicita o arquivo acima ( msg_srv.php) e aguarda uma resposta. Quando você obtém um, você age com base nos dados. Então você solicita o arquivo e espera novamente, age com base nos dados (e repita)

A seguir, é apresentado um exemplo dessa página. Quando a página é carregada, ela envia a solicitação inicial para o msgsrv.phparquivo. Se for bem-sucedida, anexamos a mensagem à #messagesdiv e, após 1 segundo, chamamos a função waitForMsg novamente, o que desencadeia a espera.

O segundo setTimeout()é um limitador de taxa realmente básico, funciona bem sem isso, mas se msgsrv.php sempre retorna instantaneamente (com um erro de sintaxe, por exemplo) - você inunda o navegador e pode congelar rapidamente. É melhor verificar se o arquivo contém uma resposta JSON válida e / ou manter um total em execução de solicitações por minuto / segundo e fazer uma pausa apropriada.

Se a página cometer erros, ele anexa o erro à #messagesdiv, aguarda 15 segundos e tenta novamente (idêntico ao modo como esperamos 1 segundo após cada mensagem)

O bom dessa abordagem é que ela é muito resistente. Se a conexão com a Internet dos clientes morrer, o tempo limite será excedido e, em seguida, tente se reconectar - isso é inerente ao tempo em que a pesquisa funciona, sem a necessidade de tratamento de erros complicado.

De qualquer forma, o long_poller.htmcódigo, usando a estrutura jQuery:

<html>
<head>
    <title>BargePoller</title>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js" type="text/javascript" charset="utf-8"></script>

    <style type="text/css" media="screen">
      body{ background:#000;color:#fff;font-size:.9em; }
      .msg{ background:#aaa;padding:.2em; border-bottom:1px #000 solid}
      .old{ background-color:#246499;}
      .new{ background-color:#3B9957;}
    .error{ background-color:#992E36;}
    </style>

    <script type="text/javascript" charset="utf-8">
    function addmsg(type, msg){
        /* Simple helper to add a div.
        type is the name of a CSS class (old/new/error).
        msg is the contents of the div */
        $("#messages").append(
            "<div class='msg "+ type +"'>"+ msg +"</div>"
        );
    }

    function waitForMsg(){
        /* This requests the url "msgsrv.php"
        When it complete (or errors)*/
        $.ajax({
            type: "GET",
            url: "msgsrv.php",

            async: true, /* If set to non-async, browser shows page as "Loading.."*/
            cache: false,
            timeout:50000, /* Timeout in ms */

            success: function(data){ /* called when request to barge.php completes */
                addmsg("new", data); /* Add response to a .msg div (with the "new" class)*/
                setTimeout(
                    waitForMsg, /* Request next message */
                    1000 /* ..after 1 seconds */
                );
            },
            error: function(XMLHttpRequest, textStatus, errorThrown){
                addmsg("error", textStatus + " (" + errorThrown + ")");
                setTimeout(
                    waitForMsg, /* Try again after.. */
                    15000); /* milliseconds (15seconds) */
            }
        });
    };

    $(document).ready(function(){
        waitForMsg(); /* Start the inital request */
    });
    </script>
</head>
<body>
    <div id="messages">
        <div class="msg old">
            BargePoll message requester!
        </div>
    </div>
</body>
</html>
dbr
fonte
7
Algumas mensagens não puderam passar usando essa ideia? Nesse intervalo de um segundo, digamos, 1000 mensagens de bate-papo foram enviadas, como o servidor saberia enviar as 1000 mensagens de mensagens especificamente para esse cliente?
DevDevDev 7/10/09
15
Provavelmente. Este é um exemplo muito simplificado, para demonstrar o conceito. Para fazer isso melhor, você precisaria de um código do lado do servidor mais elaborado, onde armazenaria essas 1000 mensagens para esse cliente específico e as enviaria em um único bloco. Você também pode seguramente reduzir o tempo limite waitForMsg
DBR
21
O nodejs é outra excelente solução do lado do servidor para solicitações longas de pesquisa, com a vantagem adicional (sobre o Twisted) de que você também pode escrever o código do servidor em Javascript.
Husky
8
Essa é apenas uma simples conexão AJAX recorrente ao servidor com intervalo de 1 segundo. Isso não tem nada a ver com "pesquisas longas". A pesquisa longa deve manter a conexão ativa, contanto que o tempo limite do cliente continue.
Deele
6
a questão é o que um script PHP real faz em vez de sleep(rand(2,10));? para não fazer nada, faça uma pesquisa no banco de dados a cada 100 milissegundos? quando ele decide morrer?
Luis Siquot 28/09/11
41

Eu tenho um exemplo muito simples de bate-papo como parte do slosh .

Editar : (já que todos estão colando seu código aqui)

Este é o bate-papo multiusuário completo baseado em JSON, usando pesquisas longas e slosh . Esta é uma demonstração de como fazer as chamadas; portanto, ignore os problemas do XSS. Ninguém deve implantar isso sem sanitá-lo primeiro.

Observe que o cliente sempre tem uma conexão com o servidor e, assim que alguém envia uma mensagem, todos devem vê-la aproximadamente instantaneamente.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- Copyright (c) 2008 Dustin Sallings <[email protected]> -->
<html lang="en">
  <head>
    <title>slosh chat</title>
    <script type="text/javascript"
      src="http://code.jquery.com/jquery-latest.js"></script>
    <link title="Default" rel="stylesheet" media="screen" href="style.css" />
  </head>

  <body>
    <h1>Welcome to Slosh Chat</h1>

    <div id="messages">
      <div>
        <span class="from">First!:</span>
        <span class="msg">Welcome to chat. Please don't hurt each other.</span>
      </div>
    </div>

    <form method="post" action="#">
      <div>Nick: <input id='from' type="text" name="from"/></div>
      <div>Message:</div>
      <div><textarea id='msg' name="msg"></textarea></div>
      <div><input type="submit" value="Say it" id="submit"/></div>
    </form>

    <script type="text/javascript">
      function gotData(json, st) {
        var msgs=$('#messages');
        $.each(json.res, function(idx, p) {
          var from = p.from[0]
          var msg = p.msg[0]
          msgs.append("<div><span class='from'>" + from + ":</span>" +
            " <span class='msg'>" + msg + "</span></div>");
        });
        // The jQuery wrapped msgs above does not work here.
        var msgs=document.getElementById("messages");
        msgs.scrollTop = msgs.scrollHeight;
      }

      function getNewComments() {
        $.getJSON('/topics/chat.json', gotData);
      }

      $(document).ready(function() {
        $(document).ajaxStop(getNewComments);
        $("form").submit(function() {
          $.post('/topics/chat', $('form').serialize());
          return false;
        });
        getNewComments();
      });
    </script>
  </body>
</html>
Dustin
fonte
1
Posso saber como isso está sempre conectado? Desculpe se estou perguntando algo bobo, mas quero saber disso.
Rocky Singh
4
Ele executa um HTTP GET e o servidor bloqueia o GET até que haja dados disponíveis. Quando os dados chegam ao servidor, o servidor retorna os dados para o cliente, enfileira o que mais estiver chegando e, em seguida, o cliente se reconecta e pega as mensagens ausentes, se houver, caso contrário, ele bloqueia novamente.
Dustin
4
Pode não ser óbvio à primeira vista, mas é que responsável por 'estado sempre ligado' é ajaxStop com getNewCommentscallback lá, por isso só dispara no final de cada solicitação ajax interminavelmente
Baldrs
32

O Tornado foi projetado para pesquisas longas e inclui um aplicativo de bate-papo muito mínimo (algumas centenas de linhas de Python) em / examples / chatdemo , incluindo código do servidor e código do cliente JS. Funciona assim:

  • Os clientes usam o JS para solicitar atualizações, pois (número da última mensagem), o servidor URLHandler as recebe e adiciona um retorno de chamada para responder ao cliente em uma fila.

  • Quando o servidor recebe uma nova mensagem, o evento onmessage é acionado, percorre os retornos de chamada e envia as mensagens.

  • O JS do lado do cliente recebe a mensagem, a adiciona à página e solicita atualizações desde esse novo ID de mensagem.

mikemaccana
fonte
25

Eu acho que o cliente parece uma solicitação AJAX assíncrona normal, mas você espera que demore "muito tempo" para voltar.

O servidor fica assim.

while (!hasNewData())
    usleep(50);

outputNewData();

Portanto, a solicitação AJAX vai para o servidor, provavelmente incluindo um carimbo de data e hora de quando foi a última atualização, para que você hasNewData()saiba quais dados você já obteve. O servidor fica parado em loop até que novos dados estejam disponíveis. O tempo todo, sua solicitação AJAX ainda está conectada, apenas aguardando por dados. Por fim, quando novos dados estão disponíveis, o servidor os envia à sua solicitação AJAX e fecha a conexão.

Greg
fonte
10
Essa é uma espera ocupada que bloqueia seu segmento atual. Isso não escala em tudo.
Wouter Lievens
10
Não, o sono não é uma espera ocupada. E o ponto principal de "esperar" é bloquear seu tópico por um tempo. Provavelmente ele quis dizer 50 milissegundos (usleep (50000)), não 50 microssegundos! Mas de qualquer maneira, com uma configuração típica do Apache / PHP, existe outra maneira de fazer isso?
Matt
Bem, pelo princípio, você não pode criar uma função de bloqueio para mensagens de bate-papo sem esperar.
Tomáš Zato - Restabelece Monica
Ótimo mesmo! Eu criei uma função recursiva no servidor para verificar novos dados. Mas qual é o melhor produto para usar a pesquisa longa com eficiência? Eu uso o Apache normal e o servidor não responde quando eu abrir mais de 4/5 abas do navegador :( Procurando algo para ser usado com o PHP
modernos
17

Aqui estão algumas classes que eu uso para pesquisas longas em C #. Existem basicamente 6 classes (veja abaixo).

  1. Controlador : Processa as ações necessárias para criar uma resposta válida (operações de banco de dados, etc.)
  2. Processador : gerencia a comunicação assíncrona com a página da web (ela mesma)
  3. IAsynchProcessor : o serviço processa instâncias que implementam essa interface
  4. Sevice : Processos solicitam objetos que implementam IAsynchProcessor
  5. Pedido : O wrapper IAsynchProcessor que contém sua resposta (objeto)
  6. Resposta : Contém objetos ou campos personalizados
Prisioneiro ZERO
fonte
2
Ok ... então por que isso foi rejeitado? Essas classes são de fato exemplos válidos de pesquisas longas.
Preso ZERO
A pesquisa longa real não é (simplesmente) a prática de aumentar o intervalo no qual você realiza uma pesquisa normal (em um recurso). É parte de um padrão maior ... que é "um pouco" sujeito a interpretação ... mas apenas em certas áreas da implementação geral. Dito isto ... essas classes seguem o dito padrão! Então, se você tem um motivo para votar isso ... eu realmente estaria interessado no motivo.
Prisioneiro ZERO
Talvez tenha sido rejeitado, pois não aborda diretamente a questão de um exemplo de código simples. É claro que não votei para baixo, então só posso adivinhar.
Andrew
16

Este é um belo screencast de 5 minutos sobre como fazer pesquisas longas usando PHP e jQuery: http://screenr.com/SNH

O código é bastante semelhante ao exemplo do dbr acima.

Sean O
fonte
3
Eu acho que você deve ver isso apenas como uma introdução à pesquisa longa, porque essa implementação certamente matará seu servidor com muitos usuários simultâneos.
Alfred
estou apenas aprendendo sobre tudo isso ... como é confiável ou não com alguns usuários ... digamos 10 conversando?
somdow
12

Aqui está um exemplo simples de pesquisa longa em PHP por Erik Dubbelboer usando o Content-type: multipart/x-mixed-replacecabeçalho:

<?

header('Content-type: multipart/x-mixed-replace; boundary=endofsection');

// Keep in mind that the empty line is important to separate the headers
// from the content.
echo 'Content-type: text/plain

After 5 seconds this will go away and a cat will appear...
--endofsection
';
flush(); // Don't forget to flush the content to the browser.


sleep(5);


echo 'Content-type: image/jpg

';

$stream = fopen('cat.jpg', 'rb');
fpassthru($stream);
fclose($stream);

echo '
--endofsection
';

E aqui está uma demonstração:

http://dubbelboer.com/multipart.php

Jasdeep Khalsa
fonte
11

Eu usei isso para entender o Comet, também configurei o Comet usando o servidor Java Glassfish e encontrei muitos outros exemplos assinando o comdaily.com

Adão
fonte
9

Abaixo está uma longa solução de pesquisa que desenvolvi para o Inform8 Web. Basicamente, você substitui a classe e implementa o método loadData. Quando o loadData retorna um valor ou a operação atinge o tempo limite, imprimirá o resultado e o retorno.

Se o processamento do seu script demorar mais de 30 segundos, talvez você precise alterar a chamada set_time_limit () para algo mais longo.

Licença Apache 2.0. Versão mais recente no github https://github.com/ryanhend/Inform8/blob/master/Inform8-web/src/config/lib/Inform8/longpoll/LongPoller.php

Ryan

abstract class LongPoller {

  protected $sleepTime = 5;
  protected $timeoutTime = 30;

  function __construct() {
  }


  function setTimeout($timeout) {
    $this->timeoutTime = $timeout;
  }

  function setSleep($sleep) {
    $this->sleepTime = $sleepTime;
  }


  public function run() {
    $data = NULL;
    $timeout = 0;

    set_time_limit($this->timeoutTime + $this->sleepTime + 15);

    //Query database for data
    while($data == NULL && $timeout < $this->timeoutTime) {
      $data = $this->loadData();
      if($data == NULL){

        //No new orders, flush to notify php still alive
        flush();

        //Wait for new Messages
        sleep($this->sleepTime);
        $timeout += $this->sleepTime;
      }else{
        echo $data;
        flush();
      }
    }

  }


  protected abstract function loadData();

}
Ryan Henderson
fonte
8

Obrigado pelo código, dbr . Apenas um pequeno erro de digitação em long_poller.htm ao redor da linha

1000 /* ..after 1 seconds */

Eu acho que deveria ser

"1000"); /* ..after 1 seconds */

para que funcione.

Para os interessados, tentei um equivalente ao Django. Inicie um novo projeto Django, digamos lp para pesquisas longas:

django-admin.py startproject lp

Ligue para o aplicativo msgsrv para o servidor de mensagens:

python manage.py startapp msgsrv

Adicione as seguintes linhas ao settings.py para ter um diretório de modelos :

import os.path
PROJECT_DIR = os.path.dirname(__file__)
TEMPLATE_DIRS = (
    os.path.join(PROJECT_DIR, 'templates'),
)

Defina seus padrões de URL em urls.py da seguinte maneira:

from django.views.generic.simple import direct_to_template
from lp.msgsrv.views import retmsg

urlpatterns = patterns('',
    (r'^msgsrv\.php$', retmsg),
    (r'^long_poller\.htm$', direct_to_template, {'template': 'long_poller.htm'}),
)

E o msgsrv / views.py deve se parecer com:

from random import randint
from time import sleep
from django.http import HttpResponse, HttpResponseNotFound

def retmsg(request):
    if randint(1,3) == 1:
        return HttpResponseNotFound('<h1>Page not found</h1>')
    else:
        sleep(randint(2,10))
        return HttpResponse('Hi! Have a random number: %s' % str(randint(1,10)))

Por fim, templates / long_poller.htm deve ser o mesmo que acima, com o erro de digitação corrigido. Espero que isto ajude.

xoblau
fonte
Na verdade, "15000"é o erro de sintaxe. setTimeout aceita um número inteiro como seu segundo parâmetro.
Andrew Hedges
Esta resposta precisa de trabalho. É o culminar de um ou mais comentários e uma resposta ou respostas separadas.
Brian Webster
8

Este é um dos cenários para os quais o PHP é uma péssima escolha. Como mencionado anteriormente, você pode amarrar todos os seus funcionários do Apache rapidamente, fazendo algo assim. O PHP foi criado para iniciar, executar, parar. Não foi construído para começar, espere ... execute, pare. Você atolará o servidor muito rapidamente e descobrirá que tem problemas incríveis de dimensionamento.

Dito isto, você ainda pode fazer isso com PHP e não matar seu servidor usando o nginx HttpPushStreamModule: http://wiki.nginx.org/HttpPushStreamModule

Você configura o nginx na frente do Apache (ou qualquer outra coisa) e ele se encarregará de manter abertas as conexões simultâneas. Você apenas responde com carga útil enviando dados para um endereço interno, o que você poderia fazer com um trabalho em segundo plano ou apenas enviar as mensagens para as pessoas que estavam esperando sempre que as novas solicitações chegavam. Isso impede que os processos PHP fiquem abertos durante uma longa pesquisa.

Isso não é exclusivo do PHP e pode ser feito usando o nginx com qualquer linguagem de back-end. O carregamento simultâneo de conexões abertas é igual ao Node.js, portanto, a maior vantagem é que ele o tira do NECESSIDADE de Nó para algo assim.

Você vê muitas outras pessoas mencionando outras bibliotecas de idiomas para realizar pesquisas longas e isso é por uma boa razão. O PHP simplesmente não está bem construído para esse tipo de comportamento naturalmente.

bola brilhante
fonte
Este é um problema do Apache ou do PHP? Eu teria problemas com a pesquisa longa se meu código PHP fosse executado diretamente no nginx ou lighttpd?
David
É menos um problema de PHP e mais um mau uso do PHP. Em cada solicitação, o PHP executa o script a partir do zero, carregando bibliotecas conforme necessário, executando seu código e, em seguida, desligando enquanto coleta de lixo tudo o que foi iniciado na solicitação. Muitas modificações foram feitas no PHP ao longo dos anos para minimizar o impacto, como ligações estáticas tardias, carregamento lento, caches de bytecode na memória para remover a E / S do disco, etc. O problema é que o PHP deve iniciar e parar o mais rápido possível. que possível. Os idiomas que carregam uma vez / boot e abrem um thread para a solicitação são muito mais adequados para pesquisas longas.
27915 brightball
Mas, para responder à pergunta, sim, você enfrentaria o problema, independentemente de estar usando o Apache ou qualquer outra coisa. É assim que o PHP funciona. Devo alterar isso para dizer que, se você tiver uma carga de tráfego máxima conhecida, o PHP ficará bem. Vi sistemas embarcados usando PHP que não têm problemas porque existem apenas algumas conexões. Potencialmente, na intranet da empresa, isso também pode ser aceitável. No entanto, para aplicativos públicos, você absolutamente mata seus servidores à medida que o tráfego aumenta.
brightball
4

Por que não considerar os soquetes da Web em vez de pesquisas longas? Eles são muito eficientes e fáceis de configurar. No entanto, eles são suportados apenas em navegadores modernos. Aqui está uma referência rápida .

shasi kanth
fonte
Acho que uma vez que os websockets sejam implementados em todos os lugares (provavelmente não nos próximos anos), eles serão o padrão para esse tipo de aplicativo. Infelizmente, por enquanto, não podemos confiar neles para aplicativos de produção.
Richard
3
@ Richard No entanto, pode usar algo como Socket.IO que fornece transportes fallback automáticas, proporcionando web-socket-como funcionalidade todo o caminho para o IE 6.
Brad
3

O grupo WS-I publicou algo chamado "Perfil Seguro Confiável" que possui uma implementação Glass Fish e .NET que aparentemente interopera bem.

Com alguma sorte existe um Javascript implementação de por aí também.

Há também uma implementação do Silverlight que usa HTTP Duplex. Você pode conectar o javascript ao Silverlight objeto para obter retornos de chamada quando ocorrer um push.

Também existem versões comerciais pagas também.

goodguys_activate
fonte
2

Você pode experimentar o icomet ( https://github.com/ideawu/icomet ), um servidor cometa C1000K C ++ criado com libevent. O icomet também fornece uma biblioteca JavaScript, é fácil de usar tão simples quanto

var comet = new iComet({
    sign_url: 'http://' + app_host + '/sign?obj=' + obj,
    sub_url: 'http://' + icomet_host + '/sub',
    callback: function(msg){
        // on server push
        alert(msg.content);
    }
});

O icomet suporta uma ampla variedade de navegadores e sistemas operacionais, incluindo Safari (iOS, Mac), IEs (Windows), Firefox, Chrome etc.

ideawu
fonte
0

NodeJS mais simples

const http = require('http');

const server = http.createServer((req, res) => {
  SomeVeryLongAction(res);
});

server.on('clientError', (err, socket) => {
  socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});

server.listen(8000);

// the long running task - simplified to setTimeout here
// but can be async, wait from websocket service - whatever really
function SomeVeryLongAction(response) {
  setTimeout(response.end, 10000);
}

Cenário de produção no Express, por exemplo, você entraria responseno middleware. Você faz o que precisa fazer, pode definir todos os métodos pesquisados ​​há muito tempo para mapear ou algo do tipo (visível para outros fluxos) e chamar<Response> response.end() sempre que estiver pronto. Não há nada de especial em conexões com sondagem longa. O resto é exatamente como você estrutura seu aplicativo.

Se você não sabe o que quero dizer com escopo, isso deve lhe dar uma ideia

const http = require('http');
var responsesArray = [];

const server = http.createServer((req, res) => {
  // not dealing with connection
  // put it on stack (array in this case)
  responsesArray.push(res);
  // end this is where normal api flow ends
});

server.on('clientError', (err, socket) => {
  socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});

// and eventually when we are ready to resolve
// that if is there just to ensure you actually 
// called endpoint before the timeout kicks in
function SomeVeryLongAction() {
  if ( responsesArray.length ) {
    let localResponse = responsesArray.shift();
    localResponse.end();
  }
}

// simulate some action out of endpoint flow
setTimeout(SomeVeryLongAction, 10000);
server.listen(8000);

Como você vê, você pode realmente responder a todas as conexões, uma, fazer o que quiser. Há idtodas as solicitações para que você possa usar o mapa e acessar chamadas específicas fora da API.

sp3c1
fonte