zoukankan      html  css  js  c++  java
  • TCP的CLOSE_WAIT 和 TIME_WAIT(转)

    原文:https://xie.infoq.cn/article/e82a53c6d69fb9852e68ce8da

    作者:linux大本营

    CLOSE_WAIT 和 TIME_WAIT 是如何产生的?大量的 CLOSE_WAIT 和 TIME_WAIT 又有何隐患?本文将通过实践角度来带你揭开 CLOSE_WAIT 和 TIME_WAIT 的神秘面纱!遇事不决先祭此图:

    稍微补充一点:TIME_WAIT 到 CLOSED,这一步是超时自动迁移。

    两条竖线分别是表示:

    • 主动关闭(active close)的一方

    • 被动关闭(passive close)的一方

    网络上类似的图有很多,但是有的细节不够,有的存在误导。有的会把两条线分别标记成 Client 和 Server。给读者造成困惑。对于断开连接这件事,客户端和服务端都能作为主动方发起,也就是「主动关闭」可以是客户端,可以是服务端。而对端相应的就是「被动关闭」。不管谁发起,状态迁移如上图。

    1. 耗尽的是谁的端口?

    首先解答初学者对 socket 的认识存在一个常见的误区。作为服务端,不管哪个 WAIT 都不会耗尽客户端的端口!

    举个例子,我在某云的云主机上有个 Server 程序:echo_server。我启动它,监听 2605 端口。然后我在自己的 MacBook 上用 telnet 去连接它。连上之后,在云主机上用 netstat -anp 看一下:

    [guodong@yun test]netstat -anp|grep 2605
    tcp    0    0 0.0.0.0:2605    0.0.0.0:*            LISTEN      3354/./echo_server
    tcp    0    172.12.0.2:2605   xx.xx.xx.xx:31559    ESTABLISHED 3354/./echo_server

    xx.xx.xx.xx 是我 Macbook 的本机 IP

    其中有两条记录,LISTEN 的表示是我的 echo_server 监听一个端口。ESTABLISHED 表示已经有一个客户端连接了。第三列的 IP 端口是我 echo_server 的(这个显示 IP 是局域网的;第四列显示的是客户端的 IP 和端口,也就是我 MacBook。

    要说明的是这个端口:31559 是客户端的。这个是建立连接时的 MacBook 分配的随机端口。

    我们看一下 echo_server 占用的 fd。使用 ls /proc/3354/fd -l 命令查看,其中 3354 是 echo_server 的 pid:

    0 -> /dev/pts/6
    1 -> /dev/pts/6
    2 -> /dev/pts/6
    3 -> anon_inode:[eventpoll]
    4 -> socket:[674802865]
    5 -> socket:[674804942]

    0,1,2 是三巨头(标准输入,输出,错误)自不必言。3 是因为我使用了 epoll,所以有一个 epfd。

    4 其实就是我服务端监听端口打开的被动套接字;

    5 就是客户端建立连接到时候,分配给客户端的连接套接字。server 程序只要给 5 这个 fd 写数据,就相当于返回数据给客户端。

    服务端怎么会耗尽客户端的端口号的。这里消耗的其实是服务端的 fd(也不是端口)!

    2. 什么时候出现 CLOSE_WAIT?

    2.1 举个例子

    回到我的 MacBook 终端,查看一下 2605 有关的连接(Mac 上 netstat 不太好用,只能用 lsof 了):

    [guodong@MacBook test]lsof -iTCP:2605
    COMMAND    PID    USER    FD    TYPE DEVICE             SIZE/OFF    NODE NAME
    telnet     74131  guodong 3u    IPv4 0x199db390a76b3eb3 0t0         TCP  192.168.199.155:50307->yy.yy.yy.yy:nsc-posa(ESTABLISHED)

    yy.yy.yy.yy 表示的远程云主机的 IP

    nsc-posa 其实就是端口 2605,因为 2605 也是某个经典协议(NSC POSA)的默认端口,所以这种网络工具直接显示成了那个协议的名称。

    客户端 pid 为 74135。当然,我其实知道我是用 telnet 连接的,只是为了查 pid 的话,ps aux|grep telnet 也可以。

    注意:为了测试。我这里的 echo_server 是写的有问题的。就是没有处理客户端异常断开的事件。

    下面我 kill 掉 telnet(kill -9 74131)。再回到云主机查看一下:

    [guodong@yun test]netstat -anp|grep 2605
    tcp    0    0 0.0.0.0:2605    0.0.0.0:*            LISTEN      3354/./echo_server
    tcp    1    172.12.0.2:2605   xx.xx.xx.xx:31559    CLOSE_WAIT  3354/./echo_server

    由于 echo_server 内没对连接异常进行侦测和处理。所以可以看到原先 ESTABLISHED 的连接变成了 CLOSE_WAIT。并且会持续下去。我们再看一下它打开的 fd:

    0 -> /dev/pts/6
    1 -> /dev/pts/6
    2 -> /dev/pts/6
    3 -> anon_inode:[eventpoll]
    4 -> socket:[674865719]
    5 -> socket:[674865835]

    5 这个 fd 还存在,并且会一直存在。所以当有大量 CLOSE_WAIT 的时候会占用服务器的 fd。而一个机器能打开的 fd 数量是有限的。超过了,因为无法分配 fd,就无法建立新连接啦!

    2.2 怎么避免客户端异常断开时的服务端 CLOSE_WAIT?

    有一个方法。比如我用了 epoll,那么我监听客户端连接套接字(5)的 EPOLLRDHUP 这个事件。当客户端意外断开时,这个事件就会被触发,触发之后。我们针对性的对这个 fd(5)执行 close()操作就可以了。改下代码,重新模拟一下上述流程,blabla 细节略过。现在我们新 echo_server 启动。MacBook 的 telnet 连接成功。然后我 kill 掉了 telnet。观察一下云主机上的状况:

    [guodong@yun test]netstat -anp|grep 2605
    tcp    0    0 0.0.0.0:2605    0.0.0.0:*            LISTEN      7678/./echo_server
    tcp    1    172.12.0.2:2605   xx.xx.xx.xx:31559    LAST_ACK    -

    出现了 LAST_ACK。我们看下 fd。命令:ls /proc/7678/fd -l

    0 -> /dev/pts/6
    1 -> /dev/pts/6
    2 -> /dev/pts/6
    3 -> anon_inode:[eventpoll]
    4 -> socket:[674905737]
    fd(5)其实已经关闭了。过一会我们重新 netstat 看下:
    [guodong@yun test]netstat -anp|grep 2605
    tcp    0    0 0.0.0.0:2605    0.0.0.0:*            LISTEN      7678/./echo_server

    LAST_ACK 也消失了。为什么出现 LAST_ACK。翻到开头,看我那张图啊!

    CLOSE_WAIT 不会自动消失,而 LAST_TACK 会超时自动消失,时间很短,即使在其存续期内,fd 其实也是关闭状态。实际我这个简单的程序,测试的时候不会每次都捕捉到 LAST_WAIT。有时候用 netstat 命令查看的时候,就是最终那副图了。

    3. 什么时候出现 TIME_WAIT?

    看我开篇那个图就知道了。

    现在我 kill 掉我的 echo_server!

    [guodong@yun test]netstat -anp|grep 2605
    tcp    0    0 172.17.0.2:2605    xx.xx.xx.xx:51327    TIME_WAIT    -

    云主机上原先 ESTABLISHED 的那条瞬间变成 TIME_WAIT 了。

    这个 TIME_WAIT 也是超时自动消失的。时间是 2MSL。MSL 是多长?

    cat /proc/sys/net/ipv4/tcp_fin_timeout

    一般是 60。2MSL 也就 2 分钟。在 2 分钟之内,对服务端有啥副作用吗?有,但问题不大。那就是这期间重新启动 Server 会报端口占用。这个等待,一方面是担心对方收不到自己的确认,等对方重发 FIN。另一方面 2MSL 是报文的最长生命周期,可以避免 Server 重启(或其他 Server 绑同样端口)接收到了上一次的数据。

    当然这个 2MLS 的等待,也可以通过给 socket 添加选项(SO_REUSEADDR)的方式来避免。Server 可以立即重启(这样 Server 的监控进程就可以放心的重新拉起 Server 啦)。

    通常情况下 TIME_WAIT 对服务端影响有限,而大量 CLOSE_WAIT 风险较高,但正确编写代码基本可以避免。为什么只说通常情况呢?因为生产环境是复杂的,一个服务通常会和多个下游服务用各种各样的协议进行通信。TIME_WAIT 和 CLOSE_WAIT 在一些异常条件下,还是会触发的。

    并不是说 TIME_WAIT 就真的无风险,其实无论是 TIME_WAIT 还是 CLOSE_WAIT,永远记住当你的服务出现这两种现象的时候,它们只是某个问题导致的结果,而不是问题本身。有些网络教程教你怎么调大这个或那个的 OS 系统设置,个人感觉只是治标不治本。找到本质原因,避免 TIME_WAIT 和 CLOSE_WAIT 的产生,才是问题解决之道!

    把这些了解清楚时候,是不是可以轻松应对什么 4 次挥手之类的面试题了?

  • 相关阅读:
    深入理解系统调用
    基于mykernel 2.0编写一个操作系统内核
    如何评测一个软件工程师的计算机网络知识水平与网络编程技能水平?
    如何评测软件工程知识技能水平?
    深入理解TCP的三次握手及其源代码
    Socket与系统调用深度分析
    未来的图书会是什么样子?
    构建调试Linux内核网络代码的环境MenuOS系统
    Python笔记005-神奇的+=
    Python笔记004-元组的拆包和命名元组
  • 原文地址:https://www.cnblogs.com/ajianbeyourself/p/15027507.html
Copyright © 2011-2022 走看看