NodeJS - 如何在不缓冲的情况下流式传输请求体

10
在下面的代码中,我无法理解为什么 req.pipe(res) 不起作用,但也不会抛出错误。我的直觉告诉我这是由于 Node.js 的异步行为导致的,但这是一个非常简单的情况,没有回调函数。
我错过了什么?
http.createServer(function (req, res) {

  res.writeHead(200, { 'Content-Type': 'text/plain' });

  res.write('Echo service: \nUrl:  ' + req.url);
  res.write('\nHeaders:\n' + JSON.stringify(req.headers, true, 2));

  res.write('\nBody:\n'); 

  req.pipe(res); // does not work

  res.end();

}).listen(8000);

以下是 curl 命令:

➜  ldap-auth-gateway git:(master) ✗ curl -v -X POST --data "test.payload" --header "Cookie:  token=12345678" --header "Content-Type:text/plain" localhost:9002 

以下是调试输出(请注意,已上传正文):
  About to connect() to localhost port 9002 (#0)
  Trying 127.0.0.1...
    connected
    Connected to localhost (127.0.0.1) port 9002 (#0)
  POST / HTTP/1.1
  User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8x zlib/1.2.5
  Host: localhost:9002
  Accept: */*
  Cookie:  token=12345678
  Content-Type:text/plain
  Content-Length: 243360
  Expect: 100-continue

  HTTP/1.1 100 Continue
  HTTP/1.1 200 OK
  Content-Type: text/plain
  Date: Sun, 04 Aug 2013 17:12:39 GMT
  Connection: keep-alive
  Transfer-Encoding: chunked

服务响应时不会回显请求体:

Echo service: 
Url:  /
Headers:
{
  "user-agent": "curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8x zlib/1.2.5",
  "host": "localhost:9002",
  "accept": "*/*",
  "cookie": "token=12345678",
  "content-type": "text/plain",
  "content-length": "243360",
  "expect": "100-continue"
}

...最后的curl调试信息如下:

Body:
 Connection #0 to host localhost left intact
 Closing connection #0

此外,当我使用大请求体进行压力测试时,会出现EPIPE错误。如何避免这种情况?
-- 编辑:通过试错,我成功解决了这个问题,它仍然指向一个时间问题。虽然这很奇怪,因为超时会导致有效载荷被返回,但超时持续时间并不重要。换句话说,无论我将超时设置为5秒还是500秒,有效载荷都会正确地传回请求,连接也会被终止。
以下是编辑内容:
http.createServer(function (req, res) {

    try {
      res.writeHead(200, { 'Content-Type': 'text/plain' });
      res.write('Echo service: ' + req.url + '\n' + JSON.stringify(req.headers, true, 2));
      res.write('\nBody:"\n');
      req.pipe(res);
    } catch(ex) {
      console.log(ex);
      // how to change response code to error here?  since headers have already been written?
    } finally {
      setTimeout((function() {
        res.end();
      }), 500000);
    }

}).listen(TARGET_SERVER.port);

?


请注意,向9002发出了请求。这是一个反向代理(简单的node-http-proxy到8000,即目标)。直接访问目标会产生相同的结果。 - Robert Christian
2个回答

8

将 req 传输到 res。Req 是可读流,响应是可写流。它应该可以正常工作。

   http.createServer(function (req, res) {

       res.writeHead(200, { 'Content-Type': 'text/plain' });    
       res.write('Echo service: ' + req.url + '\n' + JSON.stringify(req.headers, true, 2));

       // pipe request body directly into the response body
       req.pipe(res);       

   }).listen(9002);

这个会起作用……有时候……管道调用中有一些异步的东西。机器越快,这种情况就越容易发生。我能够通过在调用res.end()之前等待来使它工作。赏金是为了那些能够解释为什么我通过添加睡眠来修复这个问题的人。(请参见我的问题底部的编辑) - Robert Christian
1
PIPE调用负责在req流调用close/end时调用res.end。在管道传输后,无需再次调用res.end。您是否尝试过不使用res.end()来运行上述代码? - Chandu
你的代码加入了sleep(在nodejs中不是正确的术语 :-) )后能够正常工作的原因是,管道实际上有时间来关闭响应流,以便在请求流结束后关闭响应流。settimeout中的res.end()仅仅是关闭已经关闭的流,没有任何副作用。 - Chandu
接受您的答案,但包括 Wyatt 的答案部分,因为他更详细地解释了原因:由于在 Node 中 IO 是异步的,当您发出 .pipe 命令时,控制立即返回到当前上下文,而管道在后台工作。当您下一次调用 res.end() 时,您关闭了流,防止写入更多数据。这里的解决方案是让 .pipe 自己结束流,这是默认设置。 - Robert Christian
为什么 req.pipe(res) 起作用?它如何知道只发送 req.body 而不是所有的头信息等等? - user1944491

6
首先,看起来您的curl未开启,上传数据的文件名应该在前面加上@,具体操作可以参考这里。否则您实际上只是上传了文件名。
此外,Chandu说res.end()的调用是问题所在。
由于IO在Node中是异步的,在发出.pipe命令时,控制立即返回到当前上下文,而管道在后台工作。当您下一次调用res.end()时,您关闭了流,防止写入更多数据
解决方法是让.pipe自己结束流,这是默认设置。 我想计时可能是因为在不同的机器和不同的数据大小上,异步IO在可理论上完成(小数据集的快速IO)之前,可写流上的结束事件被完全处理。
我建议阅读这篇博客文章以获取更多上下文信息。

那很有道理。谢谢解释。 - Robert Christian
关于curl post。那是有意为之的,因为我同时在做两件事情...去掉@符号只是为了测试文件中相对较大的负载与仅有几个字符的区别。 - Robert Christian
如果我能理解这种“有趣”的超时行为……管道需要大约500毫秒,然后关闭连接。超时仍然会发生(即使将其设置在未来的500秒内),但是当调用res.close时,它不会执行任何操作,因为res已经关闭了。我希望会报告某种错误。 - Robert Christian
抛出一个错误肯定会很有用,尽管这里可能还有其他的问题。我刚刚在调用 end 之后添加了一些res.write,它们也悄无声息地失败了。根据文档应该会抛出错误才对,真有趣! - Wyatt

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