Node.js socket end、finish、close事件与stream
源文:https://github.com/cool-firer/docs/blob/main/node.js_socket_stream.md
查看代码
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
By default (
allowHalfOpen
isfalse
) the socket will send a FIN packet back and destroy its file descriptor once it has written out its pending write queue. However, ifallowHalfOpen
is set totrue
, the socket will not automatically
意思是收到了对端发来的FIN包就触发'end'事件,表示不再可读。
为了更好的理解,看一下stream的end事件:
https://nodejs.org/docs/latest-v10.x/api/stream.html#stream_event_end
'end'
注意,只有Readable stream才有end事件。
socket是Duplex stream,可以看到socket的end与Readable stream的end意义上是对应的,表示不再有数据可读。
所以,在1触发,最先打印出了end。
socket没有finish事件,那么只能是stream里的:
https://nodejs.org/docs/latest-v10.x/api/stream.html#stream_event_finish
'finish'
event is emitted after the
意思是所有的内部buffer数据都被刷到底层系统。同时注意,只有Writable stream才有finish事件。可以猜测,只有当前socket端不再可写时,才会触发,而这正是当前socket向对端发送FIN后。
对应2,打印出finish。
之后,socket的close事件:
https://nodejs.org/docs/latest-v10.x/api/net.html#net_event_close_1
hadError
true
if the socket had a transmission error.Emitted once the socket is fully closed. The argument
hadError
同时Readable stream和Writable stream都有close事件,看一下:
Readable:
https://nodejs.org/docs/latest-v10.x/api/stream.html#stream_event_close_1
'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
'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事件相对应。
对应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 isstring
. Default:'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 callingsocket.write(data, encoding)
followed bysocket.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'事件?这。。。
线索在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 callingstream.read()
repeatedly until all data has been consumed.
除非data被完全消费,否则'end'不会触发。
还有在文档的最后面,也有讲,并给出了例子:
再去看服务端的代码,没有为新来的socket绑定'data'事件、也没有'readable' + read()方法消费内部data,socket处于pause mode。或者可以理解为,FIN包被排到了内部buffer的尾部,只有消费完了前面的data,才能轮到FIN包。
所以,要让他正常走完四次挥手,需要消费一下服务端的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 receiveexception
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 towrite()
orend()
will result in anERR_STREAM_DESTROYED
error. Implementors should not override this method, but instead implementwritable._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 thestream.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)
查看代码
// 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,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 thetransform._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 theReadable
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