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

2022-08-06  本文已影响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的重要性

1.2查看time wait状态

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

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

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

1.3TIME_WAIT危害

那么上一个例子在关闭后再启动是不是意味着服务端会启动失败,因为端口被占用了 image.png
我们竟然惊讶的发现有两个占用了8080端口,那我再开呢? image.png
有三个占用了相同的端口,这与我们上文分析的端口占用不对呀,那肯定是它内部做了优化

1.4优化TIME_WAIT

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连接的关闭分以下两种情况:

int shutdown(int sockfd, int howto)
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.优雅关闭还是粗暴关闭
上一篇 下一篇

猜你喜欢

热点阅读