go与TCP连接的半关闭
在写http proxy的时候会在入(in
)、出(out
)两个TCP连接上拷贝数据给对方,下面是一段经典的拷贝程序。
func forward(in, out *net.TCPConn) {
end := make(chan bool)
go func() {
io.Copy(out, in)
out.Close()
end <- true
}()
io.Copy(in, out)
in.Close()
<-end
}
这个程序初看是没有什么问题的,但是实际跑的时候就会出现HTTP异常断开的现象。这里因为当浏览器在一个TCP连接上和网站交换数据时,如果浏览器确认在发出HTTP请求后不会再有后续的数据要发送,就可以直接关闭这个TCP连接的写
,而只专心地在这个连接上读
数据。将这种情况代入到上面的程序,就会发现当浏览器CloseWrite
后,在proxy上读in
里的数据就会遇到EOF
,也就是说io.Copy(out, in)
会结束并返回,接下来会执行out.Close()
,这一句执行后就会关闭proxy与HTTP服务器之间的连接,而这就会导致proxy读out
会遇到EOF
,进一步io.Copy(in, out)
就会结束并返回,再导致proxy与浏览器的in
连接关闭。这一连环事件后就出现了一个现象:本来浏览器只是关闭自己与proxy间的写
,并想继续从proxy读数据,但最终proxy关闭了整个连接。导致浏览器看到整个TCP连接终止。
其实不只使用浏览器会出现这种情况,任意两个使用TCP通信的程序,中间的proxy用了这种逻辑,且有一方半关闭连接,都会出现这个问题。
要解决这个问题也比较简单,将上面的代码改一改
func forward(in, out *net.TCPConn) {
end := make(chan bool)
go func() {
io.Copy(out, in)
in.CloseRead()
out.CloseWrite()
end <- true
}()
io.Copy(in, out)
out.CloseRead()
in.CloseWrite()
<-end
}
现在总共涉及到三种关闭TCP连接的方法: Close
、CloseWrite
、CloseRead
那他们有什么区别呢?可以写个程序试一下
// client
package main
import (
"fmt"
"log"
"net"
"sync"
"time"
)
func main() {
defer fmt.Println("Program Exited...")
conn, err := net.Dial("tcp", ":3333")
if err != nil {
log.Fatalf("dial %v", err)
}
handler(conn.(*net.TCPConn))
}
func handler(conn *net.TCPConn) {
// 本次测试先不调用这个
//defer conn.Close()
var wg sync.WaitGroup
wg.Add(2)
// read goroutine
go func() {
defer wg.Done()
for {
b := make([]byte, 1024)
_, err := conn.Read(b)
if err != nil {
log.Printf("client recv error: %v", err)
return
}
log.Printf("client recv: %5s", string(b))
}
}()
// write goroutine
go func() {
defer wg.Done()
for i := 0; i < 6; i++ {
n, err := conn.Write([]byte(fmt.Sprintf("%3d", i)))
if err != nil {
log.Printf("client send error: %v", err)
return
} else {
log.Printf("client send %3d bytes", n)
}
time.Sleep(1 * time.Second)
}
}()
wg.Wait()
conn.Close()
time.Sleep(3 * time.Second)
}
//server
package main
import (
"fmt"
"log"
"net"
"sync"
"time"
)
func main() {
defer fmt.Println("Program Exited...")
listener, err := net.Listen("tcp", ":3333")
if err != nil {
log.Fatalf("listen %v", err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Fatalf("accept err %v", err)
}
go handler(conn.(*net.TCPConn))
}
}
func handler(conn *net.TCPConn) {
// 本次测试先不调用这个
//defer conn.Close()
var wg sync.WaitGroup
wg.Add(2)
// read goroutine
go func() {
defer wg.Done()
for {
b := make([]byte, 1024)
_, err := conn.Read(b)
if err != nil {
log.Printf("server read error: %v", err)
return
}
log.Printf("server read: %5s", string(b))
}
}()
// write goroutine
go func() {
defer wg.Done()
for i := 0; ; i++ {
n, err := conn.Write([]byte(fmt.Sprintf("%3d", i)))
if err != nil {
log.Printf("server write error: %v", err)
return
} else {
log.Printf("server write %3d bytes", n)
}
time.Sleep(1 * time.Second)
}
}()
time.Sleep(3 * time.Second)
conn.CloseWrite()
log.Printf("server close write")
wg.Wait()
}
这个测试项目是,客户端和服务器每秒向对方发一个数据包。服务器在第3秒CloseWrite
,客户端在第6秒Close
。
客户端的表现如下:
2022/12/10 19:32:14 client send 6 bytes
2022/12/10 19:32:14 client recv: <S> 0
2022/12/10 19:32:15 client send 6 bytes
2022/12/10 19:32:15 client recv: <S> 1
2022/12/10 19:32:16 client send 6 bytes
2022/12/10 19:32:16 client recv: <S> 2
2022/12/10 19:32:17 client recv error: EOF
2022/12/10 19:32:17 client send 6 bytes
2022/12/10 19:32:18 client send 6 bytes
2022/12/10 19:32:19 client send 6 bytes
可以看到客户端在第3秒服务器CloseWrite
后,再conn.Read
返回的是EOF
。虽然读不到数据了,但是还是能继续往服务器写数据的。
之后在第6秒客户端主动Close
了连接。
服务器端的表现如下
2022/12/10 19:32:14 server read: <C> 0
2022/12/10 19:32:14 server write 6 bytes
2022/12/10 19:32:15 server read: <C> 1
2022/12/10 19:32:15 server write 6 bytes
2022/12/10 19:32:16 server write 6 bytes
2022/12/10 19:32:16 server read: <C> 2
2022/12/10 19:32:17 server close write
2022/12/10 19:32:17 server read: <C> 3
2022/12/10 19:32:17 server write error: write tcp 127.0.0.1:3333->127.0.0.1:60838: write: broken pipe
2022/12/10 19:32:18 server read: <C> 4
2022/12/10 19:32:19 server read: <C> 5
2022/12/10 19:32:20 server read error: EOF
服务器在CloseWrite
后,再写就会返回broken pipe
错误。但是还是能继续在这个连接上读来自客户端的数据。
在第6秒,客户端Close
了连接之后,服务器再读数据就会返回EOF
了。
抓包结果如下:
根据抓包结果:
- 服务器在第3秒给客户端发了
FIN
包,对应CloseWrite
操作。 - 客户端在第6秒给服务器发了
FIN
包,对应Close
操作。
那么如果在第6秒,把客户端的Close
操作换成CloseWrite
会怎么样呢?
客户端
2022/12/10 19:38:16 client send 6 bytes
2022/12/10 19:38:16 client recv: <S> 0
2022/12/10 19:38:17 client send 6 bytes
2022/12/10 19:38:17 client recv: <S> 1
2022/12/10 19:38:18 client send 6 bytes
2022/12/10 19:38:18 client recv: <S> 2
2022/12/10 19:38:19 client recv error: EOF
2022/12/10 19:38:19 client send 6 bytes
2022/12/10 19:38:20 client send 6 bytes
2022/12/10 19:38:21 client send 6 bytes
服务器
2022/12/10 19:38:16 server write 6 bytes
2022/12/10 19:38:16 server read: <C> 0
2022/12/10 19:38:17 server read: <C> 1
2022/12/10 19:38:17 server write 6 bytes
2022/12/10 19:38:18 server read: <C> 2
2022/12/10 19:38:18 server write 6 bytes
2022/12/10 19:38:19 server close write
2022/12/10 19:38:19 server read: <C> 3
2022/12/10 19:38:19 server write error: write tcp 127.0.0.1:3333->127.0.0.1:61428: write: broken pipe
2022/12/10 19:38:20 server read: <C> 4
2022/12/10 19:38:21 server read: <C> 5
2022/12/10 19:38:22 server read error: EOF
可以看到,并无区别。
那如果,在服务器端把CloseWrite
改成Close
,客户端在收到FIN
后调用Close
会出现什么情况?
为了说明问题,需要在客户端的Close
前加一个3秒的延时
wg.Wait()
time.Sleep(3 * time.Second) // 加在这里
conn.Close()
time.Sleep(3 * time.Second)
客户端
2022/12/10 19:56:58 client send 6 bytes
2022/12/10 19:56:58 client recv: <S> 0
2022/12/10 19:56:59 client send 6 bytes
2022/12/10 19:56:59 client recv: <S> 1
2022/12/10 19:57:00 client send 6 bytes
2022/12/10 19:57:00 client recv: <S> 2
2022/12/10 19:57:01 client recv error: EOF
2022/12/10 19:57:01 client send 6 bytes
2022/12/10 19:57:02 client send error: write tcp 127.0.0.1:62740->127.0.0.1:3333: write: broken pipe
服务器
2022/12/10 19:56:58 server read: <C> 0
2022/12/10 19:56:58 server write 6 bytes
2022/12/10 19:56:59 server write 6 bytes
2022/12/10 19:56:59 server read: <C> 1
2022/12/10 19:57:00 server read: <C> 2
2022/12/10 19:57:00 server write 6 bytes
2022/12/10 19:57:01 server close write
2022/12/10 19:57:01 server read error: read tcp 127.0.0.1:3333->127.0.0.1:62740: use of closed network connection
2022/12/10 19:57:01 server write error: write tcp 127.0.0.1:3333->127.0.0.1:62740: use of closed network connection
测试结果如预期,服务器Close
后,客户端收不到数据,发数据也失败。服务器端也一样。
但协议层是怎么表现的呢,在收到服务器的FIN
客户端是怎么做的呢?
客户端在收到FIN
包后,还是继续尝试发送数据,但是服务器立即回了RST
。在这之后再无数据包,注意客户端是在等了3秒才调用Close
此时网络上依然没有任何数据包。
因此我们可以总结出:
CloseWrite
后会给对方发送一个FIN
包,但连接不会完全关闭,对方可以继续给我方发送数据,我方也可以正常接收- 在收到对方的
FIN
包后再调用Close
与调用CloseWrite
在协议层表现一致,也会给对方发送FIN
包 - 在收到对方的
FIN
包后是可以继续向对方发送数据的,但根据对方不同的情况有两种表现- 对方的
FIN
包是CloseWrite
产生的,则一切正常 - 对方的
FIN
包是Close
产生的,则会收到一个RST
响应
- 对方的
测试完Close
和CloseWrite
,那么CloseRead
又会有什么表现呢?
这次把服务器的最后几行代码改成如下,也就是说在收到客户端请求3秒后,改为调用CloseRead
。
time.Sleep(3 * time.Second)
conn.CloseRead()
log.Printf("server close read")
wg.Wait()
客户端:
在第3秒服务器CloseRead
后,客户端读会产生connection reset by peer
的错误,客户端写会产生broken pipe
的错误。也就是客户端不仅不能写,连读也不行了。
2022/12/10 20:06:17 client send 6 bytes
2022/12/10 20:06:17 client recv: <S> 0
2022/12/10 20:06:18 client send 6 bytes
2022/12/10 20:06:18 client recv: <S> 1
2022/12/10 20:06:19 client send 6 bytes
2022/12/10 20:06:19 client recv: <S> 2
2022/12/10 20:06:20 client recv: <S> 3
2022/12/10 20:06:20 client send 6 bytes
2022/12/10 20:06:20 client recv error: read tcp 127.0.0.1:63445->127.0.0.1:3333: read: connection reset by peer
2022/12/10 20:06:21 client send error: write tcp 127.0.0.1:63445->127.0.0.1:3333: write: broken pipe
服务器:
服务器端也基本一样,CloseRead
后,读产生EOF
是预期内的,但写也出错了: broken pipe
2022/12/10 20:06:17 server read: <C> 0
2022/12/10 20:06:17 server write 6 bytes
2022/12/10 20:06:18 server read: <C> 1
2022/12/10 20:06:18 server write 6 bytes
2022/12/10 20:06:19 server write 6 bytes
2022/12/10 20:06:19 server read: <C> 2
2022/12/10 20:06:20 server close read
2022/12/10 20:06:20 server read error: EOF
2022/12/10 20:06:20 server write 6 bytes
2022/12/10 20:06:21 server write error: write tcp 127.0.0.1:3333->127.0.0.1:63445: write: broken pipe
抓包:
第3秒服务器调用了CloseRead
后,服务器直接给客户端发了一个RST
包,终止连接。
这个表现就有点在预期之外了。
需要说明的是以上测试都是在MacOS上进行的。
因此对于CloseRead
这一项,在Linux上继续测试了一次。这次测试与MacOS的表现不同。服务器CloseRead
后,读数据会产生EOF
,这在预期内,同时客户端依然能继续给服务器发送数据,服务器也能正常读这也在预期内。
客户端
2022/12/10 20:20:43 client send 6 bytes
2022/12/10 20:20:43 client recv: <S> 0
2022/12/10 20:20:44 client send 6 bytes
2022/12/10 20:20:44 client recv: <S> 1
2022/12/10 20:20:45 client recv: <S> 2
2022/12/10 20:20:45 client send 6 bytes
2022/12/10 20:20:46 client send 6 bytes
2022/12/10 20:20:46 client recv: <S> 3
2022/12/10 20:20:47 client recv: <S> 4
2022/12/10 20:20:47 client send 6 bytes
2022/12/10 20:20:48 client recv: <S> 5
2022/12/10 20:20:48 client send 6 bytes
2022/12/10 20:20:49 client recv: <S> 6
2022/12/10 20:20:50 client recv: <S> 7
2022/12/10 20:20:51 client recv: <S> 8
服务器
2022/12/10 20:20:43 server write 6 bytes
2022/12/10 20:20:43 server read: <C> 0
2022/12/10 20:20:44 server write 6 bytes
2022/12/10 20:20:44 server read: <C> 1
2022/12/10 20:20:45 server write 6 bytes
2022/12/10 20:20:45 server read: <C> 2
2022/12/10 20:20:46 server close read
2022/12/10 20:20:46 server read error: EOF
2022/12/10 20:20:46 server write 6 bytes
2022/12/10 20:20:47 server write 6 bytes
2022/12/10 20:20:48 server write 6 bytes
2022/12/10 20:20:49 server write 6 bytes
2022/12/10 20:20:50 server write 6 bytes
2022/12/10 20:20:51 server write 6 bytes
对CloseRead
在Mac上的这种不在预期内的表现,还可以再进一步测试一下:如果服务器的CloseRead
是在收到客户端的FIN
包后调用的呢?会出现什么情况?
首先修改代码
修改客户端最后几行代码,让客户端在第3秒调用CloseWrite
。
time.Sleep(3 * time.Second)
conn.CloseWrite()
log.Printf("client close write")
wg.Wait()
修改服务器最后几行代码,让服务器在第6秒调用CloseRead
。
time.Sleep(6 * time.Second)
conn.CloseRead()
log.Printf("server close read")
wg.Wait()
客户端
这一次,首先,第3秒客户端CloseWrite
后能继续发送数据这一点如预期,在第6秒服务器CloseRead
后可以继续收服务器发来的数据。
2022/12/10 20:32:46 client send 6 bytes
2022/12/10 20:32:46 client recv: <S> 0
2022/12/10 20:32:47 client send 6 bytes
2022/12/10 20:32:47 client recv: <S> 1
2022/12/10 20:32:48 client send 6 bytes
2022/12/10 20:32:48 client recv: <S> 2
2022/12/10 20:32:49 client close write
2022/12/10 20:32:49 client send error: write tcp 127.0.0.1:65322->127.0.0.1:3333: write: broken pipe
2022/12/10 20:32:49 client recv: <S> 3
2022/12/10 20:32:50 client recv: <S> 4
2022/12/10 20:32:51 client recv: <S> 5
2022/12/10 20:32:52 client recv: <S> 6
2022/12/10 20:32:53 client recv: <S> 7
2022/12/10 20:32:54 client recv: <S> 8
2022/12/10 20:32:55 client recv: <S> 9
2022/12/10 20:32:56 client recv: <S> 10
2022/12/10 20:32:57 client recv: <S> 11
服务器
服务器也一样,在第3秒客户端CloseWrite
后服务器收到FIN
,读数据返回EOF
。第6秒主动调用CloseRead
之后,没有异常产生,还是可以继续向客户端写数据。
2022/12/10 20:32:46 server read: <C> 0
2022/12/10 20:32:46 server write 6 bytes
2022/12/10 20:32:47 server write 6 bytes
2022/12/10 20:32:47 server read: <C> 1
2022/12/10 20:32:48 server read: <C> 2
2022/12/10 20:32:48 server write 6 bytes
2022/12/10 20:32:49 server read error: EOF
2022/12/10 20:32:49 server write 6 bytes
2022/12/10 20:32:50 server write 6 bytes
2022/12/10 20:32:51 server write 6 bytes
2022/12/10 20:32:52 server close read
2022/12/10 20:32:52 server write 6 bytes
2022/12/10 20:32:53 server write 6 bytes
2022/12/10 20:32:54 server write 6 bytes
2022/12/10 20:32:55 server write 6 bytes
2022/12/10 20:32:56 server write 6 bytes
2022/12/10 20:32:57 server write 6 bytes
抓包记录
在第3秒,客户端CloseWrite
向服务器发FIN
包,第6秒服务器CloseRead
后协议层除正常数据包外没有其它的特殊如FIN
、RST
的数据包。
因此,对于CloseRead
涉及到跨平台的话,需谨慎调用。最好不要主动调用。
所以回到最开头的那个proxy程序,为了适应TCP连接半关闭的场景,我们可以将之修改如下:
func forward(in, out *net.TCPConn) {
end := make(chan bool)
go func() {
io.Copy(out, in)
in.CloseRead()
out.CloseWrite()
end <- true
}()
io.Copy(in, out)
out.CloseRead()
in.CloseWrite()
<-end
}