一旦服务器有数据,如何在网页上显示结果

8 投票
4 回答
9769 浏览
提问于 2025-04-15 16:50

我正在用Python写一个cgi页面。假设有一个客户端向我的cgi页面发送请求。我的cgi页面会进行计算,一旦有了第一个结果,就会把这个结果发送回客户端,但它会继续进行计算,并在发送第一个结果之后再发送其他的结果。

我想知道我说的这种情况是否可能?我问这个问题是因为根据我有限的知识,在cgi页面中,响应是一次性发送的,一旦发送了响应,cgi页面就停止运行。这种情况是发生在服务器端还是客户端?我该如何实现呢?

我的服务器是用Apache运行的。非常感谢。

我尝试过论坛里“dbr”的客户端代码(多亏了他,我才明白了长轮询是怎么回事)。

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

这是我的服务器代码:

import sys
if __name__ == "__main__":
    sys.stdout.write("Content-Type: text/html\r\n\r\n")
    print "<html><body>"
    for i in range(10):
        print "<div>%s</div>" % i
        sys.stdout.flush()
    print "</body></html>"

我希望我的客户端页面一次显示一个数字(0,1,2,...),但数据总是一次性全部显示出来(01234...)。请帮我解决这个问题。非常感谢大家。

稍微偏离一下,我正在尝试使用jquery comet插件,但找不到足够的文档。如果能提供帮助,我会非常感激。再次感谢 :D

[编辑] 好吧,大家,最终多亏了你们的指导,我成功让它工作了。你们说得对,mod_deflate是问题的根源。

总结一下,我在这里做了什么:

  • 对于客户端,制作一个长轮询页面,就像上面的html代码。

  • 对于服务器,禁用mod_deflate:编辑文件/etc/apache2/mods-available/deflate.conf,注释掉包含text/html的那一行,然后重启服务器。为了确保Python不会自己缓存输出,在页面开头加上#!/usr/bin/python -u。记得在每次打印想要在客户端显示的内容后使用sys.stdout.flush()。效果可能不是很明显,可以加上time.sleep(1)来测试。:D

非常感谢大家的支持和帮助解决这个问题 :D

4 个回答

1

有几种方法可以做到这一点。

最传统的方式是继续传输数据,让浏览器逐步渲染这些数据。就像老式的CGI那样,你可以使用 sys.stdout.flush()。这样做会显示一个部分加载的页面,你可以不断添加内容,但在浏览器中看起来会有点笨拙,因为加载指示器会一直转动,看起来就像服务器卡住了或者超负荷了。

有些浏览器支持一种特殊的多部分MIME类型 multipart/x-mixed-replace,这允许你保持连接打开,但当你发送下一个多部分数据块时,浏览器会完全替换页面(这个数据块必须是MIME格式的)。我不太确定这个方法是否好用——因为Internet Explorer不支持它,其他浏览器可能也不太兼容。

更现代的方法是使用JavaScript的 XMLHttpRequest 来轮询服务器获取结果。这要求你能够从不同的服务器线程或进程中检查操作的结果,这在服务器端代码中可能会比较复杂。不过,这种方法可以让你创建一个更漂亮的网页。

如果你想要更复杂的方案,可以看看“Comet”模型或者“Web Sockets”。

1

是的,这是可能的,你不需要做太多事情。只要你把数据打印出来,服务器就会发送这些数据。为了确保这一点,记得要定期清空输出缓冲区。

10

当然可以。

这里有一种传统的服务器驱动的方法,脚本只运行一次,但完成的时间很长,期间会逐步输出页面的部分内容:

import sys, time

sys.stdout.write('Content-Type: text/html;charset=utf-8\r\n\r\n')

print '<html><body>'
for i in range(10):
    print '<div>%i</div>'%i
    sys.stdout.flush()
    time.sleep(1)

在写一个WSGI应用时,可以让应用返回一个可迭代的对象,这样它就能逐个输出想要发送的内容块。我真的很推荐使用WSGI;现在你可以通过CGI来部署,但将来当你的应用需要更好的性能时,你可以通过更快的服务器/接口来部署,而不需要重写代码。

WSGI与CGI的例子:

import time, wsgiref.handlers

class MyApplication(object):
    def __call__(self, environ, start_response):
        start_response('200 OK', [('Content-Type', 'text/html;charset=utf-8')])
        return self.page()

    def page(self):
        yield '<html><body>'
        for i in range(10):
            yield '<div>%i</div>'%i
            time.sleep(1)

application= MyApplication()
if __name__=='__main__':
    wsgiref.handlers.CGIHandler().run(application)

注意,你的网络服务器可能会干扰这种方法(无论是CGI还是WSGI),因为它可能会自己添加缓冲。这通常发生在你使用像mod_deflate这样的输出转换过滤器来自动压缩网页应用的输出时。你需要关闭压缩,以便让部分响应生成的脚本正常工作。

这限制了你只能在新数据到来时逐步渲染页面。你可以通过让客户端处理页面的更新来让它看起来更美观,例如:

def page(self):
    yield (
        '<html><body><div id="counter">-</div>'
        '<script type="text/javascript">'
        '    function update(n) {'
        '        document.getElementById("counter").firstChild.data= n;'
        '    }'
        '</script>'
    )
    for i in range(10):
        yield '<script type="text/javascript">update(%i);</script>'%i
        time.sleep(1)

这依赖于客户端脚本,所以在最后加上一个不依赖脚本的备份输出可能是个好主意。

在这个过程中,页面会看起来一直在加载。如果你不想这样,那就需要把脚本分成两个请求,第一个请求只输出静态内容,包括一个客户端脚本,它会通过一个XMLHttpRequest来向服务器请求新数据,或者在一些特别长时间运行的情况下,使用多个XMLHttpRequests,每个请求返回状态和任何新数据。这种方法要复杂得多,因为这意味着你需要将工作过程作为一个后台守护进程运行,而不是直接在网络服务器上,并通过例如管道或数据库在守护进程和前端CGI/WSGI请求之间传递数据。

撰写回答