让消息跨网络送达:从零搭建 MQTT 发布订阅实验
2026/9/19 22:52:48 网站建设 项目流程

电脑上的程序已经能生成温湿度数据,接下来怎样把它交给另一端?如果每多一个接收程序,就给发送端增加一个地址,设备数量一多,配置很快变得难以维护。MQTT 提供了另一种组织方式:发送者把消息发到指定主题,接收者订阅这个主题,中间由消息服务器负责转交。

这篇文章用一台 Mac 和一台 Ubuntu 云服务器,搭建一个能够实际运行的小实验。Ubuntu 运行 Mosquitto,Mac 用 Python 模拟设备上报,并启动独立客户端接收消息。两端通过星空组网地址通信,完成从官网登录、设备接入,到创建项目、部署服务和核对结果的完整过程。温湿度数据由程序模拟,不代表真实传感器采样。

一、先明确三种角色和两个地址

MQTT 中,发布者负责发送,订阅者负责接收,Broker 负责转发。这里的主题可以理解为双方约定的消息名称,例如lab/sensor/temperature。它不是文件路径,也不需要提前在服务器上建立同名文件夹。发布者和订阅者连接同一个 Broker,再使用一致的主题,就具备了通信的基本条件。

设备本次角色组网地址
Ubuntu 22.04 云服务器Mosquitto Broker192.168.188.5
MacPython 发布者与订阅者192.168.188.1

两种客户端都运行在 Mac 上,但它们建立各自的连接,消息经过远端 Ubuntu 转发,并不是两个本地函数直接交换数据。组网软件解决设备之间如何连接,Mosquitto 决定消息转给谁,Python 则负责组织和读取内容。这三层分开理解,出错时也更容易定位。

这个小场景可以对应机房环境上报、实验设备状态通知,或家中设备向个人服务器发送测量结果。先用模拟程序把路径跑通,可以把硬件驱动、传感器接线等问题暂时排除。以后换成真实采样,只需要替换生成数据的部分,仍应保留消息格式、设备标识和接收端校验。

本次使用已有 Docker 与 Compose,Mosquitto 固定为2.0.22,Python 依赖固定为paho-mqtt==2.1.0。这些是本次实测版本,不表示永远适合使用同一个旧版本。实际安装时,应先核对系统架构和官方发布说明。

二、从官网登录,为两台设备准备成员

打开星空组网官网,通过首页的“后台管理”进入控制台。尚无主账号时,可以先通过注册入口完成注册;已有账号则直接登录。注册、验证码和密码填写由账号持有人操作,不要把这些内容放进教程截图。

登录页用于进入管理后台,设备客户端使用的成员账号则在后台管理,两者不要混淆。本次已有可用登录状态,所以保留了空白登录页展示入口,后续复用现有会话,没有为了截图退出账号、重新注册或修改旧密码。

进入“设备管理—成员列表”。新环境应分别给 Mac 和 Ubuntu 准备成员,填写便于辨认的名称,并记录各自虚拟 IP。不同长期在线设备使用独立成员,避免把同一个成员登录到多台设备上。已有成员时,先核对名称、状态和地址,不必为了照着教程操作而重复创建。

Mac 从官网对应下载入口安装客户端;Ubuntu 参考官方 Linux 文档,按照架构选择安装方式。在客户端中分别完成成员登录,确认两端处于可互访的网络。本次两台设备的客户端版本均为6.1.0,后台显示两台在线。

三、检查网络,再创建独立项目

先在 Mac 检查到服务器组网地址的路由:

route-nget192.168.188.5

本次结果中的接口是utun9,后续 TCP 连接也确认 Mac 的源地址为192.168.188.1。你的接口名称可能不同,不需要照搬这个编号。路由存在只说明系统知道向哪里发送,还不能证明 Broker 正常运行或账号有权限,后面仍要检查端口和 MQTT 返回结果。

