本文详解 go 反向代理场景下连接复用的常见误区,指出 tcp 连接不可“恢复性复用”,强调断连后必须重建连接,并提供基于 `io.copy` + 双通道协调的健壮代理模式实现。
在构建 NAT 穿透类反向代理(如:内网设备通过第三方服务器暴露服务)时,一个典型架构是:
此时极易陷入一个关键误区:试图“复用”已
关闭或出错的 net.Conn。你的日志 [DEBUG] socks: Copied 0 bytes to client 正是典型症状——io.Copy 返回 n=0, err=nil 或 use of closed network connection,说明连接已处于不可用状态,继续写入将失败或静默丢包。
Go 的 net.Conn 是一次性资源。除极少数临时错误(如 net.Error.Temporary() 返回 true 的超时、拒绝等),任何读/写错误(包括对端关闭、RST、EOF、use of closed network connection)均表示该连接已终止,无法安全恢复。强行重用会导致:
应采用 “连接生命周期隔离” 原则:每个客户端会话独占一对连接(srcConn ↔ dstConn),任一端异常即整体退出,不尝试挽救旧连接。核心逻辑如下:
func Proxy(srcConn, dstConn net.Conn) {
// 使用两个 channel 同步两端关闭事件
srcDone := make(chan struct{})
dstDone := make(chan struct{})
// 并发双向转发
go copyAndClose(srcConn, dstConn, srcDone)
go copyAndClose(dstConn, srcConn, dstDone)
// 任一端关闭,立即通知另一端停止读取(优雅中断)
select {
case <-srcDone:
dstConn.CloseRead() // 阻止 dst 继续读 src
<-dstDone // 等待 dst 完全退出
case <-dstDone:
srcConn.CloseRead()
<-srcDone
}
}
// copyAndClose 封装 io.Copy + 关闭逻辑
func copyAndClose(src, dst net.Conn, done chan<- struct{}) {
_, err := io.Copy(dst, src)
if err != nil && err != io.EOF {
log.Printf("Copy error: %v", err)
}
// 关闭 src 的读端(不影响 dst 写入),触发对方 read loop 退出
if err := src.Close(); err != nil {
log.Printf("Close error: %v", err)
}
done <- struct{}{}
}TCP 连接不是可重置的状态机,而是有明确生命周期的资源。反向代理的健壮性不在于“挽救失败连接”,而在于快速识别失败、干净释放资源、及时建立新连接。遵循“一请求一连接”原则,配合 io.Copy + CloseRead() 协同关闭模式,即可构建高可用、易调试的 Go 代理服务。