[Go] 如何妥善处理 TCP 代理中连接的关闭
如何妥善处理 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 系统调用。shutdown 与 close 相比可以更精细的控制连接的读写,但是不负责 fd 资源的释放,换而言之,无论是否调用 shutdown, close 最后都是需要调用的。
对于 shutdown 第二参数的说明
- SHUT_RD 连接关闭读,仍然可以继续写。
- SHUT_WR 连接关闭写,仍然可以继续读;并且会发送一个 FIN 包。
- SHUT_RDWR 连接读写都被关闭;并且会发送一个 FIN 包。
对于上层应用而言,只需要关注 read 的结果,收到 FIN(也就是 EOF)虽然不能判断对端是关闭读写还是只关闭写,但后续处理并不会受影响。
根据读取数据来处理后续逻辑
- 判断已读数据是否符合预期来决定 关闭连接 或者 写入数据
- 向连接写入数据,失败的话直接关闭连接即可,不失败的话当前连接则为单工模式
- 关闭连接
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 同样适用) 发起,是一个击鼓传花的过程:
- Client 关闭 UConn 连接的写端(或读端,后续数据写入报错则进入错误处理)
- Proxy 收到 UConn 的 EOF,关闭 RConn 连接的写端
- Server 收到 RConn 的 EOF,关闭 RConn 连接的写端
- Proxy 收到 RConn 的 EOF,关闭 UConn 连接的写端
- 所有单工连接被关闭,连接代理完成
核心实现
直接拿 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
}
参考
- https://github.com/moby/moby/blob/master/cmd/docker-proxy/tcp_proxy_linux.go#L27, docker-proxy 的 tcp 代理实现
[Go] 如何妥善处理 TCP 代理中连接的关闭的更多相关文章
- 网络编程中 TCP 半开连接和TIME_WAIT 学习
https://blog.csdn.net/chrisnotfound/article/details/80112736 上面的链接就是说明来 SO_KEEPALIVE 选项 为什么还需要 在应用层开 ...
- nginx : TCP代理和负载均衡的stream模块
一直以来,Nginx 并不支持tcp协议,所以后台的一些基于TCP的业务就只能通过其他高可用负载软件来完成了,比如Haproxy. 这算是一个nginx比较明显的缺憾.不过,在1.90发布后这个认知将 ...
- iOS进阶之TCP代理鉴权过程
这段时间接触了网络代理,而自己的任务是完成TCP和UDP的网络代理,所以在这里写些自己的理解吧. 这篇文章先介绍一下TCP代理的鉴权过程(采用的是用户名和密码鉴权),下一篇文章再介绍UDP代理的鉴权过 ...
- Nginx多进程高并发、低时延、高可靠机制在缓存(redis、memcache)twemproxy代理中的应用
1. 开发背景 现有开源缓存代理中间件有twemproxy.codis等,其中twemproxy为单进程单线程模型,只支持memcache单机版和redis单机版,都不支持集群版功能. 由于twemp ...
- Nginx多进程高并发、低时延、高可靠机制在缓存代理中的应用
1. 开发背景 现有开源缓存代理中间件有twemproxy.codis等,其中twemproxy为单进程单线程模型,只支持memcache单机版和redis单机版,都不支持集群版功能. 由于twemp ...
- Nginx多进程高并发、低时延、高可靠机制缓存代理中的应用
1. 开发背景 现有开源缓存代理中间件有twemproxy.codis等,其中twemproxy为单进程单线程模型,只支持memcache单机版和redis单机版,都不支持集群版功能. 由于twemp ...
- 京东的Netty实践,京麦TCP网关长连接容器架构
背景 早期京麦搭建 HTTP 和 TCP 长连接功能主要用于消息通知的推送,并未应用于 API 网关.随着逐步对 NIO 的深入学习和对 Netty 框架的了解,以及对系统通信稳定能力越来越高的要求, ...
- tcp 代理的作用
http://www.h3c.com.cn/Service/Document_Center/IP_Security/FW_VPN/F1000-E/Configure/Operation_Manual/ ...
- Black Hat Python之#2:TCP代理
在本科做毕设的时候就接触到TCP代理这东西,当时需要使用代理来对发送和收到的数据做修改,同时使用代理也让我对HTTP协议有了更深的了解. TCP Proxy用到的一个主要的东西就是socket.pro ...
- Envoy 代理中的请求的生命周期
Envoy 代理中的请求的生命周期 翻译自Envoy官方文档. 目录 Envoy 代理中的请求的生命周期 术语 网络拓扑 配置 高层架构 请求流 总览 1.Listener TCP连接的接收 2.监听 ...
随机推荐
- mybatis-plus系统化学习之配置精讲
1.背景 mybatis-plus给出了很多配置, 大部分的配置使用默认的就可以了, 但是还是有很多需要的配置比如: # mybatis-plus相关配置 mybatis-plus: # xml扫描, ...
- Java项目生产启动、关闭脚本
1.直接启动 #!/bin/bash #这里可替换为你自己的执行程序,其他代码无需更改 APP_NAME=XXXX-api-1.0.jar #使用说明,用来提示输入参数 usage() { echo ...
- Django框架创建运行最小程序过程记录
基于 python语言 Django web框架下 用pycharm创建,修改,运行 最简单程序.旨在过程 ========================================== 步骤一 ...
- 万字长文带你了解Java日志框架使用Java日志框架
大家好,我是晓凡 一.日志概念 日志的重要性不用我多说了,日志,简单来说就是记录. 用来记录程序运行时发生的事情.比如,程序启动了.执行了某个操作.遇到了问题等等,这些都可以通过日志记录下来. 想象一 ...
- 别再被坑了! JavaScript类型检测的最佳实践
别再被坑了! JavaScript类型检测的最佳实践 在 JavaScript 中,我们经常需要判断一个变量的类型.这个需求在编程中非常常见,因为不同类型的数据会影响到我们的代码逻辑. JavaScr ...
- 【CMake系列】10-cmake测试 ctest
cmake作为一个强大的构建系统指导工具,同时也提供了测试功能,可用于项目的单元测试等,也可以与其他测试框架协作,如googletest,共同完成项目开发中的测试工作,本节我们就来学习 如何借助cma ...
- Go 进程在容器中无 coredump 产生问题分析
Go 进程在容器中无 coredump 产生问题分析 0x01 起因 coredump 作为一种非常重要的高度手段,在日常开发中经常用到,切换到容器环境后一直没关注.最近测试了下,发现出不了 core ...
- WPF 设备输入事件封装
本文主要介绍WPF应用对鼠标输入.触摸屏触笔以及触摸事件的封装 之前有简单说明设备输入类型 WPF 屏幕点击的设备类型 - 唐宋元明清2188 - 博客园 (cnblogs.com) 1.鼠标 - 通 ...
- Python中的分布式框架Ray的安装与使用
技术背景 假设我们在一个局域网内有多台工作站(不是服务器),那么有没有一个简单的方案可以实现一个小集群,提交分布式的任务呢?Ray为我们提供了一个很好的解决方案,允许你通过conda和Python灵活 ...
- 【Python自动化】之特殊的自动化定位操作
今天有时间了,想好好的把之前遇到过的自动化问题总结一下,以后有新的总结再更新 目录: 一.上传文件(4.11) 二.下拉框选择(4.11) 1.Select下拉框 2.非Select下拉框 三.下拉框 ...