Node.js socket end、finish、close事件与stream

源文:https://github.com/cool-firer/docs/blob/main/node.js_socket_stream.md

 

net socket与stream事件

测试程序

tcp_server.js

查看代码
const net = require('net');

net.createServer(function(c) {
  console.log('conneceted');

  c.on('finish', function() {
    console.log('finish 111');
  })
  
  c.on('close', function() {
    console.log('close');
  })
  
  c.on('finish', function() {
    console.log('finish 222');
  })

  c.on('end', function() {
    console.log('end');
  });

}).listen(9988);

console.log('listen on 9988', ' pid:', process.pid)

 

tcp_client.js

查看代码
const net = require('net');

const c = net.createConnection({
  port: 9988
})

c.on('finish', function() {
  console.log('finish 111');
})

c.on('close', function() {
  console.log('close');
})

c.on('finish', function() {
  console.log('finish 222');
})

 

启动server,再启动cilent,ctrl + c 直接退出client,server端打印出:

$ node tcp_server.js 
listen on 9988  pid: 27157
conneceted
end
finish 111
finish 222
close

 

需要查socket的文档和stream的文档,再配合tcp的四次挥手理解。 

 

socket的end事件:

https://nodejs.org/docs/latest-v10.x/api/net.html#net_event_end

Emitted when the other end of the socket sends a FIN packet, thus ending the readable side of the socket.

By default (allowHalfOpen is false) the socket will send a FIN packet back and destroy its file descriptor once it has written out its pending write queue. However, if allowHalfOpen is set to true, the socket will not automatically end() its writable side, allowing the user to write arbitrary amounts of data. The user must call end() explicitly to close the connection (i.e. sending a FIN packet back).

意思是收到了对端发来的FIN包就触发'end'事件,表示不再可读。

为了更好的理解,看一下stream的end事件:

https://nodejs.org/docs/latest-v10.x/api/stream.html#stream_event_end

The 'end' event is emitted when there is no more data to be consumed from the stream.

注意,只有Readable stream才有end事件。

socket是Duplex stream,可以看到socket的end与Readable stream的end意义上是对应的,表示不再有数据可读。

socket_stream_normal

所以,在1触发,最先打印出了end。

 

socket没有finish事件,那么只能是stream里的:

https://nodejs.org/docs/latest-v10.x/api/stream.html#stream_event_finish

The 'finish' event is emitted after the stream.end() method has been called, and all data has been flushed to the underlying system.

意思是所有的内部buffer数据都被刷到底层系统。同时注意,只有Writable stream才有finish事件。可以猜测,只有当前socket端不再可写时,才会触发,而这正是当前socket向对端发送FIN后。

socket_stream_normal

对应2,打印出finish。

 

之后,socket的close事件:

https://nodejs.org/docs/latest-v10.x/api/net.html#net_event_close_1

Added in: v0.1.90

  • hadError true if the socket had a transmission error.

Emitted once the socket is fully closed. The argument hadError is a boolean which says if the socket was closed due to a transmission error.

socket完全被关闭时触发。

同时Readable stream和Writable stream都有close事件,看一下:

Readable:

https://nodejs.org/docs/latest-v10.x/api/stream.html#stream_event_close_1

The 'close' event is emitted when the stream and any of its underlying resources (a file descriptor, for example) have been closed. The event indicates that no more events will be emitted, and no further computation will occur.

 

Writable:

https://nodejs.org/docs/latest-v10.x/api/stream.html#stream_event_close

The 'close' event is emitted when the stream and any of its underlying resources (a file descriptor, for example) have been closed. The event indicates that no more events will be emitted, and no further computation will occur.

Readable和Writable两种流对close事件的描述高度一致,都是说流的底层资源(文件描述符)被关闭了,这也与socket的close事件相对应。

socket_stream_normal

对应3,打印close。

 

socket.end与消费

如果我们改一下tcp_client.js的代码,把ctrl + c换成socket.end()方法,服务端保持不变呢?

查看代码
// tcp_client.js
const net = require('net');

const c = net.createConnection({
  port: 9988
})

