io.Copy单向阻塞导致TCP双向代理卡死或丢数据,须用errgroup并发启动两个goroutine分别处理双向流量,并共享context、统一关闭连接、禁用Nagle算法。

为什么 io.Copy 直接转发会卡住或丢数据
因为 io.Copy 是单向阻塞复制,TCP 双向代理必须同时处理客户端→服务端和服务端→客户端两路流量,只调一个 io.Copy 会让其中一端等在 EOF 或缓冲区满上,连接就僵死。
- 典型现象:
curl发请求后无响应,或只返回部分响应体 - 根本原因:TCP 是全双工,但
io.Copy不自动“双向接力”,需显式并发启动两个副本 - 必须用
go io.Copy(...)启两个 goroutine,否则任一方向卡住都会拖垮整个连接 - 注意别漏掉
defer conn.Close()和defer remote.Close(),否则连接泄漏极快
如何正确启动双向 io.Copy 并捕获错误
不能只等一个 io.Copy 返回,得用 sync.WaitGroup 或 errgroup.Group 汇总两路错误——否则某一路提前出错(比如远程服务断连),另一路还在跑,主 goroutine 就无法感知和退出。
- 推荐用
errgroup.Group(需引入golang.org/x/sync/errgroup),它天然支持并发、错误传播和上下文取消 - 两路
io.Copy必须共享同一个context.Context,否则超时或中断信号无法同步生效 -
io.Copy返回io.EOF是正常结束,不是错误;真正要关注的是net.OpError、io.ErrUnexpectedEOF这类 - 示例关键片段:
eg, _ := errgroup.WithContext(ctx)<br>eg.Go(func() error { return io.Copy(remote, conn) })<br>eg.Go(func() error { return io.Copy(conn, remote) })<br>err := eg.Wait()
net.Conn 关闭时机与资源泄漏风险
很多人在 io.Copy 返回后立刻 Close(),但 goroutine 可能还在读写,导致 “use of closed network connection” panic,或更隐蔽的 syscall 错误。
- 必须等两个
io.Copy都返回后再统一关闭两端连接 - 用
errgroup的Wait()就是为此服务的——它确保两路都结束才返回 - 别在 handler 里用
defer conn.Close(),这会关早了;应在eg.Wait()后显式关 - 如果代理链路中加了 TLS 或自定义 buffer,记得也一并清理,比如
bufio.Reader不会自动关底层Conn
性能瓶颈常出在哪儿:缓冲区、Nagle 和 KeepAlive
默认 io.Copy 用 32KB 缓冲区,对小包多的场景(如 WebSocket 握手、HTTP/1.1 头部)不够用,容易放大延迟;而 Nagle 算法又可能把多个小包攒一起发,破坏实时性。
立即学习“go语言免费学习笔记(深入)”;
- 对低延迟要求高的代理(如 SSH 转发),建议禁用 Nagle:
conn.SetNoDelay(true) - 远程连接也要设
SetKeepAlive,避免中间 NAT 设备静默断连 - 不用改
io.Copy缓冲区大小——它内部已优化;真要调优,优先从SetReadBuffer/SetWriteBuffer入手 - 实测发现:未设
SetNoDelay时,SSH 连接偶尔卡 200ms,加上后稳定在 1–5ms
最易被忽略的是 context 生命周期管理——如果代理长期运行,每个连接都要绑定带 timeout 的 context.WithTimeout,否则一个卡死的连接会永久占着 goroutine 和文件描述符。










