Press "Enter" to skip to content

NodeJs断开socket链接应该使用destory还是end?

最近在写一点通讯方面的代码,我的程序逻辑可能在判断到某些异常是需要断开客户端socket的链接。

看官方文档,提供了 client.end() 方法和 client.destory()方法,好像都可以用来断开连接。

看官方描述,如果你调用了end方法,你会将socket置于半关闭(Half-closes)状态,然后他会发送一个 FIN 包给对端,这个时候呢,服务器可能还是会发送数据给你的。(表示这个时候的还是可读的?)

如果你调用的是destroy方法,官方描述是 “确保此套接字上不再发生I/O活动,破坏流并关闭连接。”(Ensures that no more I/O activity happens on this socket. Destroys the stream and closes the connection.),看起来像是会彻底摧毁套接字,此时应该不可以再进行读写了。

网上查了一些资料,有点发现。

这张图是默认TCP链接被终止时,应该走的流程。

主动断开的是左侧,被动断开的是右侧

  1. 左端主动关闭要发起一个FIN包,告诉右端我想关闭连接,然后将自己置于终止等待1(FIN_WAIT_1)状态。
  2. 右端收到FIN包时被动关闭,此时知道左端想关闭连接。右端会立刻回复一个ACK包给左端,然后将自己状态改成等待关闭(CLOSE WAIT),并且通知上层应用,对端想关闭连接了,咋办?
  3. 左端收到右端的ACK之后,知道右端已经知道我想关闭链接啦,此时进入终止等待2(FIN_WAIT_2)状态,等待右侧的FIN包。
  4. 右端的上层应用会说,好吧,关闭吧,然后协议栈就会发送FIN包给左端(此时仍然可以发送数据给左端,左端也可以读到)。然后进入最后确认状态(LAST_ACK),此刻等待左端的一个ACK就能真的关闭了。
  5. 左端收到右端的FIN包之后,就知道状况了,进入一个时间等待(TIME_WAIT)的状态,然后等待2MSL时间,然后关闭连接
  6. 右端收到这个ACK包之后,也会从LAST_ACK状态关闭连接,释放端口号和资源。

这大概是TCP断开连接的四次挥手过程,虽然复杂但是很严谨。

这里说下最后这个2MSL等待时间是啥意思,不是2毫秒的意思哦。

通信所要解决的首要问题就是,保持通信双方的信息对称,使通信双方处于同步状态。

TCP四次挥手遵循的套路大概是这样:

主动断开的一侧为A,被动断开的一侧为B。

第一个消息:A发FIN

第二个消息:B回复ACK

第三个消息:B发出FIN

此时此刻:B单方面认为自己与A达成了共识,即双方都同意关闭连接。

此时,B能释放这个TCP连接占用的内存资源吗?不能,B一定要确保A收到自己的ACK、FIN。

所以B需要静静地等待A的第四个消息的到来:

第四个消息:A发出ACK,用于确认收到B的FIN

当B接收到此消息,即认为双方达成了同步:双方都知道连接可以释放了,此时B可以安全地释放此TCP连接所占用的内存资源、端口号。

所以被动关闭的B无需任何wait time,直接释放资源。

但,A并不知道B是否接到自己的ACK,A是这么想的:

1)如果B没有收到自己的ACK,会超时重传FiN

那么A再次接到重传的FIN,会再次发送ACK

2)如果B收到自己的ACK,也不会再发任何消息,包括ACK

无论是1还是2,A都需要等待,要取这两种情况等待时间的最大值,以应对最坏的情况发生,这个最坏情况是:

去向ACK消息最大存活时间(MSL) + 来向FIN消息的最大存活时间(MSL)。

这恰恰就是2MSL( Maximum Segment Life)。

等待2MSL时间,A就可以放心地释放TCP占用的资源、端口号,此时可以使用该端口号连接任何服务器。为何一定要等2MSL?
如果不等,释放的端口可能会重连刚断开的服务器端口,这样依然存活在网络里的老的TCP报文可能与新TCP连接报文冲突,造成数据冲突,为避免此种情况,需要耐心等待网络老的TCP连接的活跃报文全部死翘翘,2MSL时间可以满足这个需求(尽管非常保守)!

我擦,好像跑题了。

下面是我的个人理解,不一定准确。

如果你调用client.end()方法,会触发end事件和close事件。如果你调用client.destory()方法,只会触发close事件。

net.Server这个对象在构造的时候,可以传一个重要的参数 allowHalfOpen,默认是false。对于这个参数的理解也很重要,如果这个值是默认值的false的话,如果服务端收到客户端的FIN之后,服务端会自动的给客户端ACK和FIN,链接关闭。但是如果你指定这个值是true的话,如果服务端收到客户的FIN,服务端不会自动的回FIN给客户端,你需要监听end事件来捕获这一情况,并且手动调用socket的end方法,来发送FIN给客户端。

所以,这个end()方法会尝试发送FIN来走TCP的正常的四次挥手来拆除tcp链接。是不是自动拆除取决于你的allowHalfOpen

至于这个destory()方法,官方说法叫“确保在此 socket 上不再有 I/O 活动。 销毁流并关闭连接。(Ensures that no more I/O activity happens on this socket. Destroys the stream and closes the connection.)”,用一种强硬的手段来确保socket上不再有I/O活动,销毁流,并关闭连接。官方文档还说,更多详情见writable.destroy();

writable.destroy([error])

销毁流。 可选地触发 'error',并且触发 'close' 事件(除非将 emitClose 设置为 false)。 调用该方法后,可写流就结束了,之后再调用 write() 或 end() 都会导致 ERR_STREAM_DESTROYED 错误。 这是销毁流的最直接的方式。 前面对 write() 的调用可能没有耗尽,并且可能触发 ERR_STREAM_DESTROYED 错误。 如果数据在关闭之前应该刷新,则使用 end() 而不是销毁,或者在销毁流之前等待 'drain' 事件。

一旦调用 destroy(),则不会再执行任何其他操作,并且除了 _destroy() 以外的其他错误都不会作为 'error' 触发。

更多细节资料未查到。

看资料说,好像如果在destroy之后,如果客户端还发数据过来,底层好像会发一个RST包给对端。未验证。

结论:

大部分解释说,你应该要使用end来关闭socket会话,这样更加优雅。

参考资料:

https://nodejs.org/api/net.html

https://stackoverflow.com/questions/9191587/how-to-disconnect-from-tcp-socket-in-nodejs

https://blog.csdn.net/anonymalias/article/details/69488863

https://www.zhihu.com/question/67013338

发表评论

邮箱地址不会被公开。 必填项已用*标注