不能,因为paho.mqtt.golang是客户端sdk,非服务端框架;每设备连接新建一个client会导致goroutine泛滥、内存暴涨、无法复用net.conn、acl/路由/retain需重复实现,且qos2状态无法统一管理。

直接用 paho.mqtt.golang 写十万级 MQTT 网关,不出三天就会被 goroutine 泛滥和内存暴涨拖垮——它根本不是为服务端设计的,而是客户端 SDK。
为什么不能把 paho.mqtt.golang 当网关用
你每 accept 一个设备连接,就 new 一个 paho.mqtt.golang.Client,等于在服务端硬生生模拟了十万台“客户端”。后果很直接:
-
Connect()是阻塞调用,握手期间 goroutine 挂起,无法复用; - 不暴露底层
net.Conn,没法做 TLS 分流、报文预检、连接限速等网关级控制; - ACL、路由、retain 消息投递逻辑全得自己重写,等于在别人封装好的客户端里再造一遍服务端;
- QoS 2 的状态管理分散在每个 client 实例中,无法统一协调,超时/重传极易错乱。
真实压测中,5 万设备连接后,runtime.NumGoroutine() 超过 12 万,GC 频率飙升到每秒 3–5 次,延迟毛刺明显——这不是业务问题,是架构误用。
gnet.React 中安全解析 MQTT CONNECT 报文
MQTT 协议头前 2 字节固定,但剩余长度字段最多占 4 字节(可变字节数),直接 conn.Read(buf[:2]) 后就 decode,极大概率 panic 或读错位。必须手动缓冲 + 定长提取。
- 在
gnet.OnOpened里给每个连接分配固定 buffer(如 1024 字节),绑定到c.Context(),避免每次 React 都 malloc; - React 回调开头先检查
ctx.offset ,不够就 <code>return nil, gnet.None,等下次事件触发; - 解析完前 2 字节后,按 MQTT 规范循环计算剩余长度字段实际字节数(
(b & 0x7F) ),再判断当前 <code>ctx.offset是否 ≥ 总长度; - 不足就暂存已读数据,不 panic、不丢包、不阻塞 event-loop。
严禁在 React 里调 fmt.Println、time.Sleep 或 DB 查询——整个 gnet event-loop 会卡死。
连接生命周期必须显式管控
gnet 不帮你管连接超时、心跳、异常关闭,全靠你自己在 React 和 OnClosed 里补全逻辑,否则设备掉线后连接长期滞留,fd 和内存只增不减。
- 每次成功读取后,更新
lastActive时间戳(存在c.Context()); - 用后台 goroutine 批量扫描空闲连接(比如每 5 秒扫一次),对超时连接调
c.Close(); - 在
OnClosed里清理 session、释放 buffer、从路由表中移除设备 ID; - 务必设置
conn.SetReadDeadline和conn.SetWriteDeadline,否则弱网设备可能让协程永久 hang 在Read()上。
实测发现:没加 SetReadDeadline 时,TIME_WAIT 连接数在 2 小时内涨到 4.2 万;加上后稳定在 200 以内。
Linux 系统层参数不调,gnet 再快也白搭
Go 代码写得再精妙,卡在 OS 层就全失效。两个参数必须改,且要生效于 systemd service 级别:
-
/proc/sys/net/core/somaxconn至少设为65535(默认常为 128),否则 listen 完成队列溢出,新连接被内核静默丢弃,现象是设备 connect() 随机超时; -
ulimit -n必须 > 实际连接数 × 2,建议设为1048576,且必须在.service文件里加LimitNOFILE=1048576——仅在 shell 里 ulimit 不影响 daemon 进程。
真正难的从来不是写 React 函数,而是把 conn.SetKeepAlive、TCP_USER_TIMEOUT、buffer 复用、session 缓存这四件事串成闭环。漏掉任意一环,十万连接都撑不过首轮压测。











