PHP长轮询返回2个结果而不是一个

13
我正在尝试创建一个类似Facebook的发布系统。因此,我对Facebook如何实现它进行了一些研究,发现Facebook使用长轮询技术。于是我搜索了如何实现它,并进行了实现。最终,我打开了Firefox和Chrome进行测试。在发布2或3篇文章后,它可以正常工作,但随后会出现结果重复的问题,如下所示: Duplicate results 顺便说一下,这是第一篇文章。
在该过程中,我的网络选项卡如下所示: It makes 3 requests instead of two 它发送了3个请求而不是一个。
最后,以下是我的代码: init.js 包含了所有的JavaScript代码。
function getNewPosts(timestamp) {
  var t;
  $.ajax({
    url: 'stream.php',
    data: 'timestamp=' + timestamp,
    dataType: 'JSON',
})
  .done(function(data) {
    clearInterval( t );
    // If there was results or no results
    // In both cases we start another AJAX request for long polling after 1 second
    if (data.message_content == 'results' || data.message_content == 'no-results') {
        t = setTimeout( function() {
            getNewPosts(data.timestamp);
        }, 1000);
        // If there was results we will append it to the post div
        if (data.message_content ==  'results') {
            // Loop through each post and output it to the screen
            $.each(data.posts, function(index, val) {
                $("<div class='post'>" + val.post_content + "<div class='post_datePosted'>"+ val.posted_date +"</div> <br>" + "</div>").prependTo('.posts');
            });
        }
    }
})
}

$(document).ready(function(){

    // Start the autosize function
    $('textarea').autosize();

    // Create an AJAX request to the server for the first time to get the posts
    $.ajax({
        async: false,
        url: 'stream.php?full_page_reload=1',
        type: 'GET',
        dataType: 'JSON',
    })
    .done(function(data) {
        // Assign the this variable to the server timestamp
        // that was given by the PHP script
        serverTimestamp = data.timestamp;
        $.each(data.posts, function(index, val) {
            $("<div class='post'>" + val.post_content + "<div class='post_datePosted'>"+ val.posted_date +"</div>" + "</div>").prependTo('.posts');
        });
    })
    .fail(function() {
        alert('There was an error!');
    })
    // When the form is submitted
    $('#post_form').on('submit', function(event) {
        $.ajax({
            url: 'ajax/post.php',
            type: 'POST',
            dataType: 'JSON',
            data: $('#post_form').serialize()
        })
        .done(function(data) {
            // Reset the form values
            $('#post_form')[0].reset();
        })
        .fail(function() {
            // When there was an error
            alert('An error occured');
        })
        // Prevent the default action
        event.preventDefault();
    });
    // Start the actual long polling when DOM is ready
    getNewPosts(serverTimestamp);
});

我的 stream.php 文件

<?php
header('Content-type: application/json');
// If it was a full page reload
$lastId = isset($_GET['lastId']) && !empty($_GET['lastId']) ? $_GET['lastId'] : 0;
if (isset($_GET['full_page_reload']) && $_GET['full_page_reload'] == 1) {
    $first_ajax_call = (int)$_GET['full_page_reload'];

    // Create a database connection
    $pdo = new PDO('mysql:host=localhost;dbname=test', 'akar', 'raparen');
    $sql = "SELECT * FROM `posts`";
    $stmt = $pdo->prepare($sql);
    $stmt->execute();
    $posts = $stmt->fetchAll(PDO::FETCH_ASSOC);
    // Output the timestamp since its full page reload
    echo json_encode(array(
        'fullPageReload' => 'true',
        'timestamp' => time(),
        'posts' => $posts
        ));
} else if (isset($_GET['timestamp'])) {
    // The wasted time
    $time_wasted = 0;
    // Database connection
    $pdo = new PDO('mysql:host=localhost;dbname=test', 'akar', 'raparen');
    $timestamp = $_GET['timestamp'];
    // Format the timestamp to SQL format
    $curr_time = date('Y-m-d H:i:s', $timestamp);
    $sql = "SELECT * FROM `posts` WHERE posted_date >= :curr_time";
    $stmt = $pdo->prepare($sql);
    $stmt->bindValue(':curr_time', $curr_time);
    $stmt->execute();
    // Fetch the results as an Associative array
    $posts = $stmt->fetchAll(PDO::FETCH_ASSOC);
    // If there wasn't any results
    if ( $stmt->rowCount() <= 0 ) {
        // Create the main loop
        while ($stmt->rowCount() <= 0) {
            // If there is still no results or new posts
            if ($stmt->rowCount() <= 0) {
                // If we waited 60 seconds and still no results
                if ($time_wasted >= 60) {
                    die(json_encode(array(
                        'message_type' => 'error',
                        'message_content' => 'no-results',
                        'timestamp' => time()
                        )));
                }
                // Helps the server a little bit
                sleep(1);
                $sql = "SELECT * FROM `posts` WHERE posted_date >= :curr_time";
                $stmt = $pdo->prepare($sql);
                $stmt->bindParam(':curr_time', $curr_time);
                $stmt->execute();
                $posts = $stmt->fetchAll(PDO::FETCH_ASSOC);
                // Increment the time_wasted variable by one
                $time_wasted += 1;
            }
        }
    }
    // If there was results then we output it.
    if ($stmt->rowCount() > 0) {
        die( json_encode( array(
            'message_content' => 'results',
            'timestamp' => time(),
            'posts' => $posts,
            )));
        exit();
    }
}

