¿Cómo implemento el "sondeo largo" básico?

776

Puedo encontrar mucha información sobre cómo funciona Long Polling (por ejemplo, esto y esto ), pero no hay ejemplos simples de cómo implementar esto en el código.

Todo lo que puedo encontrar es cometd , que se basa en el marco Dojo JS y un sistema de servidor bastante complejo.

Básicamente, ¿cómo usaría Apache para atender las solicitudes y cómo escribiría un script simple (por ejemplo, en PHP) que "sondearía" al servidor en busca de nuevos mensajes?

El ejemplo no tiene que ser escalable, seguro o completo, ¡solo tiene que funcionar!

dbr
fuente

Respuestas:

512

Es más simple de lo que pensé inicialmente. Básicamente, tiene una página que no hace nada, hasta que los datos que desea enviar estén disponibles (por ejemplo, llega un nuevo mensaje).

Aquí hay un ejemplo realmente básico, que envía una cadena simple después de 2-10 segundos. 1 de cada 3 posibilidades de devolver un error 404 (para mostrar el manejo de errores en el próximo ejemplo 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: con un sitio real, ejecutar esto en un servidor web normal como Apache vinculará rápidamente todos los "subprocesos de trabajo" y dejará que no pueda responder a otras solicitudes. Hay formas de evitar esto, pero se recomienda escribir un "servidor de sondeo largo" en algo como Python torcido , que no se basa en un hilo por solicitud. cometD es popular (que está disponible en varios idiomas), y Tornado es un nuevo marco creado específicamente para tales tareas (fue construido para el código de sondeo largo de FriendFeed) ... pero como un simple ejemplo, Apache es más que adecuado ! Este script podría escribirse fácilmente en cualquier idioma (elegí Apache / PHP, ya que son muy comunes, y resultó que los ejecutaba localmente)

Luego, en Javascript, solicita el archivo anterior ( msg_srv.php) y espera una respuesta. Cuando obtienes uno, actúas sobre los datos. Luego solicita el archivo y espera nuevamente, actúa sobre los datos (y repite)

Lo que sigue es un ejemplo de dicha página ... Cuando se carga la página, envía la solicitud inicial del msgsrv.phparchivo ... Si tiene éxito, agregamos el mensaje al #messagesdiv, luego de 1 segundo llamamos a la función waitForMsg nuevamente, lo que desencadena la espera.

El 1 segundo setTimeout()es un limitador de velocidad realmente básico, funciona bien sin esto, pero si msgsrv.php siempre regresa instantáneamente (con un error de sintaxis, por ejemplo): inunda el navegador y puede congelarse rápidamente. Será mejor que verifique si el archivo contiene una respuesta JSON válida y / o mantenga un total acumulado de solicitudes por minuto / segundo, y pause adecuadamente.

Si la página falla, agrega el error al #messagesdiv, espera 15 segundos y luego lo intenta nuevamente (idéntico a cómo esperamos 1 segundo después de cada mensaje)

Lo bueno de este enfoque es que es muy resistente. Si la conexión a Internet del cliente se corta, se agota el tiempo de espera, luego intenta volver a conectarse; esto es inherente a la duración del sondeo, no se requiere un manejo de errores complicado

De todos modos, el long_poller.htmcódigo, usando el marco 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
fuente
77
¿No podrían pasar algunos mensajes usando esta idea? En ese 1 segundo tiempo de espera, digamos que se enviaron 1000 mensajes de chat, ¿cómo sabría el servidor enviar los 1000 mensajes específicamente a ese cliente?
DevDevDev
15
Probablemente. Este es un ejemplo muy simplificado, para demostrar el concepto. Para hacerlo mejor, necesitaría un código más elaborado del lado del servidor, donde almacenaría esos 1000 mensajes para ese cliente específico y los enviaría en una porción. También puede reducir de manera segura el tiempo de espera
waitForMsg
21
nodejs es otra excelente solución del lado del servidor para solicitudes de sondeo largas, con la ventaja adicional (sobre Twisted) de que también puede escribir el código del servidor en Javascript.
Husky
8
Esta es solo una conexión AJAX recurrente al servidor con un intervalo de 1 segundo. Esto no tiene nada que ver con "encuestas largas". Las encuestas largas deben mantener la conexión activa, siempre que el tiempo de espera del cliente se prolongue.
Deele
66
la pregunta es ¿qué hace un script PHP real en lugar de sleep(rand(2,10));? para no hacer nada, sondear la base de datos cada 100 milisegundos? cuando decide morir?
Luis Siquot
41

Tengo un ejemplo de chat realmente simple como parte de slosh .

Editar : (ya que todos están pegando su código aquí)

Este es el chat multiusuario completo basado en JSON que utiliza sondeos largos y slosh . Esta es una demostración de cómo hacer las llamadas, así que ignore los problemas de XSS. Nadie debería implementar esto sin desinfectarlo primero.

Tenga en cuenta que el cliente siempre tiene una conexión con el servidor, y tan pronto como alguien envíe un mensaje, todos deberían verlo de manera aproximada al instante.

<?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
fuente
1
¿Puedo saber cómo está siempre conectado? Lo siento si estoy preguntando algo tonto, pero quiero saber eso.
Rocky Singh
44
Hace un HTTP GET y el servidor bloquea el GET hasta que haya datos disponibles. Cuando los datos llegan al servidor, el servidor devuelve los datos al cliente, pone en cola cualquier otra cosa que pueda estar entrando y luego el cliente se vuelve a conectar y recoge los mensajes que faltan, de lo contrario, se bloquea nuevamente.
Dustin
44
Puede que no sea obvio al principio, pero la cosa es que el responsable del 'estado siempre conectado' es ajaxStop con getNewCommentsdevolución de llamada allí, por lo que solo se dispara al final de cada solicitud de ajax sin fin
calvicie
32

Tornado está diseñado para sondeos largos e incluye una aplicación de chat mínima (unos cientos de líneas de Python) en / examples / chatdemo , que incluye el código del servidor y el código del cliente JS. Funciona así:

  • Los clientes usan JS para solicitar actualizaciones ya que (número del último mensaje), el servidor URLHandler las recibe y agrega una devolución de llamada para responder al cliente a una cola.

  • Cuando el servidor recibe un nuevo mensaje, se activa el evento onmessage, recorre las devoluciones de llamada y envía los mensajes.

  • El JS del lado del cliente recibe el mensaje, lo agrega a la página y luego solicita actualizaciones desde este nuevo ID de mensaje.

mikemaccana
fuente
25

Creo que el cliente parece una solicitud AJAX asíncrona normal, pero se espera que tarde "mucho tiempo" en volver.

El servidor se ve así.

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

outputNewData();

Por lo tanto, la solicitud de AJAX va al servidor, probablemente incluyendo una marca de tiempo de cuándo fue la última actualización para que hasNewData()sepa qué datos ya tiene. El servidor se queda en un bucle de espera hasta que haya nuevos datos disponibles. Todo el tiempo, su solicitud AJAX todavía está conectada, simplemente colgando allí esperando datos. Finalmente, cuando hay nuevos datos disponibles, el servidor los entrega a su solicitud AJAX y cierra la conexión.

Greg
fuente
10
Esta es una espera ocupada que bloquea su hilo actual. Eso no escala en absoluto.
Wouter Lievens
10
No, usleep no es una espera ocupada. Y el objetivo de "esperar" es bloquear el hilo por un tiempo. ¡Probablemente quiso decir 50 milisegundos (usleep (50000)), no 50 microsegundos! Pero de todos modos, con una configuración típica de Apache / PHP, ¿hay alguna otra forma de hacer esto?
Matt
Bueno, desde el principio, no puede hacer una función de bloqueo para mensajes de chat sin esperar.
Tomáš Zato - Restablece a Mónica el
Genial de verdad! Construí una función recursiva en el servidor para verificar si hay nuevos datos. Pero, ¿cuál es el mejor producto para utilizar las encuestas largas de manera eficiente? Uso el Apache normal y el servidor no responde cuando abro más de 4/5 pestañas del navegador :( Buscando algo para usar con el PHP
modernos
17

Aquí hay algunas clases que uso para encuestas largas en C #. Básicamente hay 6 clases (ver más abajo).

  1. Controlador : procesa las acciones necesarias para crear una respuesta válida (operaciones db, etc.)
  2. Procesador : gestiona la comunicación asíncrona con la página web (en sí)
  3. IAsynchProcessor : el servicio procesa instancias que implementan esta interfaz
  4. Servicio : procesa objetos de solicitud que implementan IAsynchProcessor
  5. Solicitud : el contenedor IAsynchProcessor que contiene su respuesta (objeto)
  6. Respuesta : contiene objetos o campos personalizados
Prisionero CERO
fuente
2
De acuerdo ... entonces, ¿POR QUÉ se rechazó esto? Estas clases son, de hecho, ejemplos válidos de sondeo largo.
Prisionero CERO
El sondeo largo real no es (simplemente) la práctica de aumentar el intervalo dentro de una encuesta normal (en un recurso). Es parte de un patrón más amplio ... que está "algo" sujeto a interpretación ... pero solo en ciertas áreas de la implementación general. Dicho eso ... ¡estas clases siguen dicho patrón! Entonces, si tiene una razón para rechazar esto ... Realmente me interesaría la razón.
Prisionero CERO
Quizás fue rechazado ya que no aborda directamente la cuestión de un ejemplo de código simple. Por supuesto que no lo rechacé, así que solo puedo adivinar.
Andrew
16

Este es un buen screencast de 5 minutos sobre cómo hacer encuestas largas usando PHP y jQuery: http://screenr.com/SNH

El código es bastante similar al ejemplo anterior de dbr .

Sean O
fuente
3
Creo que solo debería ver esto como una introducción al sondeo largo porque esta implementación seguramente matará a su servidor con muchos usuarios concurrentes.
Alfred
solo estoy aprendiendo sobre todo esto ... cuán confiable, o no, es con algunos usuarios ... digamos 10 chateando de un lado a otro.
Somdow
12

Aquí hay un ejemplo simple de sondeo largo en PHP de Erik Dubbelboer usando el Content-type: multipart/x-mixed-replaceencabezado:

<?

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
';

Y aquí hay una demostración:

http://dubbelboer.com/multipart.php

Jasdeep Khalsa
fuente
11

Utilicé esto para familiarizarme con Comet, también configuré Comet usando el servidor Java Glassfish y encontré muchos otros ejemplos suscribiéndome a cometdaily.com

Adán
fuente
9

A continuación se muestra una solución de sondeo larga que he desarrollado para Inform8 Web. Básicamente, anula la clase e implementa el método loadData. Cuando loadData devuelve un valor o la operación agota el tiempo de espera, imprimirá el resultado y regresará.

Si el procesamiento de su script puede demorar más de 30 segundos, es posible que deba modificar la llamada set_time_limit () a algo más largo.

Licencia de Apache 2.0. Última versión en 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
fuente
8

Gracias por el código, dbr . Solo un pequeño error tipográfico en long_poller.htm alrededor de la línea

1000 /* ..after 1 seconds */

Creo que debería ser

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

para que funcione

Para aquellos interesados, probé un equivalente de Django. Comience un nuevo proyecto de Django, diga lp para encuestas largas:

django-admin.py startproject lp

Llame a la aplicación msgsrv para el servidor de mensajes:

python manage.py startapp msgsrv

Agregue las siguientes líneas a settings.py para tener un directorio de plantillas :

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

Defina sus patrones de URL en urls.py como tal:

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'}),
)

Y msgsrv / views.py debería verse así:

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 último, templates / long_poller.htm debería ser el mismo que el anterior con error tipográfico corregido. Espero que esto ayude.

xoblau
fuente
En realidad, "15000"es el error de sintaxis. setTimeout toma un número entero como su segundo parámetro.
Andrew Hedges
Esta respuesta necesita trabajo. Es la culminación de uno o más comentarios y una respuesta o respuestas separadas.
Brian Webster el
8

Este es uno de los escenarios para los que PHP es una muy mala elección. Como se mencionó anteriormente, puede vincular a todos sus trabajadores de Apache muy rápidamente haciendo algo como esto. PHP está construido para iniciar, ejecutar, detener. No está diseñado para comenzar, esperar ... ejecutar, detener. Atascará su servidor muy rápidamente y descubrirá que tiene problemas de escala increíbles.

Dicho esto, aún puede hacer esto con PHP y que no mate su servidor utilizando el nginx HttpPushStreamModule: http://wiki.nginx.org/HttpPushStreamModule

Configura nginx frente a Apache (o cualquier otra cosa) y se encargará de mantener abiertas las conexiones concurrentes. Simplemente responde con la carga útil enviando datos a una dirección interna que podría hacer con un trabajo en segundo plano o simplemente enviando los mensajes a las personas que estaban esperando cada vez que llegan las nuevas solicitudes. Esto evita que los procesos de PHP permanezcan abiertos durante largas encuestas.

Esto no es exclusivo de PHP y se puede hacer usando nginx con cualquier lenguaje de fondo. La carga de conexiones abiertas simultáneas es igual a Node.js, por lo que la mayor ventaja es que te saca de NADA NECESARIO para algo como esto.

Ves a muchas otras personas que mencionan otras bibliotecas de idiomas para lograr encuestas largas y eso es por una buena razón. PHP simplemente no está bien construido para este tipo de comportamiento, naturalmente.

bola brillante
fuente
¿Es este un problema de Apache o un problema de PHP? ¿Tendría problemas con las encuestas largas si mi código PHP se ejecutara directamente en nginx o lighttpd?
David
Es menos un problema de PHP y más un mal uso de PHP. En cada solicitud, PHP ejecuta el script desde cero, carga las bibliotecas según sea necesario, ejecuta su código y luego se apaga mientras la recolección de basura todo comenzó en la solicitud. Se han realizado muchas modificaciones en PHP a lo largo de los años para minimizar el impacto, como enlaces estáticos tardíos, carga diferida, cachés de bytecode de memoria para eliminar E / S de disco, etc. El problema sigue siendo que PHP está destinado a iniciarse y detenerse tan rápido como sea posible. Los idiomas que se cargarán una vez / arrancarán y abrirán un hilo para la solicitud son mucho más adecuados para sondeos largos.
brightball
Pero para responder la pregunta, sí, experimentaría el problema independientemente de si estaba usando Apache o algo más. Así es como funciona PHP. Debo enmendar esto para decir que, si vas a tener una carga de tráfico máxima conocida, PHP estará bien. He visto sistemas integrados que usan PHP que no tienen problemas porque solo hay un par de conexiones. Potencialmente en la intranet de una empresa, esto también podría ser aceptable. Sin embargo, para las aplicaciones públicas, matará absolutamente sus servidores a medida que crezca el tráfico.
brightball
4

¿Por qué no considerar las tomas web en lugar de las encuestas largas? Son mucho más eficientes y fáciles de configurar. Sin embargo, solo son compatibles con los navegadores modernos. Aquí hay una referencia rápida .

shasi kanth
fuente
Creo que una vez que los websockets se implementen en todas partes (probablemente no en los próximos años) serán el estándar para este tipo de aplicación. Desafortunadamente por ahora, no podemos confiar en ellos para las aplicaciones de producción.
Richard
3
@Richard Sin embargo, puede usar algo como Socket.IO que proporciona transportes de respaldo automáticos, proporcionando una funcionalidad similar a un socket web hasta IE 6.
Brad
2

Puede probar icomet ( https://github.com/ideawu/icomet ), un servidor cometa C1000K C ++ construido con libevent. icomet también proporciona una biblioteca de JavaScript, es fácil de usar tan simple como

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);
    }
});

icomet admite una amplia gama de navegadores y sistemas operativos, incluidos Safari (iOS, Mac), IE (Windows), Firefox, Chrome, etc.

ideawu
fuente
0

Nodo más simple JS

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);
}

Escenario de producción inteligente en Express por ejemplo que obtendría responseen el middleware. ¿Hace lo que necesita hacer, puede abarcar todos los métodos sondeados para asignar o algo (que es visible para otros flujos) e invocar<Response> response.end() cuando esté listo. No hay nada especial en las conexiones sondeadas largas. El descanso es la forma en que normalmente estructura su aplicación.

Si no sabes a qué me refiero con el alcance, esto debería darte una idea

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 puede ver, realmente podría responder a todas las conexiones, una, hacer lo que quiera. Hay idpara cada solicitud, por lo que debería poder usar el mapa y acceder a llamadas específicas fuera de la API.

sp3c1
fuente