ZYB ARTICLES REPOS

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连接的方法: CloseCloseWriteCloseRead那他们有什么区别呢?可以写个程序试一下

// 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了。

抓包结果如下:

根据抓包结果:

那么如果在第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此时网络上依然没有任何数据包。

因此我们可以总结出:

测试完CloseCloseWrite,那么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后协议层除正常数据包外没有其它的特殊如FINRST的数据包。

因此,对于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
}