io.copy 直接用于 tcp 会卡住或丢数据,因其阻塞等待 io.eof 而 tcp 不自然终止;需双 goroutine 并行转发、waitgroup 等待、错误检查及主动关闭,并启用 keepalive 防假连接。

为什么 io.Copy 直接用在 TCP 连接上会卡住或丢数据
因为 io.Copy 默认阻塞直到读端返回 io.EOF,而 TCP 连接不会自然 EOF —— 它可能只是暂时没数据,也可能一端已关闭但另一端还不知道。直接套用会导致转发卡死、连接无法优雅关闭。
- 必须配合
sync.WaitGroup或context.WithCancel控制两个方向的拷贝生命周期 - 不能只启动一个
io.Copy:客户端到服务端、服务端到客户端需并行运行,否则单向阻塞就断流 - 忽略错误直接 return 会导致 goroutine 泄漏,比如某边连接提前断开,另一边还在等
怎么用 net.Conn 实现双向非阻塞转发
核心是启动两个 goroutine,分别处理 src.Read → dst.Write 和反向流,并统一监听任一连接关闭或出错信号。
- 用
sync.WaitGroup等待两个io.Copy结束,再主动关闭两端连接 - 每个
io.Copy都要检查返回的error,常见如read: connection reset by peer、write: broken pipe,这些不是异常,是连接终止的正常信号 - 别在
io.Copy外层加超时(比如用time.AfterFunc关闭 conn),这会破坏 TCP 流的完整性;真要超时,得用conn.SetReadDeadline/SetWriteDeadline
go func() {
defer wg.Done()
_, err := io.Copy(dst, src)
if err != nil && err != io.EOF {
log.Printf("copy src→dst error: %v", err)
}
dst.Close()
}()
net.Listen 后要不要调用 SetKeepAlive
要,尤其在长连接转发场景下。Linux 默认 keepalive 时间是 2 小时,中间网络设备(NAT、防火墙)往往更早清空连接表,导致“假连接”——两端都以为通着,实际发包已被丢弃。
- 启用后,内核会在空闲连接上发送探测包,及时发现断连,让
io.Copy尽快返回错误 - 设置方式:
ln, _ := net.Listen("tcp", ":8080"); ln.(*net.TCPListener).SetKeepAlive(true) - 注意:Windows 上默认不开启,Linux 上默认开启但间隔太长;建议显式设为
true并调小间隔(需通过SetKeepAlivePeriod,Go 1.19+ 支持)
本地测试时 connection refused 但服务明明在跑
常见于绑定地址写错。比如用 net.Listen("tcp", "127.0.0.1:8080") 启动转发器,却从另一台机器连它 —— 这个地址只监听本地回环,外部不可达。
立即学习“go语言免费学习笔记(深入)”;
- 对外提供服务时,监听地址应为
":8080"(即0.0.0.0:8080),而不是"127.0.0.1:8080" - 若目标服务也在本机(如转发到
localhost:3000),确保目标服务监听的是127.0.0.1或0.0.0.0,而非仅::1(IPv6 回环) - 防火墙或 SELinux 可能拦截新端口,用
telnet localhost 8080先确认监听是否生效,再用ss -tlnp | grep :8080查看绑定地址
真正的难点不在拷贝本身,而在连接状态的同步:哪边先断、错误怎么归因、goroutine 怎么收干净。一个没关的 conn,就可能让整个转发链挂住几分钟才超时。










