首页 > 其他分享 >[Go] 如何妥善处理 TCP 代理中连接的关闭

[Go] 如何妥善处理 TCP 代理中连接的关闭

时间:2024-10-24 19:22:41浏览次数:1  
标签:127.0 0.1 12345 妥善处理 TCP 关闭 Go nop 连接

如何妥善处理 TCP 代理中连接的关闭

相比较于直接关闭 TCP 连接,只关闭 TCP 连接读写使用单工连接的场景较少,但通用的 TCP 代理也需要考虑这部分场景。

背景

今天在看老代码的时候,发现一个 TCP 代理的核心函数实现的比较粗糙,收到 EOF 后直接粗暴关闭两条 TCP 连接。

func ConnCat(uConn, rConn net.Conn) {
	wg := sync.WaitGroup{}
	wg.Add(2)

	go func() {
		defer wg.Done()
		io.Copy(uConn, rConn)
		uConn.Close()
		rConn.Close()
	}()

	go func() {
		defer wg.Done()
		io.Copy(rConn, uConn)
		uConn.Close()
		rConn.Close()
	}()

	wg.Wait()
}

一般场景下是感知不到问题的,但是做为一个代理,应该只透传客户端/服务端的行为,多余的动作不应该发生,比如客户端关闭写,代理只需要把关闭传递给服务端即可。

连接关闭

调用 close 关闭连接是通用做法,相关的还有一个 shutdown 系统调用。shutdownclose 相比可以更精细的控制连接的读写,但是不负责 fd 资源的释放,换而言之,无论是否调用 shutdownclose 最后都是需要调用的。

对于 shutdown 第二参数的说明

  • SHUT_RD 连接关闭读,仍然可以继续写。
  • SHUT_WR 连接关闭写,仍然可以继续读;并且会发送一个 FIN 包。
  • SHUT_RDWR 连接读写都被关闭;并且会发送一个 FIN 包。

对于上层应用而言,只需要关注 read 的结果,收到 FIN(也就是 EOF)虽然不能判断对端是关闭读写还是只关闭写,但后续处理并不会受影响。

根据读取数据来处理后续逻辑

  1. 判断已读数据是否符合预期来决定 关闭连接 或者 写入数据
  2. 向连接写入数据,失败的话直接关闭连接即可,不失败的话当前连接则为单工模式
  3. 关闭连接

Go 中连接关闭读写的示例

测试代码展示两个 TCP 连接分别关闭读(写)再进行写(读)

func TestTCPClose(t *testing.T) {
	lis, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 12345})
	if err != nil {
		t.Fatal(err)
	}

	var (
		conn0     *net.TCPConn
		conn1     *net.TCPConn
		acceptErr error
	)

	acceptDoneCh := make(chan struct{})
	go func() {
		conn0, acceptErr = lis.AcceptTCP()
		close(acceptDoneCh)
	}()

	conn1, err = net.DialTCP("tcp", nil, lis.Addr().(*net.TCPAddr))
	if err != nil {
		t.Fatal(err)
	}
	<-acceptDoneCh
	if acceptErr != nil {
		t.Fatal(acceptErr)
	}

	wg := sync.WaitGroup{}
	wg.Add(2)

	go func() {
		conn1.Write([]byte("hello"))
		time.Sleep(time.Second * 1)
		conn1.CloseWrite()
		b := make([]byte, 1024)
		conn1.Read(b)
		wg.Done()
	}()

	go func() {
		b := make([]byte, 1024)
		conn0.Read(b)
		conn0.CloseRead()
		time.Sleep(time.Second * 2)
		conn0.Write([]byte("test"))
		wg.Done()
	}()

	wg.Wait()
	conn0.Close()
	conn1.Close()
}

通过 tcpdump 抓包也可以看到 CloseWrite 会发送一个 FIN

