美文网首页
GO学习笔记(8)TCP的TIME_WAIT状态

GO学习笔记(8)TCP的TIME_WAIT状态

作者: 温岭夹糕 | 来源:发表于2022-08-06 22:28 被阅读0次

1.何为TIME_WAIT

time_wait实际上是TCP关闭连接4次挥手时的一种状态 4次挥手

TIME_WAIT is a socket state during TCP connection termination. It represents waiting for enough time to pass to be sure the remote TCP received the acknowledgment of its connection termination request.

它实际上是为了确保最后一次ACK包的发送成功(如果没送达对端会再发一遍FIN,实际上每一次的wait状态包括fin_wait、close_wait和last ack wait都是为了确保自己发送的数据包不丢失),从上图可知,time wait状态只会发生在发起连接关闭的一方。
那这个等待时间是多长?最长为2MSL(maxmum sgement lifetime),在LINUX系统中,由硬编码字段TCP_TIMEWAIT_LEN确定

#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-        WAIT state, about 60 seconds  */

一般是60秒,过了之后就进入closed关闭状态

1.1time wait的重要性

  • 确保ACK到达
  • 让旧连接的重复分节在网络中消失。
    TCP的分节可能由于路由器异常发生“迷途”,在迷途时又触发发送端的超时重传机制,因而重传报文,迷途的分节在路由器修复后会被送到目的地。但是当迷途的分节还没到达最终目的地时,我们关闭一个TCP连接又立马重新建立一个相同端口ip的TCP,此时如果迷途的分节经过一段时间后也到达,那么上一次业务就会对这次业务造成影响(如果没有time wait,新的完全一样连接中可能出现上一次连接的报文),time wait的目的就是让两个方向上在网络中的迷途报文全都自然消失,确保再出现的分组一定是新连接产生的

1.2查看time wait状态

demo还是之前的网络协议概要 中的server和client,在启动后在客户端输入STOP关闭连接,之后查看连接状态

netstat -alepn
image.png
这个程序目前有个buf就是客户端没写关闭连接,服务端在收到STOP后发起连接关闭,因此服务端进入time wait状态 tcpdump抓包

通过tcpdump抓包发现确实是server发起关闭连接请求,之后进行了4次握手

1.3TIME_WAIT危害

  • 内存资源占用
  • 端口占用。一个TCP连接至少消耗一个本地端口,如果TIME_WAIT状态过多,就会导致无法创建新连接
那么上一个例子在关闭后再启动是不是意味着服务端会启动失败,因为端口被占用了 image.png
我们竟然惊讶的发现有两个占用了8080端口,那我再开呢? image.png
有三个占用了相同的端口,这与我们上文分析的端口占用不对呀,那肯定是它内部做了优化

1.4优化TIME_WAIT

  • net.ipv4.tcp_max_tw_buckets 调低系统值,默认为18000,一旦time wait连接数超过,将所有time wait状态重置并打印警告信息,治标不治本,没解决迷途分节问题,上面情况也不是这种
  • 调低 TCP_TIMEWAIT_LEN,重新编译系统。麻烦
  • SO_LINGER 的设置。即设置调用close或shutdown关闭连接时的行为:
    1.为0时默认
    2 跳过4次挥手,即跳过time wait
    3 调用close后阻塞直到数据发送出去
    第二种为跨越time wait提供可能但十分危险
  • net.ipv4.tcp_tw_reuse:更安全的设置。

Allow to reuse TIME-WAIT sockets for new connections when it is safe from protocol viewpoint. Default value is 0.It should not be changed without advice/request of technical experts.

即在安全可控范围内,复用处于time wait的套接字为新连接使用,复用意味着端口也复用,这不就是上面的例子
但是什么是可控:1.连接发起方;2.time wait超过1s(使用这个选项,还有一个前提,需要打开对 TCP 时间戳的支持,即net.ipv4.tcp_timestamps=1)
net.ipv4.tcp_tw_recycle是客户端和服务器端都可以复用,但是容易造成端口接收数据混乱(这两个参数的配置分别对应 /proc/sys/net/ipv4下的两个文件)