这里是我的 ajax/post.php


<?php
if ( isset($_POST['post_content']) ) {
    $post_content = strip_tags(trim($_POST['post_content']));
    if ( empty($post_content) ) {

        /* If the user doesn't enter anything */
        echo json_encode(array(
            'message_type' => 'error',
            'message_content' => 'It seems like your post is empty'
            ));
    } else {
        $pdo = new PDO('mysql:host=localhost;dbname=test', 'akar', 'raparen');
        $sql = "INSERT INTO `posts` (`post_id`, `post_content`, `posted_date`) VALUES (NULL, :post_content, NOW());";
        $stmt = $pdo->prepare($sql);
        $stmt->bindValue(':post_content', $post_content);
        $stmt->execute();
        echo json_encode(array(
            'message_type' => 'message',
            'message_content' => 'Your post has been posted successfully.'
            ));
    }
}

如果您不理解,请随时询问我。我知道这是脏代码,我重复了很多次。我这样做是为了测试,所以并不重要。
谢谢!

这是关于stream.php或者init.js的问题,我无论如何都会把它发布出来。 - Akar
你在定义"serverTimestamp"之前使用了它,由于JavaScript是异步的,所以你应该将getNewPosts放在“第一次获取帖子请求”的.done()函数内。 - Adrian Forsius
是的,它可以使您的代码更少出错,我在这里不是给您答案,只是进行评论。 - Adrian Forsius
你考虑将查询条件 posted_date >= :curr_time 替换为 posted_date >= NOW() 吗?不过我认为这并不是一个好的实现方式。如果 Web 服务器和数据库服务器的时间不同怎么办? - lujjjh
我曾经遇到过类似的问题。在添加新事件监听器之前,尝试取消现有的事件监听器,就像这样:$('#post_form').off('submit').on(..) - Yang
显示剩余9条评论
12个回答

1

您可以这样设置超时时间:

setTimeout()

但是你正在使用。
clearInterval()

要清除它,请使用clearTimeout()


1
我知道这并不完全回答了你的问题,但你正在做的事情根本行不通,使用PHP进行长轮询时,当有更多用户时,服务器会崩溃。你使用了sleep,因此PHP进程处于"挂起"状态。PHP工作进程数(无论是Apache、nginx还是任何带有PHP的服务器)都是有限的。一旦达到该数量,新连接将被拒绝。PHP旨在快速响应。
针对这种类型的解决方案,我建议使用一些专为此设计的中间软件。例如,看看Socket.IO
它是用JavaScript编写的,用于客户端(JS库)和服务器端(Node.js)。您的Node.js服务器可以使用REST API、队列(如ZeroMQ、RabbitMQ等)或任何其他传输方式收集PHP发生的事件,例如socket.IO client itself。这样,您就不需要在PHP中轮询数据库,只需将新帖子添加到Node.js服务器,并将此信息传递给客户端JS代码即可。
$pdo->prepare('INSERT INTO ..')->execute();
$dispatcher->dispatch('new_post', new PostEvent(array('id' => 123, 'text' => 'Post text')));

长轮询是Socket.IO支持的协议之一,但绝不是效率最高的协议。

如果你想避免使用Node.js,可以尝试在客户端使用WebSockets,使用ReactPHP和Ratchet。这将作为单个php进程运行(从命令行运行),因此不采用apache方式。


