当前位置:   article > 正文

优雅地断开TCP连接_tcp长链接 优雅关闭

tcp长链接 优雅关闭

socket关闭: close()和shutdown()的差异

对于一个tcp连接,在c语言里一般有2种方法可以将其关闭:

close(sock_fd);

或者

shutdown(sock_fd, ...);

多数情况下这2个方法的效果没有区别,可以互换使用。除了:

  • close() 是针对file的操作
  • shutdown() 是针对socket的操作

nix系统里socket是1个文件,但文件不1定是1个socket;

所以在进入系统调用后和达到协议层前(发出FIN包这一段), close()和shutdown()的行为会有1点差异。

到达协议层以后,close()和shutdown()没有区别。

举几个栗子示范下close()和shutdown()的差异

下面通过几个例子演示下close()和shutdown()在多线程并发时的行为差异, 我们假设场景是:

  • sock_fd 是一个blocking mode的socket。
  • thread-1 正在对sock_fd进行阻塞的recv(),还没有返回。
  • thread-2 直接对sock_fd调用close() 或 shutdown()。
  • 不考虑linger。

栗子1: socket阻塞在recv()上, 调用close()

  1. // Close a waiting recv()
  2. Time
  3. |
  4. | thread-1 | thread-2 | tcpdump
  5. | | |
  6. | recv(sock_fd | |
  7. | <unfinished ...> | |
  8. 1| | close(sock_fd) = 0 |
  9. | | | // Some data arrived
  10. | | | // after close()
  11. 2| | | < seq 1:36 ... length 35
  12. | | | > ack 36 ...
  13. | // Data was received. | |
  14. 3| <... recv resumed>) = 35 | |
  15. 4| | | > FIN sent
  16. | | | < ack of FIN received
  17. | | | ...
  18. | // Can't be used any more | |
  19. 5v recv(sock_fd) = -1 | |

在上面的例子里:

  • (1) thread-2 调用close()立即成功返回,这时recv()还在使用sock_fd

    这里因为有另外1个线程thread-1正在使用sock_fd, 所以只是标记这个sock_fd为要关闭的。 socket并没有真正关闭。

    这时recv()还继续处于阻塞读取状态。

  • (2) close()之后,有些数据到了,recv可以读取并返回了。

  • (3) recv()收到数据, 正确退出。

  • (4) rece()结束调用,释放socket的引用,这时底层开始关闭socket的流程。

  • (5) 再次调用recv()就会得到错误。

可以看到,close()没有立即关闭socket的连接,也没有打断等待的recv()。

栗子2: socket阻塞在recv()上, 调用shutdown()

  1. // Shutdown a waiting recv()
  2. Time
  3. |
  4. | thread-1 | thread-2 | tcpdump
  5. | | |
  6. | recv(sock_fd | |
  7. | <unfinished ...> | |
  8. 1| | shutdown(sock_fd) = 0 | > FIN sent
  9. | | | < ack of FIN received
  10. | | | ...
  11. | // Woken up by shutdown() | |
  12. | // no errno set | |
  13. 2| <... recv resumed>) = 0 | |
  14. v | |

在上面的例子里:

  • (1) thread-1还在等待sock_fd, thread-2调用shutdown(), 立即开始关闭socket的流程,发FIN 包等。

    然后, 内核中tcp_shutdown中会调用sock_def_wakeup 唤醒阻塞在recv()上的thread-1。

  • (2) 这时recv()阻塞的线程被唤醒等待并立即返回。 返回码是0,表示socket已经关了。

可以看到,shutdown()和close()不同, 会立即关闭socket的连接,并唤醒等待的recv()。

以上2个例子的代码

close-or-shutdown-recv

栗子3: socket阻塞在accept()上, 调用shutdown()

类似的,对阻塞在accept()上的socket调用shutdown(),accept也会被唤醒:

  1. // Shutdown a waiting accept()
  2. Time
  3. |
  4. | thread-1 | thread-2
  5. | |
  6. | accept(sock_fd |
  7. | <unfinished ...> |
  8. 1| | shutdown(sock_fd) = 0
  9. | |
  10. | // Woken up by shutdown() |
  11. | // errno set to EINVA |
  12. 2| <... accept resumed>) = -1 |
  13. | |
  14. v |
  • (1) thread-1还在等待sock_fd, thread-2调用shutdown(), 立即开始关闭socket的流程,发FIN 包等。

    然后, 内核中tcp_shutdown中会调用sock_def_wakeup 唤醒阻塞在accept()上的thread-1。

  • (2) 这时在accept()上阻塞的线程被唤醒, 并立即返回。

    返回码是-1,errno设置为EINVA。

  • 这里如果thread-2调用的是close(), accept不会被唤醒,如果后面有请求connect进来,还能正确接受并返回。

结论

  • shutdown() 立即关闭socket;

    并可以用来唤醒等待线程;

  • close() 不一定立即关闭socket(如果有人引用, 要等到引用解除);

    不会唤醒等待线程。

现在大部分网络应用都使用nonblocking socket和事件模型如epoll的时候, 因为nonblocking所以没有线程阻塞, 上面提到的行为差别不会体现出来 。

当时注意到这个问题是在做1个go的server,因为在go的实现中, 一个tcp的accept的底层实现里,对accept()的系统调用还是阻塞的。 当另1个goroutine想要退出整个进程的时候,需要通知accept的goroutine先退出。 最初我使用`func (*TCPListener) Close`来关闭监听的socket, 但发现TCPListener:Close实际调用了系统调用close(), 无法唤醒当前正在accept()的goroutine, 必须等到有下一个连接进来才能唤醒accept(), 进而退出整个进程。 所以后来改成使用shutdown()来关闭`sock_fd`,以达到唤醒accept()的goroutine的目的。


shutdown() doesn't actually close the file descriptor—it just changes its usability. To free a socket descriptor, you need to use close().

  • shutdown是一种优雅地单方向或者双方向关闭socket的方法。 而close则立即双方向强制关闭socket并释放相关资源。

  • 如果有多个进程共享一个socket,shutdown影响所有进程,而close只影响本进程。

以下均基于单进程socket。

服务端调用shutdown()

  • server调用shutdown(),此时任何后续的send,recv都是无效的(根据关闭发送还是关闭接收有所不同)。shutdown本身并不影响底层,也就是说,此前发出的异步send/recv不会返回。其次,在所有已发送的包被client确认后,server会发送FIN包给client,开始TCP四次挥手过程。

  • 注意不管是关闭发送还是关闭接收,server端均向client端发送FIN报文。client 端收到FIN报文后,并不知道server端以何种方式shutdown,甚至不知道server端是shutdown还是close。

  • client端收到FIN报文之后,详见下文叙述......

服务端调用close()

通过参数设置不同,调用close会出现如下A,B两种情况:

A. 向客户端发送一个RST报文,丢弃本地缓冲区的未读数据,关闭socket并释放相关资源,此种方式为强制关闭。(l_onoff为非0,l_linger为0,)

B. 向客户端发送一个FIN报文,收到client端FIN ACK后,进入了FIN_WAIT_2阶段,可参考TCP四次挥手过程,此种方式为优雅关闭。如果在l_linger的时间内仍未完成四次挥手,则强制关闭。( l_onoff 为非0,l_linger为非0)

FIN与RST

  • 若server端发送FIN报文后没有收到client端的FIN ACK,会两次重传FIN报文,若一直收不到client端的FIN ACK,则会给client端发送RST信号,关闭socket并释放资源。(不同系统实现可能会不同)

  • client收到FIN信号后,再调用read函数会返回0。因为FIN的接收,表明client端以后再无数据可以接收,对方发来FIN,表明对方不在发送数据了。

(注意所有FIN及ACK报文均由操作系统自动完成发送接收)

  • client收到FIN后,会发送应答ack报文,表明收到server的FIN报文,server收到ack报文之后,就进入了FIN_WAIT_2阶段。

  • 根据tcp协议,向一个 FIN_WAIT2 状态的 TCP写入数据是没有问题的,所以此时client可以调用write函数,写入到发送缓冲区,并由tcp连接,发送到server的接收缓冲区。由于server端已经关闭了socket,所以此时的server接收缓冲区的内容都被抛弃,同时server端返回RST给客户端。

  • client端如何知道已经接收到RST报文?

server发送RST报文后,并不等待从client端接收任何ack响应,直接关闭socket。而client端收到RST报文后,也不会产生任何响应。client端收到RST报文后,程序行为如下:

  1. 阻塞模型下,内核无法主动通知应用层出错,只有应用层主动调用read()或者write()这样的IO系统调用时,内核才会利用出错来通知应用层对端已经发送RST报文。
  2. 非阻塞模型下,select或者epoll会返回sockfd可读,应用层对其进行读取时,read()会报RST错误。

通过read write函数出错返回后,获取errno来确定对端是否发送RST信号。

  • client收到RST报文后应如何处理?

client端收到RST信号后,如果调用read函数读取,则会返回RST错误。在已经产生RST错误的情况下,继续调用write,则会发生epipe错误。此时内核将向客户进程发送 SIGPIPE 信号,该信号默认会使进程终止,通常程序会异常退出(未处理SIGPIPE信号的情况下)。

在收到server发送RST报文的情况下,client端的任何read write都是毫无意义的。

作者:dacheng
链接:http://www.jianshu.com/p/eecab8d50697
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/754198
推荐阅读
相关标签
  

闽ICP备14008679号