17:21:09.877056 IP 127.0.0.1.44158 > 127.0.0.1.12345: Flags [S], seq 4257116181, win 65495, options [mss 65495,sackOK,TS val 3165750919 ecr 0,nop,wscale 7], length 0
17:21:09.877069 IP 127.0.0.1.12345 > 127.0.0.1.44158: Flags [S.], seq 188514168, ack 4257116182, win 65483, options [mss 65495,sackOK,TS val 3165750919 ecr 3165750919,nop,wscale 7], length 0
17:21:09.877081 IP 127.0.0.1.44158 > 127.0.0.1.12345: Flags [.], ack 1, win 512, options [nop,nop,TS val 3165750919 ecr 3165750919], length 0
17:21:09.877211 IP 127.0.0.1.44158 > 127.0.0.1.12345: Flags [P.], seq 1:6, ack 1, win 512, options [nop,nop,TS val 3165750920 ecr 3165750919], length 5
17:21:09.877219 IP 127.0.0.1.12345 > 127.0.0.1.44158: Flags [.], ack 6, win 512, options [nop,nop,TS val 3165750920 ecr 3165750920], length 0
17:21:10.878149 IP 127.0.0.1.44158 > 127.0.0.1.12345: Flags [F.], seq 6, ack 1, win 512, options [nop,nop,TS val 3165751920 ecr 3165750920], length 0
17:21:10.920263 IP 127.0.0.1.12345 > 127.0.0.1.44158: Flags [.], ack 7, win 512, options [nop,nop,TS val 3165751963 ecr 3165751920], length 0
17:21:11.877430 IP 127.0.0.1.12345 > 127.0.0.1.44158: Flags [P.], seq 1:5, ack 7, win 512, options [nop,nop,TS val 3165752920 ecr 3165751920], length 4
17:21:11.877460 IP 127.0.0.1.44158 > 127.0.0.1.12345: Flags [.], ack 5, win 512, options [nop,nop,TS val 3165752920 ecr 3165752920], length 0
17:21:11.882928 IP 127.0.0.1.12345 > 127.0.0.1.44158: Flags [F.], seq 5, ack 7, win 512, options [nop,nop,TS val 3165752925 ecr 3165752920], length 0
17:21:11.882957 IP 127.0.0.1.44158 > 127.0.0.1.12345: Flags [.], ack 6, win 512, options [nop,nop,TS val 3165752925 ecr 3165752925], length 0

分析

一个完整建立的 TCP 连接图如下,每条线代表一条单工连接。

┌────────┐  R              W  ┌────────┐  R              W  ┌────────┐
│        │  ◄───────────────  │        │  ◄───────────────  │        │
│ Client │       UConn        │ Proxy  │        RConn       │ Server │
│        |  ───────────────►  │        │  ───────────────►  │        │
└────────┘  W              R  └────────┘  W              R  └────────┘

对于 Proxy 而言,需要将一条连接的包传递至另外一条连接,收到数据包则进行转发,读取到 EOF 则关闭另一条连接的写(也可以关闭本连接的读,多调用一次系统调用)

整个关闭的流程由 Client(Server 同样适用) 发起,是一个击鼓传花的过程:

  1. Client 关闭 UConn 连接的写端(或读端,后续数据写入报错则进入错误处理)
  2. Proxy 收到 UConn 的 EOF,关闭 RConn 连接的写端
  3. Server 收到 RConn 的 EOF,关闭 RConn 连接的写端
  4. Proxy 收到 RConn 的 EOF,关闭 UConn 连接的写端
  5. 所有单工连接被关闭,连接代理完成

核心实现

直接拿 docker-proxy 的实现修改一下,额外支持了主动退出的逻辑。

  • from.CloseRead() 这行代码可以不需要,已经 EOF,这条连接不会再出现数据了。
  • 读取或者写入失败的场景全部包含在 io.Copy 中,并且忽略了错误处理,尽可能减小两个代理过程的相互影响。
func ConnCat(ctx context.Context, client *net.TCPConn, backend *net.TCPConn) {
	var wg sync.WaitGroup

	broker := func(to, from *net.TCPConn) {
		io.Copy(to, from)
		from.CloseRead()
		to.CloseWrite()
		wg.Done()
	}

	wg.Add(2)
	go broker(client, backend)
	go broker(backend, client)

	finish := make(chan struct{})
	go func() {
		wg.Wait()
		close(finish)
	}()

	select {
	case <-ctx.Done():
	case <-finish:
	}
	client.Close()
	backend.Close()
	<-finish
}

