Node.js/Express - 立即将标准输出流实时传输给客户端

22
我生成了以下子进程:var spw = spawn('ping', ['-n','10', '127.0.0.1']),我想在客户端(浏览器)逐个接收ping结果,而不是整体接收。

到目前为止,我尝试了这个:

app.get('/path', function(req, res) {
   ...
   spw.stdout.on('data', function (data) {
      var str = data.toString();
      res.write(str + "\n");
   });
   ...
}

而且:

...
spw.stdout.pipe(res);
...

在这两种情况下,浏览器都会等待10个ping完成,然后将结果一次性打印出来。我希望逐个显示它们,该怎么做?

(客户端只是调用.../path并console.log结果)


编辑:虽然我确实认为实现这一点需要使用WebSockets,但我想知道是否有其他方法。我看到了几个令人困惑的SO答案和博客文章(在文章中,在第一步中,OP将日志流式传输到浏览器),但这些都没有帮助,因此我决定发起悬赏以引起一些关注。

3个回答

41

这里有一个完整的例子,使用SSE(服务器发送事件)。 这在Firefox中有效,Chrome也可能有效:

var cp = require("child_process"),
         express = require("express"),
         app = express();

app.configure(function(){
    app.use(express.static(__dirname));
});


app.get('/msg', function(req, res){
    res.writeHead(200, { "Content-Type": "text/event-stream",
                         "Cache-control": "no-cache" });

    var spw = cp.spawn('ping', ['-c', '100', '127.0.0.1']),
    str = "";

    spw.stdout.on('data', function (data) {
        str += data.toString();

        // just so we can see the server is doing something
        console.log("data");

        // Flush out line by line.
        var lines = str.split("\n");
        for(var i in lines) {
            if(i == lines.length - 1) {
                str = lines[i];
            } else{
                // Note: The double-newline is *required*
                res.write('data: ' + lines[i] + "\n\n");
            }
        }
    });

    spw.on('close', function (code) {
        res.end(str);
    });

    spw.stderr.on('data', function (data) {
        res.end('stderr: ' + data);
    });
});

app.listen(4000);

还有客户端的HTML:

<!DOCTYPE Html>
<html> 
<body>
   <ul id="eventlist"> </ul>

   <script>              
    var eventList = document.getElementById("eventlist");
    var evtSource = new EventSource("http://localhost:4000/msg");

    var newElement = document.createElement("li");
    newElement.innerHTML = "Messages:";
    eventList.appendChild(newElement);


    evtSource.onmessage = function(e) {
        console.log("received event");
        console.log(e);
        var newElement = document.createElement("li");

        newElement.innerHTML = "message: " + e.data;
        eventList.appendChild(newElement);
    };      

    evtSource.onerror = function(e) {
        console.log("EventSource failed.");
    };

    console.log(evtSource);

    </script>

</body>
</html>
运行 node index.js 并在浏览器中打开 http://localhost:4000/client.html。 请注意,由于我正在运行 OS X,所以我必须使用“-c”选项而不是“-n”。

我需要的正是这个,请允许后我会给你悬赏 :) http://imgur.com/oLLbGXU - anvarik
当用户从页面导航时,取消事件流的好解决方案是什么? - Ahmed Ahmed
1
@AhmedAhmed 我会假设如果用户关闭了网页,那么res.write()会抛出异常或触发'close'事件。你为什么不自己试一下,然后在这里报告你的结果呢? - nimrodm
2
我确实尝试过 :) 从客户端,你需要调用evtSource.close(); 从服务器端,你需要监听close事件 req.connection.addListener("close", function () { console.log('client closed'); spw.stdin.pause(); spw.kill();}); - Ahmed Ahmed
1
七年后它仍然有效,但应该删除 app.configured(只需使用 app.use(express.static(__dirname)); 即可)。 - Shrike
显示剩余2条评论

3
如果您正在使用Google Chrome,将内容类型更改为"text/event-stream"就可以实现您想要的效果。
res.writeHead(200, { "Content-Type": "text/event-stream" });

查看我的代码片段以获取完整的示例:https://gist.github.com/sfarthin/9139500


你能否尝试使用 var spw = cp.spawn('ping', ['127.0.0.1', '-n', '10']) 运行你的要点,并确认你是否可以在浏览器上看到实时控制台?在@jibsales的回答之后,我开始认为只有使用 sockets 才能实现实时浏览器,但是你的回答让我困惑了... - anvarik
刚试了一下,不起作用。如果你发送10个ping,你的解决方案会等待所有完成,然后Chrome会显示全部... - anvarik
@anvarik:请查看我的答案中的代码。它基于Steve的建议并且确实有效。 - nimrodm

1

使用标准的HTTP请求/响应循环无法实现此目标。基本上,您要做的是创建一个“推送”或“实时”服务器。这只能通过xhr-polling或websocket实现。

代码示例1:

app.get('/path', function(req, res) {
   ...
   spw.stdout.on('data', function (data) {
      var str = data.toString();
      res.write(str + "\n");
   });
   ...
}

这段代码从未发送结束信号,因此永远不会响应。如果在事件处理程序中添加对 res.end() 的调用,则只会收到第一个 ping - 这是预期的行为,因为您在从 stdout 接收到第一块数据后终止了响应流。

代码示例2:

spw.stdout.pipe(res);

在这里,stdout正在将数据包刷新到浏览器,但是浏览器只有在接收到所有数据包后才会呈现数据块。因此它等待10秒钟,然后呈现整个stdout。这种方法的主要优点是在发送之前不在内存中缓冲响应-保持内存占用量轻量级。

感谢您的答案..实际上,在 spw.on('close'... 中我有一个 res.end, 只是没有写下来。我跟您想法一样,但这个答案让我困惑了: https://dev59.com/V2Ij5IYBdhLWcg3wWT6c - anvarik
该答案是关于将数据分块流式传输到服务器以避免首先将其全部缓冲到服务器中。它是否以分块方式渲染完全取决于浏览器逻辑。 - loganfsmyth
没错 - 服务器将所有内容都发送到管道上,但浏览器只有在接收到所有数据包后才会关闭连接。我修改了我的解释以反映这一点 - 不知道之前我在想什么! - srquinn

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