下面的服务器命令在 Ubuntu 执行。先确认 Docker 可用,再建立新的实验目录。示例使用 root 会话,普通用户需要根据机器已有权限添加sudo,不要改动其他正在运行的项目。

docker--versiondockercompose versionip-br-4addr show StarVPN ss-lnt'( sport = :1883 )'mkdir-p/opt/mqtt-lab-20260919/configcd/opt/mqtt-lab-20260919

开始部署前,1883 端口应没有其他服务占用。若已有 MQTT 实例,先查明用途,再为实验选择不同端口,并同步修改客户端。本文所说的“创建项目”,指在 Ubuntu 创建独立的应用目录与 Compose 项目,区别于组网后台的成员管理。

还要区分云服务器的公网地址、云内网地址和组网虚拟地址。远程终端用于管理服务器,不代表 MQTT 必须沿用终端的连接地址。本文客户端始终连接表格中的虚拟地址,也不要求在云控制台额外开放公网 MQTT 入站端口。把这个约定记清楚,能减少“服务启动正常,连接的却是另一个地址”的错误。

四、部署只在组网地址监听的 Broker

在项目目录保存compose.yaml

name:mqtt-labservices:broker:image:eclipse-mosquitto:2.0.22user:"1883:1883"network_mode:hostrestart:"no"volumes:-./config:/mosquitto/config:ro

这里采用 Linux 的 host 网络模式,让容器直接使用宿主机网络,由 Mosquitto 自己指定监听地址。没有配置ports映射;服务只绑定组网 IP,便于核对访问范围。restart: "no"表示这是手动启动的实验,不会因为容器退出而自动重启。配置目录以只读方式挂载,运行进程使用专用用户1883:1883

保存config/mosquitto.conf,其中的地址必须替换成你自己的 Ubuntu 组网地址:

listener 1883 192.168.188.5 allow_anonymous false password_file /mosquitto/config/passwd acl_file /mosquitto/config/acl persistence false log_dest stdout connection_messages true log_type error log_type warning log_type notice log_type information

保存config/acl

user mqtt_demo topic readwrite lab/sensor/temperature

mqtt_demo是本次应用账号,与星空组网账号无关。关闭匿名连接后,客户端需要通过密码验证;ACL 再把这个账号的读写范围限制到指定主题。组网成功不等于可以省略应用认证,两者承担不同的职责。上述选项可在 Mosquitto 配置手册核对。

接着拉取官方镜像,交互式设置实验账号密码。-c会创建密码文件,也可能覆盖已有文件,因此只在这个新项目首次初始化时使用:

dockerpull eclipse-mosquitto:2.0.22dockerrun--rm-it--networknone\-v"$PWD/config:/mosquitto/config"\eclipse-mosquitto:2.0.22\mosquitto_passwd-c/mosquitto/config/passwd mqtt_demochown1883:1883 config/mosquitto.conf config/acl config/passwdchmod600config/mosquitto.conf config/acl config/passwddockercompose config--quietdockercompose up-d

按提示输入两遍密码,终端不显示字符属于正常情况。不要使用把密码直接写进命令参数的方式,也不要展示passwd文件。把文件所有者和权限设置好,是为了让专用用户能读取配置,同时避免启动时出现文件权限警告。官方认证说明介绍了密码文件的使用方法。

本次 Ubuntu 已开启 UFW,仅新增以下精确规则,让 Mac 经组网网卡访问实验端口:

ufw allowinon StarVPN from192.168.188.1\to192.168.188.5 port1883proto tcp comment'mqtt-lab-demo'dockercomposepsss-lnt'( sport = :1883 )'dockercompose logs--tail10broker

验收时应同时看到容器运行、监听地址为192.168.188.5:1883,以及日志中的mosquitto version 2.0.22 running。仅看到容器名称并不足以判断服务可用。示例没有配置 MQTT TLS,只用于当前组网链路中的实验;不要把这个监听配置直接扩展成公网服务。