参考

  1. https://github.com/moby/moby/blob/master/cmd/docker-proxy/tcp_proxy_linux.go#L27, docker-proxy 的 tcp 代理实现

标签:127.0,0.1,12345,妥善处理,TCP,关闭,Go,nop,连接
From: https://www.cnblogs.com/shuqin/p/18500231

相关文章

  • 在 Go 中,如何实现一个带过期时间的字典映射
    有些时候,应用系统用不上redis,我们也可以用锁和goroutine实现一个带有过期时间的线程安全的字典。这种字典的应用场景,比较倾向于数据规模较小,没有分布式要求。下面是实现:1、定义结构typeItemstruct{valueinterface{}expireAtint64}typeTTLM......
  • Django中的ModelForm组件
    昨天开发项目的时候,发现在表单创建时,流程很繁琐,想这有没有简易方式去创建表单,结果查资料,发现django提供了一个非常简单实用且人性化的组件modelform,用起来贼快,还能做表单校验,很爽,记录一下。在Django中,modelform是一个非常有用的功能,它允许你基于Django的模型(Model)自动......
  • Go 中,`...` 运算符
    在Go语言中,...运算符有两个主要用途,分别用于变长参数函数和切片展开。1.变长参数函数在Go语言中,使用...运算符可以定义一个接受可变数量参数的函数,也就是“变长参数函数”。这种函数可以接收不确定数量的参数,并将这些参数当作切片来处理。语法:funcfunctionName(args......
  • Go语言中的位运算符
    位运算(bitwiseoperations)是计算机科学中非常基础且重要的运算类型,它直接操作二进制位。Go语言中提供了一组位运算符,用于执行位级别的操作。Go语言中的位运算符按位与(&):作用:对两个操作数的每个位进行与运算,只有对应位都为1时,结果位才为1。示例:5&3(0101&0011=0001),结......
  • Go 语言中的 切片 --slice
    为了更好地理解Go语言中的切片(slice),我们可以将它与C++中的数组或容器(如std::vector)进行比较,但要注意的是,它们之间有一些关键的区别。让我们逐步将Go的切片与C++中的概念进行对应:1.数组vs切片在C++中,数组(array)是一种固定大小的数据结构,大小必须在编译时确定,并且......
  • GO:可变长参数和切片作为函数参数
    在Go语言中,可变函数参数(可变长参数)和切片作为函数参数是两个不同的概念,虽然它们都能处理多个元素,但它们的用途和处理方式有所不同。以下是它们之间的详细区别:1.可变函数参数(VariadicFunctionParameters)可变参数函数可以接受不定数量的参数,使用...运算符来定义。这些参数......
  • Go语言中的range
    在Go语言中,range是一个用于遍历各种数据结构(如数组、切片、字符串、map和通道)的关键字。range可以返回一个索引和值,或者是键和值,具体取决于你遍历的是什么类型的数据结构。1.range的用法range常用于for循环,来遍历集合中的元素。它可以遍历数组、切片、字符串、map、甚至......
  • net.ipv4.tcp_tw_recycle = 1会导致什么问题产生
    net.ipv4.tcp_tw_recycle=1在Linux系统中启用时,会导致一系列问题,特别是在涉及NAT(网络地址转换)和负载均衡的环境中。以下是该设置可能导致的几个主要问题:1.NAT环境下的连接问题在NAT环境中,多个客户端可能通过同一个公网IP地址访问服务器。当tcp_tw_recycle启用时,服务器可能......
  • 通过 PowerShell 添加网络打印机并创建一个标准 TCP/IP 端口,您可以使用 Add-PrinterPo
    通过PowerShell添加网络打印机并创建一个标准TCP/IP端口,您可以使用Add-PrinterPort和Add-Printercmdlet。以下是一个详细的示例,演示了如何创建TCP/IP端口并添加网络打印机。步骤创建TCP/IP端口添加打印机示例代码powershellCopyCode#设置打印机的IP地址和......
  • Golang 中使用 JSON 的一些小技巧
    临时忽略struct字段typeUserstruct{Emailstring`json:"email"`Passwordstring`json:"password"`//manymorefields…}临时忽略掉Password字段json.Marshal(struct{*UserPasswordbool`json:"password,omitempty"`}{Us......