c.on('end', function() {
  console.log('end');
})
c.on('finish', function() {
  console.log('finish 111');
})
c.on('close', function() {
  console.log('close');
})
c.on('finish', function() {
  console.log('finish 222');
})
setTimeout(function() {
  c.end('what the hell');
}, 3000)

3s后,调用end()方法,关闭当前连接。

先看一下socket.end()方法描述:

https://nodejs.org/docs/latest-v10.x/api/net.html#net_socket_end_data_encoding_callback

socket.end([data][, encoding][, callback])[src]#

Added in: v0.1.90

  • data | |
  • encoding Only used when data is stringDefault: 'utf8'.
  • callback Optional callback for when the socket is finished.
  • Returns: <net.Socket> The socket itself.

Half-closes the socket. i.e., it sends a FIN packet. It is possible the server will still send some data.

If data is specified, it is equivalent to calling socket.write(data, encoding) followed by socket.end().

半关闭socket,向对端发送FIN包。

那么,按照新改的代码,服务端是不是就会走四次挥手流程,依次打印出'end'、'finish'、'close'呢?先看客户端的输出:

$ node tcp_cilent.js 
finish 111
finish 222

再看服务端的输出:

$ node tcp_server.js 
listen on 9988  pid: 32405
conneceted

调用了end()方法,连接竟然没有断开?而且服务端也没有触发'end'事件?这。。。

ineedav

线索在stream的end事件描述里:

https://nodejs.org/docs/latest-v10.x/api/stream.html#stream_event_end

Event: 'end'#

Added in: v0.9.4

The 'end' event is emitted when there is no more data to be consumed from the stream.

The 'end' event will not be emitted unless the data is completely consumed. This can be accomplished by switching the stream into flowing mode, or by calling stream.read() repeatedly until all data has been consumed.

除非data被完全消费,否则'end'不会触发。

还有在文档的最后面,也有讲,并给出了例子:

https://nodejs.org/docs/latest-v10.x/api/stream.html#stream_compatibility_with_older_node_js_versions

 

再去看服务端的代码,没有为新来的socket绑定'data'事件、也没有'readable' + read()方法消费内部data,socket处于pause mode。或者可以理解为,FIN包被排到了内部buffer的尾部,只有消费完了前面的data,才能轮到FIN包。

tcp_demo1

所以,要让他正常走完四次挥手,需要消费一下服务端的socket,像这样:

查看代码
const net = require('net');


net.createServer(function(c) {
  console.log('conneceted');

  c.on('finish', function() {
    console.log('finish 111');
  })
  c.on('close', function() {
    console.log('close');
  })
  c.on('finish', function() {
    console.log('finish 222');
  })
  c.on('end', function() {
    console.log('end');
  });

  setTimeout(async () => {
    /**
      几种方法选一种
    */
    
    // 方法1: 用flow mode
    c.on('data', (chunk) => {
      console.log(`Received ${chunk.length} bytes of data. chunkStr:${chunk.toString()}`);
    });
  }, 5000);
  
  	// 方法2: pause mode readable + read方法
    c.on('readable', () => {
      let chunk;
      while (null !== (chunk = c.read())) {
        console.log(`Received ${chunk.length} bytes of data. chunkStr:${chunk.toString()}`);
      }
    });
  
  	// 方法3: pause mode 直接read
    for(let i = 0; i < 16;i++) {
      const internelBuf = c.read(1);
      console.log(`${i} Received ${internelBuf ? internelBuf.length + ' bytes of data. chunkStr:' +  internelBuf.toString() : null }`);
  
      await new Promise((r,j) => {
        setTimeout(() => {
          r(true);
        }, 2000)
      })
    }
  
  	// 方法4: flow mode resume方法
    c.resume();
  

}).listen(9988);

console.log('listen on 9988', ' pid:', process.pid)

 如此一来,客户端、服务端都正常打印。

$ node tcp_cilent.js 
finish 111
finish 222
end
close

 

$ node tcp_server.js 
listen on 9988  pid: 32627
conneceted
end
finish 111
finish 222
close