五、用 Python 发布与订阅模拟数据

回到 Mac,新建客户端目录和虚拟环境,避免把实验依赖装进全局 Python:

mkdir-p~/mqtt-lab-clientcd~/mqtt-lab-client python3-mvenv .venvsource.venv/bin/activate python-mpipinstallpaho-mqtt==2.1.0

将下面代码保存为mqtt_demo.py。同一个文件通过pubsub参数切换角色,连接密码通过隐藏输入读取,不写入源码。客户端 ID 每次生成不同值,避免两次运行因为身份相同而相互断开。

"""MQTT 模拟温湿度实验:先运行 sub,再运行 pub。"""importargparseimportgetpassimportjsonimportrandomimportthreadingimporttimeimportuuidimportpaho.mqtt.clientasmqtt TOPIC="lab/sensor/temperature"TIMEOUT=10defconnect(host:str,password:str)->mqtt.Client:ready=threading.Event()reasons=[]client=mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,client_id="lab-"+uuid.uuid4().hex[:10],protocol=mqtt.MQTTv5,reconnect_on_failure=False)client.username_pw_set("mqtt_demo",password)client.connect_timeout=TIMEOUTdefon_connect(c,userdata,flags,reason,properties):reasons.append(reason)ready.set()client.on_connect=on_connecttry:client.connect(host,1883,keepalive=30)client.loop_start()ifnotready.wait(TIMEOUT):raiseTimeoutError("等待 MQTT CONNACK 超时")ifreasons[0].is_failure:raiseConnectionError(f"MQTT 登录被拒绝:{reasons[0]}")print(f"CONNECTED{host}:1883 MQTTv5",flush=True)returnclientexceptException:client.disconnect()client.loop_stop()raisedefsubscribe(client:mqtt.Client,seconds:int)->None:ready=threading.Event()granted=[]defon_subscribe(c,userdata,mid,reasons,properties):granted.extend(reasons)ready.set()defon_message(c,userdata,message):print(f"RECV qos={message.qos}{message.topic}"f"{message.payload.decode('utf-8',errors='replace')}",flush=True)client.on_subscribe=on_subscribe client.on_message=on_message result,_=client.subscribe(TOPIC,qos=1)ifresult!=mqtt.MQTT_ERR_SUCCESSornotready.wait(TIMEOUT):raiseTimeoutError("订阅失败或等待 SUBACK 超时")ifnotgrantedorany(reason.is_failureforreasoningranted):raisePermissionError(f"订阅被拒绝:{granted}")print(f"SUBSCRIBED{TOPIC}; listening{seconds}s",flush=True)time.sleep(seconds)defpublish(client:mqtt.Client)->None:forsequenceinrange(1,4):payload=json.dumps({"device":"sim-01","seq":sequence,"temperature":round(random.uniform(22,26),1),"humidity":round(random.uniform(40,60),1),"simulated":True},ensure_ascii=False)info=client.publish(TOPIC,payload,qos=1,retain=False)info.wait_for_publish(timeout=TIMEOUT)ifnotinfo.is_published():raiseTimeoutError("等待 PUBACK 超时")print(f"SENT{TOPIC}{payload}",flush=True)time.sleep(1)defmain()->None:parser=argparse.ArgumentParser(description=__doc__)parser.add_argument("mode",choices=["pub","sub"])parser.add_argument("--host",default="192.168.188.5")parser.add_argument("--seconds",type=int,default=60)args=parser.parse_args()ifnot1<=args.seconds<=3600:parser.error("--seconds 必须在 1~3600 之间")client=Nonetry:client=connect(args.host,getpass.getpass("MQTT password: "))ifargs.mode=="sub":subscribe(client,args.seconds)else:publish(client)except(OSError,RuntimeError)aserror:raiseSystemExit(f"ERROR:{error}")fromerrorfinally:ifclientisnotNone:client.disconnect()client.loop_stop()if__name__=="__main__":main()

