net.Conn 长连接稳定依赖业务层精确控制超时、心跳与goroutine生命周期;SetReadDeadline需每次Read后重置,裸conn比WebSocket快6倍,goroutine泄漏比连接泄漏更致命,限流防重连风暴比扩容更有效。

net.Conn 本身不提供“长连接”语义,所谓海量设备稳定长连,靠的是业务层对超时、心跳、goroutine 生命周期的精确控制——不是加个 for 循环就能撑住百万级。
为什么 SetReadDeadline 必须每次读完立刻重置
很多人在 handleConn 开头设一次 conn.SetReadDeadline(time.Now().Add(30 * time.Second)) 就以为万事大吉,结果设备静默 25 秒后发包,服务端直接 read: connection reset by peer。根本原因是:SetReadDeadline() 只作用于**下一次** Read() 调用,且不可继承。一旦读成功,deadline 自动失效。
正确做法是:每次 n, err := conn.Read(buf) 成功后,立刻调用 conn.SetReadDeadline(time.Now().Add(30 * time.Second))。这相当于把“心跳检测权”交给读事件本身——有数据进来才续命,没数据就断,避免 NAT/SLB 静默回收后的假连接。
- 别在 goroutine 外部统一设置 deadline,多 goroutine 并发读会互相覆盖
- 客户端也需对称:发
ping后立即SetReadDeadline等pong,超时即重连 - 不要依赖
time.Sleep定时发 ping——网络丢包时,你发了但对方没收到,连接其实已死
裸 net.Conn 比 websocket.WriteMessage 快 6 倍以上
用 gorilla/websocket 对接资源受限设备(如 4G 模组跑 Lua),等于给嵌入式端加协议包袱。它的 WriteMessage 内部要加锁、拷贝 payload、掩码编码、分帧,单次调用平均多耗 0.3ms;而裸 conn.Write() 直写 socket buffer,压到 0.05ms 以内。
立即学习“go语言免费学习笔记(深入)”;
更关键的是协议适配成本:websocket 要求 HTTP 升级握手、维护帧状态,而多数物联网设备只认定长包头(如前 4 字节为 payload len)。用 bufio.Reader 配合 conn.SetReadDeadline 做定长解析,比 WebSocket 分帧逻辑更贴近硬件习惯,也更容易调试。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 设备端若不严格实现 Pong 回复,服务端还得额外起 goroutine 探活,反而引入不确定性
- 真要保活,优先用
conn.SetKeepAlive(true)+conn.SetKeepAlivePeriod(30 * time.Second)让内核处理 - 二进制协议头建议固定:4 字节长度(小端)+ 1 字节类型 + payload,避免 JSON 解析触发 GC 抖动
goroutine 泄漏比连接泄漏更致命
一个连接起两个 goroutine(一读一写)看似合理,但只要写 goroutine 还在往已关闭的 conn.Write() 发数据,就会卡死并持续占用栈内存。pprof 里常看到几百个 goroutine 堆在 writeLoop 里,而连接数却只有几十——这是典型的写 goroutine 未退出。
必须确保:读 goroutine 退出时,通过 channel 或 sync.Once 通知写 goroutine 退出,并等待其结束。不能只靠 defer conn.Close(),它关不了另一个 goroutine。
- 写操作务必加
conn.SetWriteDeadline(),否则慢设备或网络抖动会让 write 长期 hang 住 - 避免在 handler 里直接起 goroutine 处理业务逻辑(如查 DB),应投递到 worker pool,防止阻塞读循环
- 每条连接的 goroutine 数必须严格为 1(读)+ 0 或 1(写),且写 goroutine 必须可被优雅中断
限流防重连风暴比扩容更管用
设备批量掉线再重连,500 台设备在 1 秒内涌进,accept 队列打满、文件描述符耗尽、GC 频繁触发——这时加机器不如做限流。Go 的 net.Listener 没有内置连接数限制,得自己套一层。
推荐用 golang.org/x/net/netutil.LimitListener 包裹原始 listener,设 MaxOpenFiles 为系统允许上限的 80%,再配合连接速率限制(如每秒最多接受 50 个新连接)。超出的连接直接拒绝,让设备端指数退避重试,比让它连上来再崩溃更可控。
- 别信
http.Server.MaxConns,那是针对 HTTP 的,TCP 层要自己控 -
net/http默认配置在 500+ 设备高频上报时必然崩:MaxIdleConnsPerHost=2,短连接轮询会疯狂建连析构 - 真实瓶颈往往不在 CPU 或内存,而在内核态 TCP 栈开销和 file descriptor 耗尽
真正卡住高并发长连接的,从来不是语法或框架,而是对 SetReadDeadline 的误用、对 conn.Write 的盲目信任、以及对 goroutine 生命周期的放任。这些点不抠清楚,换再 fancy 的库也没用。

















