为什么个人站长会需要一台自己的 MQTT 服务器
大多数站长对 MQTT 的印象还停留在"物联网专用协议",觉得自己一个写博客、放静态资源的站用不上。但只要你稍微往自动化方向走一步,就会碰到同一类需求:一台设备或一个脚本产生了事件,需要立刻、可靠地通知到另外若干个订阅方,而且这些订阅方可能随时上下线、网络还不太稳定。用 HTTP 轮询去实现这件事,延迟高、流量浪费大、代码里全是重试逻辑;用 WebSocket 自己搭一套,又要处理心跳、断线重连、消息去重、鉴权,工作量远超预期。
MQTT 恰好就是为这个场景设计的。它是基于发布/订阅模型的轻量消息协议,报文头部最小只有两个字节,天生带 QoS 分级、遗嘱消息、保留消息、会话保持这些特性。把它装在自己的 VPS 上,你可以让监控脚本、家里的传感器、手机上的自动化 App、服务器上的守护进程之间用一套统一的方式通信,而不用把数据交给第三方云服务。对站长来说,这可能意味着:服务器 CPU 超过阈值时主动推送一条消息到你手机;网站被 cc 攻击时自动触发告警;自建的服务上下线状态变化实时同步给多个订阅端。
本文用 EMQX 这个开源 MQTT Broker 作为落地对象,从 Docker 部署、客户端认证、ACL 授权、QoS 与保留消息的取舍、TLS 加密、到 Nginx 反向代理 WebSocket 接入,把一条完整的链路走通,最后总结几个真实踩过的坑。
EMQX 是什么,为什么不直接用 Mosquitto
Mosquitto 是更小更经典的 MQTT Broker,单进程、资源占用极小,个人用完全够。EMQX 则是一个用 Erlang/OTP 写的分布式 MQTT 平台,单机能扛百万级连接,集群能力、Web 管理面板、丰富的认证与授权插件、规则引擎、数据桥接都是它相对 Mosquitto 的优势。对个人站长而言,选 EMQX 的理由通常有三个:一是官方 Docker 镜像开箱即用、有可视化 Dashboard,省得全靠命令行配置;二是它的认证授权体系是"可插拔"的,内置了用户名密码、客户端 ID、JWT、HTTP 回调等多种方式,扩展性足够一个人从小玩到大;三是它同时支持 MQTT over TCP、MQTT over WebSocket、TLS 加密等多种接入方式,前端网页直接连 MQTT 这种玩法不需要额外做桥接。
代价是 EMQX 比 Mosquitto 吃内存(Erlang 虚拟机本身有基础开销,空跑大约 200MB 上下),以及配置项更多、概念更多。如果你只是拿它转发几路传感器数据,Mosquitto 更省事;但如果你希望这套东西能长期演进、需要 Web 面板和灵活的鉴权,EMQX 更合适。下面的部署以 EMQX 5.x 为例。
Docker 部署 EMQX:端口与目录规划
EMQX 主要用到这几个端口:1883 是 MQTT over TCP,8883 是 MQTT over TLS,8083 是 MQTT over WebSocket,8084 是 MQTT over WebSocket over TLS,18083 是 Dashboard 管理面板。个人站的部署原则依旧是把不该暴露的端口收回到回环或者内网,只把真正需要公网接入的端口开放出去。
docker run -d --name emqx \
--restart always \
-p 127.0.0.1:1883:1883 \
-p 127.0.0.1:8083:8083 \
-p 127.0.0.1:18083:18083 \
-v /opt/emqx/data:/opt/emqx/data \
-v /opt/emqx/log:/opt/emqx/log \
-v /opt/emqx/etc:/opt/emqx/etc \
emqx/emqx:5.8注意这里三个端口都绑定了 127.0.0.1,也就是说它们只在本机可达,公网访问不了。这么做是刻意的:Dashboard(18083)如果直接暴露公网,等于把整个消息平台的管理权交给扫描器——EMQX 默认管理员是 admin/public,无数机器人每天在扫这个组合。1883 和 8083 先收在内网,等 TLS 和鉴权都配好了,再通过 Nginx 反代或者防火墙规则有选择地放行。数据、日志、配置三个目录挂出来持久化,容器重建时数据不会丢。
启动后浏览器访问 http://服务器IP:18083 是打不开的(因为绑了回环),需要在 SSH 里做一次端口转发临时访问:
ssh -L 18083:127.0.0.1:18083 root@你的服务器然后本地浏览器打开 http://127.0.0.1:18083,用默认账号 admin / public 登录。第一件事就是改掉这个密码,Dashboard 里"系统设置 - 用户"里可以直接改。改完之后,即便哪天不小心把 18083 暴露出去,也不再是默认凭据。
客户端认证:别让任何人都能连上来
EMQX 默认允许匿名连接,也就是说任何人只要知道你的 IP 和端口,就能连上 broker 发布和订阅任意主题。这在公网环境下是灾难性的:攻击者可以订阅你所有的消息(含敏感数据),也可以往你的主题里灌垃圾消息。关闭匿名并启用认证是上线前的硬性要求。
EMQX 5.x 里内置了一套基于文件的密码认证(built-in database),配置路径是 Dashboard 的"访问控制 - 认证",也可以直接改配置文件。最简做法是在 Dashboard 里新增一个"密码认证"数据源,然后手动添加用户名和密码。EMQX 会把密码用加盐哈希存下来,不会明文存储。
如果要批量管理,用配置文件更清晰。EMQX 5.x 的配置文件是 HOCON 格式,认证链可以这样定义(示意,实际以你安装版本的字段为准):在认证器里指定 mechanism 为 password_based、backend 为 built_in_database,然后通过 Dashboard 或 REST API 导入用户。用户密码可以用 EMQX 自带的工具加密后再导入,避免明文经过脚本。
还有一个细节值得注意:客户端 ID(ClientID)的认证方式。如果你的场景里每个设备有稳定的唯一 ID,可以启用"客户端 ID 认证"作为第二道关,只放行白名单里的 ID,配合用户名密码形成组合防线。但要注意客户端 ID 欺骗是可能的(攻击者可以伪造别人的 ClientID),所以它只适合作为辅助手段,不能替代密码认证。
ACL 授权:连接上来之后能干什么
认证解决的是"你是谁、能不能连",授权(ACL)解决的是"连上之后你能发布/订阅哪些主题"。这两件事必须分开做。默认情况下,EMQX 允许已认证客户端访问所有主题,这在多用户场景下就是越权:A 用户能订阅 B 用户的隐私数据。
ACL 的核心是主题级别的规则。EMQX 支持按用户名、按客户端 ID、按 IP 写规则,规则的匹配对象是 MQTT 主题,支持通配符。常见的几条规则:
- 允许用户订阅
users/{username}/#,只看到自己命名空间下的主题; - 允许发布到
devices/{clientid}/data,防止设备冒充别的设备上报; - 拒绝订阅
$SYS/#,普通客户端不该看到 broker 的内部运行指标(连接数、消息吞吐、主题数等); - 默认拒绝(deny all),只显式放行需要的主题,白名单思路比黑名单安全得多。
EMQX 5.x 的 ACL 在"访问控制 - 授权"里配置,也有内置数据库、文件、HTTP 回调等后端。个人站用内置数据库或文件就够了。写规则时一定要记住匹配顺序是自上而下、第一条命中的规则生效,所以把具体的 allow 规则写在前面,最后的兜底 deny 写在最后。
QoS 等级与保留消息:可靠性和资源的取舍
MQTT 的 QoS 是最容易被误用的一组概念。它有三个等级:QoS 0 是"最多一次",发出即忘,可能丢消息;QoS 1 是"至少一次",靠 PUBACK 确认保证送达,但可能重复;QoS 2 是"恰好一次",四步握手保证不丢不重,但开销最大、延迟最高。
很多教程会说"重要消息用 QoS 2",这是不准确的。QoS 2 的四次握手在弱网下会显著拖慢吞吐,而且它只保证"传输层面不重复",业务层面的幂等仍然要自己做。个人站务实的做法是:传感器心跳、状态上报这类丢一条无所谓的数据用 QoS 0;告警通知、指令下发这类不能丢的用 QoS 1,同时在应用层做去重(消息带唯一 ID,收到重复 ID 直接丢弃);QoS 2 只在确实需要、且能承受其开销时用。
"保留消息"(Retained Message)是另一个高频误用点。当你以 retain 标志发布一条消息时,broker 会把它存下来,之后任何新订阅该主题的客户端都会立刻收到这条保留消息。这非常适合"设备当前状态"这种场景——新上线的监控面板一订阅就知道每台设备的最新状态,不用等下一轮上报。但保留消息是存在 broker 内存/磁盘里的,如果主题命名不规范(比如把时间戳拼进主题名),会积累大量永远没用的保留消息,把 broker 的内存吃掉。所以主题设计要"稳定、可枚举",状态类主题就固定几个,别让基数无限膨胀。
开启 TLS:让消息在公网加密传输
明文 MQTT 在公网上传输,用户名密码和消息内容都是明文的,随便一个中间人都能抓走。给 EMQX 配 TLS 只需要准备证书和私钥,然后在监听器(listener)配置里启用 8883 端口并指向证书。
证书可以用 Let's Encrypt 免费申请。要注意的是 MQTT over TLS 的证书校验:客户端默认会校验服务器证书的域名(SNI),所以证书的 CN/SAN 必须包含你连接时使用的域名,否则客户端会报证书不匹配。如果你用 IP 直连又想用免费证书(免费证书通常不签 IP),就得在客户端关闭证书校验——这等于放弃了 TLS 的核心价值,只防了被动窃听、防不了中间人,所以更推荐用域名。
另外,TLS 握手对 CPU 有开销。如果你的 broker 要承载大量每秒新建连接短连接(比如设备频繁重连),可以开启会话保持(session expiry)和客户端 keepalive 的合理设置,减少重连次数,从而减少握手。EMQX 也支持配置 TLS 会话复用,能显著降低重复握手的开销。
通过 Nginx 反代 WebSocket 接入
如果你想让网页前端直接通过 MQTT over WebSocket 连到 broker,就需要把 8083 端口通过 Nginx 暴露出去。MQTT over WebSocket 走的是标准 WebSocket 升级流程,Nginx 反代配置和普通 WebSocket 基本一致,关键是 Upgrade 和 Connection 两个头必须正确透传:
location /mqtt {
proxy_pass http://127.0.0.1:8083;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}这里最容易被忽略的是超时时间。MQTT 的长连接如果长时间没有数据交互,Nginx 默认 60 秒的 proxy_read_timeout 会把它掐断,客户端不得不反复重连,日志里全是断开重连的记录。把读写超时设大(比如 3600 秒)给长连接留足空间。同时如果前面还有 CDN 或另一层代理,也要确认它们没有更短的空闲超时。
暴露 WebSocket 之前,务必确认认证已经开启。WebSocket 接入和 TCP 接入共用同一套认证授权,所以只要认证配好了,前端也逃不掉;但如果图省事先开 WebSocket 再补认证,就有一段裸奔窗口。
真实踩坑复盘:三个值得记住的教训
第一个坑:Dashboard 暴露公网当晚就被扫描。有一次图方便,把 18083 直接开在公网上没有改防火墙,第二天登录日志里已经有几百个不同 IP 尝试用 admin/public 登录的记录。好在密码早改了。这件事说明所有管理面板都不能裸奔到公网,即便改了默认密码也要用防火墙或者反代加一道访问控制。
第二个坑:主题命名带时间戳导致内存缓慢上涨。一开始为了让"最新状态"可查,把主题设计成 status/{设备}/{时间戳},每条状态都 retain。跑了半个月,broker 内存从 200MB 涨到了 1.5GB。原因是每个不同的时间戳都是一个独立主题,保留消息越积越多。改成 status/{设备} 固定主题、消息体内带时间戳后,内存立刻回落并稳定。教训是:保留消息的主题基数必须可控。
第三个坑:客户端 ID 重复导致互相踢下线。有两台设备因为脚本复制粘贴,用了相同的 ClientID。EMQX 默认策略是"后连接者挤掉先连接者",结果两台设备疯狂互相踢下线,表现为"连接正常但数据时断时续"。排查了很久才想到去看连接日志里的 kick 记录。结论是每个客户端必须有唯一且稳定的 ClientID,最好在应用启动时就校验并发告警,别让它悄悄重复。
总结
自建 MQTT 服务器对个人站长来说,是把"事件驱动"这件事从依赖第三方服务变成自己可控的关键一步。EMQX 用 Docker 部署的成本很低,真正的门槛在认证、授权、TLS 和主题设计这些"用对了才安全"的地方:匿名必须关、密码必须改、ACL 必须按命名空间收口、QoS 要按业务重要性分级而不是一律拉满、保留消息的主题基数必须可控、长连接的超时要在每一层代理上都留足。
把这几件事做扎实,你就得到了一套低延迟、低流量、跨设备统一的通信底座。它不是一个用完就扔的玩具,而是可以长期承载你站点各种自动化需求的"神经系统"——从服务器监控告警,到智能家居联动,再到自建服务的状态同步,都能挂在这一套之上。