代码使用 Paho 的第二版回调接口和 MQTT 5。建立 TCP 连接后,还要等待 Broker 的连接确认;订阅后,也要等到订阅确认,再认为接收端已准备好。这比程序启动就立即发送更容易复现,尤其能避免第一次消息发出时订阅尚未生效。Paho 官方文档列出了对应的回调参数。

打开第一个 Mac 终端,进入客户端目录并运行订阅:

.venv/bin/python mqtt_demo.py sub--host192.168.188.5--seconds120

输入实验密码,看到SUBSCRIBED后保持窗口打开。再打开第二个终端,进入同一目录并发布:

.venv/bin/python mqtt_demo.py pub--host192.168.188.5

发布者每秒发送一条,共三条。每条包含设备名、递增序号、温度、湿度,以及simulated: true标记。接收窗口应出现相同主题和对应 JSON。这里选用 QoS 1,它允许在特定故障情况下重复投递,因此正式业务通常还需要消息标识和去重;本次三条消息成功不代表完成了断线重连或长期可靠性测试。

阅读输出时,把发送端的SENT和接收端的RECV对照起来,逐项看设备名、序号和数值。温湿度是随机生成的,所以你运行时不必得到截图中的相同数字。关键是同一轮实验里,两端对应消息的内容一致,而不是凑出一个看起来正确的温度。订阅程序到达设定时长后会退出,需要再次实验时先重新启动订阅。

六、把“能运行”核对成“确实收到”

配套verify_mqtt.py用两个独立客户端自动完成相同链路:先等订阅就绪,再调用正文里的发布函数,核对三条消息的序号、主题、QoS 和模拟标记。它只需输入一次密码,结果写入不含凭据的verification.json,便于把发送记录与收到的数据一起检查。

本次在 Mac 上执行验收,六项检查全部通过。实际收到的数据如下:

序号温度(℃)相对湿度(%)QoS
124.657.21
225.653.51
324.143.71

验证脚本还主动尝试错误密码和匿名连接,只有收到明确的认证拒绝才记为通过,网络超时不会被当成“安全限制生效”。随后向 ACL 未授权的lab/private/blocked发布 QoS 1 消息,检查 MQTT 5 返回的拒绝原因。这样才能区分网络连不上、账号不能登录,以及账号登录成功但操作超出权限。

最后结合 Ubuntu 日志核对连接来源。这里应看到 Mac 的组网地址,而不是只根据客户端输出猜测消息走了哪条路径。本次实测不用于证明 P2P 直连、链路带宽或服务长期稳定性。

本例给一个实验账号授予同一主题的读写权限,是为了让初次复现更简洁。真实部署可以把上报端设为只写、查看端设为只读,并为不同设备分配不同身份。不要把本次单账号实验的配置直接解释成已经完成了多租户隔离,或者认为拥有密码就应该能访问所有主题。

七、遇到问题,沿着消息路径检查

如果连接超时,先确认两端客户端在线,再检查路由、监听地址与防火墙。容器退出时优先看docker compose logs broker,常见原因包括 IP 不属于本机、端口被占用,以及挂载目录或文件权限不正确。不要一开始就关闭整机防火墙。

如果明确提示登录被拒绝,应核对 MQTT 用户名和实验密码,不要把组网登录密码填进去。若显示已连接却收不到数据,检查主题是否逐字一致、订阅是否先于发布完成,以及两个客户端是否使用不同的 ID。本例关闭持久化且不保留消息,订阅之前发布的普通消息不会自动补发。

实验结束后,可在 Ubuntu 的项目目录执行docker compose down停止容器。文件仍保留,便于复查配置;下次启动前先确认组网地址仍然有效。如果不再使用本实验,应同时移除仅为它添加的防火墙规则。后续接入真实设备时,再逐项考虑独立账号、主题分权、消息去重与持久化,先保持当前这条消息链路简单、可验证。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询