如何妥善处理 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 代理实现

[Go] 如何妥善处理 TCP 代理中连接的关闭的更多相关文章

  1. 网络编程中 TCP 半开连接和TIME_WAIT 学习

    https://blog.csdn.net/chrisnotfound/article/details/80112736 上面的链接就是说明来 SO_KEEPALIVE 选项 为什么还需要 在应用层开 ...

  2. nginx : TCP代理和负载均衡的stream模块

    一直以来,Nginx 并不支持tcp协议,所以后台的一些基于TCP的业务就只能通过其他高可用负载软件来完成了,比如Haproxy. 这算是一个nginx比较明显的缺憾.不过,在1.90发布后这个认知将 ...

  3. iOS进阶之TCP代理鉴权过程

    这段时间接触了网络代理,而自己的任务是完成TCP和UDP的网络代理,所以在这里写些自己的理解吧. 这篇文章先介绍一下TCP代理的鉴权过程(采用的是用户名和密码鉴权),下一篇文章再介绍UDP代理的鉴权过 ...

  4. Nginx多进程高并发、低时延、高可靠机制在缓存(redis、memcache)twemproxy代理中的应用

    1. 开发背景 现有开源缓存代理中间件有twemproxy.codis等,其中twemproxy为单进程单线程模型,只支持memcache单机版和redis单机版,都不支持集群版功能. 由于twemp ...

  5. Nginx多进程高并发、低时延、高可靠机制在缓存代理中的应用

    1. 开发背景 现有开源缓存代理中间件有twemproxy.codis等,其中twemproxy为单进程单线程模型,只支持memcache单机版和redis单机版,都不支持集群版功能. 由于twemp ...

  6. Nginx多进程高并发、低时延、高可靠机制缓存代理中的应用

    1. 开发背景 现有开源缓存代理中间件有twemproxy.codis等,其中twemproxy为单进程单线程模型,只支持memcache单机版和redis单机版,都不支持集群版功能. 由于twemp ...

  7. 京东的Netty实践,京麦TCP网关长连接容器架构

    背景 早期京麦搭建 HTTP 和 TCP 长连接功能主要用于消息通知的推送,并未应用于 API 网关.随着逐步对 NIO 的深入学习和对 Netty 框架的了解,以及对系统通信稳定能力越来越高的要求, ...

  8. tcp 代理的作用

    http://www.h3c.com.cn/Service/Document_Center/IP_Security/FW_VPN/F1000-E/Configure/Operation_Manual/ ...

  9. Black Hat Python之#2:TCP代理

    在本科做毕设的时候就接触到TCP代理这东西,当时需要使用代理来对发送和收到的数据做修改,同时使用代理也让我对HTTP协议有了更深的了解. TCP Proxy用到的一个主要的东西就是socket.pro ...

  10. Envoy 代理中的请求的生命周期

    Envoy 代理中的请求的生命周期 翻译自Envoy官方文档. 目录 Envoy 代理中的请求的生命周期 术语 网络拓扑 配置 高层架构 请求流 总览 1.Listener TCP连接的接收 2.监听 ...

随机推荐

  1. 【转载】 实时调度论文中经常出现的术语 ties broken arbitrary的意思 —— 看伪代码时出现 ties broken arbitrary

    看伪代码时突然看到这样的一个Ps标注, ties broken arbitrary,  不明白是啥意思,后来看到下文:https://blog.csdn.net/kangkanglhb88008/ar ...

  2. Apache SeaTunnel 及 Web 功能部署指南(小白版)

    在大数据处理领域,Apache SeaTunnel 已成为一款备受青睐的开源数据集成平台,它不仅可以基于Apache Spark和Flink,而且还有社区单独开发专属数据集成的Zeta引擎,提供了强大 ...

  3. 关于vscode自动格式化的坑(Prettier - Code formatter)

    在入坑vscode的时候在网上找了一些扩展包,其中有一款名为Prettier - Code formatter的代码格式化工具,其作用为当按下ctrl+s的时候自动进行格式化(当你进行格式化操作的时候 ...

  4. 折腾 Quickwit,Rust 编写的分布式搜索引擎 - 从不同的来源摄取数据

    摄取 API 在这节教程中,我们将介绍如何使用 Ingest API 向 Quickwit 发送数据. 要跟随这节教程,您需要有一个本地的 Quickwit 实例正在运行. https://quick ...

  5. 使用 iRingo 解锁本该属于你的苹果服务

    为什么别人的 Spotlight 可以通过航班号查询航班信息,而我的不行?为什么别人的 Spotlight 可以直接看英超联赛的比分信息?为什么我的 Apple News 打不开?这其实是因为这些功能 ...

  6. Redis实战之session共享

    当线上集群时候,会出现session共享问题. 虽然Tomcat提供了session copy的功能,但是缺点比较明显: 1:当Tomcat多的时候,session需要大量同步到多台集群上,占用内网宽 ...

  7. Docker网络上篇-网络介绍

    通过前面的学习,我们已经可以把自己写的微服务项目通过dockerfile文件方式部署到docker上面了.那么微服务之间通信,怎么通信的?是在同一个网络还是在不同的网络环境下?docker中怎么配置网 ...

  8. 音视频FAQ(三):音画不同步

    摘要 本文介绍了音画不同步问题的五个因素:编码和封装阶段.网络传输阶段.播放器中的处理阶段.源内容产生的问题以及转码和编辑.针对这些因素,提出了相应的解决方案,如使用标准化工具.选择强大的传输协议.自 ...

  9. Angular 16+ 高级教程 – Angular 和其它技术方案的比较

    前言 上一篇我有提到 Angular 适合用于哪些项目,但讲的太含糊,什么大中小项目的...这篇我将更具体的去讲解,Angular 的定位,还有它和其它方案的优缺点. Web 技术可以用来做许许多多不 ...

  10. 月薪20k以上的软件测试工程师的必备知识点?全部拿走吧!

    我们都知道作为一个软件测试工程师,入门相对比较简单,但是要达到技术精通,甚至薪资能达到20k以上的话,那绝对需要对测试开发有一个系统的了解,以及对这些系统的知识能够熟练掌握. 今天的话是我从阿里以为做 ...