所以,socket 'end'事件的触发,需要加上一个条件:就是当前socket需要被消费完并且收到FIN包,才会触发。

 

socket.destroy与finish

如果,把end改为destroy呢?

查看代码
// tcp_client.js
const net = require('net');

const c = net.createConnection({
  port: 9988
})

c.on('end', function() {
  console.log('end');
})
c.on('finish', function() {
  console.log('finish 111');
})
c.on('close', function() {
  console.log('close');
})
c.on('finish', function() {
  console.log('finish 222');
})

setTimeout(function() {
  // c.end('what the hell');
  c.destroy();

}, 3000)

在官方文档里,关于destroy的描述是这样:

socket部分: https://nodejs.org/docs/latest-v10.x/api/net.html#net_socket_destroy_exception

socket.destroy([exception])

Added in: v0.1.90

Ensures that no more I/O activity happens on this socket. Only necessary in case of errors (parse error or so).

If exception is specified, an 'error' event will be emitted and any listeners for that event will receive exception as an argument.

 

stream部分: https://nodejs.org/docs/latest-v10.x/api/stream.html#stream_writable_destroy_error

writable.destroy([error])#

Added in: v8.0.0

Destroy the stream, and emit the passed 'error' and a 'close' event. After this call, the writable stream has ended and subsequent calls to write() or end() will result in an ERR_STREAM_DESTROYED error. Implementors should not override this method, but instead implement writable._destroy().

stream部分说,销毁流,并触发'close'事件,客户端确实是这样:

$ node tcp_cilent.js 
close

而服务端,不管有没有消费socket,都正常打印:

$ node tcp_server.js 
listen on 9988  pid: 32712
conneceted
end
finish 111
finish 222
close

之前说过,发送FIN包后会触发'finish',但这里destroy并没有触发'finish',按照来说,不管是end还是destroy,都会向对端发送FIN,只是destroy发完后就直接销毁fd, 不等对端的ACK。

Event: 'finish'#

Added in: v0.9.4

The 'finish' event is emitted after the stream.end() method has been called, and all data has been flushed to the underlying system.

所以,发送FIN包后就不会马上触发'finish',而是发送FIN包,并且内部buffer被刷到底层fd后才会触发。

 

socket与Transform

再来变一下,如果把socket pipe进一个自定义的Transform呢?很多网络NPM客户端库都是这么做的,比如mysql2、AMQ等。

改写一下服务端代码:

查看代码
// tcp_server.js
const net = require('net');

class Incoming extends require('stream').Transform {
  _flush(callback) {
    console.log('  incoming _flush')
    this.push(null);
    callback();
  }

  _transform(chunk, encoding, callback) {
    callback(null, chunk);
  }
}
const income = new Incoming();


net.createServer(function(c) {
  console.log('conneceted');
  
  c.on('finish', function() {
    console.log('finish 111');
  })
  c.on('close', function() {
    console.log('close');
  })
  c.on('finish', function() {
    console.log('finish 222');
  })
  c.on('end', function() {
    console.log('end');
  })

  }, 5000)

  c.pipe(income);

  income.on('end' ,function() {
    console.log('  incoming end')
  })
  income.on('finish' ,function() {
    console.log('  incoming finish')
  })
  income.on('close' ,function() {
    console.log('  incoming close')
  })

}).listen(9988);

console.log('listen on 9988', ' pid:', process.pid)
客户端保持不变,2s后end
查看代码
// tcp_client.js
const net = require('net');


const c = net.createConnection({
  port: 9988
})

c.on('end', function() {
  console.log('end');
})
c.on('finish', function() {
  console.log('finish 111');
})
c.on('close', function() {
  console.log('close');
})
c.on('finish', function() {
  console.log('finish 222');
})

setTimeout(function() {
  c.end('what the hell');
}, 3000)


此场景下,客户端输出:

$ node tcp_cilent.js 
finish 111
finish 222
end
close

 

服务端输出:

$ node tcp_server.js 
listen on 9988  pid: 34930
conneceted
end
  incoming _flush
  incoming finish
