如何妥善处理 TCP 代理中连接的关闭
相比较于直接关闭 TCP 连接,只关闭 TCP 连接读写使用单工连接的场景较少,但通用的 TCP 代理也需要考虑这部分场景。
背景
今天在看老代码的时候,发现一个 TCP 代理的核心函数实现的比较粗糙,收到 EOF 后直接粗暴关闭两条 TCP 连接。
一般场景下是感知不到问题的,但是做为一个代理,应该只透传客户端/服务端的行为,多余的动作不应该发生,比如客户端关闭写,代理只需要把关闭传递给服务端即可。
连接关闭
调用 close 关闭连接是通用做法,相关的还有一个 shutdown 系统调用。shutdown 与 close 相比可以更精细的控制连接的读写,但是不负责 fd 资源的释放,换而言之,无论是否调用 shutdown, close 最后都是需要调用的。
对于 shutdown 第二参数的说明
SHUT_RD 连接关闭读,仍然可以继续写。
SHUT_WR 连接关闭写,仍然可以继续读;并且会发送一个 FIN 包。
SHUT_RDWR 连接读写都被关闭;并且会发送一个 FIN 包。
对于上层应用而言,只需要关注 read 的结果,收到 FIN(也就是 EOF)虽然不能判断对端是关闭读写还是只关闭写,但后续处理并不会受影响。
根据读取数据来处理后续逻辑
判断已读数据是否符合预期来决定 关闭连接 或者 写入数据
向连接写入数据,失败的话直接关闭连接即可,不失败的话当前连接则为单工模式
关闭连接
Go 中连接关闭读写的示例
测试代码展示两个 TCP 连接分别关闭读(写)再进行写(读)
通过 tcpdump 抓包也可以看到 CloseWrite 会发送一个 FIN 包
分析
一个完整建立的 TCP 连接图如下,每条线代表一条单工连接。
对于 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
中,并且忽略了错误处理,尽可能减小两个代理过程的相互影响。
文章转载自:文一路挖坑侠
评论