这也许不是直接的答案,但从长远来看肯定是正确的方法。注意:是的,我是一名PHP开发人员。是的,我两者都用过。 - risyasin

1
坦白说,我不明白你为什么要费心进行这种优化,除非你打算处理成千上万条消息。每秒钟从每个客户端向服务器发送请求会产生大量的流量,所以优化应该从定义一个更合理的轮询周期或者更智能、自适应的刷新机制开始,我个人认为。
现在,如果你真的想这么做,你就必须进行适当的同步。如果你搞乱了时间戳,可能会在另一个客户端触发自动刷新时跳过其他人添加的消息,或者重复获取相同的消息。
你所有的超时处理都是多余的。通过AJAX进行的服务器请求如果出现问题,将会产生一个错误事件,这意味着连接或者你的服务器出现故障,或者你的PHP代码出现问题需要修复。
关于应用程序结构的一个想法:
  • 一个更新请求将传递一个时间戳给PHP,要求检索所有新于该时间戳的帖子。初始值将为1/1/1970或其他值,以便初始获取所有现有帖子。响应中将包含一个新的时间戳,以便增量请求可以跳过已经获取的数据。
  • JavaScript将定期生成这样的请求(我更倾向于将周期设置为30秒左右,以避免过多的服务器负载 - 假设您的普通用户可以忍受等待下一批伪推文那么长时间的沮丧)
  • 提交新帖子只需将其添加到数据库中,但由于所有操作都在服务器端完成,您不需要担心竞态条件。

你所有关于"time_wasted"和"cur_time"的代码都应该被丢弃。 唯一需要的时间参考是来自特定客户端的最后读取请求的日期。
在服务器端(在你的"stream" PHP文件中),你所需要的只是一个数据库请求,用于获取新于客户端提供的时间戳的帖子,它将返回一个(可能为空的)帖子列表和相同时间戳的更新值。

坦率地说,与其使用这些潜在令人困惑的时间戳,您也可以使用最后获取的帖子的唯一标识符(使用0或任何常规值作为初始请求)。


今天我做到了。我有一个代码,每15秒检查一次数据库,而不是长轮询。谢谢! - Akar

1
你可以使用这个:
$pdo->prepare('INSERT INTO ..')->execute();
$dispatcher->dispatch('new_post', new PostEvent(array('id' => 123, 'text' => 'Post text')));

0

正如您在评论中所指出的,存在大量冗余代码,这使得诊断问题变得困难。最好整理一下,这样其他人阅读代码时就能更好地诊断问题。

经过代码审查,我看到正在发生的情况如下:

  1. Dom Ready函数启动ajax请求进行完全加载
  2. Dom Ready函数使用默认服务器时间开始getNewPosts()
  3. 完全ajax加载返回
  4. getNewPosts()返回

您可以通过向各种功能添加console.log()命令来验证此顺序。确切的顺序可能会根据服务器响应的速度而有所不同。然而,根本问题是在第二步开始时未设置serverTimestamp值。

解决方案很容易,只需正确设置serverTimestamp变量即可。为此,将getNewPosts()函数调用移动到完全加载ajax请求的.done()处理程序中。此时,服务器已返回可用于进一步轮询的初始时间戳值。

// Create an AJAX request to the server for the first time to get the posts
$.ajax({
    async: false,
    url: 'stream.php?full_page_reload=1',
    type: 'GET',
    dataType: 'JSON',
})
.done(function(data) {
    // Assign the this variable to the server timestamp
    // that was given by the PHP script
    serverTimestamp = data.timestamp;
    $.each(data.posts, function(index, val) {
        $("<div class='post'>" + val.post_content + "<div class='post_datePosted'>"+ val.posted_date +"</div>" + "</div>").prependTo('.posts');
    });

    // Start the long poll loop
    getNewPosts(serverTimestamp);
})

嗯...我也这样做了,但它仍然重复了。 - Akar
@Akar 请将serverTimestamp和interval变量“t”放置在全局范围内(即,在所有函数之外)。如果问题仍然存在,我建议在您的JavaScript中添加一些console.log()调用来跟踪调用的位置/方式。 - Kami
@Akar 感谢您的反馈,但是哪个方面没有起作用呢?页面没有加载吗?JavaScript 报错了吗?Console.log 没有输出到控制台吗?您需要提供更多的信息。 - Kami
我的意思是,这和我之前尝试的一样。在你回答之前我也试过了,我想我必须从头开始编码。 - Akar

0

目前我无法真正测试这个,但我看到的最大问题是:

  1. 您定义间隔变量 t 的范围
  2. 您传递时间戳的方式
  3. 您设置和清除间隔的时间点
  4. 您不一致地使用 setTimeoutclearInterval

我将编写缩写代码,主要是为了保持概念上的简洁。我能给出的最大建议是不要使用间隔,因为 AJAX 调用可能比您的间隔时间更长。每次 ajax 完成时,您只需设置一个新的超时。

// create a closure around everything
(function() {
    var timer,
        lastTimeStamp = <?php echo some_timestamp; ?>;

    function getNewPosts() {
        if(timer) clearTimeout(timer);

        $.ajax({
            data: lastTimeStamp
            // other values here
        })
        .done(function() {
            if( there_is_data ) {
                lastTimeStamp = data.timestamp;
                // render your data here
            }
        })
        .always(function() {
            timer = setTimeout(getNewPosts, 1000);
        });
    }

    $(document).ready(function() {
        // don't do your first ajax call, just let getNewPosts do it below

        // define your form submit code here

        getNewPosts();
    });
}());

0

我相信要解决这个问题,你需要 unbind 之前的表单提交。我曾经遇到过我创建的脚本类似的问题。

// When the form is submitted
$('#post_form').unbind('submit').on('submit', function(event) {
    $.ajax({
        url: 'ajax/post.php',
        type: 'POST',
        dataType: 'JSON',
        data: $('#post_form').serialize()
    })
    .done(function(data) {
        // Reset the form values
        $('#post_form')[0].reset();
    })
    .fail(function() {
        // When there was an error
        alert('An error occured');
    })
    // Prevent the default action
    event.preventDefault();
});

0
// If there was results we will append it to the post div
    if (data.message_content ==  'results') {
        // Loop through each post and output it to the screen
        $.each(data.posts, function(index, val) {
            $("<div class='post'>" + val.post_content + "<div class='post_datePosted'>"+ val.posted_date +"</div> <br>" + "</div>").prependTo('.posts');
        });
    }

我认为在将数据添加到('.post')之前,您必须清除先前的数据。

例如: 第一次:ajax结果是post1

第二次:ajax结果是post1 + post2

--> .prependTo('.posts')的结果是post1 + post1 + post2


0

我认为这里有不必要的代码。你所需要的只有两个部分。 1- 你的表单。 2- 你的消息查看器。

好的,第一次加载表单时,去数据库检索信息(可以是JSON格式),并填充你的查看器。 为了做到这一点,在ajax完成后,将php JSON转换为数组,创建一个循环。 对于每个元素,使用append jquery添加每个帖子。http://api.jquery.com/append/

对于点击事件(提交),也要做同样的事情。 在填充新帖子之前,必须清除HTML内容。


0

在您的网络选项卡中,如果您重现了该问题,请检查显示条目两次的请求,并检查持续时间,您会发现响应时间超过了1秒,这是您JavaScript中的“1000”超时,因此我不认为有必要使用该超时。

所有正常工作并仅显示一次条目的请求,它们应该在“1000”(1秒)之前收到服务器响应,您可以通过悬停在时间轴属性上来检查:

enter image description here

或者通过点击特定请求并切换到“时间”选项卡:

enter image description here

根据您下面的代码,以下是导致条目显示两次的情况:

  1. ajax请求
  2. 服务器响应超过1秒钟
  3. JavaScript计时器重新启动相同的请求。
  4. 服务器响应请求(1),JavaScript停止计时器。
  5. 服务器响应请求(2)

那我应该改变超时时间吗? - Akar
@Akar 为什么你要使用超时,有什么原因吗?如果你想检查错误,你应该检查服务器响应的状态(可以使用.error(data){//*在此处处理错误逻辑*/}),如果出现错误(连接问题、服务器问题等),那么你应该重新发起相同的请求,但仍然需要设置一个限制,以防止它在5次尝试后停止重复请求?10次尝试?这都取决于你的逻辑。 - Mehdi Karamosly
@Akar 这个问题有进展了吗?你试过我建议的吗? - Mehdi Karamosly
其实我从头开始重新编写了代码,并且摆脱了那些糟糕的代码,现在它可以正常运行了。 - Akar
@Akar 那么以下回答都没有解决你的问题?或者帮助你解决了吗? - Mehdi Karamosly

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接