必须绕开http.Server用裸net.Conn——因HTTP/1.1连接复用机制不适应物联网高频小包场景,且http.Server同步阻塞、协议不兼容;需设Read/WriteDeadline、用io.ReadFull替代binary.Read、用context控制goroutine生命周期。
直接用 net/http 跑设备接入,延迟必崩——这不是 go 不行,是默认 http 模型根本不适配物联网长连接、高频小包、资源受限的真实场景。
为什么必须绕开 http.Server 用裸 net.Conn
HTTP/1.1 的连接复用机制(MaxIdleConnsPerHost 默认为 2)在设备数超 500、上报频率 >5Hz 时立刻失效。设备端若用短连接轮询,服务端每秒建连/析构 TCP 栈,内核态开销陡增;更关键的是,http.Server 的 handler 是同步阻塞执行的,一个设备查数据库卡住,后续所有请求排队等待。
真实设备协议(MQTT CONNECT、CoAP UDP 报文、NB-IoT 自定义二进制帧)根本不是 HTTP 流量,http.ReadRequest 会直接拒绝或阻塞解析,根本进不到业务逻辑。
- 监听统一
net.Listener,对每个accept()到的net.Conn启动独立 goroutine 处理 - 禁用所有
http.Server相关配置(ReadTimeout、WriteTimeout等在裸连接里无效) - 若需暴露管理接口(如健康检查、设备列表),另起一个独立
http.Server绑定不同端口,与设备接入层物理隔离
net.Conn 必须设 ReadDeadline 和 WriteDeadline
TCP 连接空闲时,对端静默断开(比如 NB-IoT 模组休眠),本端 conn.Read 会永久阻塞——这是 TCP 协议特性,不是 Go bug。不设 deadline,goroutine 就卡死,连接数涨不上去,P99 延迟从毫秒跳到秒级。
-
SetReadDeadline值应略大于设备心跳周期(例如心跳 30s,设为 45s),不能比心跳还短 -
Write前必须先检查连接是否活跃:可结合conn.RemoteAddr()+SetWriteDeadline(5 * time.Second),避免向已关闭连接发包触发系统重传 - 不要依赖
io.EOF判断断连;设备突然掉电、AP 断网时,Read可能返回nil, nil或 panic;应显式用errors.Is(err, os.ErrDeadlineExceeded)捕获超时
binary.Read 在协议解析中会隐式阻塞
很多私有协议用“4 字节长度 + payload”结构,习惯写 binary.Read(conn, binary.BigEndian, &length),但这个调用在数据未收全时会一直阻塞,直到超时或连接断开——它底层调用了 io.ReadFull,而 io.ReadFull 不受 SetReadDeadline 控制(除非你传入带 deadline 的 io.Reader)。
立即学习“go语言免费学习笔记(深入)”;
正确做法是手动用 io.ReadFull 配合 bytes.Buffer 或固定大小数组:
var header [4]byte
if _, err := io.ReadFull(conn, header[:]); err != nil {
if errors.Is(err, os.ErrDeadlineExceeded) {
conn.Close()
return
}
// 处理其他错误
}
length := binary.BigEndian.Uint32(header[:])
payload := make([]byte, length)
if _, err := io.ReadFull(conn, payload); err != nil {
// 同样检查超时
}- 永远不用
binary.Read直接读net.Conn,它不可控 - 定长头解析必须用
io.ReadFull,且每次调用前确保conn.SetReadDeadline已设置 - 避免在解析路径中分配大量小对象(如反复
make([]byte, N)),改用sync.Pool复用缓冲区
goroutine 生命周期必须由 context 显式控制
为每个连接起 go handleConn(conn) 很自然,但设备反复上下线时,旧连接的 goroutine 仍在轮询 Read,且无退出信号,几分钟内 goroutine 数可破 5 万,触发调度器雪崩。
- 每个连接处理逻辑必须包裹
ctx, cancel := context.WithCancel(context.Background()),断连或超时后显式调用cancel() - 禁止在
handleConn内部启动匿名 goroutine 处理业务(如上报 Kafka),必须通过 channel + 主循环统一派发,否则无法跟踪生命周期 - 上线时加
runtime.GOMAXPROCS(2 * runtime.NumCPU()),避免默认单核调度器在高并发下成为瓶颈 - 设备断连后,除了
conn.Close(),还要确保所有相关 channel 关闭、timer 停止、buffer 归还sync.Pool
延迟不是语言给的,是每个 SetReadDeadline、每次 io.ReadFull、每个 context.Cancel 一起抠出来的;最容易被忽略的,是那些没走到 defer conn.Close() 的异常分支——它们在后台 quietly 吃光文件描述符和内存。


