finish 111
finish 222
close

可以看到,两端的socket都正常关闭了。

socket.pipe(income)实现上会为socket绑定data事件,把从socket读取的数据forward到income。

对于socket来说,是有消费数据的,所以socket可以正常走完end、finish、close。

那么,要如何理解income的finish呢?

socket_pipe

前面说过,对于socket,finish事件是在当前端发送FIN,且flush到底层fd后触发,表示不能往当前端写入任何数据,确切的说,是不能再写入数据到当前端的内部writable buffer,体现在代码上,就是socket.write('xxx')。触发finish的事件可以用这样的伪代码表示:

socket.write('xxx') ---> socket.write(FIN) ---> flushTo(fd) ---> emit('finish')

对于income,没有底层fd,它的底层fd就是它自己,由socket转来的数据在代码上相当于income.write('xxx'),同样表示不能再往income里写数据。用伪代码表示:

income.write('xxx') ---> income.write(FIN) ---> flushTo(income buffer) ---> emit('finish')

至于_flush方法在finish之前打印,是因为:

https://nodejs.org/docs/latest-v10.x/api/stream.html#stream_transform_flush_callback

transform._flush(callback)#

  • callback Function A callback function (optionally with an error argument and data) to be called when remaining data has been flushed.

Custom Transform implementations may implement the transform._flush() method. This will be called when there is no more written data to be consumed, but before the 'end' event is emitted signaling the end of the Readable stream.

 这是写入数据链会调用的最后一个方法,此时数据还没flush,必然会在finish事件之前。

 

如果想让income像socket一样,正常走完end、finish、close,那么同样的,需要消费完income的内部数据才会触发,方法也是跟前面的方法一样,绑定data事件、调用resume、多次调用read。

查看代码
const net = require('net');

class Incoming extends require('stream').Transform {
  _flush(callback) {
    console.log('  incoming _flush')
    this.push(null);
    callback();
  }

  _transform(chunk, encoding, callback) {
    callback(null, chunk);
  }
}
const income = new Incoming();


net.createServer(function(c) {
  console.log('conneceted');

  c.on('finish', function() {
    console.log('finish 111');
  })
  
  c.on('close', function() {
    console.log('close');
  })
  
  c.on('finish', function() {
    console.log('finish 222');
  })

  c.on('end', function() {
    console.log('end');
  });

  c.pipe(income);
  income.on('end' ,function() {
    console.log('  incoming end')
  })
  income.on('finish' ,function() {
    console.log('  incoming finish')
  })
  income.on('close' ,function() {
    console.log('  incoming close')
  })

  setTimeout(async () => {

    // 方法1: 用flow mode
    income.on('data', (chunk) => {
      console.log(`income Received ${chunk.length} bytes of data. chunkStr:${chunk.toString()}`);
    })
    
  	// 方法2: pause mode readable + read方法
    income.on('readable', () => {
      let chunk;
      while (null !== (chunk = income.read())) {
        console.log(`income Received ${chunk.length} bytes of data. chunkStr:${chunk.toString()}`);
      }
    });

  	// 方法3: pause mode 直接read
    for(let i = 0; i < 16;i++) {
      const internelBuf = income.read(1);
      console.log(`${i} income Received ${internelBuf ? internelBuf.length + ' bytes of data. chunkStr:' +  internelBuf.toString() : null }`);
  
      await new Promise((r,j) => {
        setTimeout(() => {
          r(true);
        }, 2000)
      })
    }

  	// 方法4: flow mode resume方法
    income.resume();
  }, 5000)


}).listen(9988);

console.log('listen on 9988', ' pid:', process.pid)

此时服务端就可以正常输出走完end、finish、close:

$ node tcp_server.js 
listen on 9988  pid: 35495
conneceted
end
  incoming _flush
  incoming finish
finish 111
finish 222
close
income Received 13 bytes of data. chunkStr:what the hell
  incoming end
  incoming close

posted on 2022-01-27 13:01  留校察看  阅读(1477)  评论(0编辑  收藏  举报

导航