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 关闭报文的,这个由抓包也可以确认:
只有握手和传输消息。
而同时 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
此时可以看到 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
可以看到 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 出来的描述符当然也需要关闭。