用 io.copy 单向转发一定会卡住或丢数据,必须用两个 goroutine 并发处理双向流,且错误、超时、连接关闭都得手动管;因为 io.copy 阻塞等待 io.eof,而 tcp 不自然终止,导致一端卡死、响应无法回传。

直接说结论:用 io.Copy 单向转发一定会卡住或丢数据,必须用两个 goroutine 并发处理双向流,且错误、超时、连接关闭都得手动管。
为什么 io.Copy 直接转发会卡死或丢响应
因为 io.Copy 是单向阻塞复制,它会一直等到读端返回 io.EOF 才结束。但 TCP 连接不会自动发 EOF —— 客户端发完请求可能还在等响应,服务端响应发了一半也可能暂停。结果就是一端卡在 io.Copy 上,另一端的数据永远发不出来。
典型现象:curl http://localhost:8080 无响应、SSH 登录后立即断开、HTTP 响应只收到 header 没有 body。
- 这不是 bug,是 TCP 全双工特性和
io.Copy语义不匹配导致的 - 别指望加个 buffer 或改
io.CopyN能解决,根本不在一个层面 - 哪怕目标服务正常,转发器本身也会因单向 copy 变成“哑巴中继”
怎么写正确的双向转发逻辑
核心就三件事:开两个 goroutine、共享上下文、统一关连接。不能靠 defer 在 handler 里关,也不能等一个 io.Copy 返回就退出。
- 用
errgroup.Group(导入golang.org/x/sync/errgroup)最省心:它自动汇总两路错误,且支持context.Context取消信号 - 两路 copy 必须写成:
io.Copy(remote, conn)(本地→远端)和io.Copy(conn, remote)(远端→本地) - 别漏掉
remote.Close()和conn.Close()—— 但必须在eg.Wait()之后,否则 goroutine 还在读写就关连接,会 panic - 每个新连接都要设
SetReadDeadline和SetWriteDeadline,比如 5 分钟,不然网络中断后连接永远 hang 着
监听地址和端口绑定常见坑
本地测试通了,换台机器连不上?八成是监听地址写死了。
-
net.Listen("tcp", "127.0.0.1:8080")只监听回环,外部机器无法访问;对外服务必须用":8080"(即0.0.0.0:8080) - 绑定
:80或:443会报bind: permission denied—— 普通用户没权限,要么切 root,要么改用非特权端口如:8080 - 反复重启时报
address already in use?不是代码问题,是 TIME_WAIT 导致端口未释放;Linux/macOS 下可用net.ListenConfig+SO_REUSEADDR解决
转发 HTTPS 流量时浏览器报错 ERR_SSL_PROTOCOL_ERROR 怎么办
这个错误和转发器本身无关,它只是透传字节流。真正出问题的是你访问的方式。
- 如果你把
localhost:8080转发到远程example.com:443,但浏览器输的是http://localhost:8080,那肯定失败 —— 你该输https://localhost:8080 - 证书校验失败?不是转发器的问题,是浏览器检查的是
localhost的证书,不是example.com的;要绕过就得加--unsafely-treat-insecure-origin-as-secure启动浏览器(仅测试) - 别试图在转发层解密 TLS —— 那就不是端口转发,是 MITM 代理,需要证书注入和协议解析,完全另一个复杂度
真正容易被忽略的点是:errgroup.Wait() 返回后才关连接,而很多人习惯在 goroutine 里 defer 关,结果两路流还没跑完,连接就被提前释放了。这个时机差一点,整条链路就不可靠。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











