golang tcp shutdown() 和 close() 的区别

golang 中的 close

在 go 中 syscall.shutdown 其实是在TCPConn.CloseRead 和 CloseWrite 中调用的,而 TCPConn.Close 调用的是 syscall.close。
那么用下面的例子抓包看看,在协议层会发生什么。

server

package main

import (
    "flag"
    "fmt"
    "log"
    "net"
    "strconv"
    "time"
)

var (
    close = "close"
    how   = 0
)

func main() {
    flag.StringVar(&close, "close", close, "close or shutdown")
    flag.IntVar(&how, "how", how, "how shutdown")
    flag.Parse()
    l, err := net.Listen("tcp", ":2000")
    if err != nil {
        log.Fatal(err)
    }

    log.Println("listening... ")
    for {
        c, err := l.Accept()
        // c.(*net.TCPConn).CloseWrite()
        if err != nil {
            panic(err)
        }
        log.Println("get conn", c.RemoteAddr())
        go func() {
            go func() {
                b := make([]byte, 1024)
                for {
                    _, err := c.Read(b)
                    if err != nil {
                        fmt.Println("read err", err)
                        return
                    }
                    fmt.Println("read", string(b))
                }
            }()
            go func() {
                for i := 10000; ; i++ {
                    _, err := c.Write([]byte(strconv.Itoa(i)))
                    if err != nil {
                        fmt.Println("write err", err)
                        return
                    }
                    time.Sleep(time.Second)
                    fmt.Println("write", i)
                }
            }()
            if close == "close" {
                if err := c.Close(); err != nil {
                    panic(err)
                }
                log.Println("closed")
            } else {
                switch how {
                case 0:
                    c.(*net.TCPConn).CloseRead() // syscall.Shutdown(fd, 0)
                case 1:
                    c.(*net.TCPConn).CloseWrite() // syscall.Shutdown(fd, 1)
                case 2:
                    c.(*net.TCPConn).Close()
                }
                // f, _ := c.(*net.TCPConn).File()
                // if err := syscall.Shutdown(int(f.Fd()), how); err != nil {
                //  panic(err)
                // }
                log.Println("shutdown, how:", how)
            }
        }()
    }
}

client

package main

import (
    "fmt"
    "net"
    "strconv"
    "sync"
    "time"
)

func main() {
    c, err := net.Dial("tcp", "192.168.18.240:2000")
    if err != nil {
        panic(err)
    }
    wg := sync.WaitGroup{}
    wg.Add(2)
    go func() {
        defer wg.Done()
        for i := 0; ; i++ {
            time.Sleep(time.Second * 1)
            _, err := c.Write([]byte(strconv.Itoa(i)))
            if err != nil {
                fmt.Println("write err", err)
                return
            }
            fmt.Println("write", i)
        }
    }()
    go func() {
        defer wg.Done()
        b := make([]byte, 1024)
        for {
            _, err := c.Read(b)
            if err != nil {
                fmt.Println("read err", err)
                return
            }
            fmt.Println("read", string(b))
        }
    }()
    wg.Wait()
}

1.使用 CloseRead()/shutdown(fd,0)

client 得到输出:

read 10000
write 0
read 10001
read 10002
write 1
read 10003
write 2
read 10004
write 3
read 10005
write 4

server 输出:

read err EOF
write 10000
write 10001
write 10002
write 10003
write 10004
write 10005
write 10006

client 端是没有感知到任何 tcp 关闭报文的,这个由抓包也可以确认:
dawxy
只有握手和传输消息。
而同时 server 端的tcp 状态是 ESTABLISHED 的。
所以只是操作系统给了服务一个 EOF 并且屏蔽了发送到的数据,客户端是可以正常发送的。

2.使用 CloseWrite()/shutdown(fd,1)

client 输出:

read err EOF
write 0
write 1
write 2
write 3
write 4
write 5
write 6

server 输出:

write err write tcp 192.168.18.240:2000->192.168.18.50:49963: write: broken pipe
read 0
read 1
read 2
read 3
read 4
read 5
read 6

dawxy
此时可以看到 server 发送了一个 FIN 包,并且由客户端确认了,所以 server 端的 tcp 进入 FIN_WAIT2 状态,客户端能读到 EOF, 而此时是可以正常发送到 server 的,如果一直保持通讯这个FIN_WAIT2 状态的 tcp 是不会被操作系统主动关掉的(因为执行的并不是 close 全关闭操作,尽管我测试机器的tcp_fin_timeout 值是 60)。

3.使用 Close()

这是执行全关闭操作,在 go 中会触发协程调度。
client 输出:

read err EOF
write 0
write err write tcp 192.168.18.50:50543->192.168.18.240:2000: write: broken pipe

server 输出:

read err read tcp 192.168.18.240:2000->192.168.18.50:50543: use of closed network connection
write err write tcp 192.168.18.240:2000->192.168.18.50:50543: use of closed network connection

dawxy
可以看到 server 发送了一个 FIN 包, client 确认后读到 EOF(此时 server 端的 tcp 状态是FIN_WAIT2),然后再发送数据到 server 端会触发 RST 导致直接关闭 tcp(此时 tcp 直接进入 close 状态)。
那如果客户端不发数据呢?
那么在 server 发送 FIN 包且有客户端确认后进入 FIN_WAIT2,直到 tcp_fin_timeout 超时(在测试机是 60 秒)。

net.TCPConn.File()

如果调用这个 file 会拿到一个复制的文件句柄(调用 dup 获得),此时会打开一个新的文件描述符,但是都指向同一个文件(/连接),所以直接使用系统调用 syscall.Shutdown how 的值在0-1的情况下也能实现相同效果,会发现每次都会新建两个文件描述符,而 CloseWrite() 和 CloseRead() 只有一个,就是这个原因。
而且 dup 会使得连接变成阻塞模式,使得进程 M 无法被 go 调度,需要自己手动 close 原来的 tcp 连接。如果只调用 syscall.Shutdown(fd, 2)或者 syscall.Close() 则只会关闭新复制出来的那个文件描述符。
所以最终还是需要 TCPConn.Close() 来关闭最初的文件描述符的,而 dup 出来的描述符当然也需要关闭。

发表评论

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

请输入正确的验证码