2次挥手关闭

我们常说的tcp流是双向的,这里的双向是指数据的写入方向和读出方向,TCP连接的关闭分以下两种情况:

  • 优雅关闭: TCP连接关闭都是先关闭一个方向,此时另一个方向可以正常数据传输(这里代码例子意味着服务端主动发起关闭连接,无法再向客户端写入数据),但不会再有新报文到达不意味着连接已经完全关闭,很有可能情况是客户端正在对服务端最后发送的报文进行处理,如访问数据库,当完成这些操作后把结果通过套接字写给服务端,我们说这个套接字目前状态是“半关闭”(A让B关闭,但是B要先把手头事情做完再关闭)
    对应c底层源码的调用就是
int shutdown(int sockfd, int howto)
  • 粗暴关闭: 对套接字进行彻底释放直接关闭两个方向。对应c源码的调用就是
int close(int sockfd)

2.1GO的Con.Close是哪种关闭连接方法?

修改我们的demo代码,让服务端在收到客户端的包后休眠5s模拟数据库查询动作,客户端没怎么变,增加了主动关闭close方法
server.go

func check(err error) {
    if err != nil {
        log.Fatal(err)
    }
}

func main() {
    lintener, err := net.Listen("tcp", ":8080")
    if err != nil {
        log.Fatal(err)
    }
    for {
        con, err := lintener.Accept()
        check(err)
        fmt.Println("con create from :", con.RemoteAddr())
        go func() {
            defer func() {
                fmt.Println("bye :", con.RemoteAddr())
                con.Close()
            }()
            for {
                netData, err := bufio.NewReader(con).ReadString('\n')
                check(err)
                if strings.TrimSpace(string(netData)) == "STOP" {
                    fmt.Println("read EOF!exiting TCP server")

                    return
                }
                //write to con
                fmt.Println("->", string(netData))
                t := time.Now()
                mytime := t.Format(time.RFC3339) + "\n"
                //Service Blocking
                time.Sleep(time.Second * 3)
                con.Write([]byte(mytime))
            }
        }()
    }

}

client.go

func main() {
    con, err := net.Dial("tcp", ":8080")
    check(err)
    for {
        reader := bufio.NewReader(os.Stdin)
        fmt.Print(">>")
        text, _ := reader.ReadString('\n')
        fmt.Fprintf(con, text+"\n")

        message, _ := bufio.NewReader(con).ReadString('\n')
        fmt.Println("->:" + message)
        if strings.TrimSpace(string(text)) == "STOP" {
            fmt.Println("TCP client exiting")
            con.Close()
            break
        }
    }
}

启动并使用tcpdump进行网络检测

sudo tcpdump -i ol port 8080
客户端情况:在输入新数据包(we)(we加油啊今年夏季赛都垫底了,好歹是御三家),立即输入STOP调用close关闭 client.go

我们发现实际上是等到we的处理结果出来后再关闭连接,但是tcpdump的抓包结果很奇怪


tcpdump
他只抓到了3次挥手,少了一次(第二次)webcache->33164 ack 28的应答(是不是可能和第四次挥手合并发送?),并且他连接的发起关闭方还是服务端webcache,我们开始发愁,建立以下假设:
  1. close是半关闭
    2.因为我们的代码write和close之间还隔着read方法,是不是read也会被阻塞?

    我们交换一下代码逻辑 image.png
    结果还是不变 image.png
    但此时握手结束的发起方确实是客户端(注意观察seq和ack序号) tcpdump
    这不就是我们所期望的优雅的关闭吗?我们的业务没有被落下,这里不得不感叹go设计的强大
    但是在没读懂源码前我们还是得抱着不到黄河心不死的想法,打个问号先

参考

1.隐藏在time wait下的细节
2.time wait生成过多套接字

  1. tcp time wait
    4.网络参数优化
    5.优雅关闭还是粗暴关闭

相关文章

网友评论

      本文标题:GO学习笔记(8)TCP的TIME_WAIT状态

      本文链接:https://www.haomeiwen.com/subject/fdliwrtx.html