【问题标题】:nodejs tcp socket sends multiple data eventnodejs tcp socket发送多个数据事件
【发布时间】:2016-11-14 14:46:35
【问题描述】:

我试图通过使用 tcp 包对 http 服务器进行编码来演示 http 服务器如何工作的最简单方式。我之前做过几次,但今天我遇到了一个意想不到的行为,因为来自套接字对象的 data 事件被触发一次或多次随机类似的请求,我想知道为什么,以及如何正确修复它。

请注意,我知道我应该使用流式处理数据的方式,这是我在第二次演示中所做的。重点是每次都增加复杂性,以使演示更容易理解。

这是服务器。如您所见,它简单易行。

const net = require('net')

const response = `HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Foo: Bar

foobar
`

net.createServer(socket => {
  socket.on('data', buffer => {
    console.log('----- socket data', Date.now())
    console.log(buffer.toString())
    socket.write(response)
    socket.end()
    console.log('-----')
  })

  socket.on('end', () => console.log('----- socket end.'))
  socket.on('close', () => console.log('----- socket close.', '\n'))
}).listen(2000)

为了测试我的服务器,我只需打开任何网络浏览器到http://localhost:2000 并获得响应;但是当使用以下有效负载(使用浏览器的 javascript 控制台)时,有时 data 事件会被触发两次,最终导致错误,因为 write/end 进程无法继续第二次。

var xhr = new XMLHttpRequest();
xhr.open("POST", "/");
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify({ foo: "bar" }));

这是来自服务器的日志快照,如果有帮助的话:

----- socket data 1479133993862
POST / HTTP/1.1
Host: localhost:2000
Connection: keep-alive
Content-Length: 13
Pragma: no-cache
Cache-Control: no-cache
Origin: http://localhost:2000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36
Content-Type: application/json
Accept: */*
Referer: http://localhost:2000/
Accept-Encoding: gzip, deflate, br
Accept-Language: fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4,id;q=0.2,ms;q=0.2,ko;q=0.2

{"foo":"bar"}
-----
----- socket end.
----- socket close.

----- socket data 1479133994515
POST / HTTP/1.1
Host: localhost:2000
Connection: keep-alive
Content-Length: 13
Pragma: no-cache
Cache-Control: no-cache
Origin: http://localhost:2000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36
Content-Type: application/json
Accept: */*
Referer: http://localhost:2000/
Accept-Encoding: gzip, deflate, br
Accept-Language: fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4,id;q=0.2,ms;q=0.2,ko;q=0.2

{"foo":"bar"}
-----
----- socket end.
----- socket close.

----- socket data 1479133995166
POST / HTTP/1.1
Host: localhost:2000
Connection: keep-alive
Content-Length: 13
Pragma: no-cache
Cache-Control: no-cache
Origin: http://localhost:2000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36
Content-Type: application/json
Accept: */*
Referer: http://localhost:2000/
Accept-Encoding: gzip, deflate, br
Accept-Language: fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4,id;q=0.2,ms;q=0.2,ko;q=0.2


-----
----- socket data 1479133995167
{"foo":"bar"}
events.js:154
      throw er; // Unhandled 'error' event
      ^

Error: write after end
    at writeAfterEnd (_stream_writable.js:167:12)
    at Socket.Writable.write (_stream_writable.js:212:5)
    at Socket.write (net.js:624:40)
    at Socket.<anonymous> (/Users/julien/Temp/foo.js:14:12)
    at emitOne (events.js:90:13)
    at Socket.emit (events.js:182:7)
    at readableAddChunk (_stream_readable.js:153:18)
    at Socket.Readable.push (_stream_readable.js:111:10)
    at TCP.onread (net.js:529:20)

如您所见,前 2 个请求很好,但第 3 个请求被分成 2 个不同的部分。请求的标头将在一个数据事件中,而正文在另一个数据事件中。

我与少数开发人员讨论过这个问题,我们猜测这可能与我的操作系统的 TCP 堆栈有关,如果这很重要的话,那就是 OSX Sierra。

除了将缓冲区累积到在上层范围中声明的变量中之外,我看不到任何其他方法来修补它,然后使用 ugly 计时器技巧,它最终 类似于 一个可取消的 setImmediate。

var timer = false, data = '';
socket.on('data', buffer => {
  data += buffer.toString();

  clearTimeout(timer);
  timer = setTimeout(() => process(socket, data), 1)
})

问题很简单:我知道这个修复在很多方面都非常错误,但是如果不使用流或 http 包,我看不到其他修复。你能启发我吗?

【问题讨论】:

    标签: node.js sockets tcp


    【解决方案1】:

    这就是 TCP 的工作原理。 TCP 是字节流。应用层上没有带有边界(甚至是请求)的数据包。在连接的一端对 n 字节的写入调用可能会在另一端导致最多 n 个 1 字节的读取调用。您必须做好准备,每次读取都会产生任意数量的字节(直到给 read 调用的缓冲区大小 - 但是当您在 node.js 中推送数据时,您无法影响它)。如果您需要应用程序级别的数据包,您需要自己处理,例如通过将长度前缀的数据包写入流。

    但是,HTTP 不需要数据包的概念,因为它已经由 HTTP 协议定义,其中标头和正文结束。

    【讨论】:

    • 别误会我,我完全理解。我不明白的是事件之间切割的随机性,以及请求总是在标头之间切割成两半的有效精度和身体。
    • 可能是客户端执行 2 次写入调用,一次用于标头,一次用于正文。并且有时这些以组合方式发送(如果标头的数据包之前未发送过),有时则不发送。
    • 但总的来说,作为流的消费者,这对你来说并不重要,因为你不能依赖这里的任何行为。
    • 这是一个竞争条件。一个进程有两次写入缓冲区。在后台有操作系统将内容从缓冲区发送到网络。如果仅在这两个写入之后操作系统的网络线程开始运行,它将尝试发送任何可用的东西。如果在它之前安排它只会发送可用的标头部分。如果网络拥塞,它可能会更少。
    • 如果 keepalive 处于活动状态,您将不会结束流。所以你需要一个解析器来确定请求在哪里结束。并且对于例外:在收到请求的任何部分后,您编写完整的响应并结束流 - 这是有效的。但是,一旦您收到另一部分请求,您就会尝试将更多数据写入(已经关闭的)传出流。这会产生异常。您可以通过跟踪您是否已经发送过东西来轻松避免这种情况。
    猜你喜欢
    • 2019-11-27
    • 2019-07-05
    • 1970-01-01
    • 2021-04-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-06-14
    • 2016-10-28
    相关资源
    